Commit 541432c3 authored by Konstantinos Togias's avatar Konstantinos Togias
Browse files

Expose isolated runtime entries from the loaded file bootstrap

parent 7c2b2693
Loading
Loading
Loading
Loading
+15 −0
Changes for README.md: 15 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -245,3 +245,18 @@ 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.

## One file bootstrap for discovery and execution

Load `BootstrapTopology.from_file(path)` once in the composition root. Its four
existing discovery methods return normalized topology. The separate
`get_control_path_configurations()` accessor returns defensive copies of runtime
entries with `ref`, `adapter_type` and `infrastructure`. Map those entries into
the registry, deriving `config["control_path_ref"]` from `ref` before calling the
existing SDK factory. Do not repeat identity inside bootstrap infrastructure.

Duplicate/missing refs, malformed entries, unknown kinds and unresolved binding
refs fail locally. Discovery does not construct execution adapters or resolve
credentials. Keep runtime config out of normalized responses and persisted
topology. The deployment owns mounts; the SDK does not parse a secret URI.
See `tests/contract/bootstrap_example.yaml` for an explicit file-mode example.
+51 −7
Changes for src/openop_tfsdk/contract/topology.py: 51 added lines, 7 removed lines.
Original line number Diff line number Diff line
@@ -12,12 +12,17 @@ and supported_runtime_kinds, which are not topology and come from the adapter
behind that control path.
"""

from copy import deepcopy
from typing import List, Optional, Tuple

import yaml

from openop_tfsdk.contract.adapters import capability_surface
from openop_tfsdk.contract.errors import InvalidTargetRef
from openop_tfsdk.contract.adapters import ADAPTERS_BY_TYPE, capability_surface
from openop_tfsdk.contract.errors import (
    InvalidTargetRef,
    UnknownAdapterKind,
    ValidationError,
)
from openop_tfsdk.contract.interfaces import TopologyDiscoveryAdapter
from openop_tfsdk.contract.models import (
    Capability,
@@ -36,15 +41,39 @@ class BootstrapTopology(TopologyDiscoveryAdapter):
    """

    def __init__(self, bootstrap: dict):
        if not isinstance(bootstrap, dict):
            raise ValidationError("Bootstrap must be a mapping")
        bootstrap = deepcopy(bootstrap)
        self._zones: List[Zone] = []
        self._domains: List[Tuple[str, Domain]] = []
        self._capabilities: List[Tuple[str, Capability]] = []
        self._bindings: List[Tuple[str, ControlPathBinding]] = []

        self._control_paths = {
            raw["ref"]: (raw.get("adapter_type"), raw.get("infrastructure"))
            for raw in bootstrap.get("control_paths") or []
        }
        self._control_paths = {}
        entries = bootstrap.get("control_paths", [])
        if not isinstance(entries, list):
            raise ValidationError("Bootstrap control_paths must be a list")
        for raw in entries:
            if not isinstance(raw, dict):
                raise ValidationError("Each bootstrap control path must be a mapping")
            ref = raw.get("ref")
            if not isinstance(ref, str) or not ref.strip():
                raise ValidationError("Each control path requires a nonempty ref")
            if ref in self._control_paths:
                raise ValidationError("Duplicate control-path ref")
            adapter_type = raw.get("adapter_type")
            if not isinstance(adapter_type, str) or adapter_type not in ADAPTERS_BY_TYPE:
                raise UnknownAdapterKind("Bootstrap contains an unregistered adapter kind")
            infrastructure = raw.get("infrastructure")
            if infrastructure is None:
                infrastructure = {}
            if not isinstance(infrastructure, dict):
                raise ValidationError("Control-path infrastructure must be a mapping")
            if "control_path_ref" in infrastructure:
                raise ValidationError(
                    "Declare identity once in ref, not infrastructure.control_path_ref"
                )
            self._control_paths[ref] = (adapter_type, infrastructure)

        for raw_zone in bootstrap.get("zones") or []:
            self._add_zone(raw_zone)
@@ -54,6 +83,18 @@ class BootstrapTopology(TopologyDiscoveryAdapter):
        with open(path, encoding="utf-8") as handle:
            return cls(yaml.safe_load(handle) or {})

    def get_control_path_configurations(self) -> List[dict]:
        """Runtime-only construction entries from this loaded revision.

        This file-bootstrap accessor is separate from normalized discovery.
        The consumer derives instance identity from ref; credentials and config
        must not be persisted as topology or logged.
        """
        return [
            {"ref": ref, "adapter_type": kind, "infrastructure": deepcopy(infrastructure)}
            for ref, (kind, infrastructure) in self._control_paths.items()
        ]

    def _add_zone(self, raw: dict) -> None:
        self._zones.append(
            Zone(
@@ -110,7 +151,10 @@ class BootstrapTopology(TopologyDiscoveryAdapter):
    def _add_binding(self, capability_ref: str, capability_kind: str, raw: dict) -> None:
        # Not read from the YAML: TF-SDK reports what a control path can execute
        # for its adapter kind and configuration, without building an adapter.
        control_path = self._control_paths.get(raw["control_path_ref"])
        ref = raw.get("control_path_ref")
        if not isinstance(ref, str) or not ref.strip():
            raise InvalidTargetRef("Binding requires a nonempty control_path_ref")
        control_path = self._control_paths.get(ref)
        if control_path is None:
            raise InvalidTargetRef(raw["control_path_ref"])
        adapter_type, infrastructure = control_path
+2 −9
Changes for tests/contract/bootstrap_example.yaml: 2 added lines, 9 removed lines.
Original line number Diff line number Diff line
@@ -18,20 +18,13 @@ control_paths:
    adapter_type: kubernetes
    adapter_contract_version: v1

    invocation:
      adapter_class: openop_tfsdk.adapters.kubernetes.KubernetesComputeAdapter

    infrastructure:
      api_server: https://k8s-athens-edge.example.local:6443
      kubeconfig_ref: secret://openop/k8s-athens-edge/kubeconfig
      # Ops resolves the reference and mounts this file before adapter execution.
      # Deployment mounts a protected Secret at this explicit runtime path.
      kubeconfig_path: /etc/openop/kubeconfigs/athens-edge.yaml
      kube_context: athens-edge
      namespace: openop-services
      default_storage_class: fast-edge-ssd
      registry_mirror: registry.athens-edge.example.local
      tls_verify: true
      timeout_seconds: 30
      runtime_directory: /var/run/openop-tfsdk

    retry:
      transient_attempts: 3
+130 −0
Changes for tests/contract/test_bootstrap_runtime_configuration.py: 130 added lines, 0 removed lines.
Original line number Diff line number Diff line
"""One SDK-loaded declaration supplies discovery and isolated runtime entries."""

import copy
import json

import pytest

from openop_tfsdk.contract.adapters import ADAPTERS_BY_TYPE
from openop_tfsdk.contract.errors import (
    InvalidTargetRef,
    UnknownAdapterKind,
    ValidationError,
)
from openop_tfsdk.contract.topology import BootstrapTopology


def declaration():
    return {
        "control_paths": [
            {
                "ref": "athens",
                "adapter_type": "kubernetes",
                "infrastructure": {
                    "in_cluster": True,
                    "namespace": "applications",
                    "nested": {"value": "original"},
                },
            },
            {
                "ref": "berlin",
                "adapter_type": "kubernetes",
                "infrastructure": {
                    "in_cluster": True,
                    "namespace": "other-applications",
                },
            },
        ],
        "zones": [
            {
                "ref": "z",
                "name": "zone",
                "kind": "resource",
                "domains": [
                    {
                        "ref": "d",
                        "name": "domain",
                        "kind": "compute",
                        "capabilities": [
                            {
                                "ref": "c",
                                "name": "workload",
                                "kind": "workload",
                                "control_path_bindings": [
                                    {"ref": "binding", "control_path_ref": "athens"},
                                ],
                            },
                        ],
                    },
                ],
            }
        ],
    }


def test_runtime_config_is_separate_from_discovery_and_defensively_copied(monkeypatch):
    source = declaration()
    expected = copy.deepcopy(source["control_paths"])
    for adapter in ADAPTERS_BY_TYPE.values():
        monkeypatch.setattr(
            adapter, "__init__", lambda *a, **k: pytest.fail("discovery constructed a backend")
        )
    topology = BootstrapTopology(source)
    source["control_paths"][0]["infrastructure"]["nested"]["value"] = "mutated input"
    returned = topology.get_control_path_configurations()
    assert returned == expected
    returned[0]["infrastructure"]["nested"]["value"] = "mutated result"
    assert topology.get_control_path_configurations() == expected
    assert topology.get_control_path_bindings()[0].control_path_ref == "athens"
    normalized = json.dumps(
        [m.model_dump(mode="json") for m in topology.get_control_path_bindings()]
    )
    assert "infrastructure" not in normalized and "applications" not in normalized
    assert "get_control_path_configurations" not in topology.__class__.__bases__[0].__dict__


@pytest.mark.parametrize("ref", [None, "", " ", 10])
def test_missing_or_invalid_control_path_ref_is_rejected(ref):
    source = declaration()
    source["control_paths"][0]["ref"] = ref
    with pytest.raises(ValidationError):
        BootstrapTopology(source)


def test_duplicate_control_paths_fail_before_last_entry_can_overwrite_the_first():
    source = declaration()
    source["control_paths"][1]["ref"] = "athens"
    with pytest.raises(ValidationError, match="Duplicate"):
        BootstrapTopology(source)


def test_identity_is_not_repeated_inside_infrastructure():
    source = declaration()
    source["control_paths"][0]["infrastructure"]["control_path_ref"] = "athens"
    with pytest.raises(ValidationError, match="control_path_ref"):
        BootstrapTopology(source)


def test_unknown_adapter_is_rejected_even_when_no_binding_uses_it():
    source = declaration()
    source["control_paths"][1]["adapter_type"] = "unknown-kind"
    with pytest.raises(UnknownAdapterKind):
        BootstrapTopology(source)


def test_binding_cannot_name_an_undeclared_control_path():
    source = declaration()
    source["zones"][0]["domains"][0]["capabilities"][0]["control_path_bindings"][0][
        "control_path_ref"
    ] = "missing"
    with pytest.raises(InvalidTargetRef):
        BootstrapTopology(source)


@pytest.mark.parametrize(
    "entries",
    ["not-a-list", [None], [{"ref": "a", "adapter_type": "kubernetes", "infrastructure": "bad"}]],
)
def test_malformed_control_path_configuration_is_a_typed_error(entries):
    with pytest.raises(ValidationError):
        BootstrapTopology({"control_paths": entries})