Loading README.md +15 −0 Changes for README.md: 15 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -193,6 +193,7 @@ its context and a namespace: ```python adapter = build_adapter("kubernetes", { "control_path_ref": "k8s-athens", "kubeconfig_path": "/etc/openop/kubeconfigs/athens.yaml", "kube_context": "athens", "namespace": "applications", Loading Loading @@ -230,3 +231,17 @@ system PATH. `helm_environment` accepts only absolute `HELM_REGISTRY_CONFIG`, mounted artifact configuration. Proxy modes require separate qualification. Operator tooling owns credential resolution and mounts; no secret resolver is implemented in the SDK. ## Execution instance identity Every contract execution adapter requires a nonempty `control_path_ref` in its construction config. SRM derives it from the registry entry key; the operator declares that identity once in the bootstrap control-path ref. Direct SDK callers must also supply it. Missing construction identity raises ValidationError. Every implemented lifecycle, query and runtime-state method checks the resolved request's `target.control_path_ref` before loading credentials or contacting a backend. A mismatch raises non-retryable InvalidTargetRef. This applies to direct concrete adapter calls as well as factory construction. The check uses an opaque identity; topology relationships and placement remain in SRM. It does not establish authorization or the provenance of an arbitrary backend ID. src/openop_tfsdk/contract/adapters/control_path.py 0 → 100644 +27 −0 Changes for src/openop_tfsdk/contract/adapters/control_path.py: 27 added lines, 0 removed lines. Original line number Diff line number Diff line """Opaque execution identity; topology relationships remain the consumer's concern.""" from dataclasses import dataclass from openop_tfsdk.contract.errors import InvalidTargetRef, ValidationError @dataclass(frozen=True) class ControlPathScope: control_path_ref: str def __post_init__(self): if not isinstance(self.control_path_ref, str) or not self.control_path_ref.strip(): raise ValidationError( "Execution adapters require a nonempty control_path_ref in config" ) def check(self, request): ref = getattr(getattr(request, "target", None), "control_path_ref", None) if not isinstance(ref, str) or ref != self.control_path_ref: raise InvalidTargetRef( "Request control path does not match this adapter instance", details={ "expected_control_path_ref": self.control_path_ref, "control_path_ref": ref if isinstance(ref, str) else None, }, ) src/openop_tfsdk/contract/adapters/edgecloud.py +5 −0 Changes for src/openop_tfsdk/contract/adapters/edgecloud.py: 5 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ import pydantic import structlog from openop_tfsdk._diagnostics import backend_diagnostics, validation_diagnostics from openop_tfsdk.contract.adapters.control_path import ControlPathScope from openop_tfsdk.contract.errors import ( DomainExecutionFailed, ResourceNotFound, Loading Loading @@ -56,6 +57,7 @@ class I2edgeCapabilityAdapter: def __init__(self, infrastructure: Optional[dict] = None): infrastructure = infrastructure or {} self._scope = ControlPathScope(infrastructure.get("control_path_ref")) self._client = I2edgeClient( base_url=infrastructure.get("base_url"), flavour_id=infrastructure.get("flavour_id"), Loading @@ -68,6 +70,7 @@ class I2edgeCapabilityAdapter: return [], [] def activate(self, request: CapabilityLifecycleRequest) -> CapabilityLifecycleResult: self._scope.check(request) if request.capability_kind != "workload": raise ValidationError(f"i2edge does not activate {request.capability_kind}") unit = request.deployment_unit Loading @@ -86,6 +89,7 @@ class I2edgeCapabilityAdapter: ) def read_runtime_state(self, request: CapabilityRuntimeStateRequest) -> CapabilityRuntimeState: self._scope.check(request) if not request.external_id: raise ValidationError("read_runtime_state needs the external_id returned by activate") try: Loading @@ -99,6 +103,7 @@ class I2edgeCapabilityAdapter: ) def terminate(self, request: CapabilityLifecycleRequest) -> CapabilityLifecycleResult: self._scope.check(request) if not request.external_id: raise ValidationError("terminate needs the external_id returned by activate") self._call("undeploy_app", request.external_id) Loading src/openop_tfsdk/contract/adapters/kubernetes.py +6 −0 Changes for src/openop_tfsdk/contract/adapters/kubernetes.py: 6 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ from kubernetes.config.config_exception import ConfigException from urllib3.exceptions import HTTPError from openop_tfsdk._diagnostics import helm_diagnostics from openop_tfsdk.contract.adapters.control_path import ControlPathScope from openop_tfsdk.contract.adapters.kubernetes_connection import KubernetesConnection from openop_tfsdk.contract.errors import ( DomainExecutionFailed, Loading Loading @@ -133,6 +134,7 @@ class KubernetesCapabilityAdapter: def __init__(self, infrastructure: Optional[dict] = None): self._infrastructure = dict(infrastructure or {}) self._scope = ControlPathScope(self._infrastructure.get("control_path_ref")) self._connection = KubernetesConnection(self._infrastructure) @staticmethod Loading @@ -142,6 +144,7 @@ class KubernetesCapabilityAdapter: return [], [] def activate(self, request: CapabilityLifecycleRequest) -> CapabilityLifecycleResult: self._scope.check(request) unit = request.deployment_unit if unit is None or not unit.artifact_ref: raise ValidationError("activate needs a deployment_unit with an artifact_ref") Loading Loading @@ -222,6 +225,7 @@ class KubernetesCapabilityAdapter: return None def read_runtime_state(self, request: CapabilityRuntimeStateRequest) -> CapabilityRuntimeState: self._scope.check(request) if not request.external_id: raise ValidationError("read_runtime_state needs the external_id returned by activate") release_name = request.external_id Loading Loading @@ -267,6 +271,7 @@ class KubernetesCapabilityAdapter: ) def terminate(self, request: CapabilityLifecycleRequest) -> CapabilityLifecycleResult: self._scope.check(request) if not request.external_id: raise ValidationError("terminate needs the external_id returned by activate") release_name = request.external_id Loading Loading @@ -298,6 +303,7 @@ class KubernetesCapabilityAdapter: ) def update(self, request: CapabilityLifecycleRequest) -> CapabilityLifecycleResult: self._scope.check(request) unit = request.deployment_unit if not request.external_id: raise ValidationError("update needs the external_id returned by activate") Loading src/openop_tfsdk/contract/adapters/network.py +7 −0 Changes for src/openop_tfsdk/contract/adapters/network.py: 7 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ import requests import structlog from openop_tfsdk._diagnostics import backend_diagnostics, validation_diagnostics from openop_tfsdk.contract.adapters.control_path import ControlPathScope from openop_tfsdk.contract.errors import ( DomainExecutionFailed, MaxAgeUnsatisfiable, Loading Loading @@ -245,12 +246,14 @@ class NetworkCapabilityAdapter: def __init__(self, infrastructure: Optional[dict] = None): infrastructure = infrastructure or {} self._scope = ControlPathScope(infrastructure.get("control_path_ref")) self._client = self.client_class( base_url=infrastructure.get("base_url"), scs_as_id=infrastructure.get("scs_as_id") ) self._notification_url = infrastructure.get("notification_url") def activate(self, request: CapabilityLifecycleRequest) -> CapabilityLifecycleResult: self._scope.check(request) self._require(request.capability_kind, SupportedAction.ACTIVATE) if request.capability_kind == "traffic_influence": resource = self._call( Loading @@ -273,6 +276,7 @@ class NetworkCapabilityAdapter: ) def read_runtime_state(self, request: CapabilityRuntimeStateRequest) -> CapabilityRuntimeState: self._scope.check(request) self._require(request.capability_kind, SupportedAction.READ_RUNTIME_STATE) if not request.external_id: raise ValidationError("read_runtime_state needs the external_id returned by activate") Loading @@ -296,6 +300,7 @@ class NetworkCapabilityAdapter: ) def terminate(self, request: CapabilityLifecycleRequest) -> CapabilityLifecycleResult: self._scope.check(request) self._require(request.capability_kind, SupportedAction.TERMINATE) if not request.external_id: raise ValidationError("terminate needs the external_id returned by activate") Loading @@ -311,6 +316,7 @@ class NetworkCapabilityAdapter: ) def update(self, request: CapabilityLifecycleRequest) -> CapabilityLifecycleResult: self._scope.check(request) self._require(request.capability_kind, SupportedAction.UPDATE) if not request.external_id: raise ValidationError("update needs the external_id returned by activate") Loading @@ -327,6 +333,7 @@ class NetworkCapabilityAdapter: ) def query(self, request: CapabilityQueryRequest) -> CapabilityQueryResult: self._scope.check(request) if ( request.capability_kind != "location_context" or "location_retrieval" not in self.client_class.capabilities Loading Loading
README.md +15 −0 Changes for README.md: 15 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -193,6 +193,7 @@ its context and a namespace: ```python adapter = build_adapter("kubernetes", { "control_path_ref": "k8s-athens", "kubeconfig_path": "/etc/openop/kubeconfigs/athens.yaml", "kube_context": "athens", "namespace": "applications", Loading Loading @@ -230,3 +231,17 @@ system PATH. `helm_environment` accepts only absolute `HELM_REGISTRY_CONFIG`, mounted artifact configuration. Proxy modes require separate qualification. Operator tooling owns credential resolution and mounts; no secret resolver is implemented in the SDK. ## Execution instance identity Every contract execution adapter requires a nonempty `control_path_ref` in its construction config. SRM derives it from the registry entry key; the operator declares that identity once in the bootstrap control-path ref. Direct SDK callers must also supply it. Missing construction identity raises ValidationError. Every implemented lifecycle, query and runtime-state method checks the resolved request's `target.control_path_ref` before loading credentials or contacting a backend. A mismatch raises non-retryable InvalidTargetRef. This applies to direct concrete adapter calls as well as factory construction. The check uses an opaque identity; topology relationships and placement remain in SRM. It does not establish authorization or the provenance of an arbitrary backend ID.
src/openop_tfsdk/contract/adapters/control_path.py 0 → 100644 +27 −0 Changes for src/openop_tfsdk/contract/adapters/control_path.py: 27 added lines, 0 removed lines. Original line number Diff line number Diff line """Opaque execution identity; topology relationships remain the consumer's concern.""" from dataclasses import dataclass from openop_tfsdk.contract.errors import InvalidTargetRef, ValidationError @dataclass(frozen=True) class ControlPathScope: control_path_ref: str def __post_init__(self): if not isinstance(self.control_path_ref, str) or not self.control_path_ref.strip(): raise ValidationError( "Execution adapters require a nonempty control_path_ref in config" ) def check(self, request): ref = getattr(getattr(request, "target", None), "control_path_ref", None) if not isinstance(ref, str) or ref != self.control_path_ref: raise InvalidTargetRef( "Request control path does not match this adapter instance", details={ "expected_control_path_ref": self.control_path_ref, "control_path_ref": ref if isinstance(ref, str) else None, }, )
src/openop_tfsdk/contract/adapters/edgecloud.py +5 −0 Changes for src/openop_tfsdk/contract/adapters/edgecloud.py: 5 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ import pydantic import structlog from openop_tfsdk._diagnostics import backend_diagnostics, validation_diagnostics from openop_tfsdk.contract.adapters.control_path import ControlPathScope from openop_tfsdk.contract.errors import ( DomainExecutionFailed, ResourceNotFound, Loading Loading @@ -56,6 +57,7 @@ class I2edgeCapabilityAdapter: def __init__(self, infrastructure: Optional[dict] = None): infrastructure = infrastructure or {} self._scope = ControlPathScope(infrastructure.get("control_path_ref")) self._client = I2edgeClient( base_url=infrastructure.get("base_url"), flavour_id=infrastructure.get("flavour_id"), Loading @@ -68,6 +70,7 @@ class I2edgeCapabilityAdapter: return [], [] def activate(self, request: CapabilityLifecycleRequest) -> CapabilityLifecycleResult: self._scope.check(request) if request.capability_kind != "workload": raise ValidationError(f"i2edge does not activate {request.capability_kind}") unit = request.deployment_unit Loading @@ -86,6 +89,7 @@ class I2edgeCapabilityAdapter: ) def read_runtime_state(self, request: CapabilityRuntimeStateRequest) -> CapabilityRuntimeState: self._scope.check(request) if not request.external_id: raise ValidationError("read_runtime_state needs the external_id returned by activate") try: Loading @@ -99,6 +103,7 @@ class I2edgeCapabilityAdapter: ) def terminate(self, request: CapabilityLifecycleRequest) -> CapabilityLifecycleResult: self._scope.check(request) if not request.external_id: raise ValidationError("terminate needs the external_id returned by activate") self._call("undeploy_app", request.external_id) Loading
src/openop_tfsdk/contract/adapters/kubernetes.py +6 −0 Changes for src/openop_tfsdk/contract/adapters/kubernetes.py: 6 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ from kubernetes.config.config_exception import ConfigException from urllib3.exceptions import HTTPError from openop_tfsdk._diagnostics import helm_diagnostics from openop_tfsdk.contract.adapters.control_path import ControlPathScope from openop_tfsdk.contract.adapters.kubernetes_connection import KubernetesConnection from openop_tfsdk.contract.errors import ( DomainExecutionFailed, Loading Loading @@ -133,6 +134,7 @@ class KubernetesCapabilityAdapter: def __init__(self, infrastructure: Optional[dict] = None): self._infrastructure = dict(infrastructure or {}) self._scope = ControlPathScope(self._infrastructure.get("control_path_ref")) self._connection = KubernetesConnection(self._infrastructure) @staticmethod Loading @@ -142,6 +144,7 @@ class KubernetesCapabilityAdapter: return [], [] def activate(self, request: CapabilityLifecycleRequest) -> CapabilityLifecycleResult: self._scope.check(request) unit = request.deployment_unit if unit is None or not unit.artifact_ref: raise ValidationError("activate needs a deployment_unit with an artifact_ref") Loading Loading @@ -222,6 +225,7 @@ class KubernetesCapabilityAdapter: return None def read_runtime_state(self, request: CapabilityRuntimeStateRequest) -> CapabilityRuntimeState: self._scope.check(request) if not request.external_id: raise ValidationError("read_runtime_state needs the external_id returned by activate") release_name = request.external_id Loading Loading @@ -267,6 +271,7 @@ class KubernetesCapabilityAdapter: ) def terminate(self, request: CapabilityLifecycleRequest) -> CapabilityLifecycleResult: self._scope.check(request) if not request.external_id: raise ValidationError("terminate needs the external_id returned by activate") release_name = request.external_id Loading Loading @@ -298,6 +303,7 @@ class KubernetesCapabilityAdapter: ) def update(self, request: CapabilityLifecycleRequest) -> CapabilityLifecycleResult: self._scope.check(request) unit = request.deployment_unit if not request.external_id: raise ValidationError("update needs the external_id returned by activate") Loading
src/openop_tfsdk/contract/adapters/network.py +7 −0 Changes for src/openop_tfsdk/contract/adapters/network.py: 7 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ import requests import structlog from openop_tfsdk._diagnostics import backend_diagnostics, validation_diagnostics from openop_tfsdk.contract.adapters.control_path import ControlPathScope from openop_tfsdk.contract.errors import ( DomainExecutionFailed, MaxAgeUnsatisfiable, Loading Loading @@ -245,12 +246,14 @@ class NetworkCapabilityAdapter: def __init__(self, infrastructure: Optional[dict] = None): infrastructure = infrastructure or {} self._scope = ControlPathScope(infrastructure.get("control_path_ref")) self._client = self.client_class( base_url=infrastructure.get("base_url"), scs_as_id=infrastructure.get("scs_as_id") ) self._notification_url = infrastructure.get("notification_url") def activate(self, request: CapabilityLifecycleRequest) -> CapabilityLifecycleResult: self._scope.check(request) self._require(request.capability_kind, SupportedAction.ACTIVATE) if request.capability_kind == "traffic_influence": resource = self._call( Loading @@ -273,6 +276,7 @@ class NetworkCapabilityAdapter: ) def read_runtime_state(self, request: CapabilityRuntimeStateRequest) -> CapabilityRuntimeState: self._scope.check(request) self._require(request.capability_kind, SupportedAction.READ_RUNTIME_STATE) if not request.external_id: raise ValidationError("read_runtime_state needs the external_id returned by activate") Loading @@ -296,6 +300,7 @@ class NetworkCapabilityAdapter: ) def terminate(self, request: CapabilityLifecycleRequest) -> CapabilityLifecycleResult: self._scope.check(request) self._require(request.capability_kind, SupportedAction.TERMINATE) if not request.external_id: raise ValidationError("terminate needs the external_id returned by activate") Loading @@ -311,6 +316,7 @@ class NetworkCapabilityAdapter: ) def update(self, request: CapabilityLifecycleRequest) -> CapabilityLifecycleResult: self._scope.check(request) self._require(request.capability_kind, SupportedAction.UPDATE) if not request.external_id: raise ValidationError("update needs the external_id returned by activate") Loading @@ -327,6 +333,7 @@ class NetworkCapabilityAdapter: ) def query(self, request: CapabilityQueryRequest) -> CapabilityQueryResult: self._scope.check(request) if ( request.capability_kind != "location_context" or "location_retrieval" not in self.client_class.capabilities Loading