Commit 55f19799 authored by Konstantinos Togias's avatar Konstantinos Togias
Browse files

Pass explicit control-path identity to SDK consumer instances

parent 42555f55
Loading
Loading
Loading
Loading
Loading
+7 −0
Changes for ci/sdk-consumer.md: 7 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -9,6 +9,13 @@ dependencies. `uv pip check` must pass. Source imports use SRM's src directory.
The normal lock pins the published 2.0.0rc2 candidate. New SDK candidates can
be overlaid for qualification before their own registry publication.

Registry construction passes each entry's control-path reference to its SDK
instance on a copied configuration. Empty or duplicate references and conflicting
config identities fail before constructing any adapter. Kubernetes fixtures
declare their connection mode and namespace explicitly. SDK construction errors
use the existing SRM error translation. These conventions work with the published
rc2 baseline and the stricter candidate without changing the dependency lock.

All tests/unit tests run, including the real SDK factory/bridge boundary. Missing
configuration, skipped tests and failures block qualification. The receipt records
both source revisions, the wheel checksum and CI pipeline URL. These offline
+15 −1
Changes for src/srm/adapters/transformation_functions/registry.py: 15 added lines, 1 removed line.
Original line number Diff line number Diff line
from collections.abc import Mapping
from copy import deepcopy

from srm.config import ControlPathSettings
from srm.domain.ports.transformation_functions import (
@@ -38,9 +39,22 @@ def build_control_path_registry(
            "control paths are configured but no TF-SDK adapter factory is installed: "
            + ", ".join(entry.control_path_ref for entry in settings.entries)
        )
    refs: set[str] = set()
    for entry in settings.entries:
        if not entry.control_path_ref.strip() or entry.control_path_ref in refs:
            raise ValueError("Control-path refs must be nonempty and unique")
        refs.add(entry.control_path_ref)
        if (
            "control_path_ref" in entry.config
            and entry.config["control_path_ref"] != entry.control_path_ref
        ):
            raise ValueError("Control-path config identity conflicts with its registry key")
    return ConfiguredControlPathRegistry(
        {
            entry.control_path_ref: factory.create_adapter(entry.adapter, entry.config)
            entry.control_path_ref: factory.create_adapter(
                entry.adapter,
                {**deepcopy(entry.config), "control_path_ref": entry.control_path_ref},
            )
            for entry in settings.entries
        }
    )
+2 −0
Changes for src/srm/adapters/transformation_functions/tfsdk_execution.py: 2 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -266,4 +266,6 @@ class OpenOpTfSdkAdapterFactory(TransformationFunctionAdapterFactory):
            adapter = build_adapter(kind, dict(config))
        except TfSdkUnknownAdapterKind as exc:
            raise UnknownAdapterKind(kind) from exc
        except TfSdkTFSDKError as exc:
            raise _translate_tfsdk_error(exc) from exc
        return TfSdkExecutionBridge(adapter)
+63 −3
Changes for tests/unit/test_tfsdk_registry_wiring.py: 63 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -5,6 +5,7 @@ from __future__ import annotations
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
from unittest.mock import Mock

import pytest
from openop_tfsdk.contract.errors import DomainExecutionFailed as TfSdkDomainExecutionFailed
@@ -363,7 +364,10 @@ def test_bridge_rejects_invalid_query_result() -> None:

def test_factory_builds_kubernetes_adapter() -> None:
    factory = OpenOpTfSdkAdapterFactory()
    adapter = factory.create_adapter("kubernetes", {"namespace": "openop-services"})
    adapter = factory.create_adapter(
        "kubernetes",
        {"in_cluster": True, "namespace": "openop-services", "control_path_ref": "k8s-athens-edge"},
    )

    assert isinstance(adapter, TfSdkExecutionBridge)
    assert not isinstance(adapter, TopologyDiscoveryPort)
@@ -382,12 +386,12 @@ def test_registry_resolves_multiple_entries() -> None:
            ControlPathEntry(
                control_path_ref="k8s-athens-edge",
                adapter="kubernetes",
                config={"namespace": "athens"},
                config={"in_cluster": True, "namespace": "athens"},
            ),
            ControlPathEntry(
                control_path_ref="k8s-other",
                adapter="kubernetes",
                config={"namespace": "other"},
                config={"in_cluster": True, "namespace": "other"},
            ),
        ]
    )
@@ -400,6 +404,62 @@ def test_registry_resolves_multiple_entries() -> None:
    assert registry.control_path_refs() == ["k8s-athens-edge", "k8s-other"]


def test_registry_derives_identity_without_mutating_input() -> None:
    settings = ControlPathSettings(
        entries=[
            ControlPathEntry(
                control_path_ref="athens",
                adapter="opaque",
                config={"nested": {"value": "original"}},
            )
        ]
    )
    factory = Mock()
    build_control_path_registry(settings, factory)
    kind, config = factory.create_adapter.call_args.args
    assert kind == "opaque"
    assert config == {"nested": {"value": "original"}, "control_path_ref": "athens"}
    config["nested"]["value"] = "changed"
    assert settings.entries[0].config == {"nested": {"value": "original"}}


@pytest.mark.parametrize(
    "entries",
    [
        [
            ControlPathEntry(control_path_ref="same", adapter="opaque"),
            ControlPathEntry(control_path_ref="same", adapter="opaque"),
        ],
        [
            ControlPathEntry(
                control_path_ref="athens", adapter="opaque", config={"control_path_ref": "berlin"}
            )
        ],
        [ControlPathEntry(control_path_ref=" ", adapter="opaque")],
    ],
)
def test_registry_rejects_ambiguous_identity_before_any_construction(
    entries: list[ControlPathEntry],
) -> None:
    factory = Mock()
    with pytest.raises(ValueError):
        build_control_path_registry(ControlPathSettings(entries=entries), factory)
    factory.create_adapter.assert_not_called()


def test_factory_translates_sdk_construction_error(monkeypatch: pytest.MonkeyPatch) -> None:
    build = Mock(side_effect=TfSdkValidationError("invalid connection configuration"))
    monkeypatch.setattr(
        "srm.adapters.transformation_functions.tfsdk_execution.build_adapter", build
    )

    with pytest.raises(ValidationError) as caught:
        OpenOpTfSdkAdapterFactory().create_adapter("kubernetes", {})

    assert caught.value.error_code == TfSdkValidationError.error_code
    assert caught.value.message == "invalid connection configuration"


def test_build_topology_discovery_none_without_path() -> None:
    assert build_topology_discovery(None) is None
    assert build_topology_discovery("") is None