diff --git a/devShell.nix b/devShell.nix index ed5fac47..206b29cf 100644 --- a/devShell.nix +++ b/devShell.nix @@ -26,6 +26,7 @@ devShells.default = pkgs.mkShell { packages = [ select-shell + pkgs.nix-unit pkgs.tea # Better error messages than nix 2.18 pkgs.nixVersions.latest diff --git a/flake.nix b/flake.nix index d520c2fe..716b4fcc 100644 --- a/flake.nix +++ b/flake.nix @@ -51,6 +51,7 @@ ./formatter.nix ./lib/flake-module.nix ./nixosModules/flake-module.nix + ./nixosModules/clanCore/vars/flake-module.nix ./pkgs/flake-module.nix ./templates/flake-module.nix ]; diff --git a/lib/inventory/flake-module.nix b/lib/inventory/flake-module.nix index fa78fdba..521d3dfe 100644 --- a/lib/inventory/flake-module.nix +++ b/lib/inventory/flake-module.nix @@ -30,7 +30,7 @@ in }; # Run: nix-unit --extra-experimental-features flakes --flake .#legacyPackages.x86_64-linux.evalTests - legacyPackages.evalTests = import ./tests { + legacyPackages.evalTests-inventory = import ./tests { inherit buildInventory; clan-core = self; }; @@ -42,7 +42,7 @@ in nix-unit --eval-store "$HOME" \ --extra-experimental-features flakes \ ${inputOverrides} \ - --flake ${self}#legacyPackages.${system}.evalTests + --flake ${self}#legacyPackages.${system}.evalTests-inventory touch $out ''; diff --git a/nixosModules/clanCore/vars/default.nix b/nixosModules/clanCore/vars/default.nix new file mode 100644 index 00000000..56a04061 --- /dev/null +++ b/nixosModules/clanCore/vars/default.nix @@ -0,0 +1,16 @@ +{ lib, ... }: +{ + options.clan.core.vars = lib.mkOption { + internal = true; + description = '' + Generated Variables + + Define generators that prompt for or generate variables like facts and secrets to store, deploy, and rotate them easily. + For example, generators can be used to: + - prompt the user for input, like passwords or host names + - generate secrets like private keys automatically when they are needed + - output multiple values like private and public keys simultaneously + ''; + type = lib.types.submoduleWith { modules = [ ./interface.nix ]; }; + }; +} diff --git a/nixosModules/clanCore/vars/eval-tests/default.nix b/nixosModules/clanCore/vars/eval-tests/default.nix new file mode 100644 index 00000000..c8ea88ac --- /dev/null +++ b/nixosModules/clanCore/vars/eval-tests/default.nix @@ -0,0 +1,50 @@ +{ lib, ... }: +let + eval = + module: + (lib.evalModules { + modules = [ + ../default.nix + module + ]; + }).config; +in +{ + single_file_single_prompt = + let + config = eval { + clan.core.vars.generators.my_secret = { + files.password = { }; + files.username.secret = false; + prompts.prompt1 = { }; + script = '' + cp $prompts/prompt1 $files/password + ''; + }; + }; + in + { + test_file_secret_by_default = { + expr = config.clan.core.vars.generators.my_secret.files.password.secret; + expected = true; + }; + test_secret_value_access_raises_error = { + expr = config.clan.core.vars.generators.my_secret.files.password.value; + expectedError.type = "ThrownError"; + expectedError.msg = "Cannot access value of secret file"; + }; + test_public_value_access = { + expr = config.clan.core.vars.generators.my_secret.files.username ? value; + expected = true; + }; + # both secret and public values must provide a path + test_secret_has_path = { + expr = config.clan.core.vars.generators.my_secret.files.password ? path; + expected = true; + }; + test_public_var_has_path = { + expr = config.clan.core.vars.generators.my_secret.files.username ? path; + expected = true; + }; + }; +} diff --git a/nixosModules/clanCore/vars/flake-module.nix b/nixosModules/clanCore/vars/flake-module.nix new file mode 100644 index 00000000..77830ad3 --- /dev/null +++ b/nixosModules/clanCore/vars/flake-module.nix @@ -0,0 +1,31 @@ +{ + self, + inputs, + lib, + ... +}: +let + inputOverrides = builtins.concatStringsSep " " ( + builtins.map (input: " --override-input ${input} ${inputs.${input}}") (builtins.attrNames inputs) + ); +in +{ + perSystem = + { system, pkgs, ... }: + { + legacyPackages.evalTests-module-clan-vars = import ./eval-tests { + inherit lib; + clan-core = self; + }; + checks.module-clan-vars-eval = pkgs.runCommand "tests" { nativeBuildInputs = [ pkgs.nix-unit ]; } '' + export HOME="$(realpath .)" + + nix-unit --eval-store "$HOME" \ + --extra-experimental-features flakes \ + ${inputOverrides} \ + --flake ${self}#legacyPackages.${system}.evalTests-module-clan-vars + + touch $out + ''; + }; +} diff --git a/nixosModules/clanCore/vars/interface.nix b/nixosModules/clanCore/vars/interface.nix new file mode 100644 index 00000000..1b51f071 --- /dev/null +++ b/nixosModules/clanCore/vars/interface.nix @@ -0,0 +1,105 @@ +{ lib, ... }: +let + inherit (lib) mkOption; + inherit (lib.types) + attrsOf + bool + enum + listOf + str + submodule + ; + options = lib.mapAttrs (_: mkOption); + subOptions = opts: submodule { options = options opts; }; +in +{ + options = options { + generators = { + type = attrsOf (subOptions { + dependencies = { + description = '' + A list of other generators that this generator depends on. + The output values of these generators will be available to the generator script as files. + For example, the file 'file1' of a dependency named 'dep1' will be available via $dependencies/dep1/file1. + ''; + type = listOf str; + default = [ ]; + }; + files = { + description = '' + A set of files to generate. + The generator 'script' is expected to produce exactly these files under $out. + ''; + type = attrsOf (subOptions { + secret = { + description = '' + Whether the file should be treated as a secret. + ''; + type = bool; + default = true; + }; + path = { + description = '' + The path to the file containing the content of the generated value. + This will be set automatically + ''; + type = str; + readOnly = true; + }; + value = { + description = '' + The content of the generated value. + Only available if the file is not secret. + ''; + type = str; + default = throw "Cannot access value of secret file"; + defaultText = "Throws error because the value of a secret file is not accessible"; + }; + }); + }; + prompts = { + description = '' + A set of prompts to ask the user for values. + Prompts are available to the generator script as files. + For example, a prompt named 'prompt1' will be available via $prompts/prompt1 + ''; + type = attrsOf (subOptions { + description = { + description = '' + The description of the prompted value + ''; + type = str; + example = "SSH private key"; + }; + type = { + description = '' + The input type of the prompt. + The following types are available: + - hidden: A hidden text (e.g. password) + - line: A single line of text + - multiline: A multiline text + ''; + type = enum [ + "hidden" + "line" + "multiline" + ]; + default = "line"; + }; + }); + }; + script = { + description = '' + The script to run to generate the files. + The script will be run with the following environment variables: + - $dependencies: The directory containing the output values of all declared dependencies + - $out: The output directory to put the generated files + - $prompts: The directory containing the prompted values as files + The script should produce the files specified in the 'files' attribute under $out. + ''; + type = str; + }; + }); + }; + }; +}