Merge pull request 'feat(api): add list_inventory_tags' (#4692) from feat/machine-tags-writeability into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4692
This commit is contained in:
hsjobeki
2025-08-12 11:33:49 +00:00
6 changed files with 141 additions and 12 deletions

View File

@@ -1,12 +0,0 @@
"""
DEPRECATED:
Don't use this module anymore
Instead use:
'clan_lib.persist.inventoryStore'
Which is an abstraction over the inventory
Interacting with 'clan_lib.inventory' is NOT recommended and will be removed
"""

View File

@@ -12,6 +12,7 @@ from clan_lib.nix_models.clan import (
InventoryMachinesType,
InventoryMetaType,
InventoryServicesType,
InventoryTagsType,
)
from .util import (
@@ -106,6 +107,7 @@ class InventorySnapshot(TypedDict):
instances: NotRequired[InventoryInstancesType]
meta: NotRequired[InventoryMetaType]
services: NotRequired[InventoryServicesType]
tags: NotRequired[InventoryTagsType]
class InventoryStore:

View File

View File

@@ -0,0 +1,54 @@
from dataclasses import dataclass
from typing import Any
from clan_lib.api import API
from clan_lib.flake import Flake
from clan_lib.persist.inventory_store import InventoryStore
@dataclass
class TagList:
options: set[str]
special: set[str]
@API.register
def list_tags(flake: Flake) -> TagList:
"""
List all tags of a clan.
Returns:
- 'options' - Existing Tags that can be added to machines
- 'special' - Prefined Tags that are special and cannot be added to machines, they can be used in roles and refer to a fixed set of machines.
"""
inventory_store = InventoryStore(flake=flake)
inventory = inventory_store.read()
machines = inventory.get("machines", {})
tags: set[str] = set()
for machine in machines.values():
machine_tags = machine.get("tags", [])
for tag in machine_tags:
tags.add(tag)
instances = inventory.get("instances", {})
for instance in instances.values():
roles: dict[str, Any] = instance.get("roles", {})
for role in roles.values():
role_tags = role.get("tags", {})
for tag in role_tags:
tags.add(tag)
global_tags = inventory.get("tags", {})
for tag in global_tags:
if tag not in tags:
continue
tags.remove(tag)
return TagList(options=tags, special=set(global_tags.keys()))

View File

@@ -0,0 +1,84 @@
from collections.abc import Callable
import pytest
from clan_lib.flake import Flake
from clan_lib.persist.inventory_store import InventoryStore
from clan_lib.persist.util import get_value_by_path, set_value_by_path
from clan_lib.tags.list import list_tags
@pytest.mark.with_core
def test_list_inventory_tags(clan_flake: Callable[..., Flake]) -> None:
flake = clan_flake(
{
"inventory": {
"machines": {
"jon": {
"tags": ["foo", "bar"],
},
"sara": {
"tags": ["foo", "baz", "fizz"],
},
"bob": {
"tags": ["foo", "bar"],
},
},
"instances": {
"instance1": {
"roles": {
"role1": {"tags": {"predefined": {}, "maybe": {}}},
"role2": {"tags": {"predefined2": {}, "maybe2": {}}},
}
},
},
}
},
raw=r"""
{
inventory.tags = {
"global" = [ "future_machine" ];
};
}
""",
)
inventory_store = InventoryStore(flake=flake)
inventory = inventory_store.read()
curr_tags = get_value_by_path(inventory, "machines.jon.tags", [])
new_tags = ["managed1", "managed2"]
set_value_by_path(inventory, "machines.jon.tags", [*curr_tags, *new_tags])
inventory_store.write(inventory, message="Test add tags via API")
# Check that the tags were updated
persisted = inventory_store._get_persisted() # noqa: SLF001
assert get_value_by_path(persisted, "machines.jon.tags", []) == new_tags
tags = list_tags(flake)
assert tags.options == set(
{
# Tags defined in nix
"bar",
"baz",
"fizz",
"foo",
"predefined",
"predefined2",
"maybe",
"maybe2",
# Tags managed by the UI
"managed1",
"managed2",
}
)
assert tags.special == set(
{
# Predefined tags
"all",
"global",
"darwin",
"nixos",
}
)

View File

@@ -36,6 +36,7 @@ COMMON_VERBS = {
# If you need a new top-level resource, create an issue to discuss it first.
TOP_LEVEL_RESOURCES = {
"clan", # clan management
"tag", # Tags
"machine", # machine management
"task", # task management
"secret", # sops & key operations