Commit ec8029d7 authored by Paris Stentoumis's avatar Paris Stentoumis
Browse files

feat: wire OpenOp TF-SDK as the control-path adapter factory

Split topology discovery from execution, build one registry from TF-SDK adapters at startup, and map lifecycle calls onto SRM ports.

Co-authored-by: default avatarCursor <cursoragent@cursor.com>
parent 548b8076
Loading
Loading
Loading
Loading
+4 −0
Changes for pyproject.toml: 4 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ dependencies = [
    "sqlalchemy>=2.0.48",
    "asyncpg>=0.31.0",
    "nats-py>=2.10.0",
    "openop-tfsdk",
]

[project.optional-dependencies]
@@ -37,6 +38,9 @@ dev = [
    "ruff>=0.15.6",
]

[tool.uv.sources]
openop-tfsdk = { path = "../tf-sdk", editable = true }

[tool.ruff]
line-length = 100
target-version = "py312"
+188 −0
Changes for src/srm/adapters/transformation_functions/tfsdk_execution.py: 188 added lines, 0 removed lines.
Original line number Diff line number Diff line
"""TF-SDK execution adapter factory and SRM execution-port bridge.

TF-SDK owns construction by adapter kind (`build_adapter`). SRM owns the
`control_path_ref` → instance map. This module bridges the short TF-SDK action
vocabulary to SRM's longer execution-port method names.
"""

from __future__ import annotations

from collections.abc import Mapping
from typing import Any

from openop_tfsdk.contract import execution_models as tfsdk_models
from openop_tfsdk.contract.adapters import build_adapter
from openop_tfsdk.contract.errors import UnknownAdapterKind as TfSdkUnknownAdapterKind

from srm.domain.models.runtime_inventory import CapabilityInstanceState
from srm.domain.models.topology import CapabilityKind
from srm.domain.ports.transformation_functions import (
    CapabilityLifecycleRequest,
    CapabilityLifecycleResult,
    CapabilityQueryRequest,
    CapabilityQueryResult,
    CapabilityRuntimeState,
    CapabilityRuntimeStateRequest,
    TransformationFunctionAdapterFactory,
    TransformationFunctionExecutionPort,
    UnknownAdapterKind,
    UnsupportedAction,
)

_LIFECYCLE_STATE_MAP: dict[str, CapabilityInstanceState] = {
    "provisioning": CapabilityInstanceState.CREATING,
    "active": CapabilityInstanceState.ACTIVE,
    "updating": CapabilityInstanceState.UPDATING,
    "failed": CapabilityInstanceState.FAILED,
    "deactivating": CapabilityInstanceState.TERMINATING,
    "deactivated": CapabilityInstanceState.TERMINATED,
}

_RUNTIME_STATE_MAP: dict[str, CapabilityInstanceState] = {
    "active": CapabilityInstanceState.ACTIVE,
    "degraded": CapabilityInstanceState.DEGRADED,
    "failed": CapabilityInstanceState.FAILED,
    "deactivated": CapabilityInstanceState.TERMINATED,
    "unknown": CapabilityInstanceState.UNKNOWN,
}


def _map_lifecycle_state(value: str) -> CapabilityInstanceState:
    try:
        return _LIFECYCLE_STATE_MAP[value]
    except KeyError:
        return CapabilityInstanceState.UNKNOWN


def _map_runtime_state(value: str) -> CapabilityInstanceState:
    try:
        return _RUNTIME_STATE_MAP[value]
    except KeyError:
        return CapabilityInstanceState.UNKNOWN


def _to_tfsdk_lifecycle_request(
    request: CapabilityLifecycleRequest,
) -> tfsdk_models.CapabilityLifecycleRequest:
    return tfsdk_models.CapabilityLifecycleRequest.model_validate(request.model_dump(mode="json"))


def _to_tfsdk_runtime_request(
    request: CapabilityRuntimeStateRequest,
) -> tfsdk_models.CapabilityRuntimeStateRequest:
    return tfsdk_models.CapabilityRuntimeStateRequest.model_validate(
        request.model_dump(mode="json")
    )


def _capability_kind(value: str, fallback: CapabilityKind) -> CapabilityKind:
    try:
        return CapabilityKind(value)
    except ValueError:
        return fallback


class TfSdkExecutionBridge(TransformationFunctionExecutionPort):
    """Adapts a TF-SDK concrete adapter to SRM's execution port."""

    def __init__(self, adapter: Any) -> None:
        self._adapter = adapter

    def activate_capability(
        self,
        request: CapabilityLifecycleRequest,
    ) -> CapabilityLifecycleResult:
        result = self._adapter.activate(_to_tfsdk_lifecycle_request(request))
        return self._map_lifecycle_result(result, request.capability_kind)

    def update_capability(
        self,
        request: CapabilityLifecycleRequest,
    ) -> CapabilityLifecycleResult:
        result = self._adapter.update(_to_tfsdk_lifecycle_request(request))
        return self._map_lifecycle_result(result, request.capability_kind)

    def deactivate_capability(
        self,
        request: CapabilityLifecycleRequest,
    ) -> CapabilityLifecycleResult:
        result = self._adapter.terminate(_to_tfsdk_lifecycle_request(request))
        return self._map_lifecycle_result(result, request.capability_kind)

    def query_capability(
        self,
        request: CapabilityQueryRequest,
    ) -> CapabilityQueryResult:
        query = getattr(self._adapter, "query", None)
        if query is None or not callable(query):
            raise UnsupportedAction(
                error_code="unsupported_action",
                message=f"adapter {type(self._adapter).__name__} does not support query",
                details={"action": "query"},
            )
        raise UnsupportedAction(
            error_code="unsupported_action",
            message=(
                f"adapter {type(self._adapter).__name__} exposes query but SRM has no "
                "TF-SDK query result mapping yet"
            ),
            details={"action": "query"},
        )

    def get_capability_runtime_state(
        self,
        request: CapabilityRuntimeStateRequest,
    ) -> CapabilityRuntimeState:
        result = self._adapter.read_runtime_state(_to_tfsdk_runtime_request(request))
        return self._map_runtime_state_result(result, request.capability_kind)

    def _map_lifecycle_result(
        self,
        result: tfsdk_models.CapabilityLifecycleResult,
        fallback_kind: CapabilityKind,
    ) -> CapabilityLifecycleResult:
        payload = result.model_dump(mode="json")
        state = payload.pop("state")
        kind = payload.pop("capability_kind")
        endpoints = payload.pop("endpoints") or []
        details = payload.pop("details") or {}
        return CapabilityLifecycleResult(
            **payload,
            capability_kind=_capability_kind(kind, fallback_kind),
            state=_map_lifecycle_state(state),
            endpoints=endpoints,
            details=details,
        )

    def _map_runtime_state_result(
        self,
        result: tfsdk_models.CapabilityRuntimeState,
        fallback_kind: CapabilityKind,
    ) -> CapabilityRuntimeState:
        payload = result.model_dump(mode="json")
        state = payload.pop("state")
        kind = payload.pop("capability_kind")
        endpoints = payload.pop("endpoints") or []
        details = payload.pop("details") or {}
        return CapabilityRuntimeState(
            **payload,
            capability_kind=_capability_kind(kind, fallback_kind),
            state=_map_runtime_state(state),
            endpoints=endpoints,
            details=details,
        )


class OpenOpTfSdkAdapterFactory(TransformationFunctionAdapterFactory):
    """SRM factory that delegates construction to TF-SDK `build_adapter`."""

    def create_adapter(
        self,
        kind: str,
        config: Mapping[str, Any],
    ) -> TransformationFunctionExecutionPort:
        try:
            adapter = build_adapter(kind, dict(config))
        except TfSdkUnknownAdapterKind as exc:
            raise UnknownAdapterKind(kind) from exc
        return TfSdkExecutionBridge(adapter)
+193 −0
Changes for src/srm/adapters/transformation_functions/tfsdk_topology.py: 193 added lines, 0 removed lines.
Original line number Diff line number Diff line
"""TF-SDK topology discovery bridge for SRM's TopologyDiscoveryPort.

Discovery is answered by TF-SDK's TopologyDiscoveryAdapter (BootstrapTopology
today). Execution adapters are unrelated (ADR-0055).
"""

from __future__ import annotations

from typing import Any

from openop_tfsdk.contract.interfaces import TopologyDiscoveryAdapter
from openop_tfsdk.contract.topology import BootstrapTopology

from srm.domain.models.catalog import RuntimeKind
from srm.domain.models.topology import (
    CapabilityKind,
    CapabilityState,
    ControlPathBindingState,
    DomainKind,
    DomainState,
    ZoneKind,
    ZoneState,
)
from srm.domain.ports.transformation_functions import (
    TopologyDiscoveryPort,
    TransformationFunctionCapability,
    TransformationFunctionControlPathBinding,
    TransformationFunctionDomain,
    TransformationFunctionZone,
)

# TF-SDK zone state uses "offline"; SRM ZoneState uses "inactive".
_ZONE_STATE_MAP: dict[str, ZoneState] = {
    "active": ZoneState.ACTIVE,
    "offline": ZoneState.INACTIVE,
    "inactive": ZoneState.INACTIVE,
    "unknown": ZoneState.UNKNOWN,
}


def _enum_or_default[T](enum_type: type[T], value: str | None, default: T) -> T:
    if value is None:
        return default
    try:
        return enum_type(value)  # type: ignore[call-arg]
    except ValueError:
        return default


def _zone_state(value: str | None) -> ZoneState:
    if value is None:
        return ZoneState.UNKNOWN
    return _ZONE_STATE_MAP.get(value, ZoneState.UNKNOWN)


def _capability_kind(value: str) -> CapabilityKind | None:
    try:
        return CapabilityKind(value)
    except ValueError:
        return None


def _runtime_kinds(values: list[Any] | None) -> list[RuntimeKind]:
    kinds: list[RuntimeKind] = []
    for value in values or []:
        raw = value.value if hasattr(value, "value") else str(value)
        try:
            kinds.append(RuntimeKind(raw))
        except ValueError:
            continue
    return kinds


def _supported_actions(values: list[Any] | None) -> list[str]:
    actions: list[str] = []
    for value in values or []:
        actions.append(value.value if hasattr(value, "value") else str(value))
    return actions


def _map_zone(zone: Any) -> TransformationFunctionZone:
    payload = zone.model_dump(mode="json")
    return TransformationFunctionZone(
        ref=payload["ref"],
        name=payload["name"],
        kind=_enum_or_default(ZoneKind, payload.get("kind"), ZoneKind.CUSTOM),
        state=_zone_state(payload.get("state")),
        location=payload.get("location") or {},
        jurisdiction=payload.get("jurisdiction") or {},
        legal_entity=payload.get("legal_entity"),
        federates_zone_refs=payload.get("federates_zone_refs") or [],
        metadata=payload.get("metadata") or {},
    )


def _map_domain(domain: Any) -> TransformationFunctionDomain:
    payload = domain.model_dump(mode="json")
    return TransformationFunctionDomain(
        ref=payload["ref"],
        name=payload["name"],
        kind=_enum_or_default(DomainKind, payload.get("kind"), DomainKind.CUSTOM),
        domain_type=payload.get("domain_type"),
        adapter_type=payload.get("adapter_type"),
        state=_enum_or_default(DomainState, payload.get("state"), DomainState.UNKNOWN),
        federation_role=payload.get("federation_role"),
        federates_domain_refs=payload.get("federates_domain_refs") or [],
        details=payload.get("details") or {},
    )


def _map_capability(capability: Any) -> TransformationFunctionCapability | None:
    payload = capability.model_dump(mode="json")
    kind = _capability_kind(payload["kind"])
    if kind is None:
        return None
    return TransformationFunctionCapability(
        ref=payload["ref"],
        name=payload["name"],
        kind=kind,
        state=_enum_or_default(CapabilityState, payload.get("state"), CapabilityState.UNKNOWN),
        metadata=payload.get("metadata") or {},
    )


def _map_binding(binding: Any) -> TransformationFunctionControlPathBinding:
    payload = binding.model_dump(mode="json")
    return TransformationFunctionControlPathBinding(
        ref=payload["ref"],
        control_path_ref=payload["control_path_ref"],
        supported_runtime_kinds=_runtime_kinds(payload.get("supported_runtime_kinds")),
        supported_actions=_supported_actions(payload.get("supported_actions")),
        priority=payload.get("priority", 100),
        is_default=payload.get("is_default", False),
        state=_enum_or_default(
            ControlPathBindingState,
            payload.get("state"),
            ControlPathBindingState.UNKNOWN,
        ),
        metadata=payload.get("metadata") or {},
    )


class TfSdkTopologyDiscoveryBridge(TopologyDiscoveryPort):
    """Adapts a TF-SDK TopologyDiscoveryAdapter to SRM's TopologyDiscoveryPort."""

    def __init__(self, adapter: TopologyDiscoveryAdapter) -> None:
        self._adapter = adapter

    def get_zones(self, kind: ZoneKind | None = None) -> list[TransformationFunctionZone]:
        kind_filter = kind.value if kind is not None else None
        return [_map_zone(zone) for zone in self._adapter.get_zones(kind=kind_filter)]

    def get_domains(
        self,
        zone_ref: str | None = None,
        kind: DomainKind | None = None,
    ) -> list[TransformationFunctionDomain]:
        kind_filter = kind.value if kind is not None else None
        return [
            _map_domain(domain)
            for domain in self._adapter.get_domains(zone_ref=zone_ref, kind=kind_filter)
        ]

    def get_capabilities(
        self,
        domain_ref: str | None = None,
        kind: CapabilityKind | None = None,
    ) -> list[TransformationFunctionCapability]:
        kind_filter = kind.value if kind is not None else None
        mapped: list[TransformationFunctionCapability] = []
        for capability in self._adapter.get_capabilities(domain_ref=domain_ref, kind=kind_filter):
            converted = _map_capability(capability)
            if converted is None:
                continue
            if kind is not None and converted.kind != kind:
                continue
            mapped.append(converted)
        return mapped

    def get_control_path_bindings(
        self,
        capability_ref: str | None = None,
    ) -> list[TransformationFunctionControlPathBinding]:
        return [
            _map_binding(binding)
            for binding in self._adapter.get_control_path_bindings(capability_ref=capability_ref)
        ]


def build_topology_discovery(bootstrap_path: str | None) -> TopologyDiscoveryPort | None:
    if not bootstrap_path:
        return None
    return TfSdkTopologyDiscoveryBridge(BootstrapTopology.from_file(bootstrap_path))
+2 −0
Changes for src/srm/app_state.py: 2 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@ from srm.adapters.databus.nats_publisher import NatsPublisher
from srm.adapters.transformation_functions.registry import ConfiguredControlPathRegistry
from srm.api.databus.nats_subscriber import NatsSubscriber
from srm.domain.ports.federation_manager.ports import FederationManagerPort
from srm.domain.ports.transformation_functions import TopologyDiscoveryPort


class AppState(Protocol):
@@ -15,5 +16,6 @@ class AppState(Protocol):
    databus_connection_manager: NatsConnectionManager
    databus_subscribers: list[NatsSubscriber]
    control_path_registry: ConfiguredControlPathRegistry
    topology_discovery: TopologyDiscoveryPort | None
    publisher: NatsPublisher
    fm_client: FederationManagerPort
+1 −1
Changes for src/srm/application/use_cases/deploy_service.py: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -428,7 +428,7 @@ class DeployServiceUseCase:
        deploy_material: list[tuple[ServiceCapabilityRequirement, RuntimeKind]],
    ) -> None:
        # TODO: TF-SDK CapabilityExecutionAdapter.activate_capability(...) call using
        # capability_kind="deploy_workload" and placement.control_path_ref.
        # capability_kind="workload" and placement.control_path_ref.
        requirements_by_id = {requirement.id: requirement for requirement, _ in deploy_material}
        for requirement_placement in target_placement.requirements:
            requirement = requirements_by_id[requirement_placement.requirement_id]
Loading