Commit 03d0e2dc authored by Konstantinos Togias's avatar Konstantinos Togias
Browse files

ci: build verified SRM consumer images from SDK candidates

parent c1fc335e
Loading
Loading
Loading
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
+42 −2
Changes for .gitlab-ci.yml: 42 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -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:
@@ -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
@@ -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
+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

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

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")
+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