vars: fix regenerating a specific generator
This was broken after re-designing the API -> added a test
This commit is contained in:
@@ -550,7 +550,13 @@ const InstallSummary = () => {
|
|||||||
return;
|
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", {
|
const runGenerators = client.fetch("run_generators", {
|
||||||
|
generators: generators.length > 0 ? generators : undefined,
|
||||||
prompt_values: store.install.promptValues,
|
prompt_values: store.install.promptValues,
|
||||||
machines: [
|
machines: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -119,6 +119,28 @@ def test_generate_public_and_secret_vars(
|
|||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
flake_with_sops: ClanFlake,
|
flake_with_sops: ClanFlake,
|
||||||
) -> None:
|
) -> 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
|
flake = flake_with_sops
|
||||||
|
|
||||||
config = flake.machines["my_machine"]
|
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 = config["clan"]["core"]["vars"]["generators"]["my_generator"]
|
||||||
my_generator["files"]["my_value"]["secret"] = False
|
my_generator["files"]["my_value"]["secret"] = False
|
||||||
my_generator["files"]["my_secret"]["secret"] = True
|
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"]["secret"] = False
|
||||||
my_generator["files"]["value_with_default"]["value"]["_type"] = "override"
|
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"]["priority"] = 1000 # mkDefault
|
||||||
my_generator["files"]["value_with_default"]["value"]["content"] = "default_value"
|
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 = config["clan"]["core"]["vars"]["generators"][
|
||||||
"my_shared_generator"
|
"my_shared_generator"
|
||||||
@@ -257,6 +278,44 @@ def test_generate_public_and_secret_vars(
|
|||||||
assert shared_value != shared_value_new, (
|
assert shared_value != shared_value_new, (
|
||||||
"Shared value should change after regeneration"
|
"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
|
# TODO: it doesn't actually test if the group has access
|
||||||
|
|||||||
@@ -37,11 +37,8 @@ def generate_command(args: argparse.Namespace) -> None:
|
|||||||
|
|
||||||
run_generators(
|
run_generators(
|
||||||
machines,
|
machines,
|
||||||
generators=args.generator
|
generators=args.generator,
|
||||||
if args.generator
|
full_closure=args.regenerate if args.regenerate is not None else False,
|
||||||
else "all"
|
|
||||||
if args.regenerate
|
|
||||||
else "minimal",
|
|
||||||
no_sandbox=args.no_sandbox,
|
no_sandbox=args.no_sandbox,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -383,7 +383,7 @@ def run_command(
|
|||||||
machine_obj: Machine = Machine(args.machine, args.flake)
|
machine_obj: Machine = Machine(args.machine, args.flake)
|
||||||
|
|
||||||
generate_facts([machine_obj])
|
generate_facts([machine_obj])
|
||||||
run_generators([machine_obj])
|
run_generators([machine_obj], generators=None, full_closure=False)
|
||||||
|
|
||||||
vm: VmConfig = inspect_vm(machine=machine_obj)
|
vm: VmConfig = inspect_vm(machine=machine_obj)
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ def run_machine_flash(
|
|||||||
system_config_nix: dict[str, Any] = {}
|
system_config_nix: dict[str, Any] = {}
|
||||||
|
|
||||||
generate_facts([machine])
|
generate_facts([machine])
|
||||||
run_generators([machine])
|
run_generators([machine], generators=None, full_closure=False)
|
||||||
|
|
||||||
if system_config.language:
|
if system_config.language:
|
||||||
if system_config.language not in list_languages():
|
if system_config.language not in list_languages():
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ def run_machine_install(opts: InstallOptions, target_host: Remote) -> None:
|
|||||||
# Notify the UI about what we are doing
|
# Notify the UI about what we are doing
|
||||||
notify_install_step("generators")
|
notify_install_step("generators")
|
||||||
generate_facts([machine])
|
generate_facts([machine])
|
||||||
run_generators([machine])
|
run_generators([machine], generators=None, full_closure=False)
|
||||||
|
|
||||||
with (
|
with (
|
||||||
TemporaryDirectory(prefix="nixos-install-") as _base_directory,
|
TemporaryDirectory(prefix="nixos-install-") as _base_directory,
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ def morph_machine(
|
|||||||
|
|
||||||
machine = Machine(name=name, flake=Flake(str(flakedir)))
|
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.secret_vars_store.populate_dir(
|
||||||
machine.name,
|
machine.name,
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ def run_machine_update(
|
|||||||
target_host_root = stack.enter_context(_target_host.become_root())
|
target_host_root = stack.enter_context(_target_host.become_root())
|
||||||
|
|
||||||
generate_facts([machine], service=None, regenerate=False)
|
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 to the target host using root
|
||||||
upload_secrets(machine, target_host_root)
|
upload_secrets(machine, target_host_root)
|
||||||
|
|||||||
@@ -97,7 +97,8 @@ strategies (e.g., interactive CLI, GUI, or programmatic).
|
|||||||
@API.register
|
@API.register
|
||||||
def run_generators(
|
def run_generators(
|
||||||
machines: list[Machine],
|
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(),
|
prompt_values: dict[str, dict[str, str]] | PromptFunc = lambda g: g.ask_prompts(),
|
||||||
no_sandbox: bool = False,
|
no_sandbox: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -105,12 +106,13 @@ def run_generators(
|
|||||||
Args:
|
Args:
|
||||||
machines: The machines to run generators for.
|
machines: The machines to run generators for.
|
||||||
generators: Can be:
|
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.
|
- list[str]: Specific generator names to run exactly as provided.
|
||||||
Dependency generators are not added automatically in this case.
|
Dependency generators are not added automatically in this case.
|
||||||
The caller must ensure that all dependencies are included.
|
The caller must ensure that all dependencies are included.
|
||||||
- "all": Run all generators (full closure)
|
full_closure: Whether to include all dependencies (True) or only missing ones (False).
|
||||||
- "minimal": Run only missing generators (minimal closure) (default)
|
Only used when generators is None or a string.
|
||||||
prompt_values: A dictionary mapping generator names to their prompt values,
|
prompt_values: A dictionary mapping generator names to their prompt values,
|
||||||
or a function that returns prompt values for a generator.
|
or a function that returns prompt values for a generator.
|
||||||
no_sandbox: Whether to disable sandboxing when executing the generator.
|
no_sandbox: Whether to disable sandboxing when executing the generator.
|
||||||
@@ -119,16 +121,8 @@ def run_generators(
|
|||||||
executing the generator.
|
executing the generator.
|
||||||
"""
|
"""
|
||||||
for machine in machines:
|
for machine in machines:
|
||||||
if generators == "all":
|
if isinstance(generators, list):
|
||||||
generator_objects = get_generators(machine, full_closure=True)
|
# List of generator names - use them exactly as provided
|
||||||
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 len(generators) == 0:
|
if len(generators) == 0:
|
||||||
return
|
return
|
||||||
# Create GeneratorKeys for this specific machine
|
# Create GeneratorKeys for this specific machine
|
||||||
@@ -138,8 +132,10 @@ def run_generators(
|
|||||||
all_generators = get_generators(machine, full_closure=True)
|
all_generators = get_generators(machine, full_closure=True)
|
||||||
generator_objects = [g for g in all_generators if g.key in generator_keys]
|
generator_objects = [g for g in all_generators if g.key in generator_keys]
|
||||||
else:
|
else:
|
||||||
msg = f"Invalid generators argument: {generators}. Must be 'all', 'minimal', a generator name, or a list of generator names"
|
# None or single string - use get_generators with closure parameter
|
||||||
raise ValueError(msg)
|
generator_objects = get_generators(
|
||||||
|
machine, full_closure=full_closure, generator_name=generators
|
||||||
|
)
|
||||||
|
|
||||||
# If prompt function provided, ask all prompts
|
# If prompt function provided, ask all prompts
|
||||||
# TODO: make this more lazy and ask for every generator on execution
|
# TODO: make this more lazy and ask for every generator on execution
|
||||||
|
|||||||
Reference in New Issue
Block a user