Commit 8ffdd848 authored by Konstantinos Togias's avatar Konstantinos Togias
Browse files

ci: explain E2E checks and expose traceable release results

parent 201649f6
Loading
Loading
Loading
Loading
Loading
+18 −6
Changes for .gitlab-ci.yml: 18 added lines, 6 removed lines.
Original line number Diff line number Diff line
workflow:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_PIPELINE_SOURCE == "push" && ($CI_COMMIT_BRANCH == "master" || $CI_COMMIT_BRANCH == "main")'
    - if: '$CI_PIPELINE_SOURCE == "push" && ($CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "develop")'
    - if: '$CI_PIPELINE_SOURCE == "push" && $CI_OPEN_MERGE_REQUESTS'
      when: never
    - when: always
@@ -156,7 +156,10 @@ platform-lab:
    DOCKER_TLS_VERIFY: "1"
    DOCKER_CERT_PATH: /certs/client
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_BRANCH == "test/platform-application-checkpoints" || $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME == "test/platform-application-checkpoints"'
    - if: &release_lab_ref >-
        $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "develop" ||
        ($CI_MERGE_REQUEST_SOURCE_PROJECT_ID == $CI_PROJECT_ID &&
        ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"))
      when: on_success
      allow_failure: false
      variables:
@@ -180,6 +183,9 @@ platform-lab:
      - $OOP_LAB_REPORT_DIR/candidate.json
      - $OOP_LAB_REPORT_DIR/feature-matrix.json
      - $OOP_LAB_REPORT_DIR/feature-matrix.md
      - $OOP_LAB_REPORT_DIR/index.html
      - $OOP_LAB_REPORT_DIR/summary.txt
      - $OOP_LAB_REPORT_DIR/acceptance-junit.xml
      - $OOP_LAB_REPORT_DIR/wrapper-junit.xml
      - $OOP_LAB_REPORT_DIR/pods.txt
      - $OOP_LAB_REPORT_DIR/events.txt
@@ -187,9 +193,11 @@ platform-lab:
      - $OOP_LAB_REPORT_DIR/health/junit.xml
      - $OOP_LAB_REPORT_DIR/health/acceptance/
    reports:
      annotations: $OOP_LAB_REPORT_DIR/annotations.json
      junit:
        - $OOP_LAB_REPORT_DIR/junit.xml
        - $OOP_LAB_REPORT_DIR/wrapper-junit.xml
        - $OOP_LAB_REPORT_DIR/acceptance-junit.xml
    expire_in: 90 days

# Same lab and lifecycle, using a chart archive instead of a container manifest.
@@ -199,7 +207,7 @@ platform-lab-helm:
    OOP_LAB_PACKAGE_TYPE: HELM
    OOP_LAB_REPORT_DIR: .cache/platform-lab/helm
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_BRANCH == "test/platform-application-checkpoints" || $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME == "test/platform-application-checkpoints"'
    - if: *release_lab_ref
      variables:
        OOP_LAB_PROFILE: ci/lab/profile.release-2.0.json
        OOP_LAB_REFRESH_CANDIDATE: "1"
@@ -213,7 +221,7 @@ platform-lab-network:
    OOP_LAB_NETWORK_PROVIDER: EURECOM
    OOP_LAB_REPORT_DIR: .cache/platform-lab/network
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_BRANCH == "test/platform-application-checkpoints" || $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME == "test/platform-application-checkpoints"'
    - if: *release_lab_ref
      variables:
        OOP_LAB_PROFILE: ci/lab/profile.release-2.0.json
        OOP_LAB_REFRESH_CANDIDATE: "1"
@@ -231,16 +239,20 @@ release-feature-report:
    - {job: platform-lab-helm, artifacts: true, optional: true}
    - {job: platform-lab-network, artifacts: true, optional: true}
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_BRANCH == "test/platform-application-checkpoints" || $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME == "test/platform-application-checkpoints"'
    - if: *release_lab_ref
    - when: never
  script:
    - python ci/merge_feature_reports.py .cache/platform-lab .cache/release-features
  artifacts:
    when: always
    expose_as: Observed release features
    expose_as: E2E results and verified features
    paths:
      - .cache/release-features/feature-matrix.md
      - .cache/release-features/feature-matrix.json
      - .cache/release-features/index.html
      - .cache/release-features/summary.txt
    reports:
      annotations: .cache/release-features/annotations.json
    expire_in: 90 days

# Explicit live mode leaves all default component tests and deployment jobs alone.
+16 −0
Changes for README.md: 16 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -4,6 +4,22 @@ Ansible for the ETSI Open Operator Platform (OOP) Release 2. Each operator in th
inventory gets its own platform on its own Kubernetes cluster; the playbooks then
federate the operators and run end-to-end tests across them.

## Understand the E2E lab and its results

Start with [OpenOP E2E tests, explained](ci/lab/EXPLAINED.md): a short overview
with diagrams for readers new to OpenOP. The [check catalogue](ci/lab/CHECKS.md)
explains individual tests. The [operator guide](ci/lab/README.md) covers running
the same isolated lab locally and in GitLab CI.

In a pipeline, open **release-feature-report** for the readable feature matrix
and its report/documentation links. Open **Tests** for individual acceptance
cases and setup/cleanup checkpoints. HTML/JSON artifacts retain exact-run
evidence; a job's success covers only its stated checks and limits.

New work targets develop. The Release 2.0.0 MR promotes develop to main after
the module leader and release owner review; the release owner creates the final
tags and GitLab Release. Historical master commits remain intact.

- `playbooks/infrastructure/k3s/`: k3s on plain Ubuntu VMs, when you have no cluster yet
- `playbooks/scenarios/r2_existing_cluster/`: deploy the `oop-platform` Helm chart per
  operator, federate the operators, run the e2e tests, undeploy
+2 −0
Changes for ansible/tests/r2_e2e/test_apps.py: 2 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -61,6 +61,7 @@ def registered_app(operators, callers, request):

@pytest.mark.e2e_suite("onboarding")
def test_app_provider_registers_reads_and_deletes_an_app(operators, callers, registered_app):
    """Onboard an application through OEG, read its stored manifest and list entry, then verify catalogue removal."""
    for op in operators:
        url, manifest = registered_app[op.host]
        token = callers[op.host]["provider"]
@@ -78,6 +79,7 @@ def test_app_provider_registers_reads_and_deletes_an_app(operators, callers, reg

@pytest.mark.e2e_suite("onboarding")
def test_app_provider_lists_its_apps(operators, callers, registered_app):
    """Verify the application registered by this test client appears in its catalogue list."""
    for op in operators:
        r = http("GET", op.oeg_url + APPS, callers[op.host]["provider"])
        assert r.status_code == 200, r.text
+3 −0
Changes for ansible/tests/r2_e2e/test_auth.py: 3 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -12,6 +12,7 @@ def _partners(op):


def test_oeg_refuses_calls_without_a_valid_token(operators):
    """Require HTTP 401 for missing or invalid bearer tokens on application and federation routes."""
    for op in operators:
        for url in (op.oeg_url + APPS, _partners(op)):
            r = http("GET", url)
@@ -27,12 +28,14 @@ def test_oeg_refuses_another_operators_token(pair, callers):


def test_oeg_keeps_federation_to_operators(operators, callers):
    """Require HTTP 403 when an App Provider calls an operator-only federation route."""
    for op in operators:
        r = http("GET", _partners(op), callers[op.host]["provider"])
        assert r.status_code == 403, r.text


def test_oeg_keeps_app_apis_to_app_providers(operators, callers):
    """Reject the operator role on application routes and let an App Provider reach the application lookup."""
    for op in operators:
        app = f"{op.oeg_url}{APPS}/{uuid.uuid4()}"
        r = http("GET", app, callers[op.host]["operator"])
+13 −0
Changes for ansible/tests/r2_e2e/test_deploy_to_ti.py: 13 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -94,6 +94,7 @@ def public_endpoint(progressive_scenario, deployed_workload, public_instance):

@pytest.mark.platform_acceptance
def test_cloud_zones_match_configured_workload_targets(request, operators, callers):
    """Compare public zone names and unique identities with the workload zones configured in this lab."""
    profile = request.config.sdk_profile
    expected = set(profile.get("expected_zones", [profile.get("zone_name")]))
    assert expected and None not in expected, "Declare expected workload zone names in the profile"
@@ -111,11 +112,13 @@ def test_cloud_zones_match_configured_workload_targets(request, operators, calle

@pytest.mark.platform_acceptance
def test_progressive_oeg_request_creates_a_ready_workload(deployed_workload):
    """Instantiate through OEG and independently require the owned Kubernetes Deployment, ready Pod and NodePort Service."""
    assert deployed_workload["pod_uid"] and deployed_workload["service_uid"]


@pytest.mark.platform_acceptance
def test_progressive_workload_applies_requested_resources(progressive_scenario, deployed_workload):
    """Compare requested CPU and decimal-MB memory with the actual Kubernetes Deployment and Pod resource requests."""
    check_requested_resources(deployed_workload, progressive_scenario.evidence["requested_resources"])
    progressive_scenario.evidence.update(resources_verified=True,
                                         last_verified_phase="resources_verified")
@@ -123,16 +126,19 @@ def test_progressive_workload_applies_requested_resources(progressive_scenario,

@pytest.mark.platform_acceptance
def test_progressive_oeg_returns_the_application_instance(public_instance):
    """Poll the public instance until ready and verify its application and zone identities."""
    assert public_instance["status"] == "ready"


@pytest.mark.platform_acceptance
def test_progressive_oeg_returns_the_observed_endpoint(public_endpoint):
    """Compare the public application endpoint with the actual owned Kubernetes Service and cluster node."""
    assert public_endpoint["port"] and public_endpoint["host"]


@pytest.mark.platform_acceptance
def test_progressive_endpoint_reaches_the_deployed_application(progressive_scenario, public_endpoint):
    """Call the returned HTTP endpoint and verify the response identifies the deployed application Pod."""
    check_reachability(public_endpoint, progressive_scenario.profile, http)
    progressive_scenario.evidence.update(endpoint_reachable=True,
                                         last_verified_phase="reachability_verified")
@@ -140,6 +146,7 @@ def test_progressive_endpoint_reaches_the_deployed_application(progressive_scena

@pytest.mark.platform_acceptance
def test_progressive_oeg_termination_removes_the_workload(progressive_scenario, deployed_workload):
    """Terminate through OEG and verify the public instance and owned Deployment, Pod and Service disappear."""
    progressive_scenario.terminate()


@@ -209,21 +216,25 @@ def qod_case(network_caller):
@pytest.mark.parametrize('profile_name',EURECOM_QOS_PROFILES)
@pytest.mark.platform_acceptance
def test_qod_profile_lifecycle(qod_case,profile_name):
    """Create and read QoD through OEG, inspect the real NEF resource and flow, then verify public and provider deletion."""
    qod_case.verify_supported(profile_name)


@pytest.mark.platform_acceptance
def test_qod_unsupported_profile_is_rejected(qod_case):
    """Reject a profile outside this control path configuration and prove no provider subscription remains."""
    qod_case.verify_refused(qod_case.profile['qod']['unsupported_profile'])


@pytest.mark.platform_acceptance
def test_qod_backend_refusal_retains_diagnostics(qod_case):
    """Use a locally allowed profile refused by the NEF and require useful backend diagnostics and cleanup."""
    qod_case.verify_refused(qod_case.profile['qod']['backend_refusal_profile'],backend=True)


@pytest.mark.platform_acceptance
def test_known_device_returns_geographic_location(network_caller):
    """Retrieve the simulated device location through OEG and verify geographic coordinates and freshness."""
    op,profile,token=network_caller
    device=profile['qod']['session_request']['device']
    response=http('POST',op.oeg_url+'/location-retrieval/v0.5/retrieve',token,
@@ -245,6 +256,7 @@ def test_known_device_returns_geographic_location(network_caller):

@pytest.mark.platform_acceptance
def test_location_without_device_is_rejected(network_caller):
    """Require the public missing-identifier error when location retrieval has no device."""
    op,_,token=network_caller
    response=http('POST',op.oeg_url+'/location-retrieval/v0.5/retrieve',token,json={})
    assert response.status_code==422 and response.json()['code']=='MISSING_IDENTIFIER'
@@ -253,6 +265,7 @@ def test_location_without_device_is_rejected(network_caller):
@pytest.mark.platform_acceptance
def test_device_ti_uses_the_deployed_application_endpoint(progressive_scenario,public_endpoint):
    # The application owner requests TI; a different test client is another tenant.
    """Activate device TI through OEG, verify real device/application IPs and notification URL in the NEF, then verify deletion."""
    op,profile,token=progressive_scenario.operator,progressive_scenario.profile,progressive_scenario.token
    qod=profile['qod']
    base=qod['base_url']+'/3gpp-traffic-influence/v1/af/subscriptions'
Loading