Synchronize the Release 2.0.0 E2E baseline into develop

Brings the already merged progressive E2E lab and bootstrap work from master into develop for Release 2.0.0. Preserves the existing commits and establishes develop as the target for subsequent work.

Source: 5bcfda5a. The source tree matches the verified !9 (merged) candidate. Native post-merge verification runs in pipeline 29123; fresh local network acceptance passed all 30 cases with cleanup.

The final release promotion will use a separate develop-to-main MR.

Merge request reports

Loading
+62 −0
Changes for ansible/inventory/r2-sdk-lab.example.yml: 62 added lines, 0 removed lines.
Original line number Diff line number Diff line
---
# Separate, single-operator lab. Copy to r2-sdk-lab.local.yml and fill the pins.
# No production inventory, federation, shared workload namespace or floating SDK install.
all:
  children:
    oop_platform_operators:
      hosts:
        sdk_lab:
          ansible_connection: local
          ansible_python_interpreter: "{{ ansible_playbook_python }}"
          oop_platform_kubeconfig: ~/.kube/oop-sdk-lab.yaml
          oop_platform_kube_context: oop-sdk-lab
          oop_platform_namespace: oop-e2e-platform
          oop_platform_public_host: sdk-lab.example.test
          oop_platform_federation_id: sdk-lab
          oop_platform_country_code: GR
          oop_platform_mcc: "202"
          oop_platform_mncs: ["01"]
          oop_platform_federation_clients: []
          oop_platform_portal_enabled: false
          oop_platform_chart_version: 52a7b0d942af88d004a0781e365b0a6db7c66e2c
          oop_platform_fm_image_tag: REPLACE_WITH_REVIEWED_FM_TAG
          oop_platform_oeg_image_tag: REPLACE_WITH_REVIEWED_OEG_TAG
          oop_platform_srm_image_tag: REPLACE_WITH_REVIEWED_SRM_TAG
          e2e_temp_dir: "{{ playbook_dir }}/../../../.cache/sdk-e2e-tmp"
          oop_platform_bootstrap:
            version: openop.srm.bootstrap/v1
            platform_instance_id: sdk-lab
            control_paths:
              - ref: sdk-lab-k8s
                adapter_family: compute
                adapter_type: kubernetes
                adapter_contract_version: v1
                infrastructure:
                  namespace: oop-e2e-workloads
                  in_cluster: true
                  endpoint_host: sdk-lab.example.test
            zones:
              - ref: sdk-lab-zone
                name: SDK lab
                kind: resource
                state: active
                domains:
                  - ref: sdk-lab-compute
                    name: SDK lab compute
                    kind: compute
                    state: active
                    capabilities:
                      - ref: sdk-lab-workload
                        name: SDK lab workload
                        kind: workload
                        state: active
                        control_path_bindings:
                          - ref: sdk-lab-workload-binding
                            control_path_ref: sdk-lab-k8s
                            is_default: true
                            state: active
          oop_platform_extra_values:
            srm:
              workloads:
                namespaces:
                  - oop-e2e-workloads
+13 −0
Changes for ansible/inventory/r2-smoke.example.yml: 13 added lines, 0 removed lines.
Original line number Diff line number Diff line
---
# HTTP-only view of an already deployed platform. Copy to r2-smoke.local.yml.
# Public URLs must be reachable from the machine/container running the command.
# No SSH credentials, kubeconfig or application image is needed for this scenario.
all:
  children:
    oop_platform_operators:
      hosts:
        local_lab:
          oop_platform_public_host: 127.0.0.1
          oop_platform_scheme: http
          oop_platform_oeg_node_port: 32263
          oop_platform_fm_node_port: 30989
+9 −14
Changes for ansible/inventory/r2.example.yml: 9 added lines, 14 removed lines.
Original line number Diff line number Diff line
@@ -42,8 +42,8 @@ all:
        # No Release 2 portal image is published yet: build one, or turn the portal off.
        oop_platform_portal_enabled: false
        # The Release 2 charts are on this branch until oop/deploy/helm!15 merges;
        # helm develop still has the Release 1 (MongoDB) charts.
        oop_platform_chart_version: refactor/helm-2.0
        # Pin the reviewed Release 2 chart, including single-bootstrap mounts.
        oop_platform_chart_version: 52a7b0d942af88d004a0781e365b0a6db7c66e2c
        # Plain HTTP between the operators: test networks only.
        oop_platform_allow_insecure_partner_endpoints: true

@@ -68,32 +68,27 @@ all:
        # deployed by Helm into namespace oop-apps of the operator's own cluster.
        oop_platform_extra_values:
          srm:
            srmcontroller:
            workloads:
              namespaces: [oop-apps]
              controlPaths:
                - control_path_ref: k8s-local
                  adapter: kubernetes
                  config:
                    namespace: oop-apps
              topology: |
        oop_platform_bootstrap:
          version: openop.srm.bootstrap/v1
                platform_instance_id: {{ oop_platform_federation_id }}
          platform_instance_id: "{{ oop_platform_federation_id }}"
          control_paths:
            - ref: k8s-local
              adapter_family: compute
              adapter_type: kubernetes
              adapter_contract_version: v1
              infrastructure:
                in_cluster: true
                namespace: oop-apps
          zones:
                  - ref: {{ oop_platform_federation_id }}-zone
                    name: {{ srm_zone_name }}
            - ref: "{{ oop_platform_federation_id }}-zone"
              name: "{{ srm_zone_name }}"
              kind: resource
              state: active
              domains:
                      - ref: {{ oop_platform_federation_id }}-compute
                        name: {{ srm_zone_name }} compute
                - ref: "{{ oop_platform_federation_id }}-compute"
                  name: "{{ srm_zone_name }} compute"
                  kind: compute
                  state: active
                  capabilities:
+141 −34
Changes for ansible/playbooks/scenarios/r2_existing_cluster/README.md: 141 added lines, 34 removed lines.
Original line number Diff line number Diff line
@@ -120,6 +120,81 @@ every workload and volume on the node, whoever created them. It removes the user

## Step 1: deploy one platform per operator

### Independent application onboarding

After deploying the platform, select the existing positive catalogue tests without
running federation, workload deployment or the other component scenarios:

```bash
ansible-playbook -i inventory/r2-sdk-lab.local.yml \
  playbooks/scenarios/r2_existing_cluster/e2e.yml \
  -e e2e_suite=onboarding \
  -e e2e_application_image=traefik/whoami@sha256:200689790a0a0ea48ca45992e0450bc26ccab5307375b41c84dfc4f2475937ab \
  -e '{"e2e_pytest_args":"--junitxml=.cache/onboarding.xml -p no:cacheprovider"}'
```

Create `.cache` first. Run this same command locally or in a GitLab job whose
runner can reach the inventory's public URLs and Kubernetes API. Install
`tests/r2_e2e/requirements.txt` in the controller's Python environment; set
`e2e_python` to a different test interpreter when needed. Retain the JUnit file
as a CI artifact. This change adds no live runner or automatic cluster access.

The two selected cases POST a unique caller-supplied application ID, read its
name/version/package/image, CPU/memory intent and external TCP interface, check
list membership, then delete and verify absence. OEG currently projects a
CONTAINER catalogue entry back to `ContainerResources`; CPU/memory comparisons
account for that representation. This suite does not certify full manifest
round-trip, topology-field retention, image pulls or resource enforcement.

Cleanup also runs after partial registration or a lost response. It deletes only
this run's known IDs and fails when deletion cannot be verified. Temporary
operator settings are removed even if pytest fails. The existing default suite
keeps its original 23 cases, and the full SDK endpoint/TI cases remain opt-in.
Generic suites cannot be combined with SDK checkpoint/profile options.

For a fresh local lab, create a separate cluster and kubeconfig, then use the
same deploy and E2E playbooks above. Do not replace an existing Kubernetes
context. Select a reviewed Release 2 revision of `refactor/helm-2.0` explicitly;
the chart repository's `develop` branch may still contain the earlier SRM/OEG
charts. Record chart/source revisions and actual running image IDs with every
live result. The SDK scenarios share this suite's supported Kubernetes manifest
builder rather than submitting the unsupported `infraKind: container` input.

Use controller temporary and fact-cache directories on disk when required:
`TMPDIR`, `ANSIBLE_LOCAL_TEMP`, `ANSIBLE_REMOTE_TEMP`,
`ANSIBLE_CACHE_PLUGIN_CONNECTION`, and the inventory's `e2e_temp_dir`.

### Cumulative SDK workload checkpoint

With the dedicated SDK inventory and completed artifact profile, pass these
options in an extra-vars YAML file (for example, an ignored `acceptance.local.yml`):

```yaml
e2e_suite: ""
e2e_pytest_args: >-
  --sdk-checkpoint=deployed
  --sdk-profile=profiles/lab.local.json
  --junitxml=.cache/deployed.xml
  -p no:cacheprovider
```

Run `e2e.yml -e @acceptance.local.yml`. The test checks the actual component image
IDs and installed SDK fingerprint before mutation, registers a fresh application,
selects the dedicated lab's single active zone, instantiates through OEG and waits
for public ready state. It then verifies the immutable workload image, matching
Service selector, allocated TCP NodePort and ready Pod. Teardown removes its
instance and waits for Services, Deployments and Pods to disappear. Sanitized
request status/correlation and workload identity evidence is saved under
`.cache/sdk-e2e/<run-id>/run.json` relative to the test process's working directory.

Select `endpoints` to extend the same flow with the original public endpoint,
allocated port and HTTP-to-this-Pod assertions. Missing endpoint output remains a
failure there even when `deployed` passes. The protected GitLab SDK lab job accepts
`SDK_CHECKPOINT=deployed` with its existing exact-artifact/profile requirements;
it still needs a configured dedicated runner and lab credentials. The current TI
case retains its earlier application-wide provider gate; device-scoped TI requires
a separate test update before it can qualify the newer public route.

The `oop-platform` role runs on the operator host, where the kubeconfig lives. A remote
host works the same way as `ansible_connection: local`. In order, it:

@@ -210,44 +285,36 @@ declaration
Today the only implementation reads a file; a live source could replace it later
without changing SRM.

The chart's `srm.srmcontroller.topology` and `controlPaths` are empty by default. Helm's
kind environment fills them in for you; on any other cluster the role leaves them empty.
SRM then logs `bootstrap_topology_not_configured` and has no zones, so OEG lists nothing
to deploy to, and any agreement in `federate.yml` fails with `no zone named ... in SRM`.
The chart's bootstrap is empty by default. The role accepts one
`oop_platform_bootstrap` declaration and generates the complete topology file
and any credential mount metadata. With empty defaults, SRM logs
`bootstrap_topology_not_configured` and exposes no zones.

Give each operator its zone through `oop_platform_extra_values`. The example below
declares one zone whose apps SRM deploys with Helm into namespace `oop-apps` of the
operator's own cluster; the test environment runs this setup:
For a local workload path, declare the connection explicitly and keep the
existing namespace/RBAC setting:

```yaml
oop_platform_extra_values:
  srm:
    srmcontroller:
    workloads:
        namespaces: [oop-apps]          # created, with a Role for SRM's ServiceAccount
      controlPaths:
        - control_path_ref: k8s-local
          adapter: kubernetes
          config:
            namespace: oop-apps
      topology: |
      namespaces: [oop-apps]
oop_platform_bootstrap:
  version: openop.srm.bootstrap/v1
        platform_instance_id: {{ oop_platform_federation_id }}
  platform_instance_id: "{{ oop_platform_federation_id }}"
  control_paths:
    - ref: k8s-local
            adapter_family: compute
      adapter_type: kubernetes
            adapter_contract_version: v1
      infrastructure:
        in_cluster: true
        namespace: oop-apps
  zones:
          - ref: {{ oop_platform_federation_id }}-zone
            name: {{ srm_zone_name }}
    - ref: "{{ oop_platform_federation_id }}-zone"
      name: "{{ srm_zone_name }}"
      kind: resource
      state: active
      domains:
              - ref: {{ oop_platform_federation_id }}-compute
                name: {{ srm_zone_name }} compute
        - ref: "{{ oop_platform_federation_id }}-compute"
          name: "{{ srm_zone_name }} compute"
          kind: compute
          state: active
          capabilities:
@@ -259,17 +326,13 @@ oop_platform_extra_values:
                - {ref: deploy-workload-via-k8s-local, control_path_ref: k8s-local, is_default: true, state: active}
```

`srm_zone_name` is a host variable (`Barcelona`, `Paris`, ...): the name users see, and the
name agreements refer to.

The zone declares one capability, `workload`, because that's all TF-SDK's `kubernetes`
adapter serves: it installs, updates, removes and reads Helm releases and containers. It
can't report free resources, so a `query_available_resources` capability bound to it gets
no actions, and placement never uses it; SRM keeps no capacity of its own either
([ADR-0002](https://labs.etsi.org/rep/oop/architecture/-/blob/develop/decisions/0002-no-stored-capacity.md)).
The adapter only reads `namespace` from its config and runs `helm` as SRM's own
ServiceAccount, so it deploys into the cluster SRM runs in, in the namespaces listed under
`workloads.namespaces`.
`srm_zone_name` is the host variable used in public zone names and agreements.
The Kubernetes adapter serves workload lifecycle operations; it does not report
free capacity. SRM retains topology/placement responsibility. For remote
clusters, see the repository README's `kubeconfig_secret` declaration. Remove
legacy `controlPaths` and manually duplicated topology/mount values when using
the generated declaration. Configure workload RBAC separately; declaring a
control path does not grant permissions.

## Step 2: federate the operators

@@ -351,7 +414,51 @@ The tests create their own Keycloak clients in realm `oop` and delete them at th
One known gap is marked `xfail(strict=True)`: OEG lets any App Provider read or delete
another tenant's app. Strict means the run fails once that's fixed, as a reminder to drop
the mark. The last run on the three test operators gave 22 passed and 1 xfailed. Pass
pytest arguments with `-e e2e_pytest_args='-k federation'`.
pytest arguments with `-e '{"e2e_pytest_args":"-k federation"}'`.

### First scenario: public HTTP health

For the first check of an already installed platform, use the same command
locally and in GitLab:

```bash
# Run from the repository root in a Python 3.12 virtual environment.
python -m pip install -r ci/requirements-smoke.txt
cp ansible/inventory/r2-smoke.example.yml ansible/inventory/r2-smoke.local.yml
# Edit the public address and ports for the selected lab.
python ci/platform_smoke.py --inventory ansible/inventory/r2-smoke.local.yml
```

This invokes the existing `e2e.yml` and selects only
`test_platform.py::test_oeg_and_fm_answer`. Each operator's OEG
`/platform/healthz` and FM `/healthz` must return 200. The test refuses an empty
operator list. It proves public HTTP access and liveness; authentication,
dependency readiness and application deployment are separate scenarios.

The command reads public addresses from the inventory and runs its tooling on
the controller, even if that inventory normally uses SSH. It needs no SSH,
Kubernetes credentials, Helm, Keycloak client or SDK artifact. The local k3d
example uses loopback addresses; a remote CI container needs a lab address it
can actually reach. The command does not create or expose a cluster.

Results are `.cache/platform-smoke/junit.xml` and `result.json`. A nonzero
Ansible exit, missing report, failure, skip or a result other than one selected
passing test makes the command fail. A fresh run removes stale reports and
cleans temporary settings/fact files from its workspace directory. Use
`--report-dir PATH` for a different report location.

The ordinary `platform-smoke-checks` CI job exercises this command against
local HTTP fixtures, including failures at either endpoint. To request the
live `platform-smoke` job on a protected ref, supply `RUN_PLATFORM_SMOKE=1` and
a protected **File** variable `OOP_SMOKE_INVENTORY` containing the chosen lab
inventory. The job runs the same command and retains the JUnit/result files.
It uses the normal untagged project runner pool; runner-to-lab reachability
must be verified. Live execution is disabled by default. The default R2 suite
and its existing selection remain available through `e2e.yml`.

The playbook also accepts `e2e_pytest_argv` as a structured argument list, so
report paths with spaces remain intact. Existing `e2e_pytest_args` callers keep
their previous behavior.

## Inventory reference

+57 −6
Changes for ansible/playbooks/scenarios/r2_existing_cluster/e2e.yml: 57 added lines, 6 removed lines.
Original line number Diff line number Diff line
@@ -6,12 +6,46 @@
#   For an operator reached over SSH, the tests use a copy of its kubeconfig on the
#   controller, pointed at the host's address (e2e_kube_api_host) when it names loopback.
# Usage: ansible-playbook -i inventory/r2.local.yml playbooks/scenarios/r2_existing_cluster/e2e.yml
#   Extra pytest arguments: -e e2e_pytest_args='-k federation'
#   Extra pytest arguments: -e '{"e2e_pytest_args":"-k federation"}'
#   Catalogue only: -e e2e_suite=onboarding -e e2e_application_image=IMAGE@sha256:DIGEST

- name: Collect the operators' settings
  hosts: oop_platform_operators
  gather_facts: true
  tasks:
    - name: Validate an independently selected acceptance suite
      ansible.builtin.assert:
        that:
          - e2e_suite | default('') in ['', 'onboarding']
          - >-
            e2e_suite | default('') != 'onboarding' or
            (e2e_application_image | default('') | regex_search('^[^\s@]+@sha256:[0-9a-f]{64}$')) is not none
          - >-
            e2e_suite | default('') | length == 0 or
            '--sdk-' not in (e2e_pytest_args | default(''))
        fail_msg: Select onboarding with an application image pinned by sha256 digest; do not combine SDK selectors.

    - name: Carry the inventory's test options to the controller play
      ansible.builtin.set_fact:
        e2e_run_options:
          suite: "{{ e2e_suite | default('') }}"
          application_image: "{{ e2e_application_image | default('') }}"
          python: "{{ e2e_python | default(ansible_playbook_python) }}"
          pytest_args: "{{ e2e_pytest_args | default('') }}"
          pytest_argv: "{{ e2e_pytest_argv | default([]) }}"
          targets: "{{ e2e_pytest_targets | default([]) }}"
          temp_dir: "{{ e2e_temp_dir | default('') }}"
          report_dir: "{{ e2e_report_dir | default('') }}"

    - name: Prepare an explicitly configured controller temporary directory
      ansible.builtin.file:
        path: "{{ e2e_temp_dir }}"
        state: directory
        mode: "0700"
      when: e2e_temp_dir is defined
      delegate_to: localhost
      run_once: true

    - name: Export the settings
      ansible.builtin.include_role:
        name: oop-platform
@@ -33,6 +67,7 @@

        - name: Make a file for it on the controller
          ansible.builtin.tempfile:
            path: "{{ e2e_temp_dir | default(omit) }}"
            suffix: .kubeconfig
          register: e2e_kubeconfig_file
          delegate_to: localhost
@@ -59,15 +94,24 @@
  connection: local
  gather_facts: false
  vars:
    e2e_pytest_args: ""
    e2e_options: "{{ hostvars[groups['oop_platform_operators'] | first].e2e_run_options }}"
    e2e_operators: >-
      {{ groups['oop_platform_operators'] | map('extract', hostvars, 'oop_platform_settings') | list }}
  tasks:
    - name: Require one consistent test selection across the operator inventory
      ansible.builtin.assert:
        that: hostvars[item].e2e_run_options == e2e_options
        fail_msg: Configure the same E2E suite, image, interpreter and report arguments for every operator.
      loop: "{{ groups['oop_platform_operators'] }}"

    - name: Write the settings for the tests
      ansible.builtin.tempfile:
        path: "{{ e2e_options.temp_dir | default(omit, true) }}"
        suffix: .json
      register: e2e_settings_file

    - name: Run tests with temporary operator settings
      block:
        - name: Fill in the settings file
          ansible.builtin.copy:
            content: "{{ {'operators': e2e_operators} | to_nice_json }}"
@@ -77,14 +121,21 @@
        - name: Run pytest
          ansible.builtin.command:
            argv: >-
          {{ [ansible_playbook_python, '-m', 'pytest', playbook_dir ~ '/../../../tests/r2_e2e', '-v', '-rsxX']
             + (e2e_pytest_args | split if e2e_pytest_args | length > 0 else []) }}
              {{ [e2e_options.python, '-m', 'pytest']
                 + (e2e_options.targets if e2e_options.targets | length > 0
                    else [playbook_dir ~ '/../../../tests/r2_e2e'])
                 + ['-v', '-rsxX']
                 + (['--e2e-suite', e2e_options.suite, '--e2e-app-image', e2e_options.application_image]
                    if e2e_options.suite | length > 0 else [])
                 + (e2e_options.pytest_argv if e2e_options.pytest_argv | length > 0
                     else (e2e_options.pytest_args | split if e2e_options.pytest_args | length > 0 else [])) }}
          environment:
            OOP_E2E_SETTINGS: "{{ e2e_settings_file.path }}"
            OOP_E2E_REPORT_DIR: "{{ e2e_options.report_dir }}"
          register: e2e_result
          changed_when: false
          failed_when: false

      always:
        - name: Remove the settings file and the kubeconfig copies
          ansible.builtin.file:
            path: "{{ item }}"
@@ -96,7 +147,7 @@

    - name: Show the test report
      ansible.builtin.debug:
        msg: "{{ e2e_result.stdout_lines }}"
        msg: "{{ e2e_result.stdout_lines + e2e_result.stderr_lines }}"

    - name: Fail when a test failed
      ansible.builtin.fail:
Loading
Loading