Merge pull request 'Make Generator validation more dynamic' (#3052) from tangential/clan-core:dynamic-vars-generator-validation into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/3052
This commit is contained in:
lassulus
2025-03-30 07:00:43 +00:00
4 changed files with 98 additions and 5 deletions

View File

@@ -154,7 +154,9 @@ class FlakeCacheEntry:
self.value = value
def insert(
self, value: str | float | dict[str, Any] | list[Any], selectors: list[Selector]
self,
value: str | float | dict[str, Any] | list[Any] | None,
selectors: list[Selector],
) -> None:
selector: Selector
if selectors == []:
@@ -244,6 +246,12 @@ class FlakeCacheEntry:
if self.value != value:
msg = "value mismatch in cache, something is fishy"
raise TypeError(msg)
elif value is None:
if self.value is not None:
msg = "value mismatch in cache, something is fishy"
raise TypeError(msg)
else:
msg = f"Cannot insert value of type {type(value)} into cache"
raise TypeError(msg)

View File

@@ -39,7 +39,6 @@ class Generator:
name: str
files: list[Var] = field(default_factory=list)
share: bool = False
validation: str | None = None
prompts: list[Prompt] = field(default_factory=list)
dependencies: list[str] = field(default_factory=list)
@@ -62,7 +61,6 @@ class Generator:
name=data["name"],
share=data["share"],
files=[Var.from_json(data["name"], f) for f in data["files"].values()],
validation=data["validationHash"],
dependencies=data["dependencies"],
migrate_fact=data["migrateFact"],
prompts=[Prompt.from_json(p) for p in data["prompts"].values()],
@@ -76,6 +74,13 @@ class Generator:
)
return final_script
@property
def validation(self) -> str | None:
assert self._machine is not None
return self._machine.eval_nix(
f'config.clan.core.vars.generators."{self.name}".validationHash'
)
def bubblewrap_cmd(generator: str, tmpdir: Path) -> list[str]:
test_store = nix_test_store()
@@ -253,6 +258,8 @@ def execute_generator(
machine.flake_dir,
f"Update vars via generator {generator.name} for machine {machine.name}",
)
if len(files_to_commit) > 0:
machine.flush_caches()
def _ask_prompts(
@@ -456,8 +463,6 @@ def generate_vars_for_machine(
public_vars_store=machine.public_vars_store,
prompt_values=_ask_prompts(generator),
)
# flush caches to make sure the new secrets are available in evaluation
machine.flush_caches()
return True

View File

@@ -17,6 +17,14 @@ def test_select() -> None:
assert not test_cache.is_cached(["x", "z", 1])
def test_insert() -> None:
test_cache = FlakeCacheEntry({}, [])
# Inserting the same thing twice should succeed
test_cache.insert(None, ["nix"])
test_cache.insert(None, ["nix"])
assert test_cache.select(["nix"]) is None
def test_out_path() -> None:
testdict = {"x": {"y": [123, 345, 456], "z": "/nix/store/bla"}}
test_cache = FlakeCacheEntry(testdict, [])

View File

@@ -919,3 +919,75 @@ def test_invalidation(
str(machine.flake.path), machine.name, "my_generator/my_value"
).printable_value
assert value2 == value2_new
@pytest.mark.with_core
def test_dynamic_invalidation(
monkeypatch: pytest.MonkeyPatch,
flake: ClanFlake,
) -> None:
gen_prefix = "config.clan.core.vars.generators"
machine = Machine(name="my_machine", flake=Flake(str(flake.path)))
config = flake.machines[machine.name]
config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"]
my_generator["files"]["my_value"]["secret"] = False
my_generator["script"] = "echo -n $RANDOM > $out/my_value"
dependent_generator = config["clan"]["core"]["vars"]["generators"][
"dependent_generator"
]
dependent_generator["files"]["my_value"]["secret"] = False
dependent_generator["dependencies"] = ["my_generator"]
dependent_generator["script"] = "echo -n $RANDOM > $out/my_value"
flake.refresh()
# this is an abuse
custom_nix = flake.path / "machines" / machine.name / "hardware-configuration.nix"
custom_nix.write_text("""
{ config, ... }: let
p = config.clan.core.vars.generators.my_generator.files.my_value.path;
in {
clan.core.vars.generators.dependent_generator.validation = if builtins.pathExists p then builtins.readFile p else null;
}
""")
flake.refresh()
machine.flush_caches()
monkeypatch.chdir(flake.path)
# before generating, dependent generator validation should be empty; see bogus hardware-configuration.nix above
# we have to avoid `*.files.value` in this initial select because the generators haven't been run yet
generators_0 = machine.eval_nix(f"{gen_prefix}.*.{{validationHash}}")
assert generators_0["dependent_generator"]["validationHash"] is None
# generate both my_generator and (the dependent) dependent_generator
cli.run(["vars", "generate", "--flake", str(flake.path), machine.name])
machine.flush_caches()
# after generating once, dependent generator validation should be set
generators_1 = machine.eval_nix(gen_prefix)
assert generators_1["dependent_generator"]["validationHash"] is not None
# after generating once, neither generator should want to run again because `clan vars generate` should have re-evaluated the dependent generator's validationHash after executing the parent generator but before executing the dependent generator
# this ensures that validation can depend on parent generators while still only requiring a single pass
cli.run(["vars", "generate", "--flake", str(flake.path), machine.name])
machine.flush_caches()
generators_2 = machine.eval_nix(gen_prefix)
assert (
generators_1["dependent_generator"]["validationHash"]
== generators_2["dependent_generator"]["validationHash"]
)
assert (
generators_1["my_generator"]["files"]["my_value"]["value"]
== generators_2["my_generator"]["files"]["my_value"]["value"]
)
assert (
generators_1["dependent_generator"]["files"]["my_value"]["value"]
== generators_2["dependent_generator"]["files"]["my_value"]["value"]
)