pkgs/cli/vars: Add dependency validation

Add explicit dependency validation to vars, so that proper error
messages can be surfaced to the user.

Instead of:
```
Traceback (most recent call last):
  File "/home/lhebendanz/Projects/clan-core/pkgs/clan-cli/clan_lib/async_run/__init__.py", line 154, in run
    self.result = AsyncResult(_result=self.function(*self.args, **self.kwargs))
                                      ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lhebendanz/Projects/clan-core/pkgs/clan-cli/clan_cli/machines/update.py", line 62, in run_update_wit
h_network
    run_machine_update(
    ~~~~~~~~~~~~~~~~~~^
        machine=machine,
        ^^^^^^^^^^^^^^^^
    ...<2 lines>...
        upload_inputs=upload_inputs,
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/home/lhebendanz/Projects/clan-core/pkgs/clan-cli/clan_lib/machines/update.py", line 158, in run_machine_u
pdate
    run_generators([machine], generators=None, full_closure=False)
    ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lhebendanz/Projects/clan-core/pkgs/clan-cli/clan_lib/vars/generate.py", line 156, in run_generators
    all_generators = get_generators(machines, full_closure=True)
  File "/home/lhebendanz/Projects/clan-core/pkgs/clan-cli/clan_lib/vars/generate.py", line 50, in get_generators
    all_generators_list = Generator.get_machine_generators(
        all_machines,
        flake,
        include_previous_values=include_previous_values,
    )
  File "/home/lhebendanz/Projects/clan-core/pkgs/clan-cli/clan_cli/vars/generator.py", line 246, in get_machine_ge
nerators
    if generators_data[dep]["share"]
       ~~~~~~~~~~~~~~~^^^^^
KeyError: 'bla'
```

We now get:
```
$> Generator 'my_generator' on machine 'my_machine' depends on generator 'non_existing_generator', but 'non_existing_generator' does not exist
```

Closes: #5698
This commit is contained in:
a-kenji
2025-10-30 12:47:15 +01:00
parent 1d228231f2
commit 83f78d9f59
2 changed files with 63 additions and 9 deletions

View File

@@ -766,6 +766,28 @@ def test_prompt(
assert sops_store.get(my_generator, "prompt_persist").decode() == "prompt_persist" assert sops_store.get(my_generator, "prompt_persist").decode() == "prompt_persist"
@pytest.mark.with_core
def test_non_existing_dependency_raises_error(
monkeypatch: pytest.MonkeyPatch,
flake_with_sops: ClanFlake,
) -> None:
"""Ensure that a generator with a non-existing dependency raises a clear error."""
flake = flake_with_sops
config = flake.machines["my_machine"] = create_test_machine_config()
my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"]
my_generator["files"]["my_value"]["secret"] = False
my_generator["script"] = 'echo "$RANDOM" > "$out"/my_value'
my_generator["dependencies"] = ["non_existing_generator"]
flake.refresh()
monkeypatch.chdir(flake.path)
with pytest.raises(
ClanError,
match="Generator 'my_generator' on machine 'my_machine' depends on generator 'non_existing_generator', but 'non_existing_generator' does not exist",
):
cli.run(["vars", "generate", "--flake", str(flake.path), "my_machine"])
@pytest.mark.with_core @pytest.mark.with_core
def test_shared_vars_must_never_depend_on_machine_specific_vars( def test_shared_vars_must_never_depend_on_machine_specific_vars(
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,

View File

@@ -66,6 +66,41 @@ class Generator:
_public_store: "StoreBase | None" = None _public_store: "StoreBase | None" = None
_secret_store: "StoreBase | None" = None _secret_store: "StoreBase | None" = None
@staticmethod
def validate_dependencies(
generator_name: str,
machine_name: str,
dependencies: list[str],
generators_data: dict[str, dict],
) -> list[GeneratorKey]:
"""Validate and build dependency keys for a generator.
Args:
generator_name: Name of the generator that has dependencies
machine_name: Name of the machine the generator belongs to
dependencies: List of dependency generator names
generators_data: Dictionary of all available generators for this machine
Returns:
List of GeneratorKey objects
Raises:
ClanError: If a dependency does not exist
"""
deps_list = []
for dep in dependencies:
if dep not in generators_data:
msg = f"Generator '{generator_name}' on machine '{machine_name}' depends on generator '{dep}', but '{dep}' does not exist. Please check your configuration."
raise ClanError(msg)
deps_list.append(
GeneratorKey(
machine=None if generators_data[dep]["share"] else machine_name,
name=dep,
)
)
return deps_list
@property @property
def key(self) -> GeneratorKey: def key(self) -> GeneratorKey:
if self.share: if self.share:
@@ -240,15 +275,12 @@ class Generator:
name=gen_name, name=gen_name,
share=share, share=share,
files=files, files=files,
dependencies=[ dependencies=cls.validate_dependencies(
GeneratorKey( gen_name,
machine=None machine_name,
if generators_data[dep]["share"] gen_data["dependencies"],
else machine_name, generators_data,
name=dep, ),
)
for dep in gen_data["dependencies"]
],
migrate_fact=gen_data.get("migrateFact"), migrate_fact=gen_data.get("migrateFact"),
validation_hash=gen_data.get("validationHash"), validation_hash=gen_data.get("validationHash"),
prompts=prompts, prompts=prompts,