diff --git a/pkgs/clan-cli/clan_cli/tests/test_vars.py b/pkgs/clan-cli/clan_cli/tests/test_vars.py index cc26b6253..9f0e80eb5 100644 --- a/pkgs/clan-cli/clan_cli/tests/test_vars.py +++ b/pkgs/clan-cli/clan_cli/tests/test_vars.py @@ -766,6 +766,28 @@ def test_prompt( 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 def test_shared_vars_must_never_depend_on_machine_specific_vars( monkeypatch: pytest.MonkeyPatch, diff --git a/pkgs/clan-cli/clan_cli/vars/generator.py b/pkgs/clan-cli/clan_cli/vars/generator.py index acb4e687c..d2c9d5e38 100644 --- a/pkgs/clan-cli/clan_cli/vars/generator.py +++ b/pkgs/clan-cli/clan_cli/vars/generator.py @@ -66,6 +66,41 @@ class Generator: _public_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 def key(self) -> GeneratorKey: if self.share: @@ -240,15 +275,12 @@ class Generator: name=gen_name, share=share, files=files, - dependencies=[ - GeneratorKey( - machine=None - if generators_data[dep]["share"] - else machine_name, - name=dep, - ) - for dep in gen_data["dependencies"] - ], + dependencies=cls.validate_dependencies( + gen_name, + machine_name, + gen_data["dependencies"], + generators_data, + ), migrate_fact=gen_data.get("migrateFact"), validation_hash=gen_data.get("validationHash"), prompts=prompts,