clan_lib/api: init update service instance
This commit is contained in:
@@ -14,7 +14,7 @@ from clan_lib.nix_models.clan import (
|
|||||||
InventoryInstancesType,
|
InventoryInstancesType,
|
||||||
)
|
)
|
||||||
from clan_lib.persist.inventory_store import InventoryStore
|
from clan_lib.persist.inventory_store import InventoryStore
|
||||||
from clan_lib.persist.util import set_value_by_path
|
from clan_lib.persist.util import get_value_by_path, set_value_by_path
|
||||||
|
|
||||||
|
|
||||||
class CategoryInfo(TypedDict):
|
class CategoryInfo(TypedDict):
|
||||||
@@ -431,3 +431,64 @@ def list_service_instances(flake: Flake) -> dict[str, InventoryInstanceInfo]:
|
|||||||
roles=instance.get("roles", {}),
|
roles=instance.get("roles", {}),
|
||||||
)
|
)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@API.register
|
||||||
|
def update_service_instance(
|
||||||
|
flake: Flake, instance_ref: str, roles: InventoryInstanceRolesType
|
||||||
|
) -> None:
|
||||||
|
"""Update the roles of a service instance
|
||||||
|
|
||||||
|
:param instance_ref: The name of the instance to update
|
||||||
|
:param roles: The roles to update
|
||||||
|
|
||||||
|
|
||||||
|
:raises ClanError: If the instance_ref is invalid or missing required fields
|
||||||
|
"""
|
||||||
|
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)
|
||||||
|
|
||||||
|
module_ref = instance.get("module")
|
||||||
|
if module_ref is None:
|
||||||
|
msg = f"Instance '{instance_ref}' seems invalid: Missing module reference"
|
||||||
|
raise ClanError(msg)
|
||||||
|
|
||||||
|
module = resolve_service_module_ref(flake, module_ref)
|
||||||
|
allowed_roles = module.info.roles.keys()
|
||||||
|
|
||||||
|
for role_name in roles:
|
||||||
|
if role_name not in allowed_roles:
|
||||||
|
msg = f"Role '{role_name}' cannot be used in the module"
|
||||||
|
description = f"Allowed roles: {', '.join(allowed_roles)}"
|
||||||
|
raise ClanError(msg, description=description)
|
||||||
|
|
||||||
|
for role_name, role_cfg in roles.items():
|
||||||
|
if forbidden_keys := {"extraModules"} & role_cfg.keys():
|
||||||
|
msg = f"Role '{role_name}' cannot contain {', '.join(f"'{k}'" for k in forbidden_keys)} directly"
|
||||||
|
raise ClanError(msg)
|
||||||
|
|
||||||
|
static = get_value_by_path(
|
||||||
|
instance, f"roles.{role_name}", {}, expected_type=InventoryInstanceRolesType
|
||||||
|
)
|
||||||
|
|
||||||
|
# override settings, machines only if passed
|
||||||
|
merged = {
|
||||||
|
**static,
|
||||||
|
**role_cfg,
|
||||||
|
}
|
||||||
|
|
||||||
|
set_value_by_path(
|
||||||
|
inventory, f"instances.{instance_ref}.roles.{role_name}", merged
|
||||||
|
)
|
||||||
|
|
||||||
|
inventory_store.write(
|
||||||
|
inventory, message=f"Update service instance '{instance_ref}'"
|
||||||
|
)
|
||||||
|
|||||||
@@ -3,8 +3,14 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from clan_cli.tests.fixtures_flakes import nested_dict
|
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.flake.flake import Flake
|
||||||
from clan_lib.services.modules import list_service_instances, list_service_modules
|
from clan_lib.nix_models.clan import Inventory
|
||||||
|
from clan_lib.services.modules import (
|
||||||
|
list_service_instances,
|
||||||
|
list_service_modules,
|
||||||
|
update_service_instance,
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from clan_lib.nix_models.clan import Clan
|
from clan_lib.nix_models.clan import Clan
|
||||||
@@ -103,3 +109,115 @@ def test_list_service_modules(
|
|||||||
assert sshd_service
|
assert sshd_service
|
||||||
assert sshd_service.usage_ref == {"name": "sshd", "input": None}
|
assert sshd_service.usage_ref == {"name": "sshd", "input": None}
|
||||||
assert set(sshd_service.instance_refs) == set({})
|
assert set(sshd_service.instance_refs) == set({})
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.with_core
|
||||||
|
def test_update_service_instance(
|
||||||
|
clan_flake: Callable[..., Flake],
|
||||||
|
) -> None:
|
||||||
|
# Data that can be mutated via API calls
|
||||||
|
mutable_inventory_json: Inventory = {
|
||||||
|
"instances": {
|
||||||
|
"hello-world": {
|
||||||
|
"roles": {
|
||||||
|
"morning": {
|
||||||
|
"machines": {
|
||||||
|
"jon": {
|
||||||
|
"settings": { # type: ignore[typeddict-item]
|
||||||
|
"greeting": "jon",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sara": {
|
||||||
|
"settings": { # type: ignore[typeddict-item]
|
||||||
|
"greeting": "sara",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"all": {},
|
||||||
|
},
|
||||||
|
"settings": { # type: ignore[typeddict-item]
|
||||||
|
"greeting": "hello",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flake = clan_flake({}, mutable_inventory_json=mutable_inventory_json)
|
||||||
|
|
||||||
|
# Ensure preconditions
|
||||||
|
instances = list_service_instances(flake)
|
||||||
|
assert set(instances.keys()) == {"hello-world"}
|
||||||
|
|
||||||
|
# Wrong instance
|
||||||
|
with pytest.raises(ClanError) as excinfo:
|
||||||
|
update_service_instance(
|
||||||
|
flake,
|
||||||
|
"admin",
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
assert "Instance 'admin' not found" in str(excinfo.value)
|
||||||
|
|
||||||
|
# Wrong roles
|
||||||
|
with pytest.raises(ClanError) as excinfo:
|
||||||
|
update_service_instance(
|
||||||
|
flake,
|
||||||
|
"hello-world",
|
||||||
|
{"default": {"machines": {}}},
|
||||||
|
)
|
||||||
|
assert "Role 'default' cannot be used" in str(excinfo.value)
|
||||||
|
|
||||||
|
# Remove 'settings' from jon machine
|
||||||
|
update_service_instance(
|
||||||
|
flake,
|
||||||
|
"hello-world",
|
||||||
|
{
|
||||||
|
"morning": {
|
||||||
|
"machines": {
|
||||||
|
"jon": {}, # Unset the machine settings
|
||||||
|
"sara": {
|
||||||
|
"settings": { # type: ignore[typeddict-item]
|
||||||
|
"greeting": "sara",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_instances = list_service_instances(flake)
|
||||||
|
updated_machines = (
|
||||||
|
updated_instances["hello-world"].roles.get("morning", {}).get("machines", {})
|
||||||
|
)
|
||||||
|
|
||||||
|
assert updated_machines == {
|
||||||
|
"jon": {"settings": {}},
|
||||||
|
"sara": {"settings": {"greeting": "sara"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove jon
|
||||||
|
update_service_instance(
|
||||||
|
flake,
|
||||||
|
"hello-world",
|
||||||
|
{
|
||||||
|
"morning": {
|
||||||
|
"machines": {
|
||||||
|
"sara": {
|
||||||
|
"settings": { # type: ignore[typeddict-item]
|
||||||
|
"greeting": "sara",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_instances = list_service_instances(flake)
|
||||||
|
updated_machines = (
|
||||||
|
updated_instances["hello-world"].roles.get("morning", {}).get("machines", {})
|
||||||
|
)
|
||||||
|
|
||||||
|
assert updated_machines == {
|
||||||
|
"sara": {"settings": {"greeting": "sara"}},
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user