Commit 6d3ac406 authored by Konstantinos Togias's avatar Konstantinos Togias
Browse files

Merge branch 'refactor/tfsdk-architecture-alignment' into 'develop'

Prepare TF-SDK for release 2.0.0

See merge request !205
parents b7dc64c1 bbc4c2e3
Loading
Loading
Loading
Loading
Loading
+4 −1
Changes for .gitignore: 4 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -7,4 +7,7 @@ __pycache__/
.log
.tar.gz
/dist
src/sunrise6g_opensdk.egg-info/
src/openop_tfsdk.egg-info/

.cache/
.coverage
+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
+2 −2
Changes for .pre-commit-config.yaml: 2 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -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:
@@ -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: []
+140 −5
Changes for README.md: 140 added lines, 5 removed lines.
Original line number Diff line number Diff line
@@ -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
@@ -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:

@@ -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
@@ -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
@@ -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
@@ -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