From 9c7f684ec5f9e57ef7eb3f9cbfc34c3791bba5f5 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Sun, 13 Jul 2025 13:54:29 +0200 Subject: [PATCH] instances: create_service_instance init --- pkgs/clan-cli/clan_lib/services/instances.py | 133 +++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 pkgs/clan-cli/clan_lib/services/instances.py diff --git a/pkgs/clan-cli/clan_lib/services/instances.py b/pkgs/clan-cli/clan_lib/services/instances.py new file mode 100644 index 000000000..b1d2c35c5 --- /dev/null +++ b/pkgs/clan-cli/clan_lib/services/instances.py @@ -0,0 +1,133 @@ +from typing import TypedDict + +from clan_lib.api import API +from clan_lib.errors import ClanError +from clan_lib.flake.flake import Flake +from clan_lib.nix_models.clan import ( + InventoryInstanceModule, + InventoryInstanceRolesType, + InventoryInstancesType, + InventoryMachinesType, +) +from clan_lib.persist.inventory_store import InventoryStore +from clan_lib.persist.util import set_value_by_path +from clan_lib.services.modules import list_service_modules + +# TODO: move imports out of cli/__init__.py causing import cycles +# from clan_lib.machines.actions import list_machines + + +@API.register +def list_service_instances(flake: Flake) -> InventoryInstancesType: + """ + Returns all currently present service instances including their full configuration + """ + + inventory_store = InventoryStore(flake) + inventory = inventory_store.read() + instances = inventory.get("instances", {}) + return instances + + +def collect_tags(machines: InventoryMachinesType) -> set[str]: + res = set() + for _, machine in machines.items(): + res |= set(machine.get("tags", [])) + + return res + + +# Removed 'module' ref - Needs to be passed seperately +class InstanceConfig(TypedDict): + roles: InventoryInstanceRolesType + + +@API.register +def create_service_instance( + flake: Flake, + module_ref: InventoryInstanceModule, + instance_name: str, + instance_config: InstanceConfig, +) -> None: + # TODO: Should take a flake + avilable_modules = list_service_modules(flake) + + input_ref = module_ref.get("input", None) + if input_ref is None: + msg = "Setting module_ref.input is currently required" + raise ClanError(msg) + + module_set = avilable_modules.get("modules", {}).get(input_ref) + + if module_set is None: + msg = f"module set for input '{input_ref}' not found" + msg += f"\nAvilable input_refs: {avilable_modules.get('modules', {}).keys()}" + raise ClanError(msg) + + module_name = module_ref.get("name") + assert module_name + module = module_set.get(module_name) + if module is None: + msg = f"module with name '{module_name}' not found" + raise ClanError(msg) + + inventory_store = InventoryStore(flake) + inventory = inventory_store.read() + + instances = inventory.get("instances", {}) + if instance_name in instances: + msg = f"service instance '{instance_name}' already exists." + raise ClanError(msg) + + target_roles = instance_config.get("roles") + if not target_roles: + msg = "Creating a service instance requires adding roles" + raise ClanError(msg) + + available_roles = set(module.get("roles", {}).keys()) + + unavailable_roles = list(filter(lambda r: r not in available_roles, target_roles)) + if unavailable_roles: + msg = f"Unknown roles: {unavailable_roles}. Use one of {available_roles}" + raise ClanError(msg) + + role_configs = instance_config.get("roles") + if not role_configs: + return + + ## Validate machine references + all_machines = inventory.get("machines", {}) + available_machine_refs = set(all_machines.keys()) + available_tag_refs = collect_tags(all_machines) + + for role_name, role_members in role_configs.items(): + machine_refs = role_members.get("machines") + msg = f"Role: '{role_name}' - " + if machine_refs: + unavailable_machines = list( + filter(lambda m: m not in available_machine_refs, machine_refs) + ) + if unavailable_machines: + msg += f"Unknown machine reference: {unavailable_machines}. Use one of {available_machine_refs}" + raise ClanError(msg) + + tag_refs = role_members.get("tags") + if tag_refs: + unavailable_tags = list( + filter(lambda m: m not in available_tag_refs, tag_refs) + ) + + if unavailable_tags: + msg += ( + f"Unknown tags: {unavailable_tags}. Use one of {available_tag_refs}" + ) + raise ClanError(msg) + + # TODO: + # Validate instance_config roles settings against role schema + + set_value_by_path(inventory, f"instances.{instance_name}", instance_config) + set_value_by_path(inventory, f"instances.{instance_name}.module", module_ref) + inventory_store.write( + inventory, message=f"services: instance '{instance_name}' init" + )