From 5eb6b703f05e9a1b7376221eaabe9bcc8b35625c Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Thu, 16 Oct 2025 12:50:54 +0200 Subject: [PATCH 1/4] inventoryStore: align class names and methods --- pkgs/clan-cli/clan_lib/clan/get.py | 2 +- pkgs/clan-cli/clan_lib/machines/actions.py | 2 +- .../clan_lib/persist/inventory_store.py | 24 +++++++++---------- .../clan_lib/persist/inventory_store_test.py | 2 +- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/pkgs/clan-cli/clan_lib/clan/get.py b/pkgs/clan-cli/clan_lib/clan/get.py index adf5a469a..f3ac702da 100644 --- a/pkgs/clan-cli/clan_lib/clan/get.py +++ b/pkgs/clan-cli/clan_lib/clan/get.py @@ -51,7 +51,7 @@ def get_clan_details_schema(flake: Flake) -> dict[str, FieldSchema]: """ inventory_store = InventoryStore(flake) - write_info = inventory_store.get_write_map() + write_info = inventory_store.get_attribute_props() field_names = retrieve_typed_field_names(InventoryMeta) diff --git a/pkgs/clan-cli/clan_lib/machines/actions.py b/pkgs/clan-cli/clan_lib/machines/actions.py index 67cc4058f..63803ff10 100644 --- a/pkgs/clan-cli/clan_lib/machines/actions.py +++ b/pkgs/clan-cli/clan_lib/machines/actions.py @@ -170,7 +170,7 @@ def get_machine_fields_schema(machine: Machine) -> dict[str, FieldSchema]: """ inventory_store = InventoryStore(machine.flake) - write_info = inventory_store.get_write_map() + write_info = inventory_store.get_attribute_props() field_names = retrieve_typed_field_names(InventoryMachine) diff --git a/pkgs/clan-cli/clan_lib/persist/inventory_store.py b/pkgs/clan-cli/clan_lib/persist/inventory_store.py index 1cac77351..de14145b8 100644 --- a/pkgs/clan-cli/clan_lib/persist/inventory_store.py +++ b/pkgs/clan-cli/clan_lib/persist/inventory_store.py @@ -77,8 +77,8 @@ def sanitize(data: Any, whitelist_paths: list[str], current_path: list[str]) -> @dataclass -class WriteInfo: - writeables: AttributeMap +class PersistenceInfo: + attribute_props: AttributeMap data_eval: "InventorySnapshot" data_disk: "InventorySnapshot" @@ -184,7 +184,7 @@ class InventoryStore: return inventory - def _get_inventory_current_priority(self) -> dict: + def _get_introspection(self) -> dict: """Returns the current priority of the inventory values machines = { @@ -203,33 +203,33 @@ class InventoryStore: """ return self._flake.select("clanInternals.inventoryClass.introspection") - def _write_map(self) -> WriteInfo: + def _get_persistence_info(self) -> PersistenceInfo: """Get the paths of the writeable keys in the inventory Load the inventory and determine the writeable keys Performs 2 nix evaluations to get the current priority and the inventory """ - current_priority = self._get_inventory_current_priority() + current_priority = self._get_introspection() data_eval: InventorySnapshot = self._load_merged_inventory() data_disk: InventorySnapshot = self._get_persisted() - write_map = compute_attribute_persistence( + attribute_props = compute_attribute_persistence( current_priority, dict(data_eval), dict(data_disk), inventory_file_name=self.inventory_file.name, ) - return WriteInfo(write_map, data_eval, data_disk) + return PersistenceInfo(attribute_props, data_eval, data_disk) - def get_write_map(self) -> Any: + def get_attribute_props(self) -> Any: """Get the writeability of the inventory :return: A dictionary with the writeability of all paths """ - write_info = self._write_map() - return write_info.writeables + persistence_info = self._get_persistence_info() + return persistence_info.attribute_props def read(self) -> InventorySnapshot: """Accessor to the merged inventory @@ -265,12 +265,12 @@ class InventoryStore: """Write the inventory to the flake directory and commit it to git with the given message """ - write_info = self._write_map() + write_info = self._get_persistence_info() patchset, delete_set = calc_patches( dict(write_info.data_disk), dict(update), dict(write_info.data_eval), - write_info.writeables, + write_info.attribute_props, ) persisted = dict(write_info.data_disk) diff --git a/pkgs/clan-cli/clan_lib/persist/inventory_store_test.py b/pkgs/clan-cli/clan_lib/persist/inventory_store_test.py index 1364e39ae..b696bfdda 100644 --- a/pkgs/clan-cli/clan_lib/persist/inventory_store_test.py +++ b/pkgs/clan-cli/clan_lib/persist/inventory_store_test.py @@ -153,7 +153,7 @@ def test_simple_deferred(setup_test_files: Path) -> None: _keys={"foo"}, # disable toplevel filtering ) - attribute_props = store._write_map().writeables + attribute_props = store._get_persistence_info().attribute_props assert attribute_props == { ("foo",): {PersistenceAttribute.WRITE}, ("foo", "a"): {PersistenceAttribute.WRITE, PersistenceAttribute.DELETE}, From 892cb1baaeb975fb878fd4c7cf64fbac533fc675 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Thu, 16 Oct 2025 12:57:02 +0200 Subject: [PATCH 2/4] inventoryStore: invert writeability terms to readonly --- pkgs/clan-cli/clan_lib/persist/patch_engine.py | 4 ++-- pkgs/clan-cli/clan_lib/persist/patch_engine_test.py | 4 ++-- pkgs/clan-cli/clan_lib/persist/validate.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkgs/clan-cli/clan_lib/persist/patch_engine.py b/pkgs/clan-cli/clan_lib/persist/patch_engine.py index 95bed408d..63924a07f 100644 --- a/pkgs/clan-cli/clan_lib/persist/patch_engine.py +++ b/pkgs/clan-cli/clan_lib/persist/patch_engine.py @@ -12,9 +12,9 @@ from clan_lib.persist.path_utils import ( from clan_lib.persist.validate import ( validate_list_uniqueness, validate_no_static_deletion, + validate_not_readonly, validate_patch_conflicts, validate_type_compatibility, - validate_writeability, ) from clan_lib.persist.write_rules import AttributeMap, PersistenceAttribute @@ -131,7 +131,7 @@ Please report this issue at https://git.clan.lol/clan/clan-core/issues continue # Validate the change is allowed - validate_writeability(path, attribute_props) + validate_not_readonly(path, attribute_props) validate_type_compatibility(path, old_value, new_value) validate_list_uniqueness(path, new_value) diff --git a/pkgs/clan-cli/clan_lib/persist/patch_engine_test.py b/pkgs/clan-cli/clan_lib/persist/patch_engine_test.py index f3b711b04..c8abc3c27 100644 --- a/pkgs/clan-cli/clan_lib/persist/patch_engine_test.py +++ b/pkgs/clan-cli/clan_lib/persist/patch_engine_test.py @@ -69,7 +69,7 @@ def test_update_add_empty_dict() -> None: data_disk: dict = {} - writeables = compute_attribute_persistence(prios, data_eval, data_disk) + attribute_props = compute_attribute_persistence(prios, data_eval, data_disk) update = deepcopy(data_eval) @@ -79,7 +79,7 @@ def test_update_add_empty_dict() -> None: data_disk, update, all_values=data_eval, - attribute_props=writeables, + attribute_props=attribute_props, ) assert patchset == {("foo", "mimi"): {}} # this is what gets persisted diff --git a/pkgs/clan-cli/clan_lib/persist/validate.py b/pkgs/clan-cli/clan_lib/persist/validate.py index d00c9f8bb..bd79207f0 100644 --- a/pkgs/clan-cli/clan_lib/persist/validate.py +++ b/pkgs/clan-cli/clan_lib/persist/validate.py @@ -20,7 +20,7 @@ def validate_no_static_deletion( raise ClanError(msg) -def validate_writeability(path: PathTuple, writeables: AttributeMap) -> None: +def validate_not_readonly(path: PathTuple, writeables: AttributeMap) -> None: """Validate that a path is writeable.""" if not is_writeable_path(path, writeables): msg = f"Path '{path_to_string(path)}' is readonly. - It seems its value is statically defined in nix." From 727474055ef77d791bbc5264d1ac409005e50ba7 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Thu, 16 Oct 2025 13:01:19 +0200 Subject: [PATCH 3/4] persistence: invert all writeability logic into not readonly --- pkgs/clan-cli/clan_lib/clan/get.py | 4 ++-- pkgs/clan-cli/clan_lib/machines/actions.py | 4 ++-- pkgs/clan-cli/clan_lib/persist/validate.py | 4 ++-- pkgs/clan-cli/clan_lib/persist/write_rules.py | 10 +++++----- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pkgs/clan-cli/clan_lib/clan/get.py b/pkgs/clan-cli/clan_lib/clan/get.py index f3ac702da..3ab7d4315 100644 --- a/pkgs/clan-cli/clan_lib/clan/get.py +++ b/pkgs/clan-cli/clan_lib/clan/get.py @@ -7,7 +7,7 @@ from clan_lib.machines.actions import FieldSchema from clan_lib.nix_models.clan import InventoryMeta from clan_lib.persist.introspection import retrieve_typed_field_names from clan_lib.persist.inventory_store import InventoryStore -from clan_lib.persist.write_rules import is_writeable_key +from clan_lib.persist.write_rules import is_readonly_key log = logging.getLogger(__name__) @@ -57,7 +57,7 @@ def get_clan_details_schema(flake: Flake) -> dict[str, FieldSchema]: return { field: { - "readonly": not is_writeable_key(f"meta.{field}", write_info), + "readonly": is_readonly_key(f"meta.{field}", write_info), # TODO: Provide a meaningful reason "reason": None, "readonly_members": [], diff --git a/pkgs/clan-cli/clan_lib/machines/actions.py b/pkgs/clan-cli/clan_lib/machines/actions.py index 63803ff10..1d117c553 100644 --- a/pkgs/clan-cli/clan_lib/machines/actions.py +++ b/pkgs/clan-cli/clan_lib/machines/actions.py @@ -18,7 +18,7 @@ from clan_lib.persist.path_utils import ( list_difference, set_value_by_path, ) -from clan_lib.persist.write_rules import is_writeable_key +from clan_lib.persist.write_rules import is_readonly_key @dataclass @@ -195,7 +195,7 @@ def get_machine_fields_schema(machine: Machine) -> dict[str, FieldSchema]: "readonly": ( True if field in protected_fields - else not is_writeable_key( + else is_readonly_key( f"machines.{machine.name}.{field}", write_info, ) diff --git a/pkgs/clan-cli/clan_lib/persist/validate.py b/pkgs/clan-cli/clan_lib/persist/validate.py index bd79207f0..6d09a6a87 100644 --- a/pkgs/clan-cli/clan_lib/persist/validate.py +++ b/pkgs/clan-cli/clan_lib/persist/validate.py @@ -7,7 +7,7 @@ from clan_lib.persist.path_utils import ( path_starts_with, path_to_string, ) -from clan_lib.persist.write_rules import AttributeMap, is_writeable_path +from clan_lib.persist.write_rules import AttributeMap, is_readonly_path def validate_no_static_deletion( @@ -22,7 +22,7 @@ def validate_no_static_deletion( def validate_not_readonly(path: PathTuple, writeables: AttributeMap) -> None: """Validate that a path is writeable.""" - if not is_writeable_path(path, writeables): + if is_readonly_path(path, writeables): msg = f"Path '{path_to_string(path)}' is readonly. - It seems its value is statically defined in nix." raise ClanError(msg) diff --git a/pkgs/clan-cli/clan_lib/persist/write_rules.py b/pkgs/clan-cli/clan_lib/persist/write_rules.py index 9918fe9ea..47b1597ce 100644 --- a/pkgs/clan-cli/clan_lib/persist/write_rules.py +++ b/pkgs/clan-cli/clan_lib/persist/write_rules.py @@ -16,7 +16,7 @@ class PersistenceAttribute(Enum): type AttributeMap = dict[PathTuple, set[PersistenceAttribute]] -def is_writeable_path( +def is_readonly_path( key: PathTuple, attributes: AttributeMap, ) -> bool: @@ -30,9 +30,9 @@ def is_writeable_path( current_path = remaining if current_path in attributes: if PersistenceAttribute.WRITE in attributes[current_path]: - return True - if PersistenceAttribute.READONLY in attributes[current_path]: return False + if PersistenceAttribute.READONLY in attributes[current_path]: + return True # Check the parent path remaining = remaining[:-1] @@ -40,7 +40,7 @@ def is_writeable_path( raise ClanError(msg) -def is_writeable_key( +def is_readonly_key( key: str, attributes: AttributeMap, ) -> bool: @@ -51,7 +51,7 @@ def is_writeable_key( In case of ambiguity use is_writeable_path with tuple keys. """ items = key.split(".") - return is_writeable_path(tuple(items), attributes) + return is_readonly_path(tuple(items), attributes) def get_priority(value: Any) -> int | None: From 17c35c42596045008f5c2a696d8537a1250a4e65 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Thu, 16 Oct 2025 13:04:24 +0200 Subject: [PATCH 4/4] persistence: align some more variable names --- pkgs/clan-cli/clan_lib/clan/get.py | 4 ++-- pkgs/clan-cli/clan_lib/machines/actions.py | 4 ++-- pkgs/clan-cli/clan_lib/machines/actions_test.py | 10 ++++++---- pkgs/clan-cli/clan_lib/persist/inventory_store.py | 10 +++++----- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/pkgs/clan-cli/clan_lib/clan/get.py b/pkgs/clan-cli/clan_lib/clan/get.py index 3ab7d4315..ba8006de0 100644 --- a/pkgs/clan-cli/clan_lib/clan/get.py +++ b/pkgs/clan-cli/clan_lib/clan/get.py @@ -51,13 +51,13 @@ def get_clan_details_schema(flake: Flake) -> dict[str, FieldSchema]: """ inventory_store = InventoryStore(flake) - write_info = inventory_store.get_attribute_props() + attribute_props = inventory_store.get_attribute_props() field_names = retrieve_typed_field_names(InventoryMeta) return { field: { - "readonly": is_readonly_key(f"meta.{field}", write_info), + "readonly": is_readonly_key(f"meta.{field}", attribute_props), # TODO: Provide a meaningful reason "reason": None, "readonly_members": [], diff --git a/pkgs/clan-cli/clan_lib/machines/actions.py b/pkgs/clan-cli/clan_lib/machines/actions.py index 1d117c553..8f2932cf3 100644 --- a/pkgs/clan-cli/clan_lib/machines/actions.py +++ b/pkgs/clan-cli/clan_lib/machines/actions.py @@ -170,7 +170,7 @@ def get_machine_fields_schema(machine: Machine) -> dict[str, FieldSchema]: """ inventory_store = InventoryStore(machine.flake) - write_info = inventory_store.get_attribute_props() + attribute_props = inventory_store.get_attribute_props() field_names = retrieve_typed_field_names(InventoryMachine) @@ -197,7 +197,7 @@ def get_machine_fields_schema(machine: Machine) -> dict[str, FieldSchema]: if field in protected_fields else is_readonly_key( f"machines.{machine.name}.{field}", - write_info, + attribute_props, ) ), # TODO: Provide a meaningful reason diff --git a/pkgs/clan-cli/clan_lib/machines/actions_test.py b/pkgs/clan-cli/clan_lib/machines/actions_test.py index c09c23392..5bbbb2254 100644 --- a/pkgs/clan-cli/clan_lib/machines/actions_test.py +++ b/pkgs/clan-cli/clan_lib/machines/actions_test.py @@ -249,13 +249,15 @@ def test_get_machine_writeability(clan_flake: Callable[..., Flake]) -> None: persisted = inventory_store._get_persisted() assert get_value_by_path(persisted, "machines.jon.tags", []) == new_tags - write_info = get_machine_fields_schema(Machine("jon", flake)) + attribute_props = get_machine_fields_schema(Machine("jon", flake)) # {'tags': {'writable': True, 'reason': None}, 'machineClass': {'writable': False, 'reason': None}, 'name': {'writable': False, 'reason': None}, 'description': {'writable': True, 'reason': None}, 'deploy.buildHost': {'writable': True, 'reason': None}, 'icon': {'writable': True, 'reason': None}, 'deploy.targetHost': {'writable': True, 'reason': None}} writeable_fields = { - field for field, info in write_info.items() if not info["readonly"] + field for field, info in attribute_props.items() if not info["readonly"] + } + read_only_fields = { + field for field, info in attribute_props.items() if info["readonly"] } - read_only_fields = {field for field, info in write_info.items() if info["readonly"]} assert writeable_fields == { "tags", @@ -267,7 +269,7 @@ def test_get_machine_writeability(clan_flake: Callable[..., Flake]) -> None: } assert read_only_fields == {"machineClass", "name"} - assert write_info["tags"]["readonly_members"] == ["nix1", "all", "nixos"] + assert attribute_props["tags"]["readonly_members"] == ["nix1", "all", "nixos"] @pytest.mark.with_core diff --git a/pkgs/clan-cli/clan_lib/persist/inventory_store.py b/pkgs/clan-cli/clan_lib/persist/inventory_store.py index de14145b8..67779929c 100644 --- a/pkgs/clan-cli/clan_lib/persist/inventory_store.py +++ b/pkgs/clan-cli/clan_lib/persist/inventory_store.py @@ -265,15 +265,15 @@ class InventoryStore: """Write the inventory to the flake directory and commit it to git with the given message """ - write_info = self._get_persistence_info() + persistence_info = self._get_persistence_info() patchset, delete_set = calc_patches( - dict(write_info.data_disk), + dict(persistence_info.data_disk), dict(update), - dict(write_info.data_eval), - write_info.attribute_props, + dict(persistence_info.data_eval), + persistence_info.attribute_props, ) - persisted = dict(write_info.data_disk) + persisted = dict(persistence_info.data_disk) for patch_path, data in patchset.items(): set_value_by_path_tuple(persisted, patch_path, data)