diff --git a/.github/workflows/docs_spread.yaml b/.github/workflows/docs_spread.yaml deleted file mode 100644 index 323130614..000000000 --- a/.github/workflows/docs_spread.yaml +++ /dev/null @@ -1,18 +0,0 @@ -name: Automated spread testing - -on: - workflow_dispatch: - pull_request: - paths: - - 'docs/tutorial/getting-started.md' - schedule: - - cron: 0 9 * * 1 # At 09:00 UTC on Monday, aligned with the weekly haproxy stable release. - -jobs: - docs-checks: - uses: canonical/operator-workflows/.github/workflows/docs_spread.yaml@main - secrets: inherit - with: - input-file: docs/tutorial/getting-started.md - output-dir: tests/spread/tutorial - spread-job: github-ci:ubuntu-24.04:tests/ diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml index 3e111ee79..7f82b09fb 100644 --- a/.github/workflows/integration_test.yaml +++ b/.github/workflows/integration_test.yaml @@ -1,74 +1,19 @@ +# Copyright 2025 Canonical Ltd. +# See LICENSE file for licensing details. + name: Integration tests on: pull_request: schedule: - - cron: "0 15 * * SAT" + - cron: "0 15 * * SAT" + - cron: "0 9 * * MON" # aligned with weekly haproxy stable release jobs: - integration-tests: - strategy: - matrix: - charm: - - name: haproxy-operator - working-directory: ./haproxy-operator - modules: | - [ - "test_action.py", - "test_actions.py", - "test_charm.py", - "test_config.py", - "test_cos.py", - "test_ha.py", - "test_haproxy_route.py", - "test_http_interface.py", - "test_ingress.py", - "test_ingress_per_unit.py", - "test_haproxy_route_tcp.py", - "test_haproxy_route_grpc.py", - "test_haproxy_route_https_backend.py" - ] - - name: haproxy-spoe-auth-operator - working-directory: ./haproxy-spoe-auth-operator - modules: '["test_charm.py"]' - - name: haproxy-ddos-protection-configurator - working-directory: ./haproxy-ddos-protection-configurator - modules: '["test_charm.py"]' - - name: haproxy-route-policy-operator - working-directory: ./haproxy-route-policy-operator - modules: '["test_charm.py", "test_haproxy_route_policy_relation.py"]' - name: Integration tests for ${{ matrix.charm.name }} - uses: canonical/operator-workflows/.github/workflows/integration_test.yaml@main - secrets: inherit - with: - provider: lxd - juju-channel: 3/stable - self-hosted-runner: true - charmcraft-channel: latest/edge - working-directory: ${{ matrix.charm.working-directory }} - modules: ${{ matrix.charm.modules }} - with-uv: true - integration-tests-global: - uses: - canonical/operator-workflows/.github/workflows/integration_test.yaml@main + integration-test: + uses: canonical/charm-ci/.github/workflows/integration-test.yml@main secrets: inherit - with: - use-canonical-k8s: true - provider: 'k8s' - channel: '1.32-classic/stable' - self-hosted-runner: true - juju-channel: 3/stable - modules: | - [ - "test_oauth_spoe.py", - "test_haproxy_ddos.py", - "test_haproxy_route_policy.py" - ] - with-uv: true - pre-run-script: ./tests/integration/setup-integration-tests.sh - allure-report: - if: ${{ !cancelled() && github.event_name == 'schedule' }} - needs: - - integration-tests - - integration-tests-global - uses: canonical/operator-workflows/.github/workflows/allure_report.yaml@main + permissions: + contents: read + packages: write + actions: read diff --git a/.gitignore b/.gitignore index 6326091fe..ec6ee4b95 100644 --- a/.gitignore +++ b/.gitignore @@ -32,5 +32,4 @@ terraform/**/*.tfstate* haproxy-route-policy/db.sqlite3 haproxy-route-policy/.python-version -tests/spread/tutorial/ **/.spread-reuse* diff --git a/artifacts.yaml b/artifacts.yaml new file mode 100644 index 000000000..38fa32839 --- /dev/null +++ b/artifacts.yaml @@ -0,0 +1,36 @@ +# Copyright 2025 Canonical Ltd. +# See LICENSE file for licensing details. + +version: 1 +rocks: [] +charms: +- name: haproxy-ddos-protection-configurator + charmcraft-yaml: haproxy-ddos-protection-configurator/charmcraft.yaml + resources: {} + platforms: + - arch: amd64 +- name: haproxy + charmcraft-yaml: haproxy-operator/charmcraft.yaml + resources: {} + platforms: + - arch: amd64 +- name: haproxy-route-policy + charmcraft-yaml: haproxy-route-policy-operator/charmcraft.yaml + resources: {} + platforms: + - arch: amd64 +- name: haproxy-spoe-auth + charmcraft-yaml: haproxy-spoe-auth-operator/charmcraft.yaml + resources: {} + platforms: + - arch: amd64 +snaps: +- name: haproxy-route-policy + snapcraft-yaml: haproxy-route-policy/snap/snapcraft.yaml + pack-dir: haproxy-route-policy + platforms: + - arch: amd64 +- name: haproxy-spoe-auth + snapcraft-yaml: haproxy-spoe-auth-snap/snapcraft.yaml + platforms: + - arch: amd64 diff --git a/concierge-ck8s.yaml b/concierge-ck8s.yaml new file mode 100644 index 000000000..af5ff4499 --- /dev/null +++ b/concierge-ck8s.yaml @@ -0,0 +1,22 @@ +# Copyright 2025 Canonical Ltd. +# See LICENSE file for licensing details. + +juju: + channel: 3/stable + model-defaults: + test-mode: "true" + automatically-retry-hooks: "false" + +providers: + lxd: + enable: true + bootstrap: true + k8s: + enable: true + bootstrap: false + channel: 1.32-classic/stable + features: + load-balancer: + enabled: true + l2-mode: true + cidrs: 10.43.45.0/24 diff --git a/concierge.yaml b/concierge.yaml new file mode 100644 index 000000000..4194f1c29 --- /dev/null +++ b/concierge.yaml @@ -0,0 +1,19 @@ +# Copyright 2025 Canonical Ltd. +# See LICENSE file for licensing details. + +juju: + channel: 3/stable + model-defaults: + test-mode: "true" + automatically-retry-hooks: "false" + +providers: + lxd: + enable: true + bootstrap: true + +host: + snaps: + charmcraft: + channel: latest/stable + classic: true diff --git a/spread.yaml b/spread.yaml index b15cc34e8..c61b077e7 100644 --- a/spread.yaml +++ b/spread.yaml @@ -1,35 +1,78 @@ -# Copyright 2026 Canonical Ltd. +# Copyright 2025 Canonical Ltd. # See LICENSE file for licensing details. -project: haproxy-operator-tests - +project: haproxy-operator +path: /home/ubuntu/proj +kill-timeout: 60m +warn-timeout: 1m backends: - github-ci: - type: adhoc - - allocate: | - echo "Allocating ad-hoc $SPREAD_SYSTEM" - if [ -z "${GITHUB_RUN_ID:-}" ]; then - FATAL "this back-end only works inside GitHub CI" - exit 1 - fi - echo 'ubuntu ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/99-spread-users - ADDRESS localhost:22 - discard: | - echo "Discarding ad-hoc $SPREAD_SYSTEM" + integration-test: + type: integration-test systems: - # username and password are required because docs-spread.yaml creates a new user (ubuntu:ubuntu) - # Before tests are ran. - - ubuntu-24.04: - username: ubuntu - password: ubuntu - workers: 1 - -suites: - tests/spread/: - summary: Automated spread testing + - ubuntu-24.04 + docs: + type: opcli-minimal systems: - - ubuntu-24.04 - -path: /home/spread/proj -kill-timeout: 1h + - ubuntu-24.04 +environment: + CONCIERGE: '$(HOST: echo "${CONCIERGE:-concierge.yaml}")' + OPCLI_GIT_REF: '$(HOST: echo "${OPCLI_GIT_REF:-main}")' + TUTORIAL: docs/tutorial/getting-started.md +suites: + tests/spread/tutorial/: + summary: Tutorial smoke test (getting-started.md) + backends: + - docs +exclude: +- .git +- .tox +- .venv +- .*_cache +integration-suites: + haproxy-operator/tests/integration/: + summary: haproxy-operator integration tests + working-dir: haproxy-operator/ + backends: + - integration-test + pytest-arguments-template: | + {% set charm = artifacts.charms | selectattr("name", "equalto", "haproxy") | first %} + {% for build in charm.builds | selectattr("arch", "equalto", arch) %} + --charm-file={{ build.path | replace('./', '../', 1) }} + {% endfor %} + haproxy-spoe-auth-operator/tests/integration/: + summary: haproxy-spoe-auth-operator integration tests + working-dir: haproxy-spoe-auth-operator/ + backends: + - integration-test + pytest-arguments-template: | + {% set charm = artifacts.charms | selectattr("name", "equalto", "haproxy-spoe-auth") | first %} + {% for build in charm.builds | selectattr("arch", "equalto", arch) %} + --charm-file={{ build.path | replace('./', '../', 1) }} + {% endfor %} + haproxy-ddos-protection-configurator/tests/integration/: + summary: haproxy-ddos-protection-configurator integration tests + working-dir: haproxy-ddos-protection-configurator/ + backends: + - integration-test + pytest-arguments-template: | + {% set charm = artifacts.charms | selectattr("name", "equalto", "haproxy-ddos-protection-configurator") | first %} + {% for build in charm.builds | selectattr("arch", "equalto", arch) %} + --charm-file={{ build.path | replace('./', '../', 1) }} + {% endfor %} + haproxy-route-policy-operator/tests/integration/: + summary: haproxy-route-policy-operator integration tests + working-dir: haproxy-route-policy-operator/ + backends: + - integration-test + pytest-arguments-template: | + {% set charm = artifacts.charms | selectattr("name", "equalto", "haproxy-route-policy") | first %} + {% for build in charm.builds | selectattr("arch", "equalto", arch) %} + --charm-file={{ build.path | replace('./', '../', 1) }} + {% endfor %} + tests/integration/: + summary: global integration tests (cross-charm, requires LXD + canonical-k8s) + working-dir: ./ + backends: + - integration-test + environment: + CONCIERGE: concierge-ck8s.yaml diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 91bdf28e5..61f6ee305 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -27,12 +27,31 @@ POSTGRESQL_APPLICATION = "db" +@pytest.fixture(scope="session", name="k8s_cloud_client") +def k8s_cloud_client_fixture(): + """Register the k8s cloud in the local Juju client config.""" + k8s_config = subprocess.run( # nosec + ["sudo", "k8s", "config"], capture_output=True + ) + if k8s_config.returncode != 0: + raise RuntimeError( + f"k8s config failed (exit {k8s_config.returncode}):\n" + f"stdout: {k8s_config.stdout.decode()}\n" + f"stderr: {k8s_config.stderr.decode()}" + ) + subprocess.run( # nosec + ["juju", "add-k8s", "ck8s", "--client"], + input=k8s_config.stdout, + check=True, + ) + + @pytest.fixture(scope="session", name="lxd_juju") def lxd_juju_fixture(request: pytest.FixtureRequest): """Bootstrap a new lxd controller and model and return a Juju fixture for it.""" juju = jubilant.Juju() - lxd_controller_name = "localhost" + lxd_controller_name = "concierge-lxd" lxd_cloud_name = "localhost" juju.wait_timeout = JUJU_WAIT_TIMEOUT try: @@ -69,7 +88,7 @@ def lxd_juju_fixture(request: pytest.FixtureRequest): @pytest.fixture(scope="session", name="k8s_juju") -def k8s_juju_fixture(lxd_juju: jubilant.Juju, request: pytest.FixtureRequest): +def k8s_juju_fixture(lxd_juju: jubilant.Juju, request: pytest.FixtureRequest, k8s_cloud_client): """Bootstrap a new k8s model in the lxd controller and return a Juju fixture for it.""" clouds_json = lxd_juju.cli("clouds", "--format=json", include_model=False) clouds = json.loads(clouds_json) @@ -355,7 +374,7 @@ def deployer(haproxy_spoe_name, hostname): ) ca_cert = ca_cert_result.results["ca-certificate"].encode("utf-8") lxd_juju.wait( - lambda status: not status.apps[haproxy_spoe_name].is_waiting, timeout=5 * 60 + lambda status: not status.apps[haproxy_spoe_name].is_waiting, timeout=330 ) logger.info(lxd_juju.status().apps[haproxy_spoe_name]) inject_ca_certificate(lxd_juju, f"{haproxy_spoe_name}/0", ca_cert) diff --git a/tests/spread/tutorial/run/task.yaml b/tests/spread/tutorial/run/task.yaml new file mode 100644 index 000000000..a8d1c822a --- /dev/null +++ b/tests/spread/tutorial/run/task.yaml @@ -0,0 +1,8 @@ +# Copyright 2025 Canonical Ltd. +# See LICENSE file for licensing details. + +summary: Run getting-started tutorial + +execute: | + opcli tutorial expand -- "${SPREAD_PATH}/${TUTORIAL}" > /tmp/tutorial-script.sh + runuser -l ubuntu -s /bin/bash -c 'set -ex; . "$1"' _ /tmp/tutorial-script.sh