From c2a3f5e4980fc43cf2f5dcaedb7943d952d561e2 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Mon, 1 Sep 2025 16:47:47 +0200 Subject: [PATCH] api/machines: populate instance_refs --- pkgs/clan-cli/clan_lib/machines/actions.py | 45 ++++++++++++++++--- .../clan_lib/machines/actions_test.py | 27 +++++++++++ 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/pkgs/clan-cli/clan_lib/machines/actions.py b/pkgs/clan-cli/clan_lib/machines/actions.py index fc6f9a373..5b466c494 100644 --- a/pkgs/clan-cli/clan_lib/machines/actions.py +++ b/pkgs/clan-cli/clan_lib/machines/actions.py @@ -7,6 +7,7 @@ from clan_lib.errors import ClanError from clan_lib.flake.flake import Flake from clan_lib.machines.machines import Machine from clan_lib.nix_models.clan import ( + InventoryInstance, InventoryMachine, ) from clan_lib.persist.inventory_store import InventoryStore @@ -45,7 +46,25 @@ class MachineState(TypedDict): class MachineResponse: data: InventoryMachine # Reference the installed service instances - instance_refs: list[str] = field(default_factory=list) + instance_refs: set[str] = field(default_factory=set) + + +def machine_instances( + machine_name: str, + instances: dict[str, InventoryInstance], + tag_map: dict[str, set[str]], +) -> set[str]: + res: set[str] = set() + for instance_name, instance in instances.items(): + for role in instance.get("roles", {}).values(): + if machine_name in role.get("machines", {}): + res.add(instance_name) + + for tag in role.get("tags", {}): + if tag in tag_map and machine_name in tag_map[tag]: + res.add(instance_name) + + return res @API.register @@ -59,14 +78,30 @@ def list_machines( raw_machines = inventory.get("machines", {}) + tag_map: dict[str, set[str]] = {} + + for machine_name, machine in raw_machines.items(): + for tag in machine.get("tags", []): + if tag not in tag_map: + tag_map[tag] = set() + tag_map[tag].add(machine_name) + + instances = inventory.get("instances", {}) + res: dict[str, MachineResponse] = {} for machine_name, machine in raw_machines.items(): + m = MachineResponse( + data=InventoryMachine(**machine), + instance_refs=machine_instances(machine_name, instances, tag_map), + ) + + # Check filters if opts and opts.filter.tags is not None: machine_tags = machine.get("tags", []) - if all(ft in machine_tags for ft in opts.filter.tags): - res[machine_name] = MachineResponse(data=InventoryMachine(**machine)) - else: - res[machine_name] = MachineResponse(data=InventoryMachine(**machine)) + if not all(ft in machine_tags for ft in opts.filter.tags): + continue + + res[machine_name] = m return res diff --git a/pkgs/clan-cli/clan_lib/machines/actions_test.py b/pkgs/clan-cli/clan_lib/machines/actions_test.py index bea24e0ed..d3dd044b9 100644 --- a/pkgs/clan-cli/clan_lib/machines/actions_test.py +++ b/pkgs/clan-cli/clan_lib/machines/actions_test.py @@ -67,6 +67,33 @@ def test_list_inventory_machines(clan_flake: Callable[..., Flake]) -> None: assert list(machines.keys()) == ["jon", "sara", "vanessa"] +@pytest.mark.with_core +def test_list_machines_instance_refs(clan_flake: Callable[..., Flake]) -> None: + flake = clan_flake( + { + "inventory": { + "machines": { + "jon": {}, + "sara": {}, + }, + "instances": { + "admin": { + "roles": {"default": {"machines": {"jon": {}}}}, + }, + "borgbackup": { + "roles": {"default": {"tags": {"all": {}}}}, + }, + }, + }, + }, + ) + + machines = list_machines(flake) + + assert machines["sara"].instance_refs == set({"borgbackup"}) + assert machines["jon"].instance_refs == set({"admin", "borgbackup"}) + + @pytest.mark.with_core def test_set_machine_no_op(clan_flake: Callable[..., Flake]) -> None: flake = clan_flake(