From 57d977efcc762c98461aa0c3ddba9d9496a6174b Mon Sep 17 00:00:00 2001 From: Aly Raffauf Date: Wed, 5 Aug 2026 13:11:31 -0400 Subject: [PATCH] add flake library + flake-parts module --- README.md | 5 +++ docs/flake-integration.md | 88 +++++++++++++++++++++++++++++++++++++++ flake.nix | 5 +++ flake/blzrd.nix | 61 +++++++++++++++++++++++++++ flake/lib.nix | 53 +++++++++++++++++++++++ 5 files changed, 212 insertions(+) create mode 100644 docs/flake-integration.md create mode 100644 flake/blzrd.nix create mode 100644 flake/lib.nix diff --git a/README.md b/README.md index 5ee1291..2514cd2 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,11 @@ The deployment user must be specified. Non-root users require passwordless `sudo } ``` +### Flake validation + +For module and library integration, see +[the flake integration guide](docs/flake-integration.md). + ## Limitations - Requires SSH root access or the ability to escalate privileges with sudo without password entry. It won't prompt for a password, it just fails. diff --git a/docs/flake-integration.md b/docs/flake-integration.md new file mode 100644 index 0000000..49d2030 --- /dev/null +++ b/docs/flake-integration.md @@ -0,0 +1,88 @@ +# Flake integration + +blzrd exposes two ways to validate a `blzrd.nodes` definition through +`nix flake check`: + +- a flake-parts module that wires the check automatically; +- a reusable Nix library for flakes that want to wire the check themselves. + +Validation happens during Nix evaluation. It does not build system closures, +connect to nodes, or deploy anything. + +## Flake-parts module + +Import `inputs.blzrd.flakeModule` and define `blzrd.nodes` in the flake-parts +configuration: + +```nix +{ + inputs.blzrd.url = "github:alyraffauf/blzrd"; + + outputs = inputs @ { self, flake-parts, ... }: + flake-parts.lib.mkFlake { inherit inputs; } { + imports = [ inputs.blzrd.flakeModule ]; + + blzrd.nodes = { + server = { + output = self.nixosConfigurations.server.config.system.build.toplevel; + user = "root"; + type = "nixos"; + }; + }; + }; +} +``` + +The module exposes `blzrd.nodes` as a flake output and adds +`checks..blzrd-nodes`. The check is enabled by default. To disable it: + +```nix +blzrd.checks.enable = false; +``` + +## Library API + +Flakes that do not use flake-parts can use `inputs.blzrd.lib` directly: + +```nix +{ + inputs = { + blzrd.url = "github:alyraffauf/blzrd"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + + outputs = { self, blzrd, nixpkgs, ... }: + let + systems = [ "x86_64-linux" "aarch64-linux" ]; + nodes = { + server = { + output = self.nixosConfigurations.server.config.system.build.toplevel; + user = "root"; + type = "nixos"; + }; + }; + + forAllSystems = nixpkgs.lib.genAttrs systems; + in { + blzrd.nodes = nodes; + + checks = forAllSystems (system: { + blzrd-nodes = blzrd.lib.checkNodes { + pkgs = nixpkgs.legacyPackages.${system}; + inherit nodes; + }; + }); + }; +} +``` + +The library provides three functions: + +- `validateNodes nodes` returns a list of validation error strings. +- `assertValidNodes nodes` returns `nodes` or throws with all validation errors. +- `checkNodes { pkgs, nodes }` returns a derivation suitable for + `checks..`. + +Each node must provide a derivation-valued `output` and a non-empty `user`. +`hostname` is optional and may be null; `type` is optional and may be null, +`"nixos"`, or `"darwin"`. diff --git a/flake.nix b/flake.nix index c1c50c2..cfc2362 100644 --- a/flake.nix +++ b/flake.nix @@ -26,5 +26,10 @@ ./flake/treefmt.nix inputs.treefmt-nix.flakeModule ]; + + flake = { + flakeModule = ./flake/blzrd.nix; + lib = import ./flake/lib.nix {lib = inputs.nixpkgs.lib;}; + }; }; } diff --git a/flake/blzrd.nix b/flake/blzrd.nix new file mode 100644 index 0000000..7871bba --- /dev/null +++ b/flake/blzrd.nix @@ -0,0 +1,61 @@ +{ + config, + lib, + ... +}: let + cfg = config.blzrd; + blzrdLib = import ./lib.nix {inherit lib;}; + validatedNodes = blzrdLib.assertValidNodes cfg.nodes; +in { + options.blzrd = { + nodes = lib.mkOption { + default = {}; + description = "Nodes managed by blzrd."; + type = lib.types.attrsOf (lib.types.submodule { + freeformType = lib.types.attrsOf lib.types.raw; + + options = { + output = lib.mkOption { + description = "The system derivation to deploy."; + type = lib.types.raw; + }; + + hostname = lib.mkOption { + default = null; + description = "The SSH hostname; defaults to the node name in blzrd."; + type = lib.types.nullOr lib.types.str; + }; + + type = lib.mkOption { + default = null; + description = "The activation backend, either nixos or darwin."; + type = lib.types.nullOr (lib.types.enum ["darwin" "nixos"]); + }; + + user = lib.mkOption { + description = "The SSH and deployment user."; + type = lib.types.str; + }; + }; + }); + }; + + checks.enable = + lib.mkEnableOption "the blzrd node flake check" + // { + default = true; + }; + }; + + config = { + flake.blzrd.nodes = validatedNodes; + + perSystem = {pkgs, ...}: + lib.mkIf cfg.checks.enable { + checks.blzrd-nodes = blzrdLib.checkNodes { + inherit pkgs; + inherit (cfg) nodes; + }; + }; + }; +} diff --git a/flake/lib.nix b/flake/lib.nix new file mode 100644 index 0000000..2aebf9f --- /dev/null +++ b/flake/lib.nix @@ -0,0 +1,53 @@ +{lib}: let + isDerivation = value: + builtins.isAttrs value && (value.type or null) == "derivation"; + + validateNode = name: node: + if !builtins.isAttrs node + then ["node '${name}': must be an attribute set"] + else let + output = node.output or null; + user = node.user or null; + hostname = node.hostname or null; + type = node.type or null; + in + (lib.optional (!isDerivation output) + "node '${name}': output must be a derivation") + ++ (lib.optional + (!(builtins.isString user) || user == "") + "node '${name}': user must be a non-empty string") + ++ (lib.optional + (!(builtins.isNull hostname || builtins.isString hostname) + || (builtins.isString hostname && hostname == "")) + "node '${name}': hostname must be null or a non-empty string") + ++ (lib.optional + (!(builtins.isNull type + || (builtins.isString type && lib.elem type ["darwin" "nixos"]))) + "node '${name}': type must be null, darwin, or nixos"); + + validateNodes = nodes: + if !builtins.isAttrs nodes + then ["blzrd.nodes must be an attribute set"] + else lib.concatLists (lib.mapAttrsToList validateNode nodes); + + assertValidNodes = nodes: let + validationErrors = validateNodes nodes; + in + if validationErrors == [] + then nodes + else + throw '' + Invalid blzrd.nodes configuration: + ${lib.concatStringsSep "\n" validationErrors} + ''; + + checkNodes = { + pkgs, + nodes, + }: + builtins.seq (assertValidNodes nodes) (pkgs.runCommand "blzrd-nodes-check" {} '' + touch "$out" + ''); +in { + inherit assertValidNodes checkNodes validateNodes; +} -- 2.51.2