feat: Compute resource (required) support for container type

Summary

Map optional SRM compute intent (cpu_millicores, memory_mb) into Kubernetes container resource requests on activate/update, and align contract models with SRM's optional fields.

Changes

  • Pass resources.requests into the bundled container chart when compute is present
  • Render resources in the container Deployment template
  • Make ComputeResources.cpu_millicores / memory_mb and ComputeRequirements.topology optional (SRM-aligned)
  • Tolerate missing compute on resource stubs so interface-only requests keep working
  • Cover none / partial / full compute → Helm --set-json mapping in contract tests

Merge request reports

Loading
+4 −0
Changes for ci/README.md: 4 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -54,3 +54,7 @@ The downstream trigger disables inheritance of SDK default variables and passes
only the explicit SDK artifact coordinates. Job-only paths such as TMPDIR must
be established by the owning project. Candidate rc1 exposed this boundary issue
and was not published; rc2 retains a new immutable tag instead of moving rc1.

Packaged container chart checks render canonical CPU millicores and decimal MB
requests, including omission and absence of invented limits. Provider regressions
exercise the same mapping through activation and update.
+17 −5
Changes for ci/verify.py: 17 added lines, 5 removed lines.
Original line number Diff line number Diff line
@@ -182,13 +182,23 @@ def charts():
    chart = Path(openop_tfsdk.__file__).parent / "contract/adapters/charts/container"
    helm = helm_binary()
    image = "example.invalid/fixture@sha256:" + "a" * 64
    for name, service_type, ports in [
        ("none", "ClusterIP", []),
        ("internal", "ClusterIP", [{"name": "p80-tcp", "port": 80, "protocol": "TCP"}]),
        ("external", "NodePort", [{"name": "p80-tcp", "port": 80, "protocol": "TCP"}]),
    for name, service_type, ports, requests in [
        ("none", "ClusterIP", [], {}),
        ("internal", "ClusterIP", [{"name": "p80-tcp", "port": 80, "protocol": "TCP"}], {}),
        ("external", "NodePort", [{"name": "p80-tcp", "port": 80, "protocol": "TCP"}], {}),
        ("requests", "ClusterIP", [], {"cpu": "250m", "memory": "257M"}),
    ]:
        values = OUT / f"chart-{name}.json"
        write_json(values, {"image": image, "serviceType": service_type, "ports": ports})
        expected_resources = {"requests": requests} if requests else {}
        write_json(
            values,
            {
                "image": image,
                "serviceType": service_type,
                "ports": ports,
                "resources": expected_resources,
            },
        )
        run(helm, "lint", chart, "--strict", "-f", values)
        raw = subprocess.check_output(
            [
@@ -208,6 +218,8 @@ def charts():
        deployment = objects["Deployment"]
        spec = deployment["spec"]["template"]["spec"]["containers"][0]
        assert spec["image"] == image
        assert spec.get("resources", {}) == expected_resources
        assert "limits" not in spec.get("resources", {})
        assert spec.get("ports", []) == [
            {"name": p["name"], "containerPort": p["port"], "protocol": p["protocol"]}
            for p in ports
+4 −0
Changes for src/openop_tfsdk/contract/adapters/charts/container/templates/deployment.yaml: 4 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -25,3 +25,7 @@ spec:
              protocol: {{ .protocol }}
            {{- end }}
          {{- end }}
          {{- with .Values.resources }}
          resources:
            {{- toYaml . | nindent 12 }}
          {{- end }}
+2 −0
Changes for src/openop_tfsdk/contract/adapters/charts/container/values.yaml: 2 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -3,3 +3,5 @@ image: ""
# Each port: {name, port, protocol}.
ports: []
serviceType: ClusterIP
# Kubernetes container resources (requests/limits). Empty when no compute intent.
resources: {}
+17 −0
Changes for src/openop_tfsdk/contract/adapters/kubernetes.py: 17 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -99,10 +99,27 @@ def _chart_args(unit) -> list:
        raise ValidationError(
            "kubernetes container does not support mixed internal and external ports"
        )
    compute = (
        getattr(unit.resource_requirements, "compute", None) if unit.resource_requirements else None
    )
    requests: dict[str, str] = {}
    if compute is not None:
        cpu_millicores = getattr(compute, "cpu_millicores", None)
        memory_mb = getattr(compute, "memory_mb", None)
        if cpu_millicores is not None and cpu_millicores < 0:
            raise ValidationError("container compute requests must be nonnegative")
        if memory_mb is not None and memory_mb < 0:
            raise ValidationError("container compute requests must be nonnegative")
        if cpu_millicores is not None and cpu_millicores > 0:
            requests["cpu"] = f"{cpu_millicores}m"
        if memory_mb is not None and memory_mb > 0:
            requests["memory"] = f"{memory_mb}M"
    resources = {"requests": requests} if requests else {}
    values = {
        "image": unit.artifact_ref,
        "ports": list(ports.values()),
        "serviceType": "NodePort" if external else "ClusterIP",
        "resources": resources,
    }
    return [CONTAINER_CHART] + [
        arg for key, value in values.items() for arg in ("--set-json", f"{key}={json.dumps(value)}")
Loading
Loading