api/modules: add foreign key to instances
This commit is contained in:
@@ -49,7 +49,7 @@ type ModuleItem = ServiceModules[number];
|
||||
|
||||
interface Module {
|
||||
value: string;
|
||||
input?: string;
|
||||
input?: string | null;
|
||||
label: string;
|
||||
description: string;
|
||||
raw: ModuleItem;
|
||||
@@ -68,19 +68,20 @@ const SelectService = () => {
|
||||
createEffect(() => {
|
||||
if (serviceModulesQuery.data && serviceInstancesQuery.data) {
|
||||
setModuleOptions(
|
||||
serviceModulesQuery.data.map((m) => ({
|
||||
value: `${m.module.name}:${m.module.input}`,
|
||||
label: m.module.name,
|
||||
description: m.info.manifest.description,
|
||||
input: m.module.input,
|
||||
raw: m,
|
||||
serviceModulesQuery.data.modules.map((currService) => ({
|
||||
value: `${currService.usage_ref.name}:${currService.usage_ref.input}`,
|
||||
label: currService.info.manifest.name,
|
||||
description: currService.info.manifest.description,
|
||||
input: currService.usage_ref.input,
|
||||
raw: currService,
|
||||
// TODO: include the instances that use this module
|
||||
instances: Object.entries(serviceInstancesQuery.data)
|
||||
.filter(
|
||||
([name, i]) =>
|
||||
i.module.module.name === m.module.name &&
|
||||
(!i.module.module.input ||
|
||||
i.module.module.input === m.module.input),
|
||||
([name, instance]) =>
|
||||
instance.resolved.usage_ref.name ===
|
||||
currService.usage_ref.name &&
|
||||
instance.resolved.usage_ref.input ===
|
||||
currService.usage_ref.input,
|
||||
)
|
||||
.map(([name, _]) => name),
|
||||
})),
|
||||
|
||||
@@ -11,6 +11,7 @@ from clan_lib.nix_models.clan import (
|
||||
InventoryInstanceModule,
|
||||
InventoryInstanceModuleType,
|
||||
InventoryInstanceRolesType,
|
||||
InventoryInstancesType,
|
||||
)
|
||||
from clan_lib.persist.inventory_store import InventoryStore
|
||||
from clan_lib.persist.util import set_value_by_path
|
||||
@@ -157,7 +158,8 @@ class Module:
|
||||
# To use this module specify: InventoryInstanceModule :: { input, name } (foreign key)
|
||||
usage_ref: InventoryInstanceModule
|
||||
info: ModuleInfo
|
||||
native: bool # Equivalent to input == None
|
||||
native: bool
|
||||
instance_refs: list[str]
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -166,6 +168,41 @@ class ClanModules:
|
||||
core_input_name: str
|
||||
|
||||
|
||||
def find_instance_refs_for_module(
|
||||
instances: InventoryInstancesType,
|
||||
module_ref: InventoryInstanceModule,
|
||||
core_input_name: str,
|
||||
) -> list[str]:
|
||||
"""Find all usages of a given module by its module_ref
|
||||
|
||||
If the module is native:
|
||||
module_ref.input := None
|
||||
<instance>.module.name := None
|
||||
|
||||
If module is from explicit input
|
||||
<instance>.module.name != None
|
||||
module_ref.input could be None, if explicit input refers to a native module
|
||||
|
||||
"""
|
||||
res: list[str] = []
|
||||
for instance_name, instance in instances.items():
|
||||
local_ref = instance.get("module")
|
||||
if not local_ref:
|
||||
continue
|
||||
|
||||
local_name: str = local_ref.get("name", instance_name)
|
||||
local_input: str | None = local_ref.get("input")
|
||||
|
||||
# Normal match
|
||||
if (
|
||||
local_name == module_ref.get("name")
|
||||
and local_input == module_ref.get("input")
|
||||
) or (local_input == core_input_name and local_name == module_ref.get("name")):
|
||||
res.append(instance_name)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
@API.register
|
||||
def list_service_modules(flake: Flake) -> ClanModules:
|
||||
"""Show information about a module"""
|
||||
@@ -178,6 +215,8 @@ def list_service_modules(flake: Flake) -> ClanModules:
|
||||
builtin_modules: dict[str, Any] = flake.select(
|
||||
"clanInternals.inventoryClass.staticModules"
|
||||
)
|
||||
inventory_store = InventoryStore(flake)
|
||||
instances = inventory_store.read().get("instances", {})
|
||||
|
||||
first_name, first_module = next(iter(builtin_modules.items()))
|
||||
clan_input_name = None
|
||||
@@ -197,12 +236,18 @@ def list_service_modules(flake: Flake) -> ClanModules:
|
||||
res: list[Module] = []
|
||||
for input_name, module_set in modules.items():
|
||||
for module_name, module_info in module_set.items():
|
||||
module_ref = InventoryInstanceModule(
|
||||
{
|
||||
"name": module_name,
|
||||
"input": None if input_name == clan_input_name else input_name,
|
||||
}
|
||||
)
|
||||
res.append(
|
||||
Module(
|
||||
usage_ref={
|
||||
"name": module_name,
|
||||
"input": None if input_name == clan_input_name else input_name,
|
||||
},
|
||||
instance_refs=find_instance_refs_for_module(
|
||||
instances, module_ref, clan_input_name
|
||||
),
|
||||
usage_ref=module_ref,
|
||||
info=ModuleInfo(
|
||||
roles=module_info.get("roles", {}),
|
||||
manifest=ModuleManifest.from_dict(module_info["manifest"]),
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
from clan_cli.tests.fixtures_flakes import nested_dict
|
||||
from clan_lib.flake.flake import Flake
|
||||
from clan_lib.services.modules import list_service_instances, list_service_modules
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from clan_lib.nix_models.clan import Clan
|
||||
|
||||
|
||||
@pytest.mark.with_core
|
||||
def test_list_service_instances(
|
||||
clan_flake: Callable[..., Flake],
|
||||
) -> None:
|
||||
# ATTENTION! This method lacks Typechecking
|
||||
config = nested_dict()
|
||||
# explicit module selection
|
||||
# We use this random string in test to avoid code dependencies on the input name
|
||||
@@ -51,3 +56,50 @@ def test_list_service_instances(
|
||||
|
||||
assert instances["baz"].resolved.usage_ref.get("input") is None
|
||||
assert instances["baz"].resolved.usage_ref.get("name") == "sshd"
|
||||
|
||||
|
||||
@pytest.mark.with_core
|
||||
def test_list_service_modules(
|
||||
clan_flake: Callable[..., Flake],
|
||||
) -> None:
|
||||
# Nice! This is typechecked :)
|
||||
clan_config: Clan = {
|
||||
"inventory": {
|
||||
"instances": {
|
||||
# No module spec -> resolves to clan-core/admin
|
||||
"admin": {},
|
||||
# Partial module spec
|
||||
"admin2": {"module": {"name": "admin"}},
|
||||
# Full explicit module spec
|
||||
"admin3": {
|
||||
"module": {
|
||||
"name": "admin",
|
||||
"input": "Y2xhbi1jaW9yZS1uZXZlci1kZXBlbmQtb24tbWU",
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
flake = clan_flake(clan_config)
|
||||
|
||||
service_modules = list_service_modules(flake)
|
||||
|
||||
# Detects the input name right
|
||||
assert service_modules.core_input_name == "Y2xhbi1jaW9yZS1uZXZlci1kZXBlbmQtb24tbWU"
|
||||
assert len(service_modules.modules)
|
||||
|
||||
admin_service = next(
|
||||
m for m in service_modules.modules if m.usage_ref.get("name") == "admin"
|
||||
)
|
||||
assert admin_service
|
||||
|
||||
assert admin_service.usage_ref == {"name": "admin", "input": None}
|
||||
assert set(admin_service.instance_refs) == {"admin", "admin2", "admin3"}
|
||||
|
||||
# Negative test: Assert not used
|
||||
sshd_service = next(
|
||||
m for m in service_modules.modules if m.usage_ref.get("name") == "sshd"
|
||||
)
|
||||
assert sshd_service
|
||||
assert sshd_service.usage_ref == {"name": "sshd", "input": None}
|
||||
assert set(sshd_service.instance_refs) == set({})
|
||||
|
||||
Reference in New Issue
Block a user