diff --git a/checks/dummy-inventory-test-from-flake/default.nix b/checks/dummy-inventory-test-from-flake/default.nix new file mode 100644 index 000000000..8e2d4555a --- /dev/null +++ b/checks/dummy-inventory-test-from-flake/default.nix @@ -0,0 +1,61 @@ +{ + pkgs, + nixosLib, + clan-core, + ... +}: + +nixosLib.runTest ( + { hostPkgs, config, ... }: + { + imports = [ + clan-core.modules.nixosVmTest.clanTest + ]; + + hostPkgs = pkgs; + + # This tests the compatibility of the inventory + # With the test framework + # - legacy-modules + # - clan.service modules + name = "dummy-inventory-test-from-flake"; + + clan.test.fromFlake = ./.; + + testScript = + { nodes, ... }: + '' + ${clan-core.legacyPackages.${hostPkgs.system}.setupNixInNixPython} + + def run_clan(cmd: list[str], **kwargs) -> str: + import subprocess + clan = "${clan-core.packages.${hostPkgs.system}.clan-cli}/bin/clan" + clan_args = ["--flake", "${config.clan.test.flakeForSandbox}"] + return subprocess.run( + ["${hostPkgs.util-linux}/bin/unshare", "--user", "--map-user", "1000", "--map-group", "1000", clan, *cmd, *clan_args], + **kwargs, + check=True, + ).stdout + + start_all() + admin1.wait_for_unit("multi-user.target") + peer1.wait_for_unit("multi-user.target") + # Provided by the legacy module + print(admin1.succeed("systemctl status dummy-service")) + print(peer1.succeed("systemctl status dummy-service")) + + # peer1 should have the 'hello' file + peer1.succeed("cat ${nodes.peer1.clan.core.vars.generators.new-service.files.not-a-secret.path}") + + ls_out = peer1.succeed("ls -la ${nodes.peer1.clan.core.vars.generators.new-service.files.a-secret.path}") + # Check that the file is owned by 'nobody' + assert "nobody" in ls_out, f"File is not owned by 'nobody': {ls_out}" + # Check that the file is in the 'users' group + assert "users" in ls_out, f"File is not in the 'users' group: {ls_out}" + # Check that the file is in the '0644' mode + assert "-rw-r--r--" in ls_out, f"File is not in the '0644' mode: {ls_out}" + + run_clan(["machines", "list"]) + ''; + } +) diff --git a/checks/dummy-inventory-test-from-flake/flake.nix b/checks/dummy-inventory-test-from-flake/flake.nix new file mode 100644 index 000000000..575f65584 --- /dev/null +++ b/checks/dummy-inventory-test-from-flake/flake.nix @@ -0,0 +1,70 @@ +{ + inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz"; + inputs.nixpkgs.follows = "clan-core/nixpkgs"; + + outputs = + { self, clan-core, ... }: + let + # Usage see: https://docs.clan.lol + clan = clan-core.clanLib.buildClan { + inherit self; + + inventory = + { ... }: + { + meta.name = "foo"; + machines.peer1 = { }; + machines.admin1 = { }; + services = { + legacy-module.default = { + roles.peer.machines = [ "peer1" ]; + roles.admin.machines = [ "admin1" ]; + }; + }; + + instances."test" = { + module.name = "new-service"; + roles.peer.machines.peer1 = { }; + }; + + modules = { + legacy-module = ./legacy-module; + }; + }; + + modules.new-service = { + _class = "clan.service"; + manifest.name = "new-service"; + roles.peer = { }; + perMachine = { + nixosModule = { + # This should be generated by: + # nix run .#generate-test-vars -- checks/dummy-inventory-test dummy-inventory-test + clan.core.vars.generators.new-service = { + files.not-a-secret = { + secret = false; + deploy = true; + }; + files.a-secret = { + secret = true; + deploy = true; + owner = "nobody"; + group = "users"; + mode = "0644"; + }; + script = '' + # This is a dummy script that does nothing + echo -n "not-a-secret" > $out/not-a-secret + echo -n "a-secret" > $out/a-secret + ''; + }; + }; + }; + }; + }; + in + { + # all machines managed by Clan + inherit (clan) nixosConfigurations nixosModules clanInternals; + }; +} diff --git a/checks/dummy-inventory-test-from-flake/legacy-module/README.md b/checks/dummy-inventory-test-from-flake/legacy-module/README.md new file mode 100644 index 000000000..2a72080ce --- /dev/null +++ b/checks/dummy-inventory-test-from-flake/legacy-module/README.md @@ -0,0 +1,10 @@ +--- +description = "Set up dummy-module" +categories = ["System"] +features = [ "inventory" ] + +[constraints] +roles.admin.min = 1 +roles.admin.max = 1 +--- + diff --git a/checks/dummy-inventory-test-from-flake/legacy-module/roles/admin.nix b/checks/dummy-inventory-test-from-flake/legacy-module/roles/admin.nix new file mode 100644 index 000000000..a56780406 --- /dev/null +++ b/checks/dummy-inventory-test-from-flake/legacy-module/roles/admin.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ../shared.nix + ]; +} diff --git a/checks/dummy-inventory-test-from-flake/legacy-module/roles/peer.nix b/checks/dummy-inventory-test-from-flake/legacy-module/roles/peer.nix new file mode 100644 index 000000000..a56780406 --- /dev/null +++ b/checks/dummy-inventory-test-from-flake/legacy-module/roles/peer.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ../shared.nix + ]; +} diff --git a/checks/dummy-inventory-test-from-flake/legacy-module/shared.nix b/checks/dummy-inventory-test-from-flake/legacy-module/shared.nix new file mode 100644 index 000000000..92b7418ca --- /dev/null +++ b/checks/dummy-inventory-test-from-flake/legacy-module/shared.nix @@ -0,0 +1,34 @@ +{ config, ... }: +{ + systemd.services.dummy-service = { + enable = true; + description = "Dummy service"; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + generated_password_path="${config.clan.core.vars.generators.dummy-generator.files.generated-password.path}" + if [ ! -f "$generated_password_path" ]; then + echo "Generated password file not found: $generated_password_path" + exit 1 + fi + host_id_path="${config.clan.core.vars.generators.dummy-generator.files.host-id.path}" + if [ ! -e "$host_id_path" ]; then + echo "Host ID file not found: $host_id_path" + exit 1 + fi + ''; + }; + + # TODO: add and prompt and make it work in the test framework + clan.core.vars.generators.dummy-generator = { + files.host-id.secret = false; + files.generated-password.secret = true; + script = '' + echo $RANDOM > "$out"/host-id + echo $RANDOM > "$out"/generated-password + ''; + }; +} diff --git a/checks/flake-module.nix b/checks/flake-module.nix index bfb5f8caf..ed3122540 100644 --- a/checks/flake-module.nix +++ b/checks/flake-module.nix @@ -51,6 +51,7 @@ in postgresql = self.clanLib.test.containerTest ./postgresql nixosTestArgs; dummy-inventory-test = import ./dummy-inventory-test nixosTestArgs; + dummy-inventory-test-from-flake = import ./dummy-inventory-test-from-flake nixosTestArgs; data-mesher = import ./data-mesher nixosTestArgs; } // lib.optionalAttrs (pkgs.stdenv.hostPlatform.system == "aarch64-linux") { diff --git a/lib/clanTest/flake-module.nix b/lib/clanTest/flake-module.nix index cbbace26a..18ca7560e 100644 --- a/lib/clanTest/flake-module.nix +++ b/lib/clanTest/flake-module.nix @@ -7,11 +7,10 @@ let inherit (lib) flatten - flip - mapAttrs' mapAttrsToList + mkForce + mkIf mkOption - removePrefix types unique ; @@ -24,9 +23,48 @@ in flake.modules.nixosVmTest.clanTest = { config, hostPkgs, ... }: let - clanFlakeResult = config.clan; testName = config.name; + clan-core = self; + + inherit (lib) + filterAttrs + flip + hasPrefix + intersectAttrs + mapAttrs' + pathExists + removePrefix + throwIf + ; + + # only relevant if config.clan.test.fromFlake is used + importFlake = + flakeDir: + let + flakeExpr = import (flakeDir + "/flake.nix"); + inputs = intersectAttrs flakeExpr.inputs clan-core.inputs; + flake = flakeExpr.outputs ( + inputs + // { + self = flake // { + outPath = flakeDir; + }; + clan-core = clan-core; + } + ); + in + throwIf (pathExists ( + flakeDir + "/flake.lock" + )) "Test ${testName} should not have a flake.lock file" flake; + + clanFlakeResult = + if config.clan.test.fromFlake != null then importFlake config.clan.test.fromFlake else config.clan; + + machineModules = flip filterAttrs clanFlakeResult.nixosModules ( + name: _module: hasPrefix "clan-machine-" name + ); + update-vars-script = "${ self.packages.${hostPkgs.system}.generate-test-vars }/bin/generate-test-vars"; @@ -48,7 +86,7 @@ in ); vars-check = - hostPkgs.runCommand "update-vars-check" + hostPkgs.runCommand "update-vars-check-${testName}" { nativeBuildInputs = generatorRuntimeInputs ++ [ hostPkgs.nix @@ -89,6 +127,16 @@ in fi touch $out ''; + + # the test's flake.nix with locked clan-core input + flakeForSandbox = hostPkgs.runCommand "offline-flake-for-test-${config.name}" { } '' + cp -r ${config.clan.directory} $out + chmod +w -R $out + substituteInPlace $out/flake.nix \ + --replace-fail \ + "https://git.clan.lol/clan/clan-core/archive/main.tar.gz" \ + "${clan-core.packages.${hostPkgs.system}.clan-core-flake}" + ''; in { imports = [ @@ -115,6 +163,9 @@ in nixpkgs nix-darwin ; + # By default clan.directory defaults to self, but we don't + # have a sensible default for self here + self = throw "set clan.directory in the test"; }; modules = [ clanLib.buildClanModule.flakePartsModule @@ -134,6 +185,28 @@ in type = types.bool; description = "Whether to use containers for the test."; }; + test.fromFlake = mkOption { + default = null; + type = types.nullOr types.path; + description = '' + path to a directory containing a `flake.nix` defining the clan + + Only use this if the clan CLI needs to be used inside the test. + Otherwise, use the other clan.XXX options instead to specify the clan. + + Loads the clan from a flake instead of using clan.XXX options. + This has the benefit that a real flake.nix will be available in the test. + This is useful to test CLI interactions which require a flake.nix. + + Using this introduces dependencies that should otherwise be avoided. + ''; + }; + test.flakeForSandbox = mkOption { + default = flakeForSandbox; + type = types.path; + description = "The flake.nix to use for the test."; + readOnly = true; + }; }; } ]; @@ -141,10 +214,12 @@ in }; }; config = { + clan.directory = mkIf (config.clan.test.fromFlake != null) (mkForce config.clan.test.fromFlake); + # Inherit all nodes from the clan # i.e. nodes.jon <- clan.machines.jon # clanInternals.nixosModules contains nixosModules per node - nodes = flip mapAttrs' clanFlakeResult.nixosModules ( + nodes = flip mapAttrs' machineModules ( name: machineModule: { name = removePrefix "clan-machine-" name; value = machineModule; @@ -169,11 +244,6 @@ in clanLib.test.sopsModule ]; - # Disable documentation - # This is nice to speed up the evaluation - # And also suppresses any warnings or errors about the documentation - documentation.enable = lib.mkDefault false; - # Disable garbage collection during the test # https://nix.dev/manual/nix/2.28/command-ref/conf-file.html?highlight=min-free#available-settings nix.settings.min-free = 0; diff --git a/lib/test/minify.nix b/lib/test/minify.nix index 411bf687d..2f7451840 100644 --- a/lib/test/minify.nix +++ b/lib/test/minify.nix @@ -8,4 +8,5 @@ nix.registry = lib.mkForce { }; documentation.doc.enable = false; documentation.man.enable = false; + documentation.enable = lib.mkDefault false; } diff --git a/pkgs/testing/flake-module.nix b/pkgs/testing/flake-module.nix index 539199148..4c6b1de41 100644 --- a/pkgs/testing/flake-module.nix +++ b/pkgs/testing/flake-module.nix @@ -1,19 +1,37 @@ { perSystem = { - legacyPackages.setupNixInNix = '' - export HOME=$TMPDIR - export NIX_STATE_DIR=$TMPDIR/nix - export NIX_CONF_DIR=$TMPDIR/etc - export IN_NIX_SANDBOX=1 - export CLAN_TEST_STORE=$TMPDIR/store - # required to prevent concurrent 'nix flake lock' operations - export LOCK_NIX=$TMPDIR/nix_lock - mkdir -p "$CLAN_TEST_STORE/nix/store" - mkdir -p "$CLAN_TEST_STORE/nix/var/nix/gcroots" - if [[ -n "''${closureInfo-}" ]]; then - xargs cp --recursive --target "$CLAN_TEST_STORE/nix/store" < "$closureInfo/store-paths" - nix-store --load-db --store "$CLAN_TEST_STORE" < "$closureInfo/registration" - fi - ''; + legacyPackages = { + setupNixInNix = '' + export HOME=$TMPDIR + export NIX_STATE_DIR=$TMPDIR/nix + export NIX_CONF_DIR=$TMPDIR/etc + export IN_NIX_SANDBOX=1 + export CLAN_TEST_STORE=$TMPDIR/store + # required to prevent concurrent 'nix flake lock' operations + export LOCK_NIX=$TMPDIR/nix_lock + mkdir -p "$CLAN_TEST_STORE/nix/store" + mkdir -p "$CLAN_TEST_STORE/nix/var/nix/gcroots" + if [[ -n "''${closureInfo-}" ]]; then + xargs cp --recursive --target "$CLAN_TEST_STORE/nix/store" < "$closureInfo/store-paths" + nix-store --load-db --store "$CLAN_TEST_STORE" < "$closureInfo/registration" + fi + ''; + setupNixInNixPython = '' + from os import environ + import subprocess + from pathlib import Path + environ['HOME'] = environ['TMPDIR'] + environ['NIX_STATE_DIR'] = environ['TMPDIR'] + '/nix' + environ['NIX_CONF_DIR'] = environ['TMPDIR'] + '/etc' + environ['IN_NIX_SANDBOX'] = '1' + environ['CLAN_TEST_STORE'] = environ['TMPDIR'] + '/store' + environ['LOCK_NIX'] = environ['TMPDIR'] + '/nix_lock' + Path(environ['CLAN_TEST_STORE'] + '/nix/store').mkdir(parents=True, exist_ok=True) + Path(environ['CLAN_TEST_STORE'] + '/nix/var/nix/gcroots').mkdir(parents=True, exist_ok=True) + if 'closureInfo' in environ: + subprocess.run(['cp', '--recursive', '--target', environ['CLAN_TEST_STORE'] + '/nix/store'] + environ['closureInfo'].split(), check=True) + subprocess.run(['nix-store', '--load-db', '--store', environ['CLAN_TEST_STORE']] + ['<', environ['closureInfo'] + '/registration'], shell=True, check=True) + ''; + }; }; }