vars: fix regenerating a specific generator

This was broken after re-designing the API -> added a test
This commit is contained in:
DavHau
2025-08-20 14:32:13 +07:00
parent 6996a6340a
commit de0b1b2d70
9 changed files with 88 additions and 30 deletions

View File

@@ -550,7 +550,13 @@ const InstallSummary = () => {
return;
}
// Extract generator names from prompt values
// TODO: This is wrong. We need to extend run_generators to be able to compute
// a sane closure over a list of provided generators.
const generators = Object.keys(store.install.promptValues || {});
const runGenerators = client.fetch("run_generators", {
generators: generators.length > 0 ? generators : undefined,
prompt_values: store.install.promptValues,
machines: [
{

View File

@@ -119,6 +119,28 @@ def test_generate_public_and_secret_vars(
monkeypatch: pytest.MonkeyPatch,
flake_with_sops: ClanFlake,
) -> None:
"""Test generation of public and secret vars with dependencies.
Generator dependency graph:
my_generator (standalone)
├── my_value (public)
├── my_secret (secret)
└── value_with_default (public, has default)
my_shared_generator (shared=True)
└── my_shared_value (public)
dependent_generator (depends on my_shared_generator)
└── my_secret (secret, copies from my_shared_value)
This test verifies:
- Public and secret vars are stored correctly
- Shared generators work across dependencies
- Default values are handled properly
- Regeneration with --regenerate updates all values
- Regeneration with --regenerate --generator only updates specified generator
"""
flake = flake_with_sops
config = flake.machines["my_machine"]
@@ -126,14 +148,13 @@ def test_generate_public_and_secret_vars(
my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"]
my_generator["files"]["my_value"]["secret"] = False
my_generator["files"]["my_secret"]["secret"] = True
my_generator["script"] = (
'echo -n public$RANDOM > "$out"/my_value; echo -n secret$RANDOM > "$out"/my_secret; echo -n non-default$RANDOM > "$out"/value_with_default'
)
my_generator["files"]["value_with_default"]["secret"] = False
my_generator["files"]["value_with_default"]["value"]["_type"] = "override"
my_generator["files"]["value_with_default"]["value"]["priority"] = 1000 # mkDefault
my_generator["files"]["value_with_default"]["value"]["content"] = "default_value"
my_generator["script"] = (
'echo -n public$RANDOM > "$out"/my_value; echo -n secret$RANDOM > "$out"/my_secret; echo -n non-default$RANDOM > "$out"/value_with_default'
)
my_shared_generator = config["clan"]["core"]["vars"]["generators"][
"my_shared_generator"
@@ -257,6 +278,44 @@ def test_generate_public_and_secret_vars(
assert shared_value != shared_value_new, (
"Shared value should change after regeneration"
)
# test that after regenerating a shared generator, it and its dependents are regenerated
cli.run(
[
"vars",
"generate",
"--flake",
str(flake.path),
"my_machine",
"--regenerate",
"--no-sandbox",
"--generator",
"my_shared_generator",
]
)
# test that the shared generator is regenerated
shared_value_after_regeneration = get_machine_var(
machine, "my_shared_generator/my_shared_value"
).printable_value
assert shared_value_after_regeneration != shared_value_new, (
"Shared value should change after regenerating my_shared_generator"
)
# test that the dependent generator is also regenerated (because it depends on my_shared_generator)
secret_value_after_regeneration = sops_store.get(
dependent_generator, "my_secret"
).decode()
assert secret_value_after_regeneration != secret_value_new, (
"Dependent generator's secret should change after regenerating my_shared_generator"
)
assert secret_value_after_regeneration == shared_value_after_regeneration, (
"Dependent generator's secret should match the new shared value"
)
# test that my_generator is NOT regenerated (it doesn't depend on my_shared_generator)
public_value_after_regeneration = get_machine_var(
machine, "my_generator/my_value"
).printable_value
assert public_value_after_regeneration == public_value_new, (
"my_generator value should NOT change after regenerating only my_shared_generator"
)
# TODO: it doesn't actually test if the group has access

View File

@@ -37,11 +37,8 @@ def generate_command(args: argparse.Namespace) -> None:
run_generators(
machines,
generators=args.generator
if args.generator
else "all"
if args.regenerate
else "minimal",
generators=args.generator,
full_closure=args.regenerate if args.regenerate is not None else False,
no_sandbox=args.no_sandbox,
)

View File

@@ -383,7 +383,7 @@ def run_command(
machine_obj: Machine = Machine(args.machine, args.flake)
generate_facts([machine_obj])
run_generators([machine_obj])
run_generators([machine_obj], generators=None, full_closure=False)
vm: VmConfig = inspect_vm(machine=machine_obj)

View File

@@ -78,7 +78,7 @@ def run_machine_flash(
system_config_nix: dict[str, Any] = {}
generate_facts([machine])
run_generators([machine])
run_generators([machine], generators=None, full_closure=False)
if system_config.language:
if system_config.language not in list_languages():

View File

@@ -88,7 +88,7 @@ def run_machine_install(opts: InstallOptions, target_host: Remote) -> None:
# Notify the UI about what we are doing
notify_install_step("generators")
generate_facts([machine])
run_generators([machine])
run_generators([machine], generators=None, full_closure=False)
with (
TemporaryDirectory(prefix="nixos-install-") as _base_directory,

View File

@@ -78,7 +78,7 @@ def morph_machine(
machine = Machine(name=name, flake=Flake(str(flakedir)))
run_generators([machine], generators="minimal")
run_generators([machine], generators=None, full_closure=False)
machine.secret_vars_store.populate_dir(
machine.name,

View File

@@ -148,7 +148,7 @@ def run_machine_update(
target_host_root = stack.enter_context(_target_host.become_root())
generate_facts([machine], service=None, regenerate=False)
run_generators([machine], generators="minimal")
run_generators([machine], generators=None, full_closure=False)
# Upload secrets to the target host using root
upload_secrets(machine, target_host_root)

View File

@@ -97,7 +97,8 @@ strategies (e.g., interactive CLI, GUI, or programmatic).
@API.register
def run_generators(
machines: list[Machine],
generators: str | list[str] = "minimal",
generators: str | list[str] | None = None,
full_closure: bool = False,
prompt_values: dict[str, dict[str, str]] | PromptFunc = lambda g: g.ask_prompts(),
no_sandbox: bool = False,
) -> None:
@@ -105,12 +106,13 @@ def run_generators(
Args:
machines: The machines to run generators for.
generators: Can be:
- str: Single generator name to run (ensuring dependencies are met)
- None: Run all generators (with closure based on full_closure parameter)
- str: Single generator name to run (with closure based on full_closure parameter)
- list[str]: Specific generator names to run exactly as provided.
Dependency generators are not added automatically in this case.
The caller must ensure that all dependencies are included.
- "all": Run all generators (full closure)
- "minimal": Run only missing generators (minimal closure) (default)
full_closure: Whether to include all dependencies (True) or only missing ones (False).
Only used when generators is None or a string.
prompt_values: A dictionary mapping generator names to their prompt values,
or a function that returns prompt values for a generator.
no_sandbox: Whether to disable sandboxing when executing the generator.
@@ -119,16 +121,8 @@ def run_generators(
executing the generator.
"""
for machine in machines:
if generators == "all":
generator_objects = get_generators(machine, full_closure=True)
elif generators == "minimal":
generator_objects = get_generators(machine, full_closure=False)
elif isinstance(generators, str) and generators not in ["all", "minimal"]:
# Single generator name - compute minimal closure for it
generator_objects = get_generators(
machine, full_closure=False, generator_name=generators
)
elif isinstance(generators, list):
if isinstance(generators, list):
# List of generator names - use them exactly as provided
if len(generators) == 0:
return
# Create GeneratorKeys for this specific machine
@@ -138,8 +132,10 @@ def run_generators(
all_generators = get_generators(machine, full_closure=True)
generator_objects = [g for g in all_generators if g.key in generator_keys]
else:
msg = f"Invalid generators argument: {generators}. Must be 'all', 'minimal', a generator name, or a list of generator names"
raise ValueError(msg)
# None or single string - use get_generators with closure parameter
generator_objects = get_generators(
machine, full_closure=full_closure, generator_name=generators
)
# If prompt function provided, ask all prompts
# TODO: make this more lazy and ask for every generator on execution