diff --git a/pkgs/clan-app/ui/src/components/Sidebar/SidebarBody.tsx b/pkgs/clan-app/ui/src/components/Sidebar/SidebarBody.tsx
index f682675ee..9ad81d549 100644
--- a/pkgs/clan-app/ui/src/components/Sidebar/SidebarBody.tsx
+++ b/pkgs/clan-app/ui/src/components/Sidebar/SidebarBody.tsx
@@ -136,7 +136,7 @@ export const SidebarBody = (props: SidebarProps) => {
)}
diff --git a/pkgs/clan-app/ui/src/workflows/AddMachine/AddMachine.stories.tsx b/pkgs/clan-app/ui/src/workflows/AddMachine/AddMachine.stories.tsx
index f1942c717..fef1ebec6 100644
--- a/pkgs/clan-app/ui/src/workflows/AddMachine/AddMachine.stories.tsx
+++ b/pkgs/clan-app/ui/src/workflows/AddMachine/AddMachine.stories.tsx
@@ -26,13 +26,19 @@ const mockFetcher: Fetcher = (
const resultData: Partial = {
list_machines: {
pandora: {
- name: "pandora",
+ data: {
+ name: "pandora",
+ },
},
enceladus: {
- name: "enceladus",
+ data: {
+ name: "enceladus",
+ },
},
dione: {
- name: "dione",
+ data: {
+ name: "dione",
+ },
},
},
};
diff --git a/pkgs/clan-app/ui/src/workflows/Service/Service.stories.tsx b/pkgs/clan-app/ui/src/workflows/Service/Service.stories.tsx
index 92359c054..663a158c8 100644
--- a/pkgs/clan-app/ui/src/workflows/Service/Service.stories.tsx
+++ b/pkgs/clan-app/ui/src/workflows/Service/Service.stories.tsx
@@ -62,20 +62,28 @@ const mockFetcher: Fetcher = (
},
list_machines: {
jon: {
- name: "jon",
- tags: ["all", "nixos", "tag1"],
+ data: {
+ name: "jon",
+ tags: ["all", "nixos", "tag1"],
+ },
},
sara: {
- name: "sara",
- tags: ["all", "darwin", "tag2"],
+ data: {
+ name: "sara",
+ tags: ["all", "darwin", "tag2"],
+ },
},
kyra: {
- name: "kyra",
- tags: ["all", "darwin", "tag2"],
+ data: {
+ name: "kyra",
+ tags: ["all", "darwin", "tag2"],
+ },
},
leila: {
- name: "leila",
- tags: ["all", "darwin", "tag2"],
+ data: {
+ name: "leila",
+ tags: ["all", "darwin", "tag2"],
+ },
},
},
list_tags: {
diff --git a/pkgs/clan-app/ui/src/workflows/Service/Service.tsx b/pkgs/clan-app/ui/src/workflows/Service/Service.tsx
index 59f913b9e..d7f04bb96 100644
--- a/pkgs/clan-app/ui/src/workflows/Service/Service.tsx
+++ b/pkgs/clan-app/ui/src/workflows/Service/Service.tsx
@@ -120,7 +120,7 @@ const SelectService = () => {
label: t,
type: "tag" as const,
members: Object.entries(machinesQuery.data || {})
- .filter(([_, m]) => m.tags?.includes(t))
+ .filter(([_, m]) => m.data.tags?.includes(t))
.map(([k]) => k),
};
});
@@ -206,7 +206,7 @@ const useOptions = (tagsQuery: TagsQuery, machinesQuery: MachinesQuery) =>
label: tag,
value: "t_" + tag,
members: Object.entries(machines)
- .filter(([_, v]) => v.tags?.includes(tag))
+ .filter(([_, v]) => v.data.tags?.includes(tag))
.map(([k]) => k),
}));
diff --git a/pkgs/clan-cli/clan_cli/machines/update.py b/pkgs/clan-cli/clan_cli/machines/update.py
index eb274e3e6..57a4cb889 100644
--- a/pkgs/clan-cli/clan_cli/machines/update.py
+++ b/pkgs/clan-cli/clan_cli/machines/update.py
@@ -103,7 +103,9 @@ def get_machines_for_update(
machines_to_update = list(
filter(
requires_explicit_update,
- instantiate_inventory_to_machines(flake, machines_with_tags).values(),
+ instantiate_inventory_to_machines(
+ flake, {name: m.data for name, m in machines_with_tags.items()}
+ ).values(),
),
)
# all machines that are in the clan but not included in the update list
@@ -128,13 +130,13 @@ def get_machines_for_update(
machines_to_update = []
valid_names = validate_machine_names(explicit_names, flake)
for name in valid_names:
- inventory_machine = machines_with_tags.get(name)
- if not inventory_machine:
+ machine = machines_with_tags.get(name)
+ if not machine:
msg = "This is an internal bug"
raise ClanError(msg)
machines_to_update.append(
- Machine.from_inventory(name, flake, inventory_machine),
+ Machine.from_inventory(name, flake, machine.data),
)
return machines_to_update
diff --git a/pkgs/clan-cli/clan_lib/machines/actions.py b/pkgs/clan-cli/clan_lib/machines/actions.py
index fd7212b5f..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
@@ -41,28 +42,68 @@ class MachineState(TypedDict):
# add more info later when retrieving remote state
+@dataclass
+class MachineResponse:
+ data: InventoryMachine
+ # Reference the installed service instances
+ 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
def list_machines(
flake: Flake,
opts: ListOptions | None = None,
-) -> dict[str, InventoryMachine]:
+) -> dict[str, MachineResponse]:
"""List machines of a clan"""
inventory_store = InventoryStore(flake=flake)
inventory = inventory_store.read()
- machines = inventory.get("machines", {})
+ raw_machines = inventory.get("machines", {})
- if opts and opts.filter.tags is not None:
- filtered_machines = {}
+ tag_map: dict[str, set[str]] = {}
- for machine_name, machine in machines.items():
+ 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):
- filtered_machines[machine_name] = machine
+ if not all(ft in machine_tags for ft in opts.filter.tags):
+ continue
- return filtered_machines
+ res[machine_name] = m
- return machines
+ return res
@API.register
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(
diff --git a/pkgs/clan-cli/clan_lib/machines/list.py b/pkgs/clan-cli/clan_lib/machines/list.py
index b80ddbd4a..17ee2f412 100644
--- a/pkgs/clan-cli/clan_lib/machines/list.py
+++ b/pkgs/clan-cli/clan_lib/machines/list.py
@@ -30,7 +30,9 @@ def list_full_machines(flake: Flake) -> dict[str, Machine]:
"""Like `list_machines`, but returns a full 'machine' instance for each machine."""
machines = list_machines(flake)
- return instantiate_inventory_to_machines(flake, machines)
+ return instantiate_inventory_to_machines(
+ flake, {name: m.data for name, m in machines.items()}
+ )
@dataclass