Merge pull request 'Clan_lib: add filtering by tag to list API' (#4197) from cli-fixup into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4197
This commit is contained in:
hsjobeki
2025-07-04 11:53:43 +00:00
8 changed files with 69 additions and 30 deletions

View File

@@ -10,6 +10,7 @@ from tempfile import TemporaryDirectory
from clan_lib.cmd import RunOpts, run
from clan_lib.errors import ClanError
from clan_lib.git import commit_files
from clan_lib.machines.list import list_full_machines
from clan_lib.machines.machines import Machine
from clan_lib.nix import nix_shell
@@ -18,7 +19,6 @@ from clan_cli.completions import (
complete_machines,
complete_services_for_machine,
)
from clan_cli.machines.list import list_full_machines
from .check import check_secrets
from .public_modules import FactStoreBase

View File

@@ -2,7 +2,7 @@ import argparse
import logging
from clan_lib.flake import Flake
from clan_lib.machines.list import list_full_machines, query_machines_by_tags
from clan_lib.machines.actions import list_machines
from clan_cli.completions import add_dynamic_completer, complete_tags
@@ -12,11 +12,7 @@ log = logging.getLogger(__name__)
def list_command(args: argparse.Namespace) -> None:
flake: Flake = args.flake
if args.tags:
for name in query_machines_by_tags(flake, args.tags):
print(name)
else:
for name in list_full_machines(flake):
for name in list_machines(flake, opts={"filter": {"tags": args.tags}}):
print(name)

View File

@@ -4,6 +4,7 @@ import sys
from clan_lib.async_run import AsyncContext, AsyncOpts, AsyncRuntime
from clan_lib.errors import ClanError
from clan_lib.machines.list import list_full_machines, query_machines_by_tags
from clan_lib.machines.machines import Machine
from clan_lib.machines.suggestions import validate_machine_names
from clan_lib.machines.update import deploy_machine
@@ -15,7 +16,6 @@ from clan_cli.completions import (
complete_machines,
complete_tags,
)
from clan_cli.machines.list import list_full_machines, query_machines_by_tags
log = logging.getLogger(__name__)

View File

@@ -14,7 +14,6 @@ from clan_cli.completions import (
complete_machines,
complete_services_for_machine,
)
from clan_cli.machines.list import list_full_machines
from clan_cli.vars._types import StoreBase
from clan_cli.vars.migration import check_can_migrate, migrate_files
from clan_lib.api import API
@@ -22,6 +21,7 @@ from clan_lib.cmd import RunOpts, run
from clan_lib.errors import ClanError
from clan_lib.flake import Flake
from clan_lib.git import commit_files
from clan_lib.machines.list import list_full_machines
from clan_lib.nix import nix_config, nix_shell, nix_test_store
from .check import check_vars

View File

@@ -1,4 +1,5 @@
from dataclasses import dataclass
from typing import TypedDict
from clan_lib.api import API
from clan_lib.errors import ClanError
@@ -10,15 +11,44 @@ from clan_lib.persist.inventory_store import InventoryStore
from clan_lib.persist.util import set_value_by_path
class MachineFilter(TypedDict):
tags: list[str]
class ListOptions(TypedDict):
filter: MachineFilter
@API.register
def list_machines(flake: Flake) -> dict[str, InventoryMachine]:
def list_machines(
flake: Flake, opts: ListOptions | None = None
) -> dict[str, InventoryMachine]:
"""
List machines in the inventory for the UI.
List machines of a clan
Usage Example:
machines = list_machines(flake, {"filter": {"tags": ["foo" "bar"]}})
lists only machines that include both "foo" AND "bar"
"""
inventory_store = InventoryStore(flake=flake)
inventory = inventory_store.read()
machines = inventory.get("machines", {})
if opts and opts.get("filter"):
filtered_machines = {}
filter_tags = opts.get("filter", {}).get("tags", [])
for machine_name, machine in machines.items():
machine_tags = machine.get("tags", [])
if all(ft in machine_tags for ft in filter_tags):
filtered_machines[machine_name] = machine
return filtered_machines
return machines

View File

@@ -16,35 +16,39 @@ from clan_lib.nix_models.clan import InventoryMachine
log = logging.getLogger(__name__)
def convert_inventory_to_machines(
flake: Flake, machines: dict[str, InventoryMachine]
) -> dict[str, Machine]:
return {
name: Machine.from_inventory(name, flake, inventory_machine)
for name, inventory_machine in machines.items()
}
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)
res: dict[str, Machine] = {}
for name in machines:
machine = Machine(name=name, flake=flake)
res[machine.name] = machine
return res
return convert_inventory_to_machines(flake, machines)
def query_machines_by_tags(flake: Flake, tags: list[str]) -> dict[str, Machine]:
def query_machines_by_tags(
flake: Flake, tags: list[str]
) -> dict[str, InventoryMachine]:
"""
Query machines by their respective tags, if multiple tags are specified
then only machines that have those respective tags specified will be listed.
It is an intersection of the tags and machines.
"""
machines = list_full_machines(flake)
machines = list_machines(flake)
filtered_machines = {}
for machine in machines.values():
inv_machine = get_machine(machine.flake, machine.name)
machine_tags = inv_machine.get("tags", [])
for machine_name, machine in machines.items():
machine_tags = machine.get("tags", [])
if all(tag in machine_tags for tag in tags):
filtered_machines[machine.name] = machine
filtered_machines[machine_name] = machine
return filtered_machines

View File

@@ -29,6 +29,15 @@ class Machine:
name: str
flake: Flake
@classmethod
def from_inventory(
cls,
name: str,
flake: Flake,
_inventory_machine: InventoryMachine,
) -> "Machine":
return cls(name=name, flake=flake)
def get_inv_machine(self) -> "InventoryMachine":
return get_machine(self.flake, self.name)
@@ -166,7 +175,7 @@ class Machine:
@dataclass(frozen=True)
class RemoteSource:
data: Remote
source: Literal["inventory", "nix_machine"]
source: Literal["inventory", "machine"]
@API.register
@@ -179,15 +188,15 @@ def get_host(
machine = Machine(name=name, flake=flake)
inv_machine = machine.get_inv_machine()
source: Literal["inventory", "nix_machine"] = "inventory"
source: Literal["inventory", "machine"] = "inventory"
host_str = inv_machine.get("deploy", {}).get(field)
if host_str is None:
machine.debug(
f"'{field}' is not set in inventory, falling back to slower Nix config, set it either through the Nix or json interface to improve performance"
machine.warn(
f"'{field}' is not set in `inventory.machines.${name}.deploy.targetHost` - falling back to _slower_ nixos option: `clan.core.networking.targetHost`"
)
host_str = machine.select(f'config.clan.core.networking."{field}"')
source = "nix_machine"
source = "machine"
if not host_str:
return None