diff --git a/pkgs/clan-cli/clan_cli/tests/test_vars.py b/pkgs/clan-cli/clan_cli/tests/test_vars.py index fb0f19547..90ff5a772 100644 --- a/pkgs/clan-cli/clan_cli/tests/test_vars.py +++ b/pkgs/clan-cli/clan_cli/tests/test_vars.py @@ -1335,6 +1335,46 @@ def test_cache_misses_for_vars_operations( ) +@pytest.mark.with_core +def test_shared_generator_conflicting_definition_raises_error( + monkeypatch: pytest.MonkeyPatch, + flake_with_sops: ClanFlake, +) -> None: + """Test that vars generation raises an error when two machines have different + definitions for the same shared generator. + """ + flake = flake_with_sops + + # Create machine1 with a shared generator + machine1_config = flake.machines["machine1"] = create_test_machine_config() + shared_gen1 = machine1_config["clan"]["core"]["vars"]["generators"][ + "shared_generator" + ] + shared_gen1["share"] = True + shared_gen1["files"]["file1"]["secret"] = False + shared_gen1["script"] = 'echo "test" > "$out"/file1' + + # Create machine2 with the same shared generator but different files + machine2_config = flake.machines["machine2"] = create_test_machine_config() + shared_gen2 = machine2_config["clan"]["core"]["vars"]["generators"][ + "shared_generator" + ] + shared_gen2["share"] = True + shared_gen2["files"]["file2"]["secret"] = False # Different file name + shared_gen2["script"] = 'echo "test" > "$out"/file2' + + flake.refresh() + monkeypatch.chdir(flake.path) + + # Attempting to generate vars for both machines should raise an error + # because they have conflicting definitions for the same shared generator + with pytest.raises( + ClanError, + match=".*differ.*", + ): + cli.run(["vars", "generate", "--flake", str(flake.path)]) + + @pytest.mark.with_core def test_dynamic_invalidation( monkeypatch: pytest.MonkeyPatch, diff --git a/pkgs/clan-cli/clan_cli/vars/generator.py b/pkgs/clan-cli/clan_cli/vars/generator.py index c1eae5775..454584b22 100644 --- a/pkgs/clan-cli/clan_cli/vars/generator.py +++ b/pkgs/clan-cli/clan_cli/vars/generator.py @@ -144,6 +144,9 @@ class Generator: flake.precache(cls.get_machine_selectors(machine_names)) generators = [] + shared_generators_raw: dict[ + str, tuple[str, dict, dict] + ] = {} # name -> (machine_name, gen_data, files_data) for machine_name in machine_names: # Get all generator metadata in one select (safe fields only) @@ -165,6 +168,38 @@ class Generator: sec_store = machine.secret_vars_store for gen_name, gen_data in generators_data.items(): + # Check for conflicts in shared generator definitions using raw data + if gen_data["share"]: + if gen_name in shared_generators_raw: + prev_machine, prev_gen_data, prev_files_data = ( + shared_generators_raw[gen_name] + ) + # Compare raw data + prev_gen_files = prev_files_data.get(gen_name, {}) + curr_gen_files = files_data.get(gen_name, {}) + # Build list of differences with details + differences = [] + if prev_gen_files != curr_gen_files: + differences.append("files") + if prev_gen_data.get("prompts") != gen_data.get("prompts"): + differences.append("prompts") + if prev_gen_data.get("dependencies") != gen_data.get( + "dependencies" + ): + differences.append("dependencies") + if prev_gen_data.get("validationHash") != gen_data.get( + "validationHash" + ): + differences.append("validation_hash") + if differences: + msg = f"Machines {prev_machine} and {machine_name} have different definitions for shared generator '{gen_name}' (differ in: {', '.join(differences)})" + raise ClanError(msg) + else: + shared_generators_raw[gen_name] = ( + machine_name, + gen_data, + files_data, + ) # Build files from the files_data files = [] gen_files = files_data.get(gen_name, {}) @@ -216,6 +251,7 @@ class Generator: _public_store=pub_store, _secret_store=sec_store, ) + generators.append(generator) # TODO: This should be done in a non-mutable way.