diff --git a/flake.lock b/flake.lock index fc8e402..6f3fefe 100644 --- a/flake.lock +++ b/flake.lock @@ -9,11 +9,11 @@ "utils": "utils" }, "locked": { - "lastModified": 1766051518, - "narHash": "sha256-znKOwPXQnt3o7lDb3hdf19oDo0BLP4MfBOYiWkEHoik=", + "lastModified": 1770019181, + "narHash": "sha256-hwsYgDnby50JNVpTRYlF3UR/Rrpt01OrxVuryF40CFY=", "owner": "serokell", "repo": "deploy-rs", - "rev": "d5eff7f948535b9c723d60cd8239f8f11ddc90fa", + "rev": "77c906c0ba56aabdbc72041bf9111b565cdd6171", "type": "github" }, "original": { @@ -68,11 +68,11 @@ "spectrum": "spectrum" }, "locked": { - "lastModified": 1771365290, - "narHash": "sha256-1XJOslVyF7yzf6yd/yl1VjGLywsbtwmQh3X1LuJcLI4=", + "lastModified": 1773018425, + "narHash": "sha256-fpgZBmZpKoEXEowBK/6m8g9FcOLWQ4UxhXHqCw2CpSM=", "owner": "microvm-nix", "repo": "microvm.nix", - "rev": "789c90b164b55b4379e7a94af8b9c01489024c18", + "rev": "25ebda3c558e923720c965832dc9a04f559a055c", "type": "github" }, "original": { @@ -129,11 +129,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1768323494, - "narHash": "sha256-yBXJLE6WCtrGo7LKiB6NOt6nisBEEkguC/lq/rP3zRQ=", + "lastModified": 1773375660, + "narHash": "sha256-SEzUWw2Rf5Ki3bcM26nSKgbeoqi2uYy8IHVBqOKjX3w=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "2c3e5ec5df46d3aeee2a1da0bfedd74e21f4bf3a", + "rev": "3e20095fe3c6cbb1ddcef89b26969a69a1570776", "type": "github" }, "original": { @@ -166,11 +166,11 @@ "systems": "systems_2" }, "locked": { - "lastModified": 1770975056, - "narHash": "sha256-ZXTz/P3zUbbM6lNXzt91u8EwfNqhXpYMu8+wvFZqQHE=", + "lastModified": 1773738366, + "narHash": "sha256-oH22HyNHEdCoCQo734sQCHUr6C0jmGQJMZ13dsgEHkk=", "owner": "cry128", "repo": "nt", - "rev": "f42dcdd49a7921a7f433512e83d5f93696632412", + "rev": "f32c3a726a3d608d30aaaa1df2301c1eaf5ef8f4", "type": "github" }, "original": { @@ -185,17 +185,38 @@ "microvm": "microvm", "nixpkgs": "nixpkgs", "nt": "nt", + "sops-nix": "sops-nix", "systems": "systems_3" } }, + "sops-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1773096132, + "narHash": "sha256-M3zEnq9OElB7zqc+mjgPlByPm1O5t2fbUrH3t/Hm5Ag=", + "owner": "Mic92", + "repo": "sops-nix", + "rev": "d1ff3b1034d5bab5d7d8086a7803c5a5968cd784", + "type": "github" + }, + "original": { + "owner": "Mic92", + "repo": "sops-nix", + "type": "github" + } + }, "spectrum": { "flake": false, "locked": { - "lastModified": 1759482047, - "narHash": "sha256-H1wiXRQHxxPyMMlP39ce3ROKCwI5/tUn36P8x6dFiiQ=", + "lastModified": 1772189877, + "narHash": "sha256-i1p90Rgssb//aNiTDFq46ZG/fk3LmyRLChtp/9lddyA=", "ref": "refs/heads/main", - "rev": "c5d5786d3dc938af0b279c542d1e43bce381b4b9", - "revCount": 996, + "rev": "fe39e122d898f66e89ffa17d4f4209989ccb5358", + "revCount": 1255, "type": "git", "url": "https://spectrum-os.org/git/spectrum" }, diff --git a/flake.nix b/flake.nix index 80faf5c..83bb612 100644 --- a/flake.nix +++ b/flake.nix @@ -44,7 +44,7 @@ nt, ... } @ inputs: - import ./cerulean + import ./nix { inherit inputs self nt; inherit (nt) mix; diff --git a/nix/default.nix b/nix/default.nix index 6c488a1..6364a63 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -15,22 +15,28 @@ mix, inputs, ... -} @ args: -mix.newMixture args (mixture: { - submods.public = [ - ./snow - ]; +} @ args: let + mixArgs = + args + // { + inherit (inputs.nixpkgs) lib; + }; +in + mix.newMixture mixArgs (mixture: { + submods.public = [ + ./snow + ]; - version = "0.2.6-alpha"; + version = "0.2.6-alpha"; - overlays = [ - # build deploy-rs as a package not from the flake input, - # hence we can rely on a nixpkg binary cache. - inputs.deploy-rs.overlays.default - ]; + overlays = [ + # build deploy-rs as a package not from the flake input, + # hence we can rely on a nixpkg binary cache. + inputs.deploy-rs.overlays.default + ]; - nixosModules = rec { - default = cerulean; - cerulean = ./nixos; - }; -}) + nixosModules = rec { + default = cerulean; + cerulean = ./nixos; + }; + }) diff --git a/nix/nixos/default.nix b/nix/nixos/default.nix index a716c2f..a91df1a 100644 --- a/nix/nixos/default.nix +++ b/nix/nixos/default.nix @@ -18,13 +18,13 @@ node, pkgs, lib, - _cerulean, + _snow, ... } @ args: { imports = [ - _cerulean.inputs.sops-nix.nixosModules.sops - # _cerulean.inputs.microvm.nixosModules.microvm + _snow.inputs.sops-nix.nixosModules.sops + # _snow.inputs.microvm.nixosModules.microvm # add support for `options.legacyImports` # ./legacy-imports.nix @@ -36,7 +36,7 @@ (import /${root}/nixpkgs.nix) ] # homemanager options declarations - ++ (lib.optional (_cerulean.homeManager != null) ./home.nix) + ++ (lib.optional (_snow.homeManager != null) ./home.nix) # remote deployment configuration ++ (lib.optional (node.deploy.ssh.host != null) ./remote-deploy); @@ -46,7 +46,7 @@ (with pkgs; [ sops ]) - ++ (with _cerulean.inputs; [ + ++ (with _snow.inputs; [ deploy-rs.packages.${system}.default ]); } diff --git a/nix/nixos/home.nix b/nix/nixos/home.nix index 82117d8..cf24f74 100644 --- a/nix/nixos/home.nix +++ b/nix/nixos/home.nix @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. { - _cerulean, + _snow, config, root, lib, @@ -30,7 +30,7 @@ ; in { imports = [ - _cerulean.homeManager.nixosModules.default + _snow.homeManager.nixosModules.default ]; options = { @@ -69,7 +69,7 @@ in { _module.args.username = name; }); - extraSpecialArgs = _cerulean.specialArgs; + extraSpecialArgs = _snow.specialArgs; sharedModules = [ ../home diff --git a/nix/snow/default.nix b/nix/snow/default.nix index 6993ff1..89e1772 100644 --- a/nix/snow/default.nix +++ b/nix/snow/default.nix @@ -12,180 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. { - this, - self, - inputs, - systems, nt, mix, ... -} @ args: let - inherit - (builtins) - all - attrNames - elem - mapAttrs - warn - ; +} @ args: +mix.newMixture (removeAttrs args ["this"]) (mixture: { + submods.public = [ + ./lib + ]; - inherit (inputs.nixpkgs) lib; - - inherit (nt) findImport; -in - mix.newMixture args (mixture: let - inherit (mixture) mapNodes; - in { - includes.private = [ - ./lib/nodes.nix - ]; - - inherit findImport; - - # snow.flake - flake = flakeInputs: root: let - module = lib.evalModules { - class = "snowflake"; - # TODO: abort if inputs contains reserved names - specialArgs = - (flakeInputs - // { - inherit systems root; - inherit (this) snow; - inputs = flakeInputs; - }) - |> (x: builtins.removeAttrs x ["self" "nodes"]); - - modules = [ - ./module.nix - ({config, ...}: { - _module.args = { - self = config; - nodes = config.nodes.nodes; - }; - }) - ]; - }; - - nodes = module.config.nodes; - in rec { - nixosConfigurations = mapNodes nodes ( - { - base, - lib, - name, - node, - groupModules, - ... - }: let - homeManager = - if node.homeManager != null - then node.homeManager - else if nodes.homeManager != null - then nodes.homeManager - else - warn '' - [snowflake] Neither `nodes.homeManager` nor `nodes.nodes.${name}.homeManager` were specified! - [snowflake] home-manager will NOT be used! User configuration will be ignored! - '' - null; - - userArgs = nodes.args // node.args; - ceruleanArgs = { - inherit systems root base nodes node; - inherit (node) system; - inherit (this) snow; - hostname = name; - - _cerulean = { - inherit inputs userArgs ceruleanArgs homeManager; - specialArgs = userArgs // ceruleanArgs; - }; - }; - specialArgs = assert (userArgs - |> attrNames - |> all (argName: - ! ceruleanArgs ? argName - || abort '' - `specialArgs` are like super important to Cerulean my love... rollback; - magicRollback = magicRollback -> rollback; - activationTimeout = activationTimeout; - confirmTimeout = confirmTimeout; - - remoteBuild = remoteBuild; - sshUser = ssh.user; - sshOpts = - ssh.opts - ++ ( - if elem "-p" ssh.opts - then [] - else ["-p" (toString ssh.port)] - ) - ++ ( - if elem "-A" ssh.opts - then [] - else ["-A"] - ); - }; - }); - - checks = - inputs.deploy-rs.lib - |> mapAttrs (system: deployLib: - deployLib.deployChecks deploy); - }; - }) + includes.public = [ + ./flake + ]; +}) diff --git a/nix/snow/flake/default.nix b/nix/snow/flake/default.nix new file mode 100644 index 0000000..828f59b --- /dev/null +++ b/nix/snow/flake/default.nix @@ -0,0 +1,68 @@ +{ + self, + this, + inputs, + systems, + ... +}: let + inherit + (builtins) + attrNames + concatStringsSep + filter + length + warn + ; + + inherit (inputs.nixpkgs) lib; +in { + # snow.flake + # XXX: TODO: stop taking in root as parameter (maybe take self instead?) + flake = flakeInputs: root: let + snowflake = lib.evalModules { + class = "snowflake"; + specialArgs = let + reservedSpecialArgs = { + # inherit (this) snow; + snow = this; + inherit systems root; + inputs = flakeInputs; + + _snowFlake = { + inherit self inputs; + }; + }; + + warnIfReserved = let + getReservedNames = names: + reservedSpecialArgs + |> attrNames + |> filter (name: names?${name}); + + reservedNames = + flakeInputs + |> attrNames + |> getReservedNames; + in + (length reservedNames == 0) + || warn '' + [snow] Your `flake.nix` declares inputs with reserved names! + [snow] These will be accessible only via `inputs.''${NAME}` + [snow] Please rename the following: + [snow] ${concatStringsSep reservedNames ", "} + '' + true; + in + assert warnIfReserved; + flakeInputs // reservedSpecialArgs; + + modules = [ + ./nodes + ./modules + ./outputs + (this.lib.findImport /${root}/snow) + ]; + }; + in + snowflake.config.outputs; +} diff --git a/nix/snow/flake/modules/README.md b/nix/snow/flake/modules/README.md new file mode 100644 index 0000000..d19dbc1 --- /dev/null +++ b/nix/snow/flake/modules/README.md @@ -0,0 +1,3 @@ +# Snow Module Backend +This source code was tedious so it's just a modified version of the module backend of +[github:hercules-ci/flake-parts](https://github.com/hercules-ci/flake-parts/tree/main/modules). diff --git a/nix/snow/flake/modules/apps.nix b/nix/snow/flake/modules/apps.nix new file mode 100644 index 0000000..cbe12f0 --- /dev/null +++ b/nix/snow/flake/modules/apps.nix @@ -0,0 +1,69 @@ +{ + lib, + snow, + ... +}: let + inherit + (lib) + mkOption + types + ; + + inherit + (snow.lib) + mkPerSystemFlakeOutput + ; + + derivationType = + lib.types.package + // { + check = lib.isDerivation; + }; + + programType = lib.types.coercedTo derivationType lib.getExe lib.types.str; + + appType = lib.types.submodule { + options = { + type = mkOption { + type = lib.types.enum ["app"]; + default = "app"; + description = '' + A type tag for `apps` consumers. + ''; + }; + program = mkOption { + type = programType; + description = '' + A path to an executable or a derivation with `meta.mainProgram`. + ''; + }; + meta = mkOption { + type = types.lazyAttrsOf lib.types.raw; + default = {}; + # TODO refer to Nix manual 2.25 + description = '' + Metadata information about the app. + Standardized in Nix at . + + Note: `nix flake check` is only aware of the `description` attribute in `meta`. + ''; + }; + }; + }; +in + mkPerSystemFlakeOutput { + name = "apps"; + option = mkOption { + type = types.lazyAttrsOf appType; + default = {}; + description = '' + Programs runnable with nix run ``. + ''; + example = lib.literalExpression '' + { + default.program = "''${config.packages.hello}/bin/hello"; + } + ''; + }; + file = ./apps.nix; + } diff --git a/nix/snow/flake/modules/checks.nix b/nix/snow/flake/modules/checks.nix new file mode 100644 index 0000000..d10d6d5 --- /dev/null +++ b/nix/snow/flake/modules/checks.nix @@ -0,0 +1,26 @@ +{ + lib, + snow, + ... +}: let + inherit + (lib) + mkOption + types + ; + inherit + (snow.lib) + mkPerSystemFlakeOutput + ; +in + mkPerSystemFlakeOutput { + name = "checks"; + option = mkOption { + type = types.lazyAttrsOf types.package; + default = {}; + description = '' + Derivations to be built by [`nix flake check`](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-flake-check.html). + ''; + }; + file = ./checks.nix; + } diff --git a/nix/snow/flake/modules/default.nix b/nix/snow/flake/modules/default.nix new file mode 100644 index 0000000..e6914a0 --- /dev/null +++ b/nix/snow/flake/modules/default.nix @@ -0,0 +1,15 @@ +{...}: { + imports = [ + ./outputs.nix + + ./apps.nix + ./checks.nix + ./devShells.nix + ./formatter.nix + ./legacyPackages.nix + ./nixosConfigurations.nix + ./nixosModules.nix + ./overlays.nix + ./packages.nix + ]; +} diff --git a/nix/snow/flake/modules/devShells.nix b/nix/snow/flake/modules/devShells.nix new file mode 100644 index 0000000..04ace01 --- /dev/null +++ b/nix/snow/flake/modules/devShells.nix @@ -0,0 +1,35 @@ +{ + lib, + snow, + ... +}: let + inherit + (lib) + mkOption + types + literalExpression + ; + inherit + (snow.lib) + mkPerSystemFlakeOutput + ; +in + mkPerSystemFlakeOutput { + name = "devShells"; + option = mkOption { + type = types.lazyAttrsOf types.package; + default = {}; + description = '' + An attribute set of packages to be used as shells. + [`nix develop .#`](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-develop.html) will run `devShells.`. + ''; + example = literalExpression '' + { + default = pkgs.mkShell { + nativeBuildInputs = with pkgs; [ wget bat cargo ]; + }; + } + ''; + }; + file = ./devShells.nix; + } diff --git a/nix/snow/flake/modules/formatter.nix b/nix/snow/flake/modules/formatter.nix new file mode 100644 index 0000000..5cce36b --- /dev/null +++ b/nix/snow/flake/modules/formatter.nix @@ -0,0 +1,26 @@ +{ + lib, + snow, + ... +}: let + inherit + (lib) + mkOption + types + ; + inherit + (snow.lib) + mkPerSystemFlakeOutput + ; +in + mkPerSystemFlakeOutput { + name = "formatter"; + option = mkOption { + type = types.nullOr types.package; + default = null; + description = '' + A package used by [`nix fmt`](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-fmt.html). + ''; + }; + file = ./apps.nix; + } diff --git a/nix/snow/flake/modules/legacyPackages.nix b/nix/snow/flake/modules/legacyPackages.nix new file mode 100644 index 0000000..9adc91d --- /dev/null +++ b/nix/snow/flake/modules/legacyPackages.nix @@ -0,0 +1,26 @@ +{ + lib, + snow, + ... +}: let + inherit + (lib) + mkOption + types + ; + inherit + (snow.lib) + mkPerSystemFlakeOutput + ; +in + mkPerSystemFlakeOutput { + name = "legacyPackages"; + option = mkOption { + type = types.lazyAttrsOf types.raw; + default = {}; + description = '' + Used for nixpkgs packages, also accessible via `nix build .#` [`nix build .#`](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-build.html). + ''; + }; + file = ./legacyPackages.nix; + } diff --git a/nix/snow/flake/modules/nixosConfigurations.nix b/nix/snow/flake/modules/nixosConfigurations.nix new file mode 100644 index 0000000..7db8077 --- /dev/null +++ b/nix/snow/flake/modules/nixosConfigurations.nix @@ -0,0 +1,35 @@ +{lib, ...}: let + inherit + (lib) + mkOption + types + literalExpression + ; +in { + options = { + outputs.nixosConfigurations = mkOption { + type = types.lazyAttrsOf types.raw; + default = {}; + description = '' + Instantiated NixOS configurations. Used by `nixos-rebuild`. + + `nixosConfigurations` is for specific machines. If you want to expose + reusable configurations, add them to [`nixosModules`](#opt-flake.nixosModules) + in the form of modules (no `lib.nixosSystem`), so that you can reference + them in this or another flake's `nixosConfigurations`. + ''; + example = literalExpression '' + { + my-machine = inputs.nixpkgs.lib.nixosSystem { + # system is not needed with freshly generated hardware-configuration.nix + # system = "x86_64-linux"; # or set nixpkgs.hostPlatform in a module. + modules = [ + ./my-machine/nixos-configuration.nix + config.nixosModules.my-module + ]; + }; + } + ''; + }; + }; +} diff --git a/nix/snow/flake/modules/nixosModules.nix b/nix/snow/flake/modules/nixosModules.nix new file mode 100644 index 0000000..6819570 --- /dev/null +++ b/nix/snow/flake/modules/nixosModules.nix @@ -0,0 +1,29 @@ +{ + lib, + moduleLocation, + ... +}: let + inherit + (lib) + mapAttrs + mkOption + types + ; +in { + options = { + outputs.nixosModules = mkOption { + type = types.lazyAttrsOf types.deferredModule; + default = {}; + apply = mapAttrs (k: v: { + _class = "nixos"; + _file = "${toString moduleLocation}#nixosModules.${k}"; + imports = [v]; + }); + description = '' + NixOS modules. + + You may use this for reusable pieces of configuration, service modules, etc. + ''; + }; + }; +} diff --git a/nix/snow/flake/modules/outputs.nix b/nix/snow/flake/modules/outputs.nix new file mode 100644 index 0000000..bf7d9f5 --- /dev/null +++ b/nix/snow/flake/modules/outputs.nix @@ -0,0 +1,46 @@ +{ + lib, + config, + ... +}: let + inherit + (lib) + mkOption + types + ; +in { + options = { + outputs = mkOption { + type = types.submoduleWith { + modules = [ + { + freeformType = + types.lazyAttrsOf + (types.unique + { + message = '' + No option has been declared for this flake output attribute, so its definitions can't be merged automatically. + Possible solutions: + - Load a module that defines this flake output attribute + - Declare an option for this flake output attribute + - Make sure the output attribute is spelled correctly + - Define the value only once, with a single definition in a single module + ''; + } + types.raw); + } + ]; + }; + description = '' + Raw flake output attributes. Any attribute can be set here, but some + attributes are represented by options, to provide appropriate + configuration merging. + ''; + }; + }; + + config = { + # ensure a minimal version is set + outputs = {}; + }; +} diff --git a/nix/snow/flake/modules/overlays.nix b/nix/snow/flake/modules/overlays.nix new file mode 100644 index 0000000..55423c2 --- /dev/null +++ b/nix/snow/flake/modules/overlays.nix @@ -0,0 +1,31 @@ +{lib, ...}: let + inherit + (lib) + mkOption + types + ; +in { + options = { + outputs.overlays = mkOption { + # uniq -> ordered: https://github.com/NixOS/nixpkgs/issues/147052 + # also update description when done + type = types.lazyAttrsOf (types.uniq (types.functionTo (types.functionTo (types.lazyAttrsOf types.unspecified)))); + # This eta expansion exists for the sole purpose of making nix flake check happy. + apply = lib.mapAttrs (_k: f: final: prev: f final prev); + default = {}; + example = lib.literalExpression '' + { + default = final: prev: {}; + } + ''; + description = '' + An attribute set of [overlays](https://nixos.org/manual/nixpkgs/stable/#chap-overlays). + + Note that the overlays themselves are not mergeable. While overlays + can be composed, the order of composition is significant, but the + module system does not guarantee sufficiently deterministic + definition ordering, across versions and when changing `imports`. + ''; + }; + }; +} diff --git a/nix/snow/flake/modules/packages.nix b/nix/snow/flake/modules/packages.nix new file mode 100644 index 0000000..ef970bc --- /dev/null +++ b/nix/snow/flake/modules/packages.nix @@ -0,0 +1,29 @@ +{ + lib, + snow, + ... +}: let + inherit + (lib) + mkOption + types + ; + + inherit + (snow.lib) + mkPerSystemFlakeOutput + ; +in + mkPerSystemFlakeOutput { + name = "packages"; + option = mkOption { + type = types.lazyAttrsOf types.package; + default = {}; + description = '' + An attribute set of packages to be built by [`nix build`](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-build.html). + + `nix build .#` will build `packages.`. + ''; + }; + file = ./packages.nix; + } diff --git a/nix/snow/nodes/default.nix b/nix/snow/flake/nodes/default.nix similarity index 50% rename from nix/snow/nodes/default.nix rename to nix/snow/flake/nodes/default.nix index d3bc9b7..07ad153 100644 --- a/nix/snow/nodes/default.nix +++ b/nix/snow/flake/nodes/default.nix @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. { + _snowFlake, lib, specialArgs, ... @@ -25,38 +26,20 @@ in mkOption { description = '' - Cerulean node declarations. + Snowflake node declarations. ''; type = types.submoduleWith { inherit specialArgs; modules = [ - { - imports = [./shared.nix]; - - options = { - groups = mkOption { - type = types.attrs; - default = {}; - example = lib.literalExpression "{ servers = { staging = {}; production = {}; }; }"; - description = '' - Hierarchical groups that nodes can be a member of. - ''; - }; - - nodes = mkOption { - type = types.attrsOf (types.submoduleWith { - inherit specialArgs; - modules = [(import ./submodule.nix)]; - }); - # example = { ... }; # TODO - description = '' - Node (host systems) declarations. - ''; - }; - }; - } + ./nodes.nix ]; }; }; + + config = { + nodes = { + base = _snowFlake.inputs.nixpkgs; + }; + }; } diff --git a/nix/snow/nodes/submodule.nix b/nix/snow/flake/nodes/node.nix similarity index 61% rename from nix/snow/nodes/submodule.nix rename to nix/snow/flake/nodes/node.nix index 6b4ae05..11c2b98 100644 --- a/nix/snow/nodes/submodule.nix +++ b/nix/snow/flake/nodes/node.nix @@ -14,16 +14,24 @@ { lib, systems, + nodesConfig, + groups, + groupLibs, ... }: { - imports = [./shared.nix]; - options = let inherit (lib) mkOption types ; + + inherit + (groupLibs) + resolveGroupsInheritance + ; + + flakeRef = types.either types.str types.path; in { enabled = lib.mkOption { type = types.bool; @@ -43,6 +51,65 @@ ''; }; + base = lib.mkOption { + # In newer Nix versions, particularly with lazy trees, outPath of + # flakes becomes a Nix-language path object. We deliberately allow this + # to gracefully come through the interface in discussion with @roberth. + # + # See: https://github.com/NixOS/nixpkgs/pull/278522#discussion_r1460292639 + type = types.nullOr flakeRef; + + default = nodesConfig.base; + defaultText = "nodes.base"; + + example = lib.literalExpression "inputs.nixpkgs"; + + description = '' + The path to the nixpkgs source used to build a system. A `base` package set + is required to be set, and can be specified via either: + 1. `options.nodes.base` (default `base` used for all systems) + 2. `options.nodes.nodes..base` (takes prescedence over `options.nodes.base`) + + This can also be optionally set if the NixOS system is not built with a flake but still uses + pinned sources: set this to the store path for the nixpkgs sources used to build the system, + as may be obtained by `fetchTarball`, for example. + + Note: the name of the store path must be "source" due to + . + ''; + }; + + homeManager = mkOption { + type = types.nullOr flakeRef; + default = nodesConfig.homeManager; + defaultText = "nodes.homeManager"; + example = lib.literalExpression "inputs.home-manager"; + description = '' + The path to the home-manager source. A `homeManager` flake reference + is required to be set for `homes/` to be evaluated, and can be specified via either: + 1. `options.nodes.homeManager` (default `homManager` used for all systems) + 2. `options.nodes.nodes..homeManager` (takes prescedence over `options.nodes.homeManager`) + ''; + }; + + modules = mkOption { + type = types.listOf types.raw; + default = []; + example = lib.literalExpression "[ { environment.systemPackages = [ pkgs.git ]; } ]"; + description = '' + Shared modules to import; equivalent to the NixOS module system's `extraModules`. + ''; + }; + + args = mkOption { + type = types.attrs; + default = {}; + example = lib.literalExpression "{ inherit inputs; }"; + description = '' + Shared args to provided for each node; equivalent to the NixOS module system's `specialArgs`. + ''; + }; + groups = mkOption { # TODO: write a custom group type that validates better than types.attrs lol type = types.functionTo (types.listOf types.attrs); @@ -51,6 +118,9 @@ description = '' A function from the `groups` hierarchy to a list of groups this node inherits from. ''; + + # apply = groupsFn: + # groupsFn nodesConfig.groups |> resolveGroupsInheritance; }; deploy = { @@ -91,7 +161,7 @@ example = false; description = '' Whether to enable interactive sudo (password based sudo). - NOT RECOMMENDED. Use one of Cerulean's recommended auth methods instead. + NOT RECOMMENDED. Use one of Snowflake's recommended auth methods instead. ''; }; @@ -164,7 +234,7 @@ user = mkOption { type = types.str; - default = "cerubld"; + default = "snowbld"; example = "custom-user"; description = '' The user to connect to over ssh during deployment. @@ -183,7 +253,7 @@ publicKeys = mkOption { type = types.listOf types.str; default = []; - example = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIeyZuUUmyUYrYaEJwEMvcXqZFYm1NaZab8klOyK6Imr me@puter"]; + example = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIeyZuUUmyUYrYaEJwEMvcXqZFYm1NaZab8klOyK6Imr me@myputer"]; description = '' SSH public keys that will be authorized to the deployment user. This key is intended solely for deployment, allowing for fine-grained permission control. @@ -201,4 +271,33 @@ }; }; }; + + # config = let + # throwGotNull = name: + # throw '' + # [snow] `nodes..${name}` must be set for all nodes! (got: ) + # ''; + # givenSystem = + # (config.system != null) + # || throwGotNull "system"; + + # givenBase = + # (config.base != null) + # || throwGotNull "base"; + + # givenHomeManager = + # (config.homeManager != null) + # || throwGotNull "homeManager"; + + # givenDeployHost = + # (config.deploy.ssh.host != null) + # || throwGotNull "deploy.ssh.host"; + # in + # assert givenSystem + # && givenBase + # && givenHomeManager + # && givenDeployHost; { + # # extend these from the nodes configuration + # inherit (nodesConfig) modules args; + # }; } diff --git a/nix/snow/nodes/shared.nix b/nix/snow/flake/nodes/nodes.nix similarity index 80% rename from nix/snow/nodes/shared.nix rename to nix/snow/flake/nodes/nodes.nix index c840d22..d5e5a59 100644 --- a/nix/snow/nodes/shared.nix +++ b/nix/snow/flake/nodes/nodes.nix @@ -11,7 +11,15 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -{lib, ...}: let +{ + _snowFlake, + snow, + root, + lib, + config, + specialArgs, + ... +}: let inherit (lib) mkOption @@ -19,6 +27,11 @@ ; flakeRef = types.either types.str types.path; + + groupLibs = import ./groups.nix { + inherit snow root; + inherit (_snowFlake.inputs) nt; + }; in { options = { base = lib.mkOption { @@ -49,6 +62,18 @@ in { ''; }; + homeManager = mkOption { + type = types.nullOr flakeRef; + default = null; + example = lib.literalExpression "inputs.home-manager"; + description = '' + The path to the home-manager source. A `homeManager` flake reference + is required to be set for `homes/` to be evaluated, and can be specified via either: + 1. `options.nodes.homeManager` (default `homManager` used for all systems) + 2. `options.nodes.nodes..homeManager` (takes prescedence over `options.nodes.homeManager`) + ''; + }; + modules = mkOption { type = types.listOf types.raw; default = []; @@ -67,15 +92,27 @@ in { ''; }; - homeManager = mkOption { - type = types.nullOr flakeRef; - default = null; - example = lib.literalExpression "inputs.home-manager"; + groups = mkOption { + type = types.attrs; + default = {}; + example = lib.literalExpression "{ servers = { staging = {}; production = {}; }; }"; description = '' - The path to the home-manager source. A `homeManager` flake reference - is required to be set for `homes/` to be evaluated, and can be specified via either: - 1. `options.nodes.homeManager` (default `homManager` used for all systems) - 2. `options.nodes.nodes..homeManager` (takes prescedence over `options.nodes.homeManager`) + Hierarchical groups that nodes can be a member of. + ''; + }; + + nodes = mkOption { + type = types.attrsOf (types.submoduleWith { + specialArgs = + specialArgs + // { + nodesConfig = config; + inherit groupLibs; + }; + modules = [./node.nix]; + }); + description = '' + Node (host systems) declarations. ''; }; }; diff --git a/nix/snow/flake/outputs/checks.nix b/nix/snow/flake/outputs/checks.nix new file mode 100644 index 0000000..de6a9c0 --- /dev/null +++ b/nix/snow/flake/outputs/checks.nix @@ -0,0 +1,10 @@ +{ + config, + _snowFlake, + ... +}: { + outputs.checks = + _snowFlake.inputs.deploy-rs.lib + |> builtins.mapAttrs (system: deployLib: + deployLib.deployChecks config.outputs.deploy); +} diff --git a/nix/snow/flake/outputs/default.nix b/nix/snow/flake/outputs/default.nix new file mode 100644 index 0000000..2c0a9ca --- /dev/null +++ b/nix/snow/flake/outputs/default.nix @@ -0,0 +1,7 @@ +{...}: { + imports = [ + ./checks.nix + ./deploy.nix + ./nixosConfigurations.nix + ]; +} diff --git a/nix/snow/flake/outputs/deploy.nix b/nix/snow/flake/outputs/deploy.nix new file mode 100644 index 0000000..59bbb49 --- /dev/null +++ b/nix/snow/flake/outputs/deploy.nix @@ -0,0 +1,63 @@ +{ + _snowFlake, + snow, + config, + ... +}: { + outputs.deploy.nodes = snow.lib.mapNodes config.nodes ({ + name, + node, + ... + }: let + inherit + (node.deploy) + ssh + user + interactiveSudo + remoteBuild + rollback + autoRollback + magicRollback + activationTimeout + confirmTimeout + ; + + nixosFor = system: _snowFlake.inputs.deploy-rs.lib.${system}.activate.nixos; + in { + hostname = + if ssh.host != null + then ssh.host + else ""; + + profilesOrder = ["default"]; # profiles priority + profiles.default = { + path = nixosFor node.system config.outputs.nixosConfigurations.${name}; + + user = user; + sudo = "sudo -u"; + interactiveSudo = interactiveSudo; + + fastConnection = false; + + autoRollback = autoRollback -> rollback; + magicRollback = magicRollback -> rollback; + activationTimeout = activationTimeout; + confirmTimeout = confirmTimeout; + + remoteBuild = remoteBuild; + sshUser = ssh.user; + sshOpts = + ssh.opts + ++ ( + if builtins.elem "-p" ssh.opts + then [] + else ["-p" (toString ssh.port)] + ) + ++ ( + if builtins.elem "-A" ssh.opts + then [] + else ["-A"] + ); + }; + }); +} diff --git a/nix/snow/flake/outputs/nixosConfigurations.nix b/nix/snow/flake/outputs/nixosConfigurations.nix new file mode 100644 index 0000000..977fa22 --- /dev/null +++ b/nix/snow/flake/outputs/nixosConfigurations.nix @@ -0,0 +1,84 @@ +{ + _snowFlake, + snow, + config, + systems, + root, + ... +}: let + inherit + (builtins) + all + attrNames + warn + ; + + inherit + (config) + nodes + ; +in { + outputs.nixosConfigurations = let + groups = snow.lib.parseGroupDecls root config.nodes.groups; + in + snow.lib.mapNodes nodes ( + { + base, + lib, + name, + node, + ... + }: let + nodeGroups = + (node.groups groups) + |> snow.lib.resolveGroupsInheritance + |> snow.lib.groupModules; + + homeManager = + if node.homeManager != null + then node.homeManager + else if nodes.homeManager != null + then nodes.homeManager + else + warn '' + [snowflake] Neither `nodes.homeManager` nor `nodes.nodes.${name}.homeManager` were specified! + [snowflake] home-manager will NOT be used! User configuration will be ignored! + '' + null; + + userArgs = nodes.args // node.args; + snowArgs = { + inherit systems snow root base nodes node; + inherit (node) system; + hostname = name; + + _snow = { + inherit (_snowFlake) inputs; + inherit userArgs snowArgs homeManager; + specialArgs = userArgs // snowArgs; + }; + }; + specialArgs = assert (userArgs + |> attrNames + |> all (argName: + ! snowArgs ? argName + || abort '' + `specialArgs` are like super important to Snow my love... = 0) + || abort '' + The nixpkgs dependency of snow was overridden but is too old. + The minimum supported version of nixpkgs-lib is ${minVersion}, + but the actual version is ${lib.version}${revInfo}. + ''; +in + assert isNixpkgsValidVersion; { + # Helper function for defining a per-system option that + # gets transposed by the usual flake system logic to a + # top-level outputs attribute. + mkPerSystemFlakeOutput = { + name, + option, + file, + }: { + _file = file; + + options = { + outputs.${name} = mkOption { + type = types.attrsWith { + elemType = option.type; + lazy = true; + placeholder = "system"; + }; + default = {}; + description = '' + See {option}`perSystem.${name}` for description and examples. + ''; + }; + }; + }; + } diff --git a/nix/snow/lib/nodes.nix b/nix/snow/lib/nodes.nix index 48a583d..6f799cf 100644 --- a/nix/snow/lib/nodes.nix +++ b/nix/snow/lib/nodes.nix @@ -1,17 +1,8 @@ -# Copyright 2025-2026 _cry64 (Emile Clark-Boman) -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -{nt, ...}: let +{ + this, + nt, + ... +}: let inherit (builtins) concatLists @@ -23,53 +14,9 @@ typeOf ; + inherit (nt.prim) uniq; + rootGroupName = "all"; - - parseGroupsDecl = groups: let - validGroup = g: - isAttrs g - || throw '' - Cerulean Nexus groups must be provided as attribute sets, got "${typeOf g}" instead! - Ensure all the group definitions are attribute sets under your call to `cerulean.mkNexus`. - NOTE: Groups can be accessed via `self.groups.PATH.TO.YOUR.GROUP` - ''; - delegate = parent: gName: g: let - result = - (g - // { - _name = gName; - _parent = parent; - }) - |> mapAttrs (name: value: - if elem name ["_name" "_parent"] - # ignore metadata fields - then value - else assert validGroup value; (delegate result name value)); - in - result; - in - assert validGroup groups; - delegate null rootGroupName groups; - - getGroupModules = root: groups: - # ensure root group is always added - groups - # add all inherited groups via _parent - |> map (let - delegate = g: - if g._parent == null - then [g] - else [g] ++ delegate (g._parent); - in - delegate) - # flatten recursion result - |> concatLists - # find import location - |> map (group: nt.findImport /${root}/groups/${group._name}) - # filter by uniqueness - |> nt.prim.unique - # ignore missing groups - |> filter pathExists; in { mapNodes = nodes: f: nodes.nodes @@ -82,7 +29,7 @@ in { then nodes.base else abort '' - Cerulean cannot construct nodes node "${name}" without a base package source. + snow cannot construct nodes node "${name}" without a base package source. Ensure `nodes.nodes.*.base` or `nodes.base` is a flake reference to the github:NixOS/nixpkgs repository. ''; in @@ -90,7 +37,51 @@ in { inherit name node base; inherit (base) lib; - groups = node.groups (parseGroupsDecl nodes.groups); - groupModules = root: getGroupModules root groups; + inherit (node) groups; }); + + groupModules = map (group: group._module); + + parseGroupDecls = root: groupDecls: let + validGroup = g: + isAttrs g + || throw '' + Snow node groups must be provided as attribute sets, got "${typeOf g}" instead! + Ensure all the group definitions are attribute sets under your call to `snow.flake`. + ''; + delegate = parent: gName: g: let + result = + (g + // { + _name = gName; + _parent = parent; + _module = this.lib.findImport /${root}/groups/${gName}; + }) + |> mapAttrs (name: value: + if elem name ["_name" "_parent" "_module"] + # ignore metadata fields + then value + else assert validGroup value; (delegate result name value)); + in + result; + in + assert validGroup groupDecls; + delegate null rootGroupName groupDecls; + + resolveGroupsInheritance = groups: + groups + # add all inherited groups via _parent + |> map (let + delegate = g: + if g._parent == null + then [g] + else [g] ++ delegate (g._parent); + in + delegate) + # flatten recursion result + |> concatLists + # ignore missing groups + |> filter (group: pathExists group._module) + # filter by uniqueness + |> uniq; } diff --git a/nix/snow/lib/util.nix b/nix/snow/lib/util.nix new file mode 100644 index 0000000..6bfcd17 --- /dev/null +++ b/nix/snow/lib/util.nix @@ -0,0 +1,3 @@ +{nt, ...}: { + inherit (nt) findImport; +}