Merge pull request 'UI,API: migrate admin service api bindings' (#2093) from hsjobeki/clan-core:hsjobeki-main into main
This commit is contained in:
@@ -7,7 +7,7 @@ from typing import Any, get_args, get_type_hints
|
||||
|
||||
from clan_cli.cmd import run_no_stdout
|
||||
from clan_cli.errors import ClanCmdError, ClanError
|
||||
from clan_cli.inventory import Inventory, load_inventory_json, save_inventory
|
||||
from clan_cli.inventory import Inventory, load_inventory_json, set_inventory
|
||||
from clan_cli.inventory.classes import Service
|
||||
from clan_cli.nix import nix_eval
|
||||
|
||||
@@ -187,7 +187,7 @@ def set_service_instance(
|
||||
|
||||
setattr(inventory.services, module_name, module_instance_map)
|
||||
|
||||
save_inventory(
|
||||
set_inventory(
|
||||
inventory, base_path, f"Update {module_name} instance {instance_name}"
|
||||
)
|
||||
|
||||
|
||||
@@ -202,6 +202,8 @@ def construct_value(
|
||||
return construct_value(base_type, field_value)
|
||||
|
||||
# elif get_origin(t) is Union:
|
||||
if t is Any:
|
||||
return field_value
|
||||
|
||||
# Unhandled
|
||||
msg = f"Unhandled field type {t} with value {field_value}"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from clan_cli.api import API
|
||||
from clan_cli.inventory import Meta, load_inventory_json, save_inventory
|
||||
from clan_cli.inventory import Meta, load_inventory_json, set_inventory
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -15,6 +15,6 @@ def update_clan_meta(options: UpdateOptions) -> Meta:
|
||||
inventory = load_inventory_json(options.directory)
|
||||
inventory.meta = options.meta
|
||||
|
||||
save_inventory(inventory, options.directory, "Update clan metadata")
|
||||
set_inventory(inventory, options.directory, "Update clan metadata")
|
||||
|
||||
return inventory.meta
|
||||
|
||||
@@ -58,6 +58,7 @@ default_inventory = Inventory(
|
||||
)
|
||||
|
||||
|
||||
@API.register
|
||||
def load_inventory_eval(flake_dir: str | Path) -> Inventory:
|
||||
"""
|
||||
Loads the actual inventory.
|
||||
@@ -89,6 +90,7 @@ def load_inventory_eval(flake_dir: str | Path) -> Inventory:
|
||||
return inventory
|
||||
|
||||
|
||||
@API.register
|
||||
def load_inventory_json(
|
||||
flake_dir: str | Path, default: Inventory = default_inventory
|
||||
) -> Inventory:
|
||||
@@ -116,7 +118,8 @@ def load_inventory_json(
|
||||
return inventory
|
||||
|
||||
|
||||
def save_inventory(inventory: Inventory, flake_dir: str | Path, message: str) -> None:
|
||||
@API.register
|
||||
def set_inventory(inventory: Inventory, flake_dir: str | Path, message: str) -> None:
|
||||
""" "
|
||||
Write the inventory to the flake directory
|
||||
and commit it to git with the given message
|
||||
@@ -143,4 +146,4 @@ def init_inventory(directory: str, init: Inventory | None = None) -> None:
|
||||
# Write inventory.json file
|
||||
if inventory is not None:
|
||||
# Persist creates a commit message for each change
|
||||
save_inventory(inventory, directory, "Init inventory")
|
||||
set_inventory(inventory, directory, "Init inventory")
|
||||
|
||||
@@ -10,7 +10,7 @@ from clan_cli.inventory import (
|
||||
MachineDeploy,
|
||||
load_inventory_eval,
|
||||
load_inventory_json,
|
||||
save_inventory,
|
||||
set_inventory,
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -34,7 +34,7 @@ def create_machine(flake: FlakeId, machine: Machine) -> None:
|
||||
print(f"Define machine {machine.name}", machine)
|
||||
|
||||
inventory.machines.update({machine.name: machine})
|
||||
save_inventory(inventory, flake.path, f"Create machine {machine.name}")
|
||||
set_inventory(inventory, flake.path, f"Create machine {machine.name}")
|
||||
|
||||
|
||||
def create_command(args: argparse.Namespace) -> None:
|
||||
|
||||
@@ -6,7 +6,7 @@ from clan_cli.clan_uri import FlakeId
|
||||
from clan_cli.completions import add_dynamic_completer, complete_machines
|
||||
from clan_cli.dirs import specific_machine_dir
|
||||
from clan_cli.errors import ClanError
|
||||
from clan_cli.inventory import load_inventory_json, save_inventory
|
||||
from clan_cli.inventory import load_inventory_json, set_inventory
|
||||
|
||||
|
||||
@API.register
|
||||
@@ -18,7 +18,7 @@ def delete_machine(flake: FlakeId, name: str) -> None:
|
||||
msg = f"Machine {name} does not exist"
|
||||
raise ClanError(msg)
|
||||
|
||||
save_inventory(inventory, flake.path, f"Delete machine {name}")
|
||||
set_inventory(inventory, flake.path, f"Delete machine {name}")
|
||||
|
||||
folder = specific_machine_dir(flake.path, name)
|
||||
if folder.exists():
|
||||
|
||||
@@ -8,7 +8,7 @@ from typing import Literal
|
||||
from clan_cli.api import API
|
||||
from clan_cli.cmd import run_no_stdout
|
||||
from clan_cli.errors import ClanCmdError, ClanError
|
||||
from clan_cli.inventory import Machine, load_inventory_eval, save_inventory
|
||||
from clan_cli.inventory import Machine, load_inventory_eval, set_inventory
|
||||
from clan_cli.nix import nix_eval, nix_shell
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -20,7 +20,7 @@ def set_machine(flake_url: str | Path, machine_name: str, machine: Machine) -> N
|
||||
|
||||
inventory.machines[machine_name] = machine
|
||||
|
||||
save_inventory(inventory, flake_url, "machines: edit '{machine_name}'")
|
||||
set_inventory(inventory, flake_url, "machines: edit '{machine_name}'")
|
||||
|
||||
|
||||
@API.register
|
||||
|
||||
@@ -8,7 +8,7 @@ from clan_cli.inventory import (
|
||||
Machine,
|
||||
MachineDeploy,
|
||||
load_inventory_json,
|
||||
save_inventory,
|
||||
set_inventory,
|
||||
)
|
||||
from clan_cli.machines.create import create_machine
|
||||
from clan_cli.nix import nix_eval, run_no_stdout
|
||||
@@ -74,7 +74,7 @@ def test_add_module_to_inventory(
|
||||
}
|
||||
}
|
||||
|
||||
save_inventory(inventory, base_path, "Add borgbackup service")
|
||||
set_inventory(inventory, base_path, "Add borgbackup service")
|
||||
|
||||
cmd = ["facts", "generate", "--flake", str(test_flake_with_core.path), "machine1"]
|
||||
cli.run(cmd)
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import { QueryClient } from "@tanstack/query-core";
|
||||
import { get_inventory } from "./inventory";
|
||||
|
||||
export const instance_name = (machine_name: string) =>
|
||||
`${machine_name}-single-disk` as const;
|
||||
|
||||
export async function set_single_disk_id(
|
||||
client: QueryClient,
|
||||
base_path: string,
|
||||
machine_name: string,
|
||||
disk_id: string,
|
||||
) {
|
||||
const inventory = await get_inventory(base_path);
|
||||
if (!inventory.services) {
|
||||
const r = await get_inventory(client, base_path);
|
||||
if (r.status === "error") {
|
||||
return r;
|
||||
}
|
||||
if (!r.data.services) {
|
||||
return new Error("No services found in inventory");
|
||||
}
|
||||
if (!inventory.services["single-disk"]) {
|
||||
inventory.services["single-disk"] = {};
|
||||
}
|
||||
const inventory = r.data;
|
||||
inventory.services = inventory.services || {};
|
||||
inventory.services["single-disk"] = inventory.services["single-disk"] || {};
|
||||
|
||||
inventory.services["single-disk"][instance_name(machine_name)] = {
|
||||
meta: {
|
||||
name: instance_name(machine_name),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import schema from "@/api/API.json" assert { type: "json" };
|
||||
import { API } from "@/api/API";
|
||||
import { API, Error } from "@/api/API";
|
||||
import { nanoid } from "nanoid";
|
||||
import { Schema as Inventory } from "@/api/Inventory";
|
||||
|
||||
@@ -7,6 +7,14 @@ export type OperationNames = keyof API;
|
||||
export type OperationArgs<T extends OperationNames> = API[T]["arguments"];
|
||||
export type OperationResponse<T extends OperationNames> = API[T]["return"];
|
||||
|
||||
export type ApiEnvelope<T> =
|
||||
| {
|
||||
status: "success";
|
||||
data: T;
|
||||
op_key: string;
|
||||
}
|
||||
| Error;
|
||||
|
||||
export type Services = NonNullable<Inventory["services"]>;
|
||||
export type ServiceNames = keyof Services;
|
||||
export type ClanService<T extends ServiceNames> = Services[T];
|
||||
|
||||
@@ -1,40 +1,112 @@
|
||||
import { callApi, ClanService, ServiceNames, Services } from ".";
|
||||
import { QueryClient } from "@tanstack/solid-query";
|
||||
import {
|
||||
ApiEnvelope,
|
||||
callApi,
|
||||
ClanServiceInstance,
|
||||
ServiceNames,
|
||||
Services,
|
||||
} from ".";
|
||||
import { Schema as Inventory } from "@/api/Inventory";
|
||||
|
||||
export async function get_inventory(base_path: string) {
|
||||
const r = await callApi("get_inventory", {
|
||||
base_path,
|
||||
export async function get_inventory(client: QueryClient, base_path: string) {
|
||||
const data = await client.ensureQueryData({
|
||||
queryKey: [base_path, "inventory"],
|
||||
queryFn: () => {
|
||||
console.log("Refreshing inventory");
|
||||
return callApi("get_inventory", { base_path }) as Promise<
|
||||
ApiEnvelope<Inventory>
|
||||
>;
|
||||
},
|
||||
revalidateIfStale: true,
|
||||
staleTime: 60 * 1000,
|
||||
});
|
||||
if (r.status == "error") {
|
||||
throw new Error("Failed to get inventory");
|
||||
}
|
||||
const inventory: Inventory = r.data;
|
||||
return inventory;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export const single_instance_name = <T extends keyof Services>(
|
||||
export const generate_instance_name = <T extends keyof Services>(
|
||||
machine_name: string,
|
||||
service_name: T,
|
||||
) => `${machine_name}_${service_name}_0` as const;
|
||||
) => [machine_name, service_name, 1].filter(Boolean).join("_");
|
||||
|
||||
function get_service<T extends ServiceNames>(base_path: string, service: T) {
|
||||
return callApi("get_inventory", { base_path }).then((r) => {
|
||||
if (r.status == "error") {
|
||||
return null;
|
||||
}
|
||||
const inventory: Inventory = r.data;
|
||||
export const get_first_instance_name = async <T extends keyof Services>(
|
||||
client: QueryClient,
|
||||
base_path: string,
|
||||
service_name: T,
|
||||
): Promise<string | null> => {
|
||||
const r = await get_inventory(client, base_path);
|
||||
if (r.status === "success") {
|
||||
const service = r.data.services?.[service_name];
|
||||
if (!service) return null;
|
||||
return Object.keys(service)[0] || null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const serviceInstance = inventory.services?.[service];
|
||||
return serviceInstance;
|
||||
});
|
||||
async function get_service<T extends ServiceNames>(
|
||||
client: QueryClient,
|
||||
base_path: string,
|
||||
service_name: T,
|
||||
) {
|
||||
const r = await get_inventory(client, base_path);
|
||||
if (r.status === "success") {
|
||||
const service = r.data.services?.[service_name];
|
||||
return service as Services[T];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function get_single_service<T extends keyof Services>(
|
||||
client: QueryClient,
|
||||
base_path: string,
|
||||
service_name: T,
|
||||
): Promise<ClanServiceInstance<T>> {
|
||||
const instance_key = await get_first_instance_name(
|
||||
client,
|
||||
base_path,
|
||||
service_name,
|
||||
);
|
||||
|
||||
if (!instance_key) {
|
||||
throw new Error("No instance found");
|
||||
}
|
||||
const service: Services[T] | null = await get_service(
|
||||
client,
|
||||
base_path,
|
||||
service_name,
|
||||
);
|
||||
if (service) {
|
||||
const clanServiceInstance = service[instance_key] as ClanServiceInstance<T>;
|
||||
return clanServiceInstance;
|
||||
}
|
||||
throw new Error("No service found");
|
||||
}
|
||||
|
||||
export async function set_single_service<T extends keyof Services>(
|
||||
client: QueryClient,
|
||||
base_path: string,
|
||||
machine_name: string,
|
||||
service_name: T,
|
||||
service_config: ClanServiceInstance<T>,
|
||||
) {
|
||||
const instance_key = single_instance_name(machine_name, service_name);
|
||||
const service = await get_service(base_path, "admin");
|
||||
return service?.[instance_key];
|
||||
const instance_key =
|
||||
(await get_first_instance_name(client, base_path, service_name)) ||
|
||||
generate_instance_name(machine_name, service_name);
|
||||
const r = await get_inventory(client, base_path);
|
||||
if (r.status === "success") {
|
||||
const inventory = r.data;
|
||||
inventory.services = inventory.services || {};
|
||||
inventory.services[service_name] = inventory.services[service_name] || {};
|
||||
|
||||
// @ts-expect-error: This cannot be undefined, because of the line above
|
||||
inventory.services[service_name][instance_key] = service_config;
|
||||
console.log("saving inventory", inventory);
|
||||
return callApi("set_inventory", {
|
||||
// @ts-expect-error: This doesn't check
|
||||
inventory,
|
||||
message: `update_single_service ${service_name}`,
|
||||
flake_dir: base_path,
|
||||
});
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { HostList } from "./routes/hosts/view";
|
||||
import { Welcome } from "./routes/welcome";
|
||||
import { Toaster } from "solid-toast";
|
||||
|
||||
const client = new QueryClient();
|
||||
export const client = new QueryClient();
|
||||
|
||||
const root = document.getElementById("app");
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
} from "@modular-forms/solid";
|
||||
import { TextInput } from "@/src/components/TextInput";
|
||||
import toast from "solid-toast";
|
||||
import { get_single_service } from "@/src/api/inventory";
|
||||
import { get_single_service, set_single_service } from "@/src/api/inventory";
|
||||
|
||||
interface AdminModuleFormProps {
|
||||
admin: AdminData;
|
||||
@@ -190,20 +190,34 @@ const AdminModuleForm = (props: AdminModuleFormProps) => {
|
||||
const handleSubmit = async (values: AdminSettings) => {
|
||||
console.log("submitting", values, getValues(formStore));
|
||||
|
||||
// const r = await callApi("set_admin_service", {
|
||||
// base_url: props.base_url,
|
||||
// allowed_keys: values.allowedKeys.reduce(
|
||||
// (acc, curr) => ({ ...acc, [curr.name]: curr.value }),
|
||||
// {}
|
||||
// ),
|
||||
// });
|
||||
// if (r.status === "success") {
|
||||
// toast.success("Successfully updated admin settings");
|
||||
// }
|
||||
// if (r.status === "error") {
|
||||
// toast.error(`Failed to update admin settings: ${r.errors[0].message}`);
|
||||
toast.error(`Failed to update admin settings: feature disabled`);
|
||||
// }
|
||||
const r = await set_single_service(
|
||||
queryClient,
|
||||
props.base_url,
|
||||
"",
|
||||
"admin",
|
||||
{
|
||||
meta: {
|
||||
name: "admin",
|
||||
},
|
||||
roles: {
|
||||
default: {
|
||||
tags: ["all"],
|
||||
},
|
||||
},
|
||||
config: {
|
||||
allowedKeys: values.allowedKeys.reduce(
|
||||
(acc, curr) => ({ ...acc, [curr.name]: curr.value }),
|
||||
{},
|
||||
),
|
||||
},
|
||||
},
|
||||
);
|
||||
if (r.status === "success") {
|
||||
toast.success("Successfully updated admin settings");
|
||||
}
|
||||
if (r.status === "error") {
|
||||
toast.error(`Failed to update admin settings: ${r.errors[0].message}`);
|
||||
}
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [props.base_url, "get_admin_service"],
|
||||
});
|
||||
@@ -340,10 +354,11 @@ type AdminData = ClanServiceInstance<"admin">;
|
||||
|
||||
export const ClanDetails = () => {
|
||||
const params = useParams();
|
||||
const queryClient = useQueryClient();
|
||||
const clan_dir = window.atob(params.id);
|
||||
// Fetch general meta data
|
||||
const clanQuery = createQuery(() => ({
|
||||
queryKey: [clan_dir, "meta"],
|
||||
queryKey: [clan_dir, "inventory", "meta"],
|
||||
queryFn: async () => {
|
||||
const result = await callApi("show_clan_meta", { uri: clan_dir });
|
||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||
@@ -352,11 +367,11 @@ export const ClanDetails = () => {
|
||||
}));
|
||||
// Fetch admin settings
|
||||
const adminQuery = createQuery(() => ({
|
||||
queryKey: [clan_dir, "get_admin_service"],
|
||||
queryKey: [clan_dir, "inventory", "services", "admin"],
|
||||
queryFn: async () => {
|
||||
const result = await get_single_service(clan_dir, "", "admin");
|
||||
const result = await get_single_service(queryClient, clan_dir, "admin");
|
||||
if (!result) throw new Error("Failed to fetch data");
|
||||
return result || null;
|
||||
return result;
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -371,7 +386,7 @@ export const ClanDetails = () => {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Switch>
|
||||
<Switch fallback={<>General data not available</>}>
|
||||
<Match when={clanQuery.data}>
|
||||
{(d) => <EditClanForm initial={d()} directory={clan_dir} />}
|
||||
</Match>
|
||||
@@ -386,7 +401,7 @@ export const ClanDetails = () => {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Switch>
|
||||
<Switch fallback={<>Admin data not available</>}>
|
||||
<Match when={adminQuery.data}>
|
||||
{(d) => <AdminModuleForm admin={d()} base_url={clan_dir} />}
|
||||
</Match>
|
||||
|
||||
@@ -21,7 +21,11 @@ import {
|
||||
setValue,
|
||||
} from "@modular-forms/solid";
|
||||
import { useParams } from "@solidjs/router";
|
||||
import { createQuery, QueryObserver } from "@tanstack/solid-query";
|
||||
import {
|
||||
createQuery,
|
||||
QueryObserver,
|
||||
useQueryClient,
|
||||
} from "@tanstack/solid-query";
|
||||
import {
|
||||
createSignal,
|
||||
For,
|
||||
@@ -115,6 +119,7 @@ const InstallMachine = (props: InstallMachineProps) => {
|
||||
toast.success("Machine installed successfully");
|
||||
}
|
||||
};
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleDiskConfirm = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
@@ -125,7 +130,12 @@ const InstallMachine = (props: InstallMachineProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const r = await set_single_disk_id(curr_uri, props.name, disk_id);
|
||||
const r = await set_single_disk_id(
|
||||
queryClient,
|
||||
curr_uri,
|
||||
props.name,
|
||||
disk_id,
|
||||
);
|
||||
if (!r) {
|
||||
toast.success("Disk set successfully");
|
||||
setConfirmDisk(true);
|
||||
|
||||
Reference in New Issue
Block a user