From 02740078b7cdfe6752e1b63832943d706b6e8061 Mon Sep 17 00:00:00 2001 From: Cassandra Volkova Date: Thu, 23 Jul 2026 02:10:36 +0300 Subject: [PATCH] fix(iam): align binding agent kinds with manifest schema Register the plural permission and role binding resource kinds emitted by the manifest schema while retaining the legacy singular aliases. Migrate persisted core-agent configs during bootstrap so upgrades take effect without resetting stand configuration. --- etc/exordos_core/core_agent.conf.j2 | 2 + exordos/manifests/examples/user.yaml | 4 +- exordos_core/cmd/bootstrap.py | 67 ++++++++++++++++ .../cmd/test_bootstrap_core_agent_config.py | 77 +++++++++++++++++++ 4 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 exordos_core/tests/unit/cmd/test_bootstrap_core_agent_config.py diff --git a/etc/exordos_core/core_agent.conf.j2 b/etc/exordos_core/core_agent.conf.j2 index a54a7610..b85870cd 100644 --- a/etc/exordos_core/core_agent.conf.j2 +++ b/etc/exordos_core/core_agent.conf.j2 @@ -26,9 +26,11 @@ em_core_dns_domains_records = exordos_core.user_api.dns.dm.models:Record em_core_iam_organizations = exordos_core.user_api.iam.dm.models:Organization em_core_iam_organizations_members = exordos_core.user_api.iam.dm.models:OrganizationMember em_core_iam_roles = exordos_core.user_api.iam.dm.models:Role +em_core_iam_role_bindings = exordos_core.user_api.iam.dm.models:RoleBinding em_core_iam_rolebinding = exordos_core.user_api.iam.dm.models:RoleBinding em_core_iam_projects = exordos_core.user_api.iam.dm.models:Project em_core_iam_permissions = exordos_core.user_api.iam.dm.models:Permission +em_core_iam_permission_bindings = exordos_core.user_api.iam.dm.models:PermissionBinding em_core_iam_permissionbinding = exordos_core.user_api.iam.dm.models:PermissionBinding em_core_vs_profiles = exordos_core.vs.dm.models:Profile em_core_vs_variables = exordos_core.vs.dm.models:Variable diff --git a/exordos/manifests/examples/user.yaml b/exordos/manifests/examples/user.yaml index e7179fe0..2d4c506a 100644 --- a/exordos/manifests/examples/user.yaml +++ b/exordos/manifests/examples/user.yaml @@ -29,7 +29,7 @@ resources: last_name: "Doe" description: "production engineer" - $core.iam.rolebinding: + $core.iam.role_bindings: jdoe_global_role_binding: role: "726f6c65-0000-0000-0000-000000000002" user: $core.iam.users.$jdoe:uuid @@ -42,4 +42,4 @@ resources: jdoe_organizations_member: organization: "$core.iam.organizations.$jdoe_corp:uuid" user: $core.iam.users.$jdoe:uuid - role: "OWNER" \ No newline at end of file + role: "OWNER" diff --git a/exordos_core/cmd/bootstrap.py b/exordos_core/cmd/bootstrap.py index ade2212b..27843403 100644 --- a/exordos_core/cmd/bootstrap.py +++ b/exordos_core/cmd/bootstrap.py @@ -303,8 +303,10 @@ def _ensure_bootstrap_repo(manifests_dir: str) -> repo_models.Repository: MIGRATION_REPO_NAME = "migration-dummy-repo" CORE_CONFIG_PATH = "/etc/exordos_core/exordos_core.conf" +CORE_AGENT_CONFIG_PATH = "/etc/exordos_core/core_agent.conf" UA_CONFIG_PATH = "/etc/exordos_universal_agent/exordos_universal_agent.conf" CORE_CONFIG_DATA_PATH = "/var/lib/exordos/data/etc/exordos_core/exordos_core.conf" +CORE_AGENT_CONFIG_DATA_PATH = "/var/lib/exordos/data/etc/exordos_core/core_agent.conf" UA_CONFIG_DATA_PATH = ( "/var/lib/exordos/data/etc/exordos_universal_agent/exordos_universal_agent.conf" ) @@ -374,6 +376,70 @@ def _migrate_installed_elements_configs() -> None: """ _UA_BORDER_DRIVER_LINE = " BorderAgentCapabilityDriver,\n" +_CORE_AGENT_IAM_BINDING_KINDS = ( + ("em_core_iam_role_bindings", "em_core_iam_rolebinding"), + ("em_core_iam_permission_bindings", "em_core_iam_permissionbinding"), +) + + +def _ensure_core_agent_config_current() -> None: + """Add canonical IAM binding kinds to an existing core-agent config. + + Older persisted configs register only the deprecated singular kinds. Keep + those aliases for existing manifests and add the plural kinds emitted by + the current manifest schema. + """ + try: + with open(CORE_AGENT_CONFIG_PATH, "r", encoding="utf-8") as f: + content = f.read() + except FileNotFoundError: + LOG.warning("Core agent config not found: %s", CORE_AGENT_CONFIG_PATH) + return + + new_content = content + for canonical_kind, compatibility_kind in _CORE_AGENT_IAM_BINDING_KINDS: + if re.search(rf"^{re.escape(canonical_kind)}\s*=", new_content, re.MULTILINE): + continue + + compatibility_line = re.search( + rf"^{re.escape(compatibility_kind)}(?P\s*=.*)$", + new_content, + re.MULTILINE, + ) + if compatibility_line is None: + LOG.warning( + "Could not add %s because compatibility kind %s is absent in %s", + canonical_kind, + compatibility_kind, + CORE_AGENT_CONFIG_PATH, + ) + continue + + canonical_line = canonical_kind + compatibility_line.group("assignment") + new_content = new_content.replace( + compatibility_line.group(0), + canonical_line + "\n" + compatibility_line.group(0), + 1, + ) + + if new_content == content: + LOG.info("Core agent config already up to date in %s", CORE_AGENT_CONFIG_PATH) + return + + with open(CORE_AGENT_CONFIG_PATH, "w", encoding="utf-8") as f: + f.write(new_content) + LOG.info("Updated core agent config %s", CORE_AGENT_CONFIG_PATH) + _sync_config_to_data_path(new_content, CORE_AGENT_CONFIG_DATA_PATH) + + try: + subprocess.run( + ["systemctl", "try-restart", "ec-core-agent"], + check=True, + ) + LOG.info("Restarted core agent service to apply the new config") + except (OSError, subprocess.CalledProcessError) as e: + LOG.warning("Failed to restart core agent service: %s", e) + def _ensure_ua_config_current() -> None: """Bring the universal agent config up to date with this image. @@ -736,6 +802,7 @@ def main() -> None: # working installations are migrated. _migrate_installed_elements_to_repo() + _ensure_core_agent_config_current() _ensure_ua_config_current() if not os.path.exists(SPEC_PATH): diff --git a/exordos_core/tests/unit/cmd/test_bootstrap_core_agent_config.py b/exordos_core/tests/unit/cmd/test_bootstrap_core_agent_config.py new file mode 100644 index 00000000..86825714 --- /dev/null +++ b/exordos_core/tests/unit/cmd/test_bootstrap_core_agent_config.py @@ -0,0 +1,77 @@ +# Copyright 2026 Genesis Corporation. +# +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from unittest import mock + +from exordos_core.cmd import bootstrap + +_OLD_CORE_AGENT_CONFIG = """\ +[models] +em_core_iam_roles = exordos_core.user_api.iam.dm.models:Role +em_core_iam_rolebinding = exordos_core.user_api.iam.dm.models:RoleBinding +em_core_iam_permissions = exordos_core.user_api.iam.dm.models:Permission +em_core_iam_permissionbinding = exordos_core.user_api.iam.dm.models:PermissionBinding +""" + + +def _run(tmp_path): + etc_path = tmp_path / "core_agent.conf" + data_path = tmp_path / "data" / "core_agent.conf" + with ( + mock.patch.object(bootstrap, "CORE_AGENT_CONFIG_PATH", str(etc_path)), + mock.patch.object(bootstrap, "CORE_AGENT_CONFIG_DATA_PATH", str(data_path)), + mock.patch.object(bootstrap.subprocess, "run") as run, + ): + bootstrap._ensure_core_agent_config_current() + return etc_path, data_path, run + + +def test_adds_canonical_iam_binding_kinds_to_old_config(tmp_path): + etc_path = tmp_path / "core_agent.conf" + etc_path.write_text(_OLD_CORE_AGENT_CONFIG, encoding="utf-8") + + etc_path, data_path, run = _run(tmp_path) + + content = etc_path.read_text(encoding="utf-8") + assert "em_core_iam_role_bindings =" in content + assert "em_core_iam_permission_bindings =" in content + assert "em_core_iam_rolebinding =" in content + assert "em_core_iam_permissionbinding =" in content + assert data_path.read_text(encoding="utf-8") == content + run.assert_called_once_with( + ["systemctl", "try-restart", "ec-core-agent"], check=True + ) + + +def test_noop_on_current_config(tmp_path): + etc_path = tmp_path / "core_agent.conf" + etc_path.write_text(_OLD_CORE_AGENT_CONFIG, encoding="utf-8") + _run(tmp_path) + content = etc_path.read_text(encoding="utf-8") + + etc_path, data_path, run = _run(tmp_path) + + assert etc_path.read_text(encoding="utf-8") == content + assert data_path.read_text(encoding="utf-8") == content + run.assert_not_called() + + +def test_missing_config_is_skipped(tmp_path): + etc_path, data_path, run = _run(tmp_path) + + assert not etc_path.exists() + assert not data_path.exists() + run.assert_not_called()