Loading .dockerignore 0 → 100644 +13 −0 Changes for .dockerignore: 13 added lines, 0 removed lines. Original line number Diff line number Diff line ** !Dockerfile !pyproject.toml !uv.lock !src/ !src/** !.cache/ !.cache/sdk-consumer/ !.cache/sdk-consumer/project/ !.cache/sdk-consumer/project/pyproject.toml !.cache/sdk-consumer/project/uv.lock !.cache/sdk-consumer/project/wheels/ !.cache/sdk-consumer/project/wheels/*.whl .gitlab-ci.yml +42 −2 Changes for .gitlab-ci.yml: 42 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -78,6 +78,7 @@ test: - docker:dind script: - pytest --cov=src/srm --cov-branch --cov-report=term-missing - pytest --confcutdir=ci/tests ci/tests coverage: '/TOTAL.+ ([0-9]{1,3}(?:\.[0-9]+)?%)/' build-and-push: Loading @@ -92,14 +93,23 @@ build-and-push: - docker info script: - export IMAGE_TAG="${CI_COMMIT_TAG:-ci-${CI_COMMIT_REF_SLUG}-${CI_COMMIT_SHORT_SHA}}" - if [ "$CI_PIPELINE_SOURCE" = "merge_request_event" ]; then export IMAGE_TAG="ci-mr${CI_MERGE_REQUEST_IID}-${CI_COMMIT_SHORT_SHA}-p${CI_PIPELINE_ID}"; fi - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" "$CI_REGISTRY" --password-stdin - docker buildx build --provenance=false --network=host -t "$CI_REGISTRY_IMAGE:$IMAGE_TAG" --push . - mkdir -p .cache/openop - docker buildx build --provenance=false --network=host --metadata-file .cache/openop/build.json -t "$CI_REGISTRY_IMAGE:$IMAGE_TAG" --push . - apk add --no-cache python3 - python3 ci/image_receipt.py - docker logout "$CI_REGISTRY" artifacts: paths: - .cache/openop/image.json expire_in: 90 days rules: - if: '$CI_PIPELINE_SOURCE == "pipeline" && $SDK_PROJECT_ID == "396"' when: never - if: '$CI_COMMIT_TAG =~ /^\d+\.\d+\.\d+$/' - if: '$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "develop"' - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' sdk-consumer: stage: test Loading @@ -114,13 +124,43 @@ sdk-consumer: before_script: - mkdir -p "$TMPDIR" - pip install uv==0.11.26 - uv sync --locked --extra dev --no-install-project script: - python ci/sdk_consumer.py artifacts: when: always paths: - .cache/sdk-consumer/qualification.json - .cache/sdk-consumer/project/pyproject.toml - .cache/sdk-consumer/project/uv.lock - .cache/sdk-consumer/project/wheels/ reports: junit: .cache/sdk-consumer/junit.xml expire_in: 90 days sdk-consumer-image: stage: build-and-push image: docker:29.5.1-cli@sha256:b40b3737eb3bf588d25bb856d3564dd3f9fdb32ac2fc19ebe85cc58d761692a5 timeout: 20 minutes variables: DOCKER_HOST: tcp://docker:2375 DOCKER_TLS_CERTDIR: "" services: - docker:29.5.1-dind rules: - if: '$CI_PIPELINE_SOURCE == "pipeline" && $SDK_PROJECT_ID == "396" && $SDK_BUILD_IMAGE == "1"' - when: never needs: - job: sdk-consumer artifacts: true before_script: - apk add --no-cache python3 - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" "$CI_REGISTRY" --password-stdin script: - python3 ci/sdk_consumer.py image after_script: - docker logout "$CI_REGISTRY" artifacts: paths: - .cache/openop/image.json - .cache/sdk-consumer/image.json expire_in: 90 days Dockerfile +14 −6 Changes for Dockerfile: 14 added lines, 6 removed lines. Original line number Diff line number Diff line # syntax=docker/dockerfile:1 FROM python:3.12-slim AS builder COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ ARG PYTHON_IMAGE=python:3.12-slim FROM ${PYTHON_IMAGE} AS builder COPY --from=ghcr.io/astral-sh/uv:0.11.26 /uv /uvx /bin/ WORKDIR /app Loading @@ -10,12 +11,19 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ PIP_NO_CACHE_DIR=1 \ PIP_DEFAULT_TIMEOUT=120 COPY pyproject.toml ./ COPY uv.lock ./ ARG DEPENDENCY_PROJECT=. COPY ${DEPENDENCY_PROJECT}/pyproject.toml ./ COPY ${DEPENDENCY_PROJECT}/uv.lock ./ RUN uv sync --locked RUN --mount=type=bind,target=/build-context \ if [ "$DEPENDENCY_PROJECT" = "." ]; then \ uv sync --locked; \ else \ cp -r "/build-context/$DEPENDENCY_PROJECT/wheels" ./wheels && \ uv sync --locked --no-install-project; \ fi FROM python:3.12-slim AS runtime FROM ${PYTHON_IMAGE} AS runtime COPY --from=alpine/helm:3.18.6 /usr/bin/helm /usr/local/bin/helm Loading ci/image_receipt.py 0 → 100644 +27 −0 Changes for ci/image_receipt.py: 27 added lines, 0 removed lines. Original line number Diff line number Diff line """Record the immutable image produced by the normal SRM build job.""" import json import os import re from pathlib import Path def receipt(metadata, environment): digest = metadata.get("containerimage.digest", "") if not re.fullmatch(r"sha256:[a-f0-9]{64}", digest): raise ValueError("BuildKit did not return an image manifest digest") if environment["CI_REGISTRY_IMAGE"] != "labs.etsi.org:5050/oop/code/service-resource-manager": raise ValueError("Unexpected image repository") return { "schema_version": 1, "source_sha": environment["CI_COMMIT_SHA"], "pipeline_id": environment["CI_PIPELINE_ID"], "build_job_id": environment["CI_JOB_ID"], "image": environment["CI_REGISTRY_IMAGE"] + "@" + digest, } if __name__ == "__main__": directory = Path(".cache/openop") result = receipt(json.loads((directory / "build.json").read_text()), os.environ) (directory / "image.json").write_text(json.dumps(result, indent=2) + "\n") ci/sdk-consumer.md +38 −23 Changes for ci/sdk-consumer.md: 38 added lines, 23 removed lines. Original line number Diff line number Diff line # SDK candidate qualification The `sdk-consumer` job accepts candidates only in a downstream pipeline from TF-SDK project 396. CI_JOB_TOKEN downloads a single build job's artifact; source SHA, manifest membership and wheel SHA-256 must match the request. The normal locked environment is prepared without installing SRM's distribution; the candidate wheel forcibly replaces the baseline SDK, including same-version rebuilds and installs its declared runtime dependencies. `uv pip check` must pass. Source imports use SRM's src directory. The normal lock pins the published 2.0.0rc2 candidate. New SDK candidates can be overlaid for qualification before their own registry publication. An SDK pipeline can select an SRM branch or open MR before either source change merges. It sends one wheel's build job, source revision and checksum. SRM checks its own checkout against `EXPECTED_SRM_SHA` before resolving anything. A changed branch head requires a new run. Registry construction passes each entry's control-path reference to its SDK instance on a copied configuration. Empty or duplicate references and conflicting config identities fail before constructing any adapter. Kubernetes fixtures declare their connection mode and namespace explicitly. SDK construction errors use the existing SRM error translation. These conventions work with the published rc2 baseline and the stricter candidate without changing the dependency lock. The `sdk-consumer` job downloads that exact artifact and copies SRM's project and lock into `.cache/sdk-consumer/project`. `uv add --no-sync` selects the local wheel, including a new or same-version candidate, and resolves its dependencies. `uv sync --locked --extra dev --no-install-project` creates the test environment from this candidate lock. SRM's committed `pyproject.toml` and `uv.lock` stay unchanged. Resolution conflicts fail this job; there are no package exceptions. All tests/unit tests run, including the real SDK factory/bridge boundary. Missing configuration, skipped tests and failures block qualification. The receipt records both source revisions, the wheel checksum and CI pipeline URL. These offline checks cannot establish cluster, NEF or OEG-driven acceptance. Other SRM pipeline jobs retain their normal behavior outside this explicit consumer mode. The job checks installed SDK files against the wheel, runs dependency checks, and executes the offline unit/bridge and CI tests without skips. Its artifacts retain the wheel, candidate project/lock, dependency changes and qualification. These checks alone do not establish cluster, NEF or OEG acceptance. Local execution accepts SDK_WHEEL_FILE and SDK_MANIFEST_FILE instead of a remote job; also supply SDK_SOURCE_SHA, SDK_WHEEL_NAME and SDK_WHEEL_SHA256. Prepare `.venv` with `uv sync --locked --extra dev --no-install-project`, then run `python ci/sdk_consumer.py` using a Python 3.12 environment and task TMPDIR. With `SDK_BUILD_IMAGE=1`, `sdk-consumer-image` builds through the existing Dockerfile using that tested project/lock and the pinned test Python image. It checks the installed wheel, runtime dependency versions and package metadata before pushing a unique `sdk-candidate-p<PIPELINE>-<SDK-SHA>` image. The image receipt records its immutable digest, both source identities, wheel checksum, lock hash and actual dependencies. Ansible deploys that finished image; it performs no further package installation. Normal MR pipelines also build images after their usual checks, using unique `ci-mr<MR>-<SHA>-p<PIPELINE>` tags and the normal committed dependency lock. Main/develop and numeric release-tag publication keep their existing tag rules. Neither candidate route overwrites a release tag or changes the runtime API. For local offline consumer verification, use Python 3.12 and uv 0.11.26. Set `SDK_WHEEL_FILE`, `SDK_MANIFEST_FILE`, `SDK_SOURCE_SHA`, `SDK_WHEEL_NAME` and `SDK_WHEEL_SHA256`, then run `python ci/sdk_consumer.py` in a clean checkout. Use a disk-backed `TMPDIR` and a fresh `.cache/sdk-consumer` directory. The resolver creates its own isolated environment; an existing SDK version need not be published first. Supply the matching pinned `CI_JOB_IMAGE` if preparing inputs for the CI image build. The SRM job-token allowlist must admit the SDK producer for branch/MR metadata and Ansible for read-only consumer artifacts/registry access. Configure these project entries through maintainer review. No personal token is needed. Loading
.dockerignore 0 → 100644 +13 −0 Changes for .dockerignore: 13 added lines, 0 removed lines. Original line number Diff line number Diff line ** !Dockerfile !pyproject.toml !uv.lock !src/ !src/** !.cache/ !.cache/sdk-consumer/ !.cache/sdk-consumer/project/ !.cache/sdk-consumer/project/pyproject.toml !.cache/sdk-consumer/project/uv.lock !.cache/sdk-consumer/project/wheels/ !.cache/sdk-consumer/project/wheels/*.whl
.gitlab-ci.yml +42 −2 Changes for .gitlab-ci.yml: 42 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -78,6 +78,7 @@ test: - docker:dind script: - pytest --cov=src/srm --cov-branch --cov-report=term-missing - pytest --confcutdir=ci/tests ci/tests coverage: '/TOTAL.+ ([0-9]{1,3}(?:\.[0-9]+)?%)/' build-and-push: Loading @@ -92,14 +93,23 @@ build-and-push: - docker info script: - export IMAGE_TAG="${CI_COMMIT_TAG:-ci-${CI_COMMIT_REF_SLUG}-${CI_COMMIT_SHORT_SHA}}" - if [ "$CI_PIPELINE_SOURCE" = "merge_request_event" ]; then export IMAGE_TAG="ci-mr${CI_MERGE_REQUEST_IID}-${CI_COMMIT_SHORT_SHA}-p${CI_PIPELINE_ID}"; fi - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" "$CI_REGISTRY" --password-stdin - docker buildx build --provenance=false --network=host -t "$CI_REGISTRY_IMAGE:$IMAGE_TAG" --push . - mkdir -p .cache/openop - docker buildx build --provenance=false --network=host --metadata-file .cache/openop/build.json -t "$CI_REGISTRY_IMAGE:$IMAGE_TAG" --push . - apk add --no-cache python3 - python3 ci/image_receipt.py - docker logout "$CI_REGISTRY" artifacts: paths: - .cache/openop/image.json expire_in: 90 days rules: - if: '$CI_PIPELINE_SOURCE == "pipeline" && $SDK_PROJECT_ID == "396"' when: never - if: '$CI_COMMIT_TAG =~ /^\d+\.\d+\.\d+$/' - if: '$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "develop"' - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' sdk-consumer: stage: test Loading @@ -114,13 +124,43 @@ sdk-consumer: before_script: - mkdir -p "$TMPDIR" - pip install uv==0.11.26 - uv sync --locked --extra dev --no-install-project script: - python ci/sdk_consumer.py artifacts: when: always paths: - .cache/sdk-consumer/qualification.json - .cache/sdk-consumer/project/pyproject.toml - .cache/sdk-consumer/project/uv.lock - .cache/sdk-consumer/project/wheels/ reports: junit: .cache/sdk-consumer/junit.xml expire_in: 90 days sdk-consumer-image: stage: build-and-push image: docker:29.5.1-cli@sha256:b40b3737eb3bf588d25bb856d3564dd3f9fdb32ac2fc19ebe85cc58d761692a5 timeout: 20 minutes variables: DOCKER_HOST: tcp://docker:2375 DOCKER_TLS_CERTDIR: "" services: - docker:29.5.1-dind rules: - if: '$CI_PIPELINE_SOURCE == "pipeline" && $SDK_PROJECT_ID == "396" && $SDK_BUILD_IMAGE == "1"' - when: never needs: - job: sdk-consumer artifacts: true before_script: - apk add --no-cache python3 - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" "$CI_REGISTRY" --password-stdin script: - python3 ci/sdk_consumer.py image after_script: - docker logout "$CI_REGISTRY" artifacts: paths: - .cache/openop/image.json - .cache/sdk-consumer/image.json expire_in: 90 days
Dockerfile +14 −6 Changes for Dockerfile: 14 added lines, 6 removed lines. Original line number Diff line number Diff line # syntax=docker/dockerfile:1 FROM python:3.12-slim AS builder COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ ARG PYTHON_IMAGE=python:3.12-slim FROM ${PYTHON_IMAGE} AS builder COPY --from=ghcr.io/astral-sh/uv:0.11.26 /uv /uvx /bin/ WORKDIR /app Loading @@ -10,12 +11,19 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ PIP_NO_CACHE_DIR=1 \ PIP_DEFAULT_TIMEOUT=120 COPY pyproject.toml ./ COPY uv.lock ./ ARG DEPENDENCY_PROJECT=. COPY ${DEPENDENCY_PROJECT}/pyproject.toml ./ COPY ${DEPENDENCY_PROJECT}/uv.lock ./ RUN uv sync --locked RUN --mount=type=bind,target=/build-context \ if [ "$DEPENDENCY_PROJECT" = "." ]; then \ uv sync --locked; \ else \ cp -r "/build-context/$DEPENDENCY_PROJECT/wheels" ./wheels && \ uv sync --locked --no-install-project; \ fi FROM python:3.12-slim AS runtime FROM ${PYTHON_IMAGE} AS runtime COPY --from=alpine/helm:3.18.6 /usr/bin/helm /usr/local/bin/helm Loading
ci/image_receipt.py 0 → 100644 +27 −0 Changes for ci/image_receipt.py: 27 added lines, 0 removed lines. Original line number Diff line number Diff line """Record the immutable image produced by the normal SRM build job.""" import json import os import re from pathlib import Path def receipt(metadata, environment): digest = metadata.get("containerimage.digest", "") if not re.fullmatch(r"sha256:[a-f0-9]{64}", digest): raise ValueError("BuildKit did not return an image manifest digest") if environment["CI_REGISTRY_IMAGE"] != "labs.etsi.org:5050/oop/code/service-resource-manager": raise ValueError("Unexpected image repository") return { "schema_version": 1, "source_sha": environment["CI_COMMIT_SHA"], "pipeline_id": environment["CI_PIPELINE_ID"], "build_job_id": environment["CI_JOB_ID"], "image": environment["CI_REGISTRY_IMAGE"] + "@" + digest, } if __name__ == "__main__": directory = Path(".cache/openop") result = receipt(json.loads((directory / "build.json").read_text()), os.environ) (directory / "image.json").write_text(json.dumps(result, indent=2) + "\n")
ci/sdk-consumer.md +38 −23 Changes for ci/sdk-consumer.md: 38 added lines, 23 removed lines. Original line number Diff line number Diff line # SDK candidate qualification The `sdk-consumer` job accepts candidates only in a downstream pipeline from TF-SDK project 396. CI_JOB_TOKEN downloads a single build job's artifact; source SHA, manifest membership and wheel SHA-256 must match the request. The normal locked environment is prepared without installing SRM's distribution; the candidate wheel forcibly replaces the baseline SDK, including same-version rebuilds and installs its declared runtime dependencies. `uv pip check` must pass. Source imports use SRM's src directory. The normal lock pins the published 2.0.0rc2 candidate. New SDK candidates can be overlaid for qualification before their own registry publication. An SDK pipeline can select an SRM branch or open MR before either source change merges. It sends one wheel's build job, source revision and checksum. SRM checks its own checkout against `EXPECTED_SRM_SHA` before resolving anything. A changed branch head requires a new run. Registry construction passes each entry's control-path reference to its SDK instance on a copied configuration. Empty or duplicate references and conflicting config identities fail before constructing any adapter. Kubernetes fixtures declare their connection mode and namespace explicitly. SDK construction errors use the existing SRM error translation. These conventions work with the published rc2 baseline and the stricter candidate without changing the dependency lock. The `sdk-consumer` job downloads that exact artifact and copies SRM's project and lock into `.cache/sdk-consumer/project`. `uv add --no-sync` selects the local wheel, including a new or same-version candidate, and resolves its dependencies. `uv sync --locked --extra dev --no-install-project` creates the test environment from this candidate lock. SRM's committed `pyproject.toml` and `uv.lock` stay unchanged. Resolution conflicts fail this job; there are no package exceptions. All tests/unit tests run, including the real SDK factory/bridge boundary. Missing configuration, skipped tests and failures block qualification. The receipt records both source revisions, the wheel checksum and CI pipeline URL. These offline checks cannot establish cluster, NEF or OEG-driven acceptance. Other SRM pipeline jobs retain their normal behavior outside this explicit consumer mode. The job checks installed SDK files against the wheel, runs dependency checks, and executes the offline unit/bridge and CI tests without skips. Its artifacts retain the wheel, candidate project/lock, dependency changes and qualification. These checks alone do not establish cluster, NEF or OEG acceptance. Local execution accepts SDK_WHEEL_FILE and SDK_MANIFEST_FILE instead of a remote job; also supply SDK_SOURCE_SHA, SDK_WHEEL_NAME and SDK_WHEEL_SHA256. Prepare `.venv` with `uv sync --locked --extra dev --no-install-project`, then run `python ci/sdk_consumer.py` using a Python 3.12 environment and task TMPDIR. With `SDK_BUILD_IMAGE=1`, `sdk-consumer-image` builds through the existing Dockerfile using that tested project/lock and the pinned test Python image. It checks the installed wheel, runtime dependency versions and package metadata before pushing a unique `sdk-candidate-p<PIPELINE>-<SDK-SHA>` image. The image receipt records its immutable digest, both source identities, wheel checksum, lock hash and actual dependencies. Ansible deploys that finished image; it performs no further package installation. Normal MR pipelines also build images after their usual checks, using unique `ci-mr<MR>-<SHA>-p<PIPELINE>` tags and the normal committed dependency lock. Main/develop and numeric release-tag publication keep their existing tag rules. Neither candidate route overwrites a release tag or changes the runtime API. For local offline consumer verification, use Python 3.12 and uv 0.11.26. Set `SDK_WHEEL_FILE`, `SDK_MANIFEST_FILE`, `SDK_SOURCE_SHA`, `SDK_WHEEL_NAME` and `SDK_WHEEL_SHA256`, then run `python ci/sdk_consumer.py` in a clean checkout. Use a disk-backed `TMPDIR` and a fresh `.cache/sdk-consumer` directory. The resolver creates its own isolated environment; an existing SDK version need not be published first. Supply the matching pinned `CI_JOB_IMAGE` if preparing inputs for the CI image build. The SRM job-token allowlist must admit the SDK producer for branch/MR metadata and Ansible for read-only consumer artifacts/registry access. Configure these project entries through maintainer review. No personal token is needed.