diff --git a/.github/workflows/_quality-gate-candidate-branch.yaml b/.github/workflows/_quality-gate-candidate-branch.yaml new file mode 100644 index 00000000..4765af4a --- /dev/null +++ b/.github/workflows/_quality-gate-candidate-branch.yaml @@ -0,0 +1,91 @@ +name: Test and promote a branch's solutions + +# Reusable workflow: discovers every solution defined under tests/solution/ +# on a single branch, deploys and tests each one, and — only if all of them +# pass — promotes every charm pinned by that branch's Terraform modules from +# /beta to /candidate. +# +# Called once per supported branch/track from quality-gates-candidate.yaml, so adding +# a new track only requires adding an entry to that workflow's branch +# matrix, and adding a new solution only requires adding a directory under +# tests/solution/ (each branch discovers its own set of solutions, so older +# branches without a newer solution are unaffected). + +on: + workflow_call: + inputs: + branch: + description: 'The branch/track to check out and test (e.g. "main", "track/3.0").' + required: true + type: string + secrets: + CHARMCRAFT_AUTH: + required: true + +jobs: + discover-solutions: + name: Discover solutions + runs-on: ubuntu-latest + outputs: + solutions: ${{ steps.discover.outputs.solutions }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch }} + + - name: List solutions with a Terraform wrapper module + id: discover + run: | + solutions=$(find tests/solution -mindepth 2 -maxdepth 2 -type d -name terraform -printf '%P\n' | sed 's|/terraform$||' | sort | jq -R -s -c 'split("\n") | map(select(length > 0))') + echo "solutions=$solutions" >> "$GITHUB_OUTPUT" + + test-solutions: + name: Test ${{ matrix.solution }} + needs: [discover-solutions] + strategy: + fail-fast: false + matrix: + solution: ${{ fromJson(needs.discover-solutions.outputs.solutions) }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch }} + + - name: Install dependencies + run: | + sudo snap install concierge --classic + sudo concierge prepare -p k8s --extra-snaps just,astral-uv,terraform + # Configure Cilium for Istio ambient service mesh compatibility + sudo k8s kubectl patch configmap cilium-config -n kube-system --type merge -p '{"data":{"bpf-lb-sock-hostns-only":"true"}}' + sudo k8s kubectl rollout restart daemonset/cilium -n kube-system + sudo k8s kubectl rollout status daemonset/cilium -n kube-system --timeout=90s + + - name: Run solution tests + run: just quality-gates test-solution ${{ matrix.solution }} + + promote: + name: Promote ${{ matrix.solution }} + needs: [discover-solutions, test-solutions] + strategy: + matrix: + solution: ${{ fromJson(needs.discover-solutions.outputs.solutions) }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch }} + + - name: Install dependencies + run: | + sudo snap install terraform --classic + sudo snap install just --classic + sudo snap install charmcraft --classic + + - name: Promote + env: + CHARMCRAFT_AUTH: ${{ secrets.CHARMCRAFT_AUTH }} + run: just quality-gates promote-solution ${{ matrix.solution }} ${{ inputs.branch }} diff --git a/.github/workflows/quality-gates-candidate.yaml b/.github/workflows/quality-gates-candidate.yaml new file mode 100644 index 00000000..6e14b825 --- /dev/null +++ b/.github/workflows/quality-gates-candidate.yaml @@ -0,0 +1,27 @@ +name: Solution tests + +# Periodically (and on demand) deploy the COS and COS Lite solutions from +# every supported branch and run their solution test suites +# (tests/solution). For each branch, when both solutions pass, promote every +# charm pinned by that branch's Terraform modules from /beta to /candidate. +# +# To support a new branch/track, add it to the `branch` matrix below; a +# separate, independent test+promote pipeline is instantiated per branch. + +on: + workflow_dispatch: {} + schedule: + # Once a month, on the 1st at 03:00 UTC. + - cron: "0 3 1 * *" + +jobs: + solutions: + name: ${{ matrix.branch }} + strategy: + fail-fast: false + matrix: + branch: [main, track/3.0] + uses: ./.github/workflows/_quality-gate-candidate-branch.yaml + secrets: inherit + with: + branch: ${{ matrix.branch }} diff --git a/justfile b/justfile index 37a26925..b8617fdc 100644 --- a/justfile +++ b/justfile @@ -4,6 +4,8 @@ set export # Just variables are exported to the environment terraform := `which terraform || which tofu || echo ""` # require 'terraform' or 'opentofu' uv_flags := "--frozen --isolated" +mod quality-gates + [private] default: just --list diff --git a/quality-gates.just b/quality-gates.just new file mode 100644 index 00000000..3ed169d6 --- /dev/null +++ b/quality-gates.just @@ -0,0 +1,70 @@ +set quiet # Recipes are silent by default +set export # Just variables are exported to the environment + +terraform := `which terraform || which tofu || echo ""` # require 'terraform' or 'opentofu' +uv_flags := "--frozen --isolated" + +[private] +@default: + just --list quality-gates + +# Init/apply a solution's Terraform module, then run its solution test suite +[group("test")] +[working-directory("./tests/solution")] +test-solution solution="cos-lite": + #!/usr/bin/env bash + set -euo pipefail + echo "==> Testing solution: {{solution}}" + if [ -z "${terraform}" ]; then echo "ERROR: please install terraform or opentofu"; exit 1; fi + $terraform -chdir="{{solution}}/terraform" init -upgrade + $terraform -chdir="{{solution}}/terraform" apply -auto-approve + uv run ${uv_flags} pytest -vv -ra --capture=no "{{solution}}" + +# Promote every charm in a solution from /beta to /candidate +[group("promote")] +promote-solution solution="cos-lite" ref="main": + #!/usr/bin/env bash + set -euo pipefail + echo "==> Promoting solution: {{solution}} (ref: {{ref}})" + charms_tracks=$(just charm-tracks "{{solution}}" "{{ref}}") + if [ -z "$charms_tracks" ]; then + echo "No pinned charms found for solution '{{solution}}'; nothing to promote." + exit 0 + fi + while read -r charm track; do + echo "--> Promoting $charm from ${track}/beta to ${track}/candidate" + charmcraft promote --name "$charm" --from-channel "${track}/beta" --to-channel "${track}/candidate" --yes + done <<< "$charms_tracks" + +# List the charm name + track pinned by each charm in a product module +[group("promote")] +charm-tracks module ref='main': + #!/usr/bin/env bash + set -euo pipefail + excluded_charms="self-signed-certificates traefik-k8s s3-integrator" + upgrades=$(git show "{{ref}}:terraform/{{module}}/upgrades.tf") + locals=$(git show "{{ref}}:terraform/{{module}}/locals.tf") + + charm_keys=$(awk ' + /data[[:space:]]*"juju_charm"[[:space:]]*"/ { in_block=1; next } + in_block && /^\}/ { in_block=0; charm=""; key=""; next } + in_block && match($0, /charm[[:space:]]*=[[:space:]]*"([^"]*)"/, m) { charm=m[1] } + in_block && match($0, /channel[[:space:]]*=[[:space:]]*local\.channels\.([a-zA-Z0-9_]+)/, m) { key=m[1] } + in_block && charm != "" && key != "" { print charm, key; charm=""; key="" } + ' <<< "$upgrades") + + declare -A tracks + while read -r tkey tval; do + [ -n "$tkey" ] && tracks["$tkey"]="$tval" + done < <(awk ' + /tracks[[:space:]]*=[[:space:]]*\{/ { in_tracks=1; next } + in_tracks && /^ \}/ { in_tracks=0; next } + in_tracks && match($0, /^[[:space:]]*([a-zA-Z0-9_]+)[[:space:]]*=[[:space:]]*"([^"]*)"/, m) { print m[1], m[2] } + ' <<< "$locals") + + while read -r charm key; do + [ -z "$charm" ] && continue + case " $excluded_charms " in *" $charm "*) continue ;; esac + track="${tracks[$key]:-}" + [ -n "$track" ] && printf '%s %s\n' "$charm" "$track" + done <<< "$charm_keys" diff --git a/tests/solution/cos-lite/terraform/main.tf b/tests/solution/cos-lite/terraform/main.tf new file mode 100644 index 00000000..5b148e99 --- /dev/null +++ b/tests/solution/cos-lite/terraform/main.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.5" + required_providers { + juju = { + source = "juju/juju" + version = ">= 1.0" + } + } +} + +# All defaults: creates its own model ("cos-lite"), edge risk, self-signed TLS. +module "cos-lite" { + source = "../../../../terraform/cos-lite" +} diff --git a/tests/solution/cos-lite/test_solution.py b/tests/solution/cos-lite/test_solution.py new file mode 100644 index 00000000..06066de6 --- /dev/null +++ b/tests/solution/cos-lite/test_solution.py @@ -0,0 +1,14 @@ +"""Copyright 2025 Canonical Ltd. +See LICENSE file for licensing details. + +Placeholder solution test for COS Lite. + +TODO: replace with real assertions against the deployed solution (e.g. via +jubilant). Feature files / BDD steps / helpers can be introduced later; this +smoke test only confirms the test harness (terraform apply -> pytest) wires +up correctly. +""" + + +def test_solution_test_harness_runs(): + assert True diff --git a/tests/solution/cos/terraform/main.tf b/tests/solution/cos/terraform/main.tf new file mode 100644 index 00000000..a8fd97f9 --- /dev/null +++ b/tests/solution/cos/terraform/main.tf @@ -0,0 +1,35 @@ +terraform { + required_version = ">= 1.5" + required_providers { + juju = { + source = "juju/juju" + version = ">= 1.0" + } + } +} + +resource "juju_model" "cos" { + name = "cos" +} + +# SeaweedFS provides an in-cluster S3-compatible store, avoiding the need for +# external S3 credentials/infrastructure in this smoke test. Its S3 gateway +# uses fixed placeholder credentials (see the seaweedfs-k8s charm source), +# and is reachable at its Juju unit's in-cluster Kubernetes service address. +module "seaweedfs" { + source = "../../../../terraform/seaweedfs" + model_uuid = juju_model.cos.uuid +} + +# All other defaults: edge risk, self-signed internal TLS (no external CA +# needed). +module "cos" { + source = "../../../../terraform/cos" + depends_on = [module.seaweedfs] + + model = { uuid = juju_model.cos.uuid } + + s3_endpoint = "http://${module.seaweedfs.app_name}.cos.svc.cluster.local:8333" + s3_access_key = "placeholder" + s3_secret_key = "placeholder" +} diff --git a/tests/solution/cos/test_solution.py b/tests/solution/cos/test_solution.py new file mode 100644 index 00000000..b0d8c986 --- /dev/null +++ b/tests/solution/cos/test_solution.py @@ -0,0 +1,14 @@ +"""Copyright 2025 Canonical Ltd. +See LICENSE file for licensing details. + +Placeholder solution test for COS. + +TODO: replace with real assertions against the deployed solution (e.g. via +jubilant). Feature files / BDD steps / helpers can be introduced later; this +smoke test only confirms the test harness (terraform apply -> pytest) wires +up correctly. +""" + + +def test_solution_test_harness_runs(): + assert True