Merge pull request 'Refactor(api/update_machine): rename to set_machine; use name, flake' (#3899) from api-narrowing into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/3899
This commit is contained in:
hsjobeki
2025-06-09 11:55:28 +00:00
15 changed files with 129 additions and 119 deletions

View File

@@ -9,7 +9,7 @@ import { Typography } from "../Typography";
import "./css/index.css"; import "./css/index.css";
import { useClanContext } from "@/src/contexts/clan"; import { useClanContext } from "@/src/contexts/clan";
type MachineDetails = SuccessQuery<"list_inv_machines">["data"][string]; type MachineDetails = SuccessQuery<"list_machines">["data"][string];
interface MachineListItemProps { interface MachineListItemProps {
name: string; name: string;

View File

@@ -63,7 +63,7 @@ export function CreateMachine() {
reset(formStore); reset(formStore);
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: [active_dir, "list_inv_machines"], queryKey: [active_dir, "list_machines"],
}); });
navigate("/machines"); navigate("/machines");

View File

@@ -411,7 +411,7 @@ const MachineForm = (props: MachineDetailsProps) => {
return; return;
} }
const machine_response = await callApi("update_machine", { const machine_response = await callApi("set_machine", {
machine: { machine: {
name: props.initialData.machine.name || "My machine", name: props.initialData.machine.name || "My machine",
flake: { flake: {

View File

@@ -10,7 +10,7 @@ import { makePersisted } from "@solid-primitives/storage";
import { useClanContext } from "@/src/contexts/clan"; import { useClanContext } from "@/src/contexts/clan";
type MachinesModel = Extract< type MachinesModel = Extract<
OperationResponse<"list_inv_machines">, OperationResponse<"list_machines">,
{ status: "success" } { status: "success" }
>["data"]; >["data"];
@@ -25,14 +25,14 @@ export const MachineListView: Component = () => {
const { activeClanURI } = useClanContext(); const { activeClanURI } = useClanContext();
const inventoryQuery = useQuery<MachinesModel>(() => ({ const inventoryQuery = useQuery<MachinesModel>(() => ({
queryKey: [activeClanURI(), "list_inv_machines"], queryKey: [activeClanURI(), "list_machines"],
placeholderData: {}, placeholderData: {},
enabled: !!activeClanURI(), enabled: !!activeClanURI(),
queryFn: async () => { queryFn: async () => {
console.log("fetching inventory", activeClanURI()); console.log("fetching inventory", activeClanURI());
const uri = activeClanURI(); const uri = activeClanURI();
if (uri) { if (uri) {
const response = await callApi("list_inv_machines", { const response = await callApi("list_machines", {
flake: { flake: {
identifier: uri, identifier: uri,
}, },
@@ -60,7 +60,7 @@ export const MachineListView: Component = () => {
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({
// Invalidates the cache for of all types of machine list at once // Invalidates the cache for of all types of machine list at once
queryKey: [clanURI, "list_inv_machines"], queryKey: [clanURI, "list_machines"],
}); });
}; };

View File

@@ -5,15 +5,6 @@ import sys
from pathlib import Path from pathlib import Path
from types import ModuleType from types import ModuleType
# These imports are unused, but necessary for @API.register to run once.
from clan_lib.api import directory, disk, mdns_discovery, modules
from .arg_actions import AppendOptionAction
from .clan import show, update
# API endpoints that are not used in the cli.
__all__ = ["directory", "disk", "mdns_discovery", "modules", "update"]
from clan_lib.custom_logger import setup_logging from clan_lib.custom_logger import setup_logging
from clan_lib.dirs import get_clan_flake_toplevel_or_env from clan_lib.dirs import get_clan_flake_toplevel_or_env
from clan_lib.errors import ClanError from clan_lib.errors import ClanError
@@ -27,6 +18,8 @@ from . import (
state, state,
vms, vms,
) )
from .arg_actions import AppendOptionAction
from .clan import show
from .facts import cli as facts from .facts import cli as facts
from .flash import cli as flash_cli from .flash import cli as flash_cli
from .hyperlink import help_hyperlink from .hyperlink import help_hyperlink

View File

@@ -16,7 +16,7 @@ from clan_lib.nix import (
nix_metadata, nix_metadata,
) )
from clan_cli.machines.list import list_machines from clan_cli.machines.list import list_full_machines
from clan_cli.vms.inspect import VmConfig, inspect_vm from clan_cli.vms.inspect import VmConfig, inspect_vm
@@ -58,7 +58,7 @@ def inspect_flake(flake_url: str | Path, machine_name: str) -> FlakeConfig:
system = config["system"] system = config["system"]
# Check if the machine exists # Check if the machine exists
machines: dict[str, Machine] = list_machines(Flake(str(flake_url))) machines: dict[str, Machine] = list_full_machines(Flake(str(flake_url)))
if machine_name not in machines: if machine_name not in machines:
msg = f"Machine {machine_name} not found in {flake_url}. Available machines: {', '.join(machines)}" msg = f"Machine {machine_name} not found in {flake_url}. Available machines: {', '.join(machines)}"
raise ClanError(msg) raise ClanError(msg)

View File

@@ -1,80 +1,11 @@
import argparse import argparse
import json
import logging import logging
from pathlib import Path
from urllib.parse import urlparse
from clan_lib.api import API from clan_lib.clan.get import show_clan_meta
from clan_lib.cmd import run
from clan_lib.errors import ClanCmdError, ClanError
from clan_lib.flake import Flake
from clan_lib.nix import nix_eval
from clan_lib.nix_models.clan import InventoryMeta as Meta
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@API.register
def show_clan_meta(flake: Flake) -> Meta:
if flake.is_local and not flake.path.exists():
msg = f"Path {flake} does not exist"
raise ClanError(msg, description="clan directory does not exist")
cmd = nix_eval(
[
f"{flake}#clanInternals.inventory.meta",
"--json",
]
)
res = "{}"
try:
proc = run(cmd)
res = proc.stdout.strip()
except ClanCmdError as e:
msg = "Evaluation failed on meta attribute"
raise ClanError(
msg,
location=f"show_clan {flake}",
description=str(e.cmd),
) from e
clan_meta = json.loads(res)
# Check if icon is a URL such as http:// or https://
# Check if icon is an relative path
# All other schemas such as file://, ftp:// are not yet supported.
icon_path: str | None = None
if meta_icon := clan_meta.get("icon"):
scheme = urlparse(meta_icon).scheme
if scheme in ["http", "https"]:
icon_path = meta_icon
elif scheme in [""]:
if Path(meta_icon).is_absolute():
msg = "Invalid absolute path"
raise ClanError(
msg,
location=f"show_clan {flake}",
description="Icon path must be a URL or a relative path",
)
icon_path = str((flake.path / meta_icon).resolve())
else:
msg = "Invalid schema"
raise ClanError(
msg,
location=f"show_clan {flake}",
description="Icon path must be a URL or a relative path",
)
return Meta(
{
"name": clan_meta.get("name"),
"description": clan_meta.get("description"),
"icon": icon_path if icon_path else "",
}
)
def show_command(args: argparse.Namespace) -> None: def show_command(args: argparse.Namespace) -> None:
flake_path = args.flake.path flake_path = args.flake.path
meta = show_clan_meta(flake_path) meta = show_clan_meta(flake_path)

View File

@@ -18,7 +18,7 @@ from clan_cli.completions import (
complete_machines, complete_machines,
complete_services_for_machine, complete_services_for_machine,
) )
from clan_cli.machines.list import list_machines from clan_cli.machines.list import list_full_machines
from .check import check_secrets from .check import check_secrets
from .public_modules import FactStoreBase from .public_modules import FactStoreBase
@@ -227,7 +227,7 @@ def generate_command(args: argparse.Namespace) -> None:
msg = "Could not find clan flake toplevel directory" msg = "Could not find clan flake toplevel directory"
raise ClanError(msg) raise ClanError(msg)
machines: list[Machine] = list(list_machines(args.flake).values()) machines: list[Machine] = list(list_full_machines(args.flake).values())
if len(args.machines) > 0: if len(args.machines) > 0:
machines = list( machines = list(
filter( filter(

View File

@@ -21,7 +21,7 @@ from clan_lib.templates import (
) )
from clan_cli.completions import add_dynamic_completer, complete_tags from clan_cli.completions import add_dynamic_completer, complete_tags
from clan_cli.machines.list import list_machines from clan_cli.machines.list import list_full_machines
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -71,7 +71,7 @@ def create_machine(
log.info(f"Found template '{template.name}' in '{template.input_variant}'") log.info(f"Found template '{template.name}' in '{template.input_variant}'")
machine_name = opts.machine.get("name") machine_name = opts.machine.get("name")
if opts.template_name in list_machines( if opts.template_name in list_full_machines(
Flake(str(clan_dir)) Flake(str(clan_dir))
) and not opts.machine.get("name"): ) and not opts.machine.get("name"):
msg = f"{opts.template_name} is already defined in {clan_dir}" msg = f"{opts.template_name} is already defined in {clan_dir}"

View File

@@ -9,10 +9,9 @@ from clan_lib.api.modules import parse_frontmatter
from clan_lib.dirs import specific_machine_dir from clan_lib.dirs import specific_machine_dir
from clan_lib.errors import ClanError from clan_lib.errors import ClanError
from clan_lib.flake import Flake from clan_lib.flake import Flake
from clan_lib.machines.actions import get_machine from clan_lib.machines.actions import get_machine, list_machines
from clan_lib.machines.machines import Machine from clan_lib.machines.machines import Machine
from clan_lib.nix_models.clan import InventoryMachine from clan_lib.nix_models.clan import InventoryMachine
from clan_lib.persist.inventory_store import InventoryStore
from clan_cli.completions import add_dynamic_completer, complete_tags from clan_cli.completions import add_dynamic_completer, complete_tags
from clan_cli.machines.hardware import HardwareConfig from clan_cli.machines.hardware import HardwareConfig
@@ -20,29 +19,20 @@ from clan_cli.machines.hardware import HardwareConfig
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@API.register def list_full_machines(
def list_inv_machines(flake: Flake) -> dict[str, InventoryMachine]:
"""
List machines in the inventory for the UI.
"""
inventory_store = InventoryStore(flake=flake)
inventory = inventory_store.read()
res = inventory.get("machines", {})
return res
def list_machines(
flake: Flake, nix_options: list[str] | None = None flake: Flake, nix_options: list[str] | None = None
) -> dict[str, Machine]: ) -> dict[str, Machine]:
inventory_store = InventoryStore(flake=flake) """
inventory = inventory_store.read() Like `list_machines`, but returns a full 'machine' instance for each machine.
res = {} """
machines = list_machines(flake)
res: dict[str, Machine] = {}
if nix_options is None: if nix_options is None:
nix_options = [] nix_options = []
for inv_machine in inventory.get("machines", {}).values(): for inv_machine in machines.values():
name = inv_machine.get("name") name = inv_machine.get("name")
# Technically, this should not happen, but we are defensive here. # Technically, this should not happen, but we are defensive here.
if name is None: if name is None:
@@ -65,7 +55,7 @@ def query_machines_by_tags(flake: Flake, tags: list[str]) -> dict[str, Machine]:
then only machines that have those respective tags specified will be listed. then only machines that have those respective tags specified will be listed.
It is an intersection of the tags and machines. It is an intersection of the tags and machines.
""" """
machines = list_machines(flake) machines = list_full_machines(flake)
filtered_machines = {} filtered_machines = {}
for machine in machines.values(): for machine in machines.values():
@@ -125,7 +115,7 @@ def list_command(args: argparse.Namespace) -> None:
for name in query_machines_by_tags(flake, args.tags): for name in query_machines_by_tags(flake, args.tags):
print(name) print(name)
else: else:
for name in list_machines(flake): for name in list_full_machines(flake):
print(name) print(name)

View File

@@ -22,7 +22,7 @@ from clan_cli.completions import (
) )
from clan_cli.facts.generate import generate_facts from clan_cli.facts.generate import generate_facts
from clan_cli.facts.upload import upload_secrets from clan_cli.facts.upload import upload_secrets
from clan_cli.machines.list import list_machines from clan_cli.machines.list import list_full_machines
from clan_cli.vars.generate import generate_vars from clan_cli.vars.generate import generate_vars
from clan_cli.vars.upload import upload_secret_vars from clan_cli.vars.upload import upload_secret_vars
@@ -225,7 +225,7 @@ def update_command(args: argparse.Namespace) -> None:
machines: list[Machine] = [] machines: list[Machine] = []
# if no machines are passed, we will update all machines # if no machines are passed, we will update all machines
selected_machines = ( selected_machines = (
args.machines if args.machines else list_machines(args.flake).keys() args.machines if args.machines else list_full_machines(args.flake).keys()
) )
if args.target_host is not None and len(args.machines) > 1: if args.target_host is not None and len(args.machines) > 1:

View File

@@ -14,7 +14,7 @@ from clan_cli.completions import (
complete_machines, complete_machines,
complete_services_for_machine, complete_services_for_machine,
) )
from clan_cli.machines.list import list_machines from clan_cli.machines.list import list_full_machines
from clan_cli.vars._types import StoreBase from clan_cli.vars._types import StoreBase
from clan_cli.vars.migration import check_can_migrate, migrate_files from clan_cli.vars.migration import check_can_migrate, migrate_files
from clan_lib.api import API from clan_lib.api import API
@@ -511,7 +511,7 @@ def generate_command(args: argparse.Namespace) -> None:
msg = "Could not find clan flake toplevel directory" msg = "Could not find clan flake toplevel directory"
raise ClanError(msg) raise ClanError(msg)
machines: list[Machine] = list(list_machines(args.flake, args.option).values()) machines: list[Machine] = list(list_full_machines(args.flake, args.option).values())
if len(args.machines) > 0: if len(args.machines) > 0:
machines = list( machines = list(

View File

@@ -0,0 +1,71 @@
import json
from pathlib import Path
from urllib.parse import urlparse
from clan_lib.api import API
from clan_lib.cmd import run
from clan_lib.errors import ClanCmdError, ClanError
from clan_lib.flake import Flake
from clan_lib.nix import nix_eval
from clan_lib.nix_models.clan import InventoryMeta as Meta
@API.register
def show_clan_meta(flake: Flake) -> Meta:
if flake.is_local and not flake.path.exists():
msg = f"Path {flake} does not exist"
raise ClanError(msg, description="clan directory does not exist")
cmd = nix_eval(
[
f"{flake}#clanInternals.inventory.meta",
"--json",
]
)
res = "{}"
try:
proc = run(cmd)
res = proc.stdout.strip()
except ClanCmdError as e:
msg = "Evaluation failed on meta attribute"
raise ClanError(
msg,
location=f"show_clan {flake}",
description=str(e.cmd),
) from e
clan_meta = json.loads(res)
# Check if icon is a URL such as http:// or https://
# Check if icon is an relative path
# All other schemas such as file://, ftp:// are not yet supported.
icon_path: str | None = None
if meta_icon := clan_meta.get("icon"):
scheme = urlparse(meta_icon).scheme
if scheme in ["http", "https"]:
icon_path = meta_icon
elif scheme in [""]:
if Path(meta_icon).is_absolute():
msg = "Invalid absolute path"
raise ClanError(
msg,
location=f"show_clan {flake}",
description="Icon path must be a URL or a relative path",
)
icon_path = str((flake.path / meta_icon).resolve())
else:
msg = "Invalid schema"
raise ClanError(
msg,
location=f"show_clan {flake}",
description="Icon path must be a URL or a relative path",
)
return Meta(
{
"name": clan_meta.get("name"),
"description": clan_meta.get("description"),
"icon": icon_path if icon_path else "",
}
)

View File

@@ -1,7 +1,8 @@
from dataclasses import dataclass
from clan_lib.api import API from clan_lib.api import API
from clan_lib.errors import ClanError from clan_lib.errors import ClanError
from clan_lib.flake.flake import Flake from clan_lib.flake.flake import Flake
from clan_lib.machines.machines import Machine
from clan_lib.nix_models.clan import ( from clan_lib.nix_models.clan import (
InventoryMachine, InventoryMachine,
) )
@@ -9,6 +10,18 @@ from clan_lib.persist.inventory_store import InventoryStore
from clan_lib.persist.util import set_value_by_path from clan_lib.persist.util import set_value_by_path
@API.register
def list_machines(flake: Flake) -> dict[str, InventoryMachine]:
"""
List machines in the inventory for the UI.
"""
inventory_store = InventoryStore(flake=flake)
inventory = inventory_store.read()
machines = inventory.get("machines", {})
return machines
@API.register @API.register
def get_machine(flake: Flake, name: str) -> InventoryMachine: def get_machine(flake: Flake, name: str) -> InventoryMachine:
inventory_store = InventoryStore(flake=flake) inventory_store = InventoryStore(flake=flake)
@@ -22,9 +35,21 @@ def get_machine(flake: Flake, name: str) -> InventoryMachine:
return InventoryMachine(**machine_inv) return InventoryMachine(**machine_inv)
# TODO: remove this machine, once the Machine class is refactored
# We added this now, to allow for dispatching actions. To require only 'name' and 'flake' of a machine.
@dataclass(frozen=True)
class MachineID:
name: str
flake: Flake
@API.register @API.register
def update_machine(machine: Machine, update: InventoryMachine) -> None: def set_machine(machine: MachineID, update: InventoryMachine) -> None:
"""
Update the machine information in the inventory.
"""
assert machine.name == update.get("name", machine.name), "Machine name mismatch" assert machine.name == update.get("name", machine.name), "Machine name mismatch"
inventory_store = InventoryStore(flake=machine.flake) inventory_store = InventoryStore(flake=machine.flake)
inventory = inventory_store.read() inventory = inventory_store.read()