Loading .gitignore +4 −1 Changes for .gitignore: 4 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -7,4 +7,7 @@ __pycache__/ .log .tar.gz /dist src/sunrise6g_opensdk.egg-info/ src/openop_tfsdk.egg-info/ .cache/ .coverage .gitlab-ci.yml +197 −52 Changes for .gitlab-ci.yml: 197 added lines, 52 removed lines. Original line number Diff line number Diff line workflow: rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_TAG =~ /^v2[.]0[.][0-9]+(rc[1-9][0-9]*)?$/ - if: $CI_PIPELINE_SOURCE == "push" && ($CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "develop") - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS when: never - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH - if: $CI_PIPELINE_SOURCE == "schedule" || $CI_PIPELINE_SOURCE == "web" - when: never stages: - validate - build - package - consumer - qualify - publish default: image: python:3.12-slim tags: - docker - vim image: python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 interruptible: true timeout: 15 minutes before_script: - mkdir -p "$TMPDIR" "$PIP_CACHE_DIR" - apt-get update -qq && apt-get install -y --no-install-recommends git ca-certificates - python --version - python -m pip install --require-hashes -r ci/requirements.lock cache: key: files: - ci/requirements.lock prefix: pip-$PYTHON_VERSION paths: - .cache/pip/ variables: PACKAGE_NAME: sunrise6g-opensdk validate-mr: PYTHON_VERSION: '3.12' PIP_CACHE_DIR: $CI_PROJECT_DIR/.cache/pip TMPDIR: $CI_PROJECT_DIR PYTHONDONTWRITEBYTECODE: '1' PIP_DISABLE_PIP_VERSION_CHECK: '1' GIT_DEPTH: '0' lint: stage: validate before_script: - echo "Running merge request validation..." - pip install -r requirements.txt - pip install -e . - pip install isort black flake8 pytest script: - echo "Running linters..." - isort src tests --check --profile black --filter-files - black src tests --check --line-length=100 - flake8 src tests - echo "Running tests..." - pytest -v tests/common/test_invoke_edgecloud_clients.py - pytest -v tests/common/test_invoke_network_clients.py only: - merge_requests - python ci/verify.py lint secret-scan: stage: validate script: - python ci/security.py secrets artifacts: when: always paths: - .cache/ci/secrets.json expire_in: 14 days build-package: stage: build before_script: - pip install build twine - pip install -r requirements.txt - pip install -e . script: - echo "Building Python package..." - python -m build - echo "Package built successfully" - ls -la dist/ - python ci/verify.py build artifacts: paths: - dist/ expire_in: 1 day only: - main - develop - tags - .cache/ci/dist/ - .cache/ci/manifest.json - .cache/ci/build.env reports: dotenv: .cache/ci/build.env expire_in: 90 days wheel-tests: stage: package needs: - job: build-package artifacts: true script: - python ci/verify.py tests artifacts: when: always paths: - .cache/ci/junit.xml - .cache/ci/coverage.xml - .cache/ci/coverage.json - .cache/ci/environment.json reports: junit: .cache/ci/junit.xml coverage_report: coverage_format: cobertura path: .cache/ci/coverage.xml expire_in: 90 days coverage: /TOTAL.*\s([0-9]+(?:\.[0-9]+)?%)$/ sdist-check: stage: package needs: - job: build-package artifacts: true script: - python ci/verify.py sdist chart-check: stage: package needs: - job: build-package artifacts: true script: - python ci/verify.py charts artifacts: when: always paths: - .cache/ci/chart-*.yaml expire_in: 14 days public-contract-types: stage: package needs: - job: build-package artifacts: true script: - python ci/verify.py typing dependency-audit: stage: package script: - python ci/security.py dependencies rules: - if: $CI_COMMIT_TAG || $CI_PIPELINE_SOURCE == "schedule" || $RUN_EXTENDED == "1" - when: manual allow_failure: true artifacts: when: always paths: - .cache/ci/vulnerabilities.json - .cache/ci/security-summary.json - .cache/ci/sbom.json expire_in: 90 days python-compatibility: stage: package image: $PYTHON_IMAGE parallel: matrix: - PYTHON_VERSION: - '3.10' PYTHON_IMAGE: - python:3.10-slim@sha256:6ff506466f8b1e981719b468cc0023b8ef000b983cdec0eafe119d01f379e31d - PYTHON_VERSION: - '3.11' PYTHON_IMAGE: - python:3.11-slim@sha256:e88e9763f943ec1834f992a4b51e0f24500486803e8bc534e5767af9ea65f6ce - PYTHON_VERSION: - '3.13' PYTHON_IMAGE: - python:3.13-slim@sha256:70729b46c69b4f1e97c4822c1af3df53a1476cf5ddc6c087c0c10bc3a5678c2f needs: - job: build-package artifacts: true rules: - if: $CI_COMMIT_TAG || $CI_PIPELINE_SOURCE == "schedule" || $RUN_EXTENDED == "1" script: - python ci/verify.py tests artifacts: when: always reports: junit: .cache/ci/junit.xml paths: - .cache/ci/environment.json expire_in: 90 days publish-gitlab: publish-package: stage: publish dependencies: - build-package before_script: - pip install twine interruptible: false resource_group: sdk-package-publication needs: - job: build-package artifacts: true - job: wheel-tests - job: sdist-check - job: chart-check - job: public-contract-types - job: dependency-audit - job: python-compatibility - job: srm-consumer rules: - if: '$CI_COMMIT_TAG =~ /^v2[.]0[.][0-9]+(rc[1-9][0-9]*)?$/ && $CI_COMMIT_REF_PROTECTED == "true"' when: manual allow_failure: false - when: never script: - echo "Validating package format..." - twine check dist/* - echo "Publishing to Package Registry..." - TWINE_PASSWORD="$CI_JOB_TOKEN" TWINE_USERNAME="gitlab-ci-token" python -m twine upload --repository-url ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi dist/* - echo "Package published successfully" only: - main - develop - tags - python ci/release.py publish artifacts: when: always paths: - .cache/ci/release-check.json - .cache/ci/manifest.json - .cache/ci/sbom.json - .cache/ci/security-summary.json expire_in: never srm-consumer: stage: consumer inherit: variables: false needs: - job: build-package artifacts: true - job: wheel-tests - job: sdist-check - job: chart-check - job: public-contract-types variables: SDK_PROJECT_ID: "396" SDK_BUILD_JOB_ID: $SDK_BUILD_JOB_ID SDK_SOURCE_SHA: $SDK_SOURCE_SHA SDK_WHEEL_NAME: $SDK_WHEEL_NAME SDK_WHEEL_SHA256: $SDK_WHEEL_SHA256 trigger: project: oop/code/service-resource-manager branch: test/tfsdk-consumer-qualification strategy: mirror forward: yaml_variables: true pipeline_variables: false .pre-commit-config.yaml +2 −2 Changes for .pre-commit-config.yaml: 2 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -13,7 +13,7 @@ repos: name: isort (python) args: ["--profile", "black", "--filter-files"] - repo: https://github.com/psf/black rev: 25.1.0 rev: 26.10.1 hooks: - id: black args: Loading @@ -21,7 +21,7 @@ repos: - "--target-version=py310" - "--line-length=100" - repo: https://github.com/pycqa/flake8 rev: 7.2.0 rev: 7.4.1 hooks: - id: flake8 additional_dependencies: [] README.md +140 −5 Changes for README.md: 140 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -78,7 +78,7 @@ Open source SDK to abstract CAMARA/GSMA Transformation Functions (TFs) for Edge For end users: ```bash pip install sunrise6g-opensdk pip install openop-tfsdk ``` ### Option 2: Development Mode Loading Loading @@ -108,7 +108,7 @@ python3 -m examples.example ## How to Contribute We welcome contributions to OpenSDK! We welcome contributions to the TF SDK! To get started: Loading Loading @@ -136,7 +136,7 @@ end participant K8s as Kubernetes note over SDK: [Config] Edge Cloud platform: Kubernetes, IP, Port API ->> SDK: from sunrise6g_opensdk import Sdk as sdkclient API ->> SDK: from openop_tfsdk import Sdk as sdkclient API ->> SDK: sdkclient.create_adapters_from(configuration) API ->> SDK: edgecloud_client = adapters.get("edgecloud") SDK ->> SDK: SDK initialized and ready to be used Loading Loading @@ -164,7 +164,7 @@ participant NEF as NEF participant 5GS as Open5GS note over SDK: [Config] Network core: Open5Gs, IP, Port API ->> SDK: from sunrise6g_opensdk import Sdk as sdkclient API ->> SDK: from openop_tfsdk import Sdk as sdkclient API ->> SDK: sdkclient.create_adapters_from(configuration) API ->> SDK: network_client = adapters.get("network") SDK ->> SDK: SDK initialized and ready to be used Loading @@ -176,7 +176,7 @@ NEF ->> 5GS: QoS session creation ``` --- ## Roadmap for Open SDK 2nd release ## Roadmap for the TF SDK 2nd release - [ ] Add support to GSMA OPG.02 TFs - [x] Include JUNIPER O-RAN adapter Loading @@ -186,3 +186,138 @@ NEF ->> 5GS: QoS session creation ## License Apache 2.0 License – see [`LICENSE`](LICENSE) file for details. # Kubernetes control-path connections Configure each adapter explicitly. File mode requires an absolute mounted file, 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", "endpoint_host": "192.0.2.10", }) ``` Alternatively use `in_cluster: True` with an explicit namespace for the Pod's service account. Missing/mixed selection, `api_server` overrides and unresolved `kubeconfig_ref` fail validation. `endpoint_host` addresses the deployed application; it is independent of the Kubernetes API server in the kubeconfig. `tls_verify`, when supplied, applies consistently to both transports. Construction performs no credential/file/network I/O. On first use, the SDK selects and pins one context/cluster/user profile. Helm receives a private snapshot of that validated profile; Kubernetes observation uses the same profile. Source kubeconfig changes require a new instance. Token files can rotate without changing the selected backend. Token/token-file and client- certificate/key credentials are supported; selected-user `exec`, `auth-provider`, basic authentication and kubeconfig proxy modes are rejected before either transport runs. Unselected credential providers are never loaded. Mount a writable `runtime_directory` (default `/var/run/openop-tfsdk`) for private execution files. Helm snapshots are removed after each command. The pinned Python client may retain temporary decoded certificate files until process exit. Do not put this directory in a persistent application catalog. The SDK never mutates process environment or global Kubernetes defaults. Helm uses a controlled child environment, the `secret` storage driver and isolated config/cache/data/plugin directories. Ambient KUBECONFIG, HELM_KUBE*, cloud profiles, proxies and plugin settings are not inherited. An absolute `helm_binary` may select the installed tool; the default `helm` uses the fixed system PATH. `helm_environment` accepts only absolute `HELM_REGISTRY_CONFIG`, `HELM_REPOSITORY_CONFIG` and `HELM_REPOSITORY_CACHE` paths for deliberately 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. ## 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. ## OAI QoD provider profiles Optionally restrict profile identifiers in an OAI control path's runtime infrastructure. For example, when the NEF offers these names: ```yaml control_paths: - ref: oai-nef-lab adapter_family: network adapter_type: oai adapter_contract_version: v1 infrastructure: base_url: http://nef.example.test:8080 scs_as_id: openop notification_url: http://notifications.example.test/nef qos_profiles: [qos-e, qos2, qos3, qos4] ``` The existing bootstrap runtime accessor and SRM registry pass this configuration to the SDK factory. The adapter snapshots the list for its own client. Profiles are opaque provider names: they reach the NEF unchanged, with no inferred aliases or QoS policy mapping. Configure the actual names and policies offered by that NEF; the example does not assert a universal OAI default. Omitting qos_profiles applies no local allowlist: syntactically valid profile identifiers reach the NEF unchanged. The NEF decides whether a profile exists and can be applied. There are no built-in provider profile names. An explicit null, empty, duplicate or malformed list fails adapter construction with ValidationError. Requests outside a configured list fail before HTTP. Each control path keeps its own optional restriction. Backend refusals remain typed execution failures even for profiles allowed locally. The existing notification destination requirement still applies; configuring profiles does not implement a receiver or establish data-plane QoS guarantees. Network DomainExecutionFailed errors include the existing allowlisted backend status/reason diagnostics in details when available. HTTP400 carries `{"backend_status": 400, "reason": "bad_request"}`; transport errors carry a safe reason. Raw response bodies, URLs and exception text are not copied. Error classes, retryable flags and wrapped HTTP404 classification are unchanged. SRM's existing bridge can preserve these details without a new contract field. ## Helm workload endpoints The Kubernetes adapter can report external NodePort endpoints after activating an application Helm chart. It reads the installed release manifest, then observes only its NodePort Services in the configured namespace through the same explicit control-path connection. Service names need not equal the Helm release name. Declare external interfaces in the deployment unit's resource requirements and configure the control path's reachable endpoint_host. An interface matches the observed Service's numeric targetPort and protocol; omitted targetPort defaults to the Service port. Each interface must have exactly one matching allocated NodePort across the release. The returned port is the allocated nodePort, not the application's listening port. Service names are read from the release; ambiguous matches are not selected. Missing, ambiguous or unavailable observation preserves ACTIVE and the installed release handle, with endpoints absent and a sanitized warning. Named targetPort, Ingress, LoadBalancer and automatic endpoint refresh on update/runtime reads are outside this bounded observation path. A DNS endpoint_host also needs the consumer's address-resolution support before it can serve an IPv4-only TI flow. ci/README.md 0 → 100644 +60 −0 Changes for ci/README.md: 60 added lines, 0 removed lines. Original line number Diff line number Diff line # SDK verification and release gates The default pipeline validates Python 3.12 using a hashed dependency/tool lock. Run `python ci/verify.py SUITE` for lint, build, tests, sdist, charts and typing. Build first; subsequent suites consume that wheel, not an editable checkout. Tests select contract, common and offline location cases explicitly. Live NEF cases are excluded and require separate acceptance evidence. JUnit, focused contract branch coverage (81% minimum), environment inventories, distribution hashes and packaged chart renders are retained as CI artifacts. Helm 3.18.6 is downloaded with its official archive checksum. Python 3.10, 3.11 and 3.13 run in scheduled and release pipelines using digest-pinned images. Update tools.in, requirements.lock, runtime.lock and toolchain.json together. Locks were compiled with `uv pip compile pyproject.toml ci/tools.in --universal --python-version 3.10 --generate-hashes` (omit ci/tools.in for runtime.lock). Review dependency updates. `python ci/security.py secrets` rejects new findings. The reviewed fingerprint file contains hashes and reasons for synthetic schema examples, generated field maps and the Helm checksum; only this hash inventory is excluded from scanning. Changing a value or moving it to another file requires a new review. Access-token fixtures use an explicit example value. Never place real credentials in fixtures. Dependency audit produces vulnerability JSON and CycloneDX SBOM; tag and nightly runs must pass. Audit service failures cannot produce a clean result. Source, sdist and wheel are checked before publication. Branch pushes cannot publish packages. Release promotion must reuse the built and tested bytes; never rebuild a package under an existing version. Final release qualification must identify the exact SDK wheel, SRM revision and OEG-driven acceptance run. The candidate version is 2.0.0rc3. A maintainer creates a protected matching release tag only when the intended candidate is ready. Publication remains a blocking manual job, uses CI_JOB_TOKEN, checks tag/version/source and artifact checksums, and retains a permanent release receipt. No branch pipeline uploads. Final 2.0.0 additionally needs exact-wheel SRM and live E2E qualification. GitLab Runner downloads artifacts before before_script. Its initial TMPDIR is the existing project directory; individual verification commands then create and use .cache/ci/tmp. This keeps temporary files off tmpfs without breaking downloads. Python support is bounded to >=3.10,<3.14, matching the classifiers and tested matrix; the current pinned pydantic-core cannot build with Python 3.14. Release qualification files are maintainer-reviewed receipts, not signed attestations or a claimed SLSA level. Each SRM/E2E receipt must carry the same SDK SHA/checksum and a pipeline URL in its owning ETSI project. Keep final publication blocked until the release scope and the corresponding live evidence are agreed. Consumer qualification follows a protected SRM candidate branch. GitLab fixes the downstream checkout when creating that pipeline, and its receipt records the exact consumer revision. Advancing SRM can requalify the same immutable SDK artifact without changing the package or inventing a new SDK version. Use RUN_EXTENDED=1 to run the full Python/security matrix in a web pipeline too. 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. Loading
.gitignore +4 −1 Changes for .gitignore: 4 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -7,4 +7,7 @@ __pycache__/ .log .tar.gz /dist src/sunrise6g_opensdk.egg-info/ src/openop_tfsdk.egg-info/ .cache/ .coverage
.gitlab-ci.yml +197 −52 Changes for .gitlab-ci.yml: 197 added lines, 52 removed lines. Original line number Diff line number Diff line workflow: rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_TAG =~ /^v2[.]0[.][0-9]+(rc[1-9][0-9]*)?$/ - if: $CI_PIPELINE_SOURCE == "push" && ($CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "develop") - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS when: never - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH - if: $CI_PIPELINE_SOURCE == "schedule" || $CI_PIPELINE_SOURCE == "web" - when: never stages: - validate - build - package - consumer - qualify - publish default: image: python:3.12-slim tags: - docker - vim image: python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 interruptible: true timeout: 15 minutes before_script: - mkdir -p "$TMPDIR" "$PIP_CACHE_DIR" - apt-get update -qq && apt-get install -y --no-install-recommends git ca-certificates - python --version - python -m pip install --require-hashes -r ci/requirements.lock cache: key: files: - ci/requirements.lock prefix: pip-$PYTHON_VERSION paths: - .cache/pip/ variables: PACKAGE_NAME: sunrise6g-opensdk validate-mr: PYTHON_VERSION: '3.12' PIP_CACHE_DIR: $CI_PROJECT_DIR/.cache/pip TMPDIR: $CI_PROJECT_DIR PYTHONDONTWRITEBYTECODE: '1' PIP_DISABLE_PIP_VERSION_CHECK: '1' GIT_DEPTH: '0' lint: stage: validate before_script: - echo "Running merge request validation..." - pip install -r requirements.txt - pip install -e . - pip install isort black flake8 pytest script: - echo "Running linters..." - isort src tests --check --profile black --filter-files - black src tests --check --line-length=100 - flake8 src tests - echo "Running tests..." - pytest -v tests/common/test_invoke_edgecloud_clients.py - pytest -v tests/common/test_invoke_network_clients.py only: - merge_requests - python ci/verify.py lint secret-scan: stage: validate script: - python ci/security.py secrets artifacts: when: always paths: - .cache/ci/secrets.json expire_in: 14 days build-package: stage: build before_script: - pip install build twine - pip install -r requirements.txt - pip install -e . script: - echo "Building Python package..." - python -m build - echo "Package built successfully" - ls -la dist/ - python ci/verify.py build artifacts: paths: - dist/ expire_in: 1 day only: - main - develop - tags - .cache/ci/dist/ - .cache/ci/manifest.json - .cache/ci/build.env reports: dotenv: .cache/ci/build.env expire_in: 90 days wheel-tests: stage: package needs: - job: build-package artifacts: true script: - python ci/verify.py tests artifacts: when: always paths: - .cache/ci/junit.xml - .cache/ci/coverage.xml - .cache/ci/coverage.json - .cache/ci/environment.json reports: junit: .cache/ci/junit.xml coverage_report: coverage_format: cobertura path: .cache/ci/coverage.xml expire_in: 90 days coverage: /TOTAL.*\s([0-9]+(?:\.[0-9]+)?%)$/ sdist-check: stage: package needs: - job: build-package artifacts: true script: - python ci/verify.py sdist chart-check: stage: package needs: - job: build-package artifacts: true script: - python ci/verify.py charts artifacts: when: always paths: - .cache/ci/chart-*.yaml expire_in: 14 days public-contract-types: stage: package needs: - job: build-package artifacts: true script: - python ci/verify.py typing dependency-audit: stage: package script: - python ci/security.py dependencies rules: - if: $CI_COMMIT_TAG || $CI_PIPELINE_SOURCE == "schedule" || $RUN_EXTENDED == "1" - when: manual allow_failure: true artifacts: when: always paths: - .cache/ci/vulnerabilities.json - .cache/ci/security-summary.json - .cache/ci/sbom.json expire_in: 90 days python-compatibility: stage: package image: $PYTHON_IMAGE parallel: matrix: - PYTHON_VERSION: - '3.10' PYTHON_IMAGE: - python:3.10-slim@sha256:6ff506466f8b1e981719b468cc0023b8ef000b983cdec0eafe119d01f379e31d - PYTHON_VERSION: - '3.11' PYTHON_IMAGE: - python:3.11-slim@sha256:e88e9763f943ec1834f992a4b51e0f24500486803e8bc534e5767af9ea65f6ce - PYTHON_VERSION: - '3.13' PYTHON_IMAGE: - python:3.13-slim@sha256:70729b46c69b4f1e97c4822c1af3df53a1476cf5ddc6c087c0c10bc3a5678c2f needs: - job: build-package artifacts: true rules: - if: $CI_COMMIT_TAG || $CI_PIPELINE_SOURCE == "schedule" || $RUN_EXTENDED == "1" script: - python ci/verify.py tests artifacts: when: always reports: junit: .cache/ci/junit.xml paths: - .cache/ci/environment.json expire_in: 90 days publish-gitlab: publish-package: stage: publish dependencies: - build-package before_script: - pip install twine interruptible: false resource_group: sdk-package-publication needs: - job: build-package artifacts: true - job: wheel-tests - job: sdist-check - job: chart-check - job: public-contract-types - job: dependency-audit - job: python-compatibility - job: srm-consumer rules: - if: '$CI_COMMIT_TAG =~ /^v2[.]0[.][0-9]+(rc[1-9][0-9]*)?$/ && $CI_COMMIT_REF_PROTECTED == "true"' when: manual allow_failure: false - when: never script: - echo "Validating package format..." - twine check dist/* - echo "Publishing to Package Registry..." - TWINE_PASSWORD="$CI_JOB_TOKEN" TWINE_USERNAME="gitlab-ci-token" python -m twine upload --repository-url ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi dist/* - echo "Package published successfully" only: - main - develop - tags - python ci/release.py publish artifacts: when: always paths: - .cache/ci/release-check.json - .cache/ci/manifest.json - .cache/ci/sbom.json - .cache/ci/security-summary.json expire_in: never srm-consumer: stage: consumer inherit: variables: false needs: - job: build-package artifacts: true - job: wheel-tests - job: sdist-check - job: chart-check - job: public-contract-types variables: SDK_PROJECT_ID: "396" SDK_BUILD_JOB_ID: $SDK_BUILD_JOB_ID SDK_SOURCE_SHA: $SDK_SOURCE_SHA SDK_WHEEL_NAME: $SDK_WHEEL_NAME SDK_WHEEL_SHA256: $SDK_WHEEL_SHA256 trigger: project: oop/code/service-resource-manager branch: test/tfsdk-consumer-qualification strategy: mirror forward: yaml_variables: true pipeline_variables: false
.pre-commit-config.yaml +2 −2 Changes for .pre-commit-config.yaml: 2 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -13,7 +13,7 @@ repos: name: isort (python) args: ["--profile", "black", "--filter-files"] - repo: https://github.com/psf/black rev: 25.1.0 rev: 26.10.1 hooks: - id: black args: Loading @@ -21,7 +21,7 @@ repos: - "--target-version=py310" - "--line-length=100" - repo: https://github.com/pycqa/flake8 rev: 7.2.0 rev: 7.4.1 hooks: - id: flake8 additional_dependencies: []
README.md +140 −5 Changes for README.md: 140 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -78,7 +78,7 @@ Open source SDK to abstract CAMARA/GSMA Transformation Functions (TFs) for Edge For end users: ```bash pip install sunrise6g-opensdk pip install openop-tfsdk ``` ### Option 2: Development Mode Loading Loading @@ -108,7 +108,7 @@ python3 -m examples.example ## How to Contribute We welcome contributions to OpenSDK! We welcome contributions to the TF SDK! To get started: Loading Loading @@ -136,7 +136,7 @@ end participant K8s as Kubernetes note over SDK: [Config] Edge Cloud platform: Kubernetes, IP, Port API ->> SDK: from sunrise6g_opensdk import Sdk as sdkclient API ->> SDK: from openop_tfsdk import Sdk as sdkclient API ->> SDK: sdkclient.create_adapters_from(configuration) API ->> SDK: edgecloud_client = adapters.get("edgecloud") SDK ->> SDK: SDK initialized and ready to be used Loading Loading @@ -164,7 +164,7 @@ participant NEF as NEF participant 5GS as Open5GS note over SDK: [Config] Network core: Open5Gs, IP, Port API ->> SDK: from sunrise6g_opensdk import Sdk as sdkclient API ->> SDK: from openop_tfsdk import Sdk as sdkclient API ->> SDK: sdkclient.create_adapters_from(configuration) API ->> SDK: network_client = adapters.get("network") SDK ->> SDK: SDK initialized and ready to be used Loading @@ -176,7 +176,7 @@ NEF ->> 5GS: QoS session creation ``` --- ## Roadmap for Open SDK 2nd release ## Roadmap for the TF SDK 2nd release - [ ] Add support to GSMA OPG.02 TFs - [x] Include JUNIPER O-RAN adapter Loading @@ -186,3 +186,138 @@ NEF ->> 5GS: QoS session creation ## License Apache 2.0 License – see [`LICENSE`](LICENSE) file for details. # Kubernetes control-path connections Configure each adapter explicitly. File mode requires an absolute mounted file, 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", "endpoint_host": "192.0.2.10", }) ``` Alternatively use `in_cluster: True` with an explicit namespace for the Pod's service account. Missing/mixed selection, `api_server` overrides and unresolved `kubeconfig_ref` fail validation. `endpoint_host` addresses the deployed application; it is independent of the Kubernetes API server in the kubeconfig. `tls_verify`, when supplied, applies consistently to both transports. Construction performs no credential/file/network I/O. On first use, the SDK selects and pins one context/cluster/user profile. Helm receives a private snapshot of that validated profile; Kubernetes observation uses the same profile. Source kubeconfig changes require a new instance. Token files can rotate without changing the selected backend. Token/token-file and client- certificate/key credentials are supported; selected-user `exec`, `auth-provider`, basic authentication and kubeconfig proxy modes are rejected before either transport runs. Unselected credential providers are never loaded. Mount a writable `runtime_directory` (default `/var/run/openop-tfsdk`) for private execution files. Helm snapshots are removed after each command. The pinned Python client may retain temporary decoded certificate files until process exit. Do not put this directory in a persistent application catalog. The SDK never mutates process environment or global Kubernetes defaults. Helm uses a controlled child environment, the `secret` storage driver and isolated config/cache/data/plugin directories. Ambient KUBECONFIG, HELM_KUBE*, cloud profiles, proxies and plugin settings are not inherited. An absolute `helm_binary` may select the installed tool; the default `helm` uses the fixed system PATH. `helm_environment` accepts only absolute `HELM_REGISTRY_CONFIG`, `HELM_REPOSITORY_CONFIG` and `HELM_REPOSITORY_CACHE` paths for deliberately 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. ## 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. ## OAI QoD provider profiles Optionally restrict profile identifiers in an OAI control path's runtime infrastructure. For example, when the NEF offers these names: ```yaml control_paths: - ref: oai-nef-lab adapter_family: network adapter_type: oai adapter_contract_version: v1 infrastructure: base_url: http://nef.example.test:8080 scs_as_id: openop notification_url: http://notifications.example.test/nef qos_profiles: [qos-e, qos2, qos3, qos4] ``` The existing bootstrap runtime accessor and SRM registry pass this configuration to the SDK factory. The adapter snapshots the list for its own client. Profiles are opaque provider names: they reach the NEF unchanged, with no inferred aliases or QoS policy mapping. Configure the actual names and policies offered by that NEF; the example does not assert a universal OAI default. Omitting qos_profiles applies no local allowlist: syntactically valid profile identifiers reach the NEF unchanged. The NEF decides whether a profile exists and can be applied. There are no built-in provider profile names. An explicit null, empty, duplicate or malformed list fails adapter construction with ValidationError. Requests outside a configured list fail before HTTP. Each control path keeps its own optional restriction. Backend refusals remain typed execution failures even for profiles allowed locally. The existing notification destination requirement still applies; configuring profiles does not implement a receiver or establish data-plane QoS guarantees. Network DomainExecutionFailed errors include the existing allowlisted backend status/reason diagnostics in details when available. HTTP400 carries `{"backend_status": 400, "reason": "bad_request"}`; transport errors carry a safe reason. Raw response bodies, URLs and exception text are not copied. Error classes, retryable flags and wrapped HTTP404 classification are unchanged. SRM's existing bridge can preserve these details without a new contract field. ## Helm workload endpoints The Kubernetes adapter can report external NodePort endpoints after activating an application Helm chart. It reads the installed release manifest, then observes only its NodePort Services in the configured namespace through the same explicit control-path connection. Service names need not equal the Helm release name. Declare external interfaces in the deployment unit's resource requirements and configure the control path's reachable endpoint_host. An interface matches the observed Service's numeric targetPort and protocol; omitted targetPort defaults to the Service port. Each interface must have exactly one matching allocated NodePort across the release. The returned port is the allocated nodePort, not the application's listening port. Service names are read from the release; ambiguous matches are not selected. Missing, ambiguous or unavailable observation preserves ACTIVE and the installed release handle, with endpoints absent and a sanitized warning. Named targetPort, Ingress, LoadBalancer and automatic endpoint refresh on update/runtime reads are outside this bounded observation path. A DNS endpoint_host also needs the consumer's address-resolution support before it can serve an IPv4-only TI flow.
ci/README.md 0 → 100644 +60 −0 Changes for ci/README.md: 60 added lines, 0 removed lines. Original line number Diff line number Diff line # SDK verification and release gates The default pipeline validates Python 3.12 using a hashed dependency/tool lock. Run `python ci/verify.py SUITE` for lint, build, tests, sdist, charts and typing. Build first; subsequent suites consume that wheel, not an editable checkout. Tests select contract, common and offline location cases explicitly. Live NEF cases are excluded and require separate acceptance evidence. JUnit, focused contract branch coverage (81% minimum), environment inventories, distribution hashes and packaged chart renders are retained as CI artifacts. Helm 3.18.6 is downloaded with its official archive checksum. Python 3.10, 3.11 and 3.13 run in scheduled and release pipelines using digest-pinned images. Update tools.in, requirements.lock, runtime.lock and toolchain.json together. Locks were compiled with `uv pip compile pyproject.toml ci/tools.in --universal --python-version 3.10 --generate-hashes` (omit ci/tools.in for runtime.lock). Review dependency updates. `python ci/security.py secrets` rejects new findings. The reviewed fingerprint file contains hashes and reasons for synthetic schema examples, generated field maps and the Helm checksum; only this hash inventory is excluded from scanning. Changing a value or moving it to another file requires a new review. Access-token fixtures use an explicit example value. Never place real credentials in fixtures. Dependency audit produces vulnerability JSON and CycloneDX SBOM; tag and nightly runs must pass. Audit service failures cannot produce a clean result. Source, sdist and wheel are checked before publication. Branch pushes cannot publish packages. Release promotion must reuse the built and tested bytes; never rebuild a package under an existing version. Final release qualification must identify the exact SDK wheel, SRM revision and OEG-driven acceptance run. The candidate version is 2.0.0rc3. A maintainer creates a protected matching release tag only when the intended candidate is ready. Publication remains a blocking manual job, uses CI_JOB_TOKEN, checks tag/version/source and artifact checksums, and retains a permanent release receipt. No branch pipeline uploads. Final 2.0.0 additionally needs exact-wheel SRM and live E2E qualification. GitLab Runner downloads artifacts before before_script. Its initial TMPDIR is the existing project directory; individual verification commands then create and use .cache/ci/tmp. This keeps temporary files off tmpfs without breaking downloads. Python support is bounded to >=3.10,<3.14, matching the classifiers and tested matrix; the current pinned pydantic-core cannot build with Python 3.14. Release qualification files are maintainer-reviewed receipts, not signed attestations or a claimed SLSA level. Each SRM/E2E receipt must carry the same SDK SHA/checksum and a pipeline URL in its owning ETSI project. Keep final publication blocked until the release scope and the corresponding live evidence are agreed. Consumer qualification follows a protected SRM candidate branch. GitLab fixes the downstream checkout when creating that pipeline, and its receipt records the exact consumer revision. Advancing SRM can requalify the same immutable SDK artifact without changing the package or inventing a new SDK version. Use RUN_EXTENDED=1 to run the full Python/security matrix in a web pipeline too. 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.