diff --git a/exordos_core/cmd/bootstrap.py b/exordos_core/cmd/bootstrap.py index 47ba6eee..3225c8f8 100644 --- a/exordos_core/cmd/bootstrap.py +++ b/exordos_core/cmd/bootstrap.py @@ -347,6 +347,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" 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" +UA_CONFIG_DATA_PATH = ( + "/var/lib/exordos/data/etc/exordos_universal_agent/exordos_universal_agent.conf" +) _LAUNCHPAD_SECTION = """\ [launchpad] @@ -359,6 +363,21 @@ def _ensure_bootstrap_repo(manifests_dir: str) -> repo_models.Repository: """ +def _sync_config_to_data_path(content: str, data_path: str) -> None: + """Write the same config content to the mirrored data path. + + Ensures config files under /var/lib/exordos/data/etc/ stay in sync + with their /etc/ counterparts. + """ + try: + os.makedirs(os.path.dirname(data_path), exist_ok=True) + with open(data_path, "w", encoding="utf-8") as f: + f.write(content) + LOG.info("Synced config to %s", data_path) + except OSError as e: + LOG.warning("Failed to sync config to %s: %s", data_path, e) + + def _migrate_installed_elements_configs() -> None: """Migrate config files for repo proxy support. @@ -382,6 +401,7 @@ def _migrate_installed_elements_configs() -> None: with open(CORE_CONFIG_PATH, "w", encoding="utf-8") as f: f.write(content) LOG.info("Added [launchpad] section to %s", CORE_CONFIG_PATH) + _sync_config_to_data_path(content, CORE_CONFIG_DATA_PATH) else: LOG.info("[launchpad] section already exists in %s", CORE_CONFIG_PATH) @@ -417,6 +437,7 @@ def _migrate_installed_elements_configs() -> None: "Updated [universal_agent_scheduler] section in %s", UA_CONFIG_PATH, ) + _sync_config_to_data_path(new_content, UA_CONFIG_DATA_PATH) else: LOG.info( "[universal_agent_scheduler] section already up to date in %s", diff --git a/exordos_core/repo/agents/universal/drivers/repo_element.py b/exordos_core/repo/agents/universal/drivers/repo_element.py index 9450c5d0..d87bce0e 100644 --- a/exordos_core/repo/agents/universal/drivers/repo_element.py +++ b/exordos_core/repo/agents/universal/drivers/repo_element.py @@ -85,20 +85,10 @@ def _make_installed_manifest( project_id=em_manifest.project_id, ) - if em_manifest.openapi_spec is not None: - installed_manifest.manifest["openapi_spec"] = em_manifest.openapi_spec - - if em_manifest.exports: - installed_manifest.manifest["exports"] = em_manifest.exports - - if em_manifest.imports: - installed_manifest.manifest["imports"] = em_manifest.imports - - if em_manifest.requirements: - installed_manifest.manifest["requirements"] = em_manifest.requirements - - if em_manifest.resources: - installed_manifest.manifest["resources"] = em_manifest.resources + for field in re_builder.InstalledManifest.MANIFEST_OPTIONAL_FIELDS: + value = getattr(em_manifest, field) + if value: + installed_manifest.manifest[field] = value return installed_manifest diff --git a/exordos_core/repo/builders/element.py b/exordos_core/repo/builders/element.py index 2385877a..cdd06a2d 100644 --- a/exordos_core/repo/builders/element.py +++ b/exordos_core/repo/builders/element.py @@ -65,6 +65,14 @@ class InstalledManifest( linked. """ + MANIFEST_OPTIONAL_FIELDS = ( + "openapi_spec", + "exports", + "imports", + "requirements", + "resources", + ) + version = properties.property( ra_types.String(min_length=5, max_length=64), required=True ) @@ -101,12 +109,21 @@ def get_resource_target_fields(self) -> tp.Collection[str]: @classmethod def from_repo_element(cls, element: models.RepoElement) -> "InstalledManifest": uuid = sys_uuid.UUID(str(element.manifest.get("uuid") or element.uuid)) + # Filter out empty optional fields to keep the manifest consistent + # with _make_installed_manifest in the repo element driver, which + # also skips empty values. Without this, the agent detects a diff + # between target and actual resources on every iteration. + manifest = { + k: v + for k, v in element.manifest.items() + if k not in cls.MANIFEST_OPTIONAL_FIELDS or v + } return cls( uuid=uuid, name=element.name, description=element.manifest.get("description", ""), version=element.version, - manifest=element.manifest, + manifest=manifest, project_id=element.project_id, ) diff --git a/exordos_core/tests/unit/repo/test_element_builder.py b/exordos_core/tests/unit/repo/test_element_builder.py index d24fadb2..4cd41035 100644 --- a/exordos_core/tests/unit/repo/test_element_builder.py +++ b/exordos_core/tests/unit/repo/test_element_builder.py @@ -502,6 +502,50 @@ def test_from_repo_element_without_manifest_uuid(self): assert result.name == element.name assert result.version == "2.0.0" + def test_from_repo_element_filters_empty_optional_fields(self): + """Empty optional fields should be excluded from the manifest.""" + element_uuid = sys_uuid.uuid4() + element = FakeElement(version="1.0.0") + element.uuid = element_uuid + element.manifest = { + "uuid": element_uuid, + "key": "value", + "exports": {}, + "imports": {}, + "requirements": {}, + "resources": {}, + "openapi_spec": None, + } + element.project_id = sys_uuid.uuid4() + + result = builder_element.InstalledManifest.from_repo_element(element) + + assert "exports" not in result.manifest + assert "imports" not in result.manifest + assert "requirements" not in result.manifest + assert "resources" not in result.manifest + assert "openapi_spec" not in result.manifest + assert result.manifest["key"] == "value" + + def test_from_repo_element_keeps_non_empty_optional_fields(self): + """Non-empty optional fields should be preserved in the manifest.""" + element_uuid = sys_uuid.uuid4() + element = FakeElement(version="1.0.0") + element.uuid = element_uuid + element.manifest = { + "uuid": element_uuid, + "exports": {"exp1": {}}, + "resources": {"res1": {}}, + "openapi_spec": {"paths": {}}, + } + element.project_id = sys_uuid.uuid4() + + result = builder_element.InstalledManifest.from_repo_element(element) + + assert result.manifest["exports"] == {"exp1": {}} + assert result.manifest["resources"] == {"res1": {}} + assert result.manifest["openapi_spec"] == {"paths": {}} + def test_get_resource_target_fields(self): manifest = builder_element.InstalledManifest( name="test", diff --git a/exordos_core/tests/unit/repo/test_repo_element_driver.py b/exordos_core/tests/unit/repo/test_repo_element_driver.py index f0e95165..9edc7b1f 100644 --- a/exordos_core/tests/unit/repo/test_repo_element_driver.py +++ b/exordos_core/tests/unit/repo/test_repo_element_driver.py @@ -146,6 +146,25 @@ def test_without_repo_element_without_em_element(self): } assert result.manifest == expected_manifest + def test_with_non_empty_optional_fields(self): + """Should include non-empty optional fields in the manifest.""" + client = self._make_client() + em_manifest = self._make_em_manifest() + em_manifest.exports = {"exp1": {}} + em_manifest.imports = {"imp1": {}} + em_manifest.requirements = {"dep": {"from_version": "1.0.0"}} + em_manifest.resources = {"res1": {}} + em_manifest.openapi_spec = {"paths": {}} + em_element = self._make_em_element() + + result = client._make_installed_manifest(em_manifest, em_element) + + assert result.manifest["exports"] == {"exp1": {}} + assert result.manifest["imports"] == {"imp1": {}} + assert result.manifest["requirements"] == {"dep": {"from_version": "1.0.0"}} + assert result.manifest["resources"] == {"res1": {}} + assert result.manifest["openapi_spec"] == {"paths": {}} + class TestMakeEmManifest: """Tests for RepoEmBackendClient._make_em_manifest."""