diff --git a/pkgs/clan-cli/clan_lib/persist/path_utils.py b/pkgs/clan-cli/clan_lib/persist/path_utils.py index 408d995cd..8bd1b5a03 100644 --- a/pkgs/clan-cli/clan_lib/persist/path_utils.py +++ b/pkgs/clan-cli/clan_lib/persist/path_utils.py @@ -67,7 +67,7 @@ def set_value_by_path_tuple(d: DictLike, path: PathTuple, content: Any) -> None: current[keys[-1]] = content -def delete_by_path_tuple(d: dict[str, Any], path: PathTuple) -> Any: +def delete_by_path_tuple(d: DictLike, path: PathTuple) -> Any: """Deletes the nested entry specified by a dot-separated path from the dictionary using pop(). :param data: The dictionary to modify. @@ -126,6 +126,7 @@ def delete_by_path(d: dict[str, Any], path: str) -> Any: V = TypeVar("V") +# TODO: Use PathTuple def get_value_by_path( d: DictLike, path: str, diff --git a/pkgs/clan-cli/clan_lib/services/modules.py b/pkgs/clan-cli/clan_lib/services/modules.py index 3416fdeb6..495d02659 100644 --- a/pkgs/clan-cli/clan_lib/services/modules.py +++ b/pkgs/clan-cli/clan_lib/services/modules.py @@ -15,7 +15,11 @@ from clan_lib.nix_models.clan import ( InventoryInstancesType, ) from clan_lib.persist.inventory_store import InventoryStore -from clan_lib.persist.path_utils import get_value_by_path, set_value_by_path +from clan_lib.persist.path_utils import ( + delete_by_path_tuple, + get_value_by_path, + set_value_by_path, +) log = logging.getLogger(__name__) @@ -492,6 +496,37 @@ def list_service_instances(flake: Flake) -> dict[str, InventoryInstanceInfo]: return res +@API.register +def delete_service_instance( + flake: Flake, + instance_ref: str, +) -> None: + """Deletes an instance + + :param instance_ref: The name of the instance to update + + :raises ClanError: If the instance_ref is invalid or cannot be deleted + """ + inventory_store = InventoryStore(flake) + inventory = inventory_store.read() + + instance: InventoryInstance | None = get_value_by_path( + inventory, f"instances.{instance_ref}", None + ) + + if instance is None: + msg = f"Instance '{instance_ref}' not found" + raise ClanError(msg) + + delete_by_path_tuple(inventory, ("instances", f"{instance_ref}")) + + # TODO: improve error message + # "Cannot delete path 'instances.static" + inventory_store.write( + inventory, message=f"Delete service instance '{instance_ref}'" + ) + + @API.register def set_service_instance( flake: Flake, instance_ref: str, roles: InventoryInstanceRolesType @@ -540,6 +575,8 @@ def set_service_instance( ) # override settings, machines only if passed + # TODO: refactor after extraModules removal + # in https://git.clan.lol/clan/clan-core/pulls/5634 merged = { **static, **role_cfg, diff --git a/pkgs/clan-cli/clan_lib/services/modules_test.py b/pkgs/clan-cli/clan_lib/services/modules_test.py index ff530650c..0c3221f28 100644 --- a/pkgs/clan-cli/clan_lib/services/modules_test.py +++ b/pkgs/clan-cli/clan_lib/services/modules_test.py @@ -6,6 +6,7 @@ from clan_cli.tests.fixtures_flakes import nested_dict from clan_lib.errors import ClanError from clan_lib.flake.flake import Flake from clan_lib.services.modules import ( + delete_service_instance, get_service_readmes, list_service_instances, list_service_modules, @@ -251,3 +252,67 @@ def test_update_service_instance( assert updated_machines == { "sara": {"settings": {"greeting": "sara"}}, } + + +@pytest.mark.with_core +def test_delete_service_instance( + clan_flake: Callable[..., Flake], +) -> None: + # Data that can be mutated via API calls + mutable_inventory_json: Inventory = { + "instances": { + "to-remain": {"module": {"name": "admin"}}, + "to-delete": {"module": {"name": "admin"}}, + } + } + + flake = clan_flake({}, mutable_inventory_json=mutable_inventory_json) + + # Ensure preconditions + instances = list_service_instances(flake) + assert set(instances.keys()) == {"to-delete", "to-remain"} + + # Raises for non-existing instance + with pytest.raises(ClanError) as excinfo: + delete_service_instance(flake, "non-existing-instance") + assert "Instance 'non-existing-instance' not found" in str(excinfo.value) + + # Deletes instance + delete_service_instance(flake, "to-delete") + + updated_instances = list_service_instances(flake) + assert set(updated_instances.keys()) == {"to-remain"} + + +@pytest.mark.with_core +def test_delete_static_service_instance( + clan_flake: Callable[..., Flake], +) -> None: + # Data that can be mutated via API calls + mutable_inventory_json: Inventory = { + "instances": { + "static": {"module": {"name": "admin"}}, + } + } + + flake = clan_flake( + { + "inventory": { + "instances": { + "static": {"roles": {"default": {}}}, + } + } + }, + mutable_inventory_json=mutable_inventory_json, + ) + + # Ensure preconditions + instances = list_service_instances(flake) + assert set(instances.keys()) == {"static"} + + # Raises for non-existing instance + with pytest.raises(ClanError) as excinfo: + delete_service_instance(flake, "static") + + # TODO: improve error message + assert "Cannot delete path 'instances.static" in str(excinfo.value)