Merge pull request 'tests: actually execute vars checks in CI' (#3803) from DavHau/clan-core:vars2 into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/3803
This commit is contained in:
DavHau
2025-05-31 10:27:36 +00:00
7 changed files with 88 additions and 20 deletions

View File

@@ -10,7 +10,7 @@ clanLib.test.makeTestClan {
nixosTest = ( nixosTest = (
{ ... }: { ... }:
{ {
name = "service-hello-test"; name = "hello-service";
clan = { clan = {
directory = ./.; directory = ./.;

View File

@@ -11,7 +11,7 @@ clanLib.test.makeTestClan {
nixosTest = ( nixosTest = (
{ ... }: { ... }:
{ {
name = "wifi"; name = "wifi-service";
clan = { clan = {
directory = ./.; directory = ./.;

View File

@@ -34,7 +34,7 @@ in
else if v == null then else if v == null then
throw "Please set either clan.self or clan.directory" throw "Please set either clan.self or clan.directory"
else else
"${v}" v
) lib.types.path; ) lib.types.path;
default = builtins.toString self; default = builtins.toString self;
defaultText = "Root directory of the flake"; defaultText = "Root directory of the flake";

View File

@@ -5,7 +5,12 @@
let let
inherit (lib) inherit (lib)
mkOption mkOption
removePrefix
types types
mapAttrsToList
flip
unique
flatten
; ;
in in
@@ -37,16 +42,26 @@ in
update-vars-script = "${self.packages.${pkgs.system}.generate-test-vars}/bin/generate-test-vars"; update-vars-script = "${self.packages.${pkgs.system}.generate-test-vars}/bin/generate-test-vars";
relativeDir = removePrefix ("${self}/") (toString test.config.clan.directory);
update-vars = pkgs.writeShellScriptBin "update-vars" '' update-vars = pkgs.writeShellScriptBin "update-vars" ''
${update-vars-script} $PRJ_ROOT/checks/${testName} ${testName} ${update-vars-script} $PRJ_ROOT/${relativeDir} ${testName}
''; '';
testSrc = lib.cleanSource (self + "/checks/${testName}"); testSrc = lib.cleanSource test.config.clan.directory;
inputsForMachine =
machine:
flip mapAttrsToList machine.clan.core.vars.generators (_name: generator: generator.runtimeInputs);
generatorRuntimeInputs = unique (
flatten (flip mapAttrsToList test.config.nodes (_machineName: machine: inputsForMachine machine))
);
vars-check = vars-check =
pkgs.runCommand "update-vars-check" pkgs.runCommand "update-vars-check"
{ {
nativeBuildInputs = [ nativeBuildInputs = generatorRuntimeInputs ++ [
pkgs.nix pkgs.nix
pkgs.git pkgs.git
pkgs.age pkgs.age
@@ -54,7 +69,7 @@ in
pkgs.bubblewrap pkgs.bubblewrap
]; ];
closureInfo = pkgs.closureInfo { closureInfo = pkgs.closureInfo {
rootPaths = [ rootPaths = generatorRuntimeInputs ++ [
pkgs.bash pkgs.bash
pkgs.coreutils pkgs.coreutils
pkgs.jq.dev pkgs.jq.dev
@@ -67,14 +82,13 @@ in
}; };
} }
'' ''
# make the test depend on its vars-check derivation
echo ${vars-check} >/dev/null
${self.legacyPackages.${pkgs.system}.setupNixInNix} ${self.legacyPackages.${pkgs.system}.setupNixInNix}
cp -r ${testSrc} ./src cp -r ${testSrc} ./src
chmod +w -R ./src chmod +w -R ./src
find ./src/sops ./src/vars | sort > filesBefore find ./src/sops ./src/vars | sort > filesBefore
${update-vars-script} ./src ${testName} --repo-root ${self.packages.${pkgs.system}.clan-core-flake} ${update-vars-script} ./src ${testName} \
--repo-root ${self.packages.${pkgs.system}.clan-core-flake} \
--clean
find ./src/sops ./src/vars | sort > filesAfter find ./src/sops ./src/vars | sort > filesAfter
if ! diff -q filesBefore filesAfter; then if ! diff -q filesBefore filesAfter; then
echo "The update-vars script changed the files in ${testSrc}." echo "The update-vars script changed the files in ${testSrc}."
@@ -182,6 +196,8 @@ in
# Harder to handle advanced setups (like TPM, LUKS, or LVM-on-LUKS) but not needed since we are in a test # Harder to handle advanced setups (like TPM, LUKS, or LVM-on-LUKS) but not needed since we are in a test
# No systemd journal logs from initrd. # No systemd journal logs from initrd.
boot.initrd.systemd.enable = false; boot.initrd.systemd.enable = false;
# make the test depend on its vars-check derivation
environment.variables.CLAN_VARS_CHECK = "${vars-check}";
} }
); );

View File

@@ -246,7 +246,7 @@ in
The path to the file containing the content of the generated value. The path to the file containing the content of the generated value.
This will be set automatically This will be set automatically
''; '';
type = nullOr str; type = nullOr path;
default = null; default = null;
}; };
path = lib.mkOption { path = lib.mkOption {

View File

@@ -29,7 +29,7 @@ from .graph import (
minimal_closure, minimal_closure,
requested_closure, requested_closure,
) )
from .prompt import Prompt, ask from .prompt import Prompt, PromptType, ask
from .var import Var from .var import Var
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -295,6 +295,26 @@ def _ask_prompts(
return prompt_values return prompt_values
def _fake_prompts(
generator: Generator,
) -> dict[str, str]:
prompt_values: dict[str, str] = {}
for prompt in generator.prompts:
var_id = f"{generator.name}/{prompt.name}"
if prompt.prompt_type == PromptType.HIDDEN:
prompt_values[prompt.name] = "fake_hidden_value"
elif prompt.prompt_type == PromptType.MULTILINE_HIDDEN:
prompt_values[prompt.name] = "fake\nmultiline\nhidden\nvalue"
elif prompt.prompt_type == PromptType.MULTILINE:
prompt_values[prompt.name] = "fake\nmultiline\nvalue"
elif prompt.prompt_type == PromptType.LINE:
prompt_values[prompt.name] = "fake_line_value"
else:
msg = f"Unknown prompt type {prompt.prompt_type} for prompt {var_id} in generator {generator.name}"
raise ClanError(msg)
return prompt_values
def _get_previous_value( def _get_previous_value(
machine: "Machine", machine: "Machine",
generator: Generator, generator: Generator,
@@ -414,6 +434,7 @@ def generate_vars_for_machine_interactive(
generator_name: str | None, generator_name: str | None,
regenerate: bool, regenerate: bool,
no_sandbox: bool = False, no_sandbox: bool = False,
fake_prompts: bool = False,
) -> bool: ) -> bool:
_generator = None _generator = None
if generator_name: if generator_name:
@@ -438,7 +459,10 @@ def generate_vars_for_machine_interactive(
return False return False
all_prompt_values = {} all_prompt_values = {}
for generator in generators: for generator in generators:
all_prompt_values[generator.name] = _ask_prompts(generator) if fake_prompts:
all_prompt_values[generator.name] = _fake_prompts(generator)
else:
all_prompt_values[generator.name] = _ask_prompts(generator)
return _generate_vars_for_machine( return _generate_vars_for_machine(
machine, machine,
generators, generators,
@@ -452,13 +476,18 @@ def generate_vars(
generator_name: str | None = None, generator_name: str | None = None,
regenerate: bool = False, regenerate: bool = False,
no_sandbox: bool = False, no_sandbox: bool = False,
fake_prompts: bool = False,
) -> bool: ) -> bool:
was_regenerated = False was_regenerated = False
for machine in machines: for machine in machines:
errors = [] errors = []
try: try:
was_regenerated |= generate_vars_for_machine_interactive( was_regenerated |= generate_vars_for_machine_interactive(
machine, generator_name, regenerate, no_sandbox=no_sandbox machine,
generator_name,
regenerate,
no_sandbox=no_sandbox,
fake_prompts=fake_prompts,
) )
except Exception as exc: except Exception as exc:
errors += [(machine, exc)] errors += [(machine, exc)]
@@ -504,7 +533,11 @@ def generate_command(args: argparse.Namespace) -> None:
] ]
) )
has_changed = generate_vars( has_changed = generate_vars(
machines, args.generator, args.regenerate, no_sandbox=args.no_sandbox machines,
args.generator,
args.regenerate,
no_sandbox=args.no_sandbox,
fake_prompts=args.fake_prompts,
) )
if has_changed: if has_changed:
args.flake.invalidate_cache() args.flake.invalidate_cache()
@@ -544,4 +577,11 @@ def register_generate_parser(parser: argparse.ArgumentParser) -> None:
default=False, default=False,
) )
parser.add_argument(
"--fake-prompts",
action="store_true",
help="automatically fill prompt responses for testing (unsafe)",
default=False,
)
parser.set_defaults(func=generate_command) parser.set_defaults(func=generate_command)

View File

@@ -87,6 +87,7 @@ class Options:
repo_root: Path repo_root: Path
test_dir: Path test_dir: Path
check_attr: str check_attr: str
clean: bool
def parse_args() -> Options: def parse_args() -> Options:
@@ -107,6 +108,13 @@ def parse_args() -> Options:
required=False, required=False,
default=os.environ.get("PRJ_ROOT", find_git_repo_root()), default=os.environ.get("PRJ_ROOT", find_git_repo_root()),
) )
parser.add_argument(
"--clean",
help="wipe vars and sops directories before generating new vars",
action="store_true",
default=False,
)
parser.add_argument( parser.add_argument(
"test_dir", "test_dir",
type=Path, type=Path,
@@ -125,6 +133,7 @@ def parse_args() -> Options:
repo_root=args.repo_root, repo_root=args.repo_root,
test_dir=args.test_dir, test_dir=args.test_dir,
check_attr=args.check_attr, check_attr=args.check_attr,
clean=args.clean,
) )
@@ -133,8 +142,9 @@ def main() -> None:
opts = parse_args() opts = parse_args()
test_dir = opts.test_dir test_dir = opts.test_dir
shutil.rmtree(test_dir / "vars", ignore_errors=True) if opts.clean:
shutil.rmtree(test_dir / "sops", ignore_errors=True) shutil.rmtree(test_dir / "vars", ignore_errors=True)
shutil.rmtree(test_dir / "sops", ignore_errors=True)
flake = Flake(str(opts.repo_root)) flake = Flake(str(opts.repo_root))
machine_names = get_machine_names( machine_names = get_machine_names(
@@ -162,8 +172,10 @@ def main() -> None:
{ {
"publickey": sops_pub_key, "publickey": sops_pub_key,
"type": "age", "type": "age",
} },
indent=2,
) )
+ "\n"
) )
with NamedTemporaryFile("w") as f: with NamedTemporaryFile("w") as f:
f.write("# created: 2023-07-17T10:51:45+02:00\n") f.write("# created: 2023-07-17T10:51:45+02:00\n")
@@ -171,7 +183,7 @@ def main() -> None:
f.write(sops_priv_key) f.write(sops_priv_key)
f.seek(0) f.seek(0)
os.environ["SOPS_AGE_KEY_FILE"] = f.name os.environ["SOPS_AGE_KEY_FILE"] = f.name
generate_vars(list(machines)) generate_vars(list(machines), fake_prompts=True)
if __name__ == "__main__": if __name__ == "__main__":