continuous-delivery #413
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# This workflow executes the E2E Test Suite for a series of combinations that | |
# represent different execution environments | |
name: continuous-delivery | |
on: | |
issue_comment: | |
type: [created] | |
# Manually or triggered by another workflow | |
workflow_dispatch: | |
inputs: | |
depth: | |
description: 'Depth (push, pull_request, main (default), schedule)' | |
required: true | |
default: 'main' | |
limit: | |
description: 'Limit to the specified engines list (local, eks, aks, gke, openshift)' | |
required: false | |
test_level: | |
description: 'Test level: 0(highest) to 4(lowest). Default is 4.' | |
required: false | |
default: '4' | |
feature_type: | |
description: > | |
Feature Type (disruptive, performance, upgrade, smoke, basic, service-connectivity, self-healing, | |
backup-restore, snapshot, operator, observability, replication, plugin, postgres-configuration, | |
pod-scheduling, cluster-metadata, recovery, importing-databases, storage, security, maintenance, | |
tablespaces) | |
required: false | |
log_level: | |
description: 'Log level for operator (error, warning, info, debug(default), trace)' | |
required: false | |
default: 'debug' | |
schedule: | |
- cron: '0 1 * * *' | |
# set up environment variables to be used across all the jobs | |
env: | |
GOLANG_VERSION: "1.23.x" | |
KUBEBUILDER_VERSION: "2.3.1" | |
KIND_VERSION: "v0.24.0" | |
ROOK_VERSION: "v1.15.2" | |
EXTERNAL_SNAPSHOTTER_VERSION: "v8.1.0" | |
OPERATOR_IMAGE_NAME: "ghcr.io/${{ github.repository }}-testing" | |
BUILD_PUSH_PROVENANCE: "" | |
BUILD_PUSH_CACHE_FROM: "" | |
BUILD_PUSH_CACHE_TO: "" | |
REGISTRY: "ghcr.io" | |
REGISTRY_USER: ${{ github.actor }} | |
REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} | |
REPOSITORY_OWNER: "cloudnative-pg" | |
SLACK_USERNAME: "cnpg-bot" | |
BUILD_MANAGER_RELEASE_ARGS: "build --skip=validate --clean --id manager" | |
# Keep in mind that adding more platforms (architectures) will increase the building | |
# time even if we use the ghcache for the building process. | |
PLATFORMS: "linux/amd64,linux/arm64" | |
E2E_SUFFIX: "cnpge2e" | |
defaults: | |
run: | |
# default failure handling for shell scripts in 'run' steps | |
shell: 'bash -Eeuo pipefail -x {0}' | |
jobs: | |
# Trigger the workflow on release-* branches for smoke testing whenever it's a scheduled run. | |
# Note: this is a workaround since we can't directly schedule-run a workflow from a non default branch | |
smoke_test_release_branches: | |
runs-on: ubuntu-24.04 | |
name: smoke test release-* branches when it's a scheduled run | |
if: github.event_name == 'schedule' | |
strategy: | |
fail-fast: false | |
matrix: | |
branch: [release-1.22, release-1.23, release-1.24] | |
steps: | |
- name: Invoke workflow with inputs | |
uses: benc-uk/workflow-dispatch@v1 | |
with: | |
workflow: continuous-delivery | |
ref: ${{ matrix.branch }} | |
inputs: '{ "depth": "push", "limit": "local", "test_level": "4", "log_level": "debug" }' | |
check_commenter: | |
if: | | |
github.event_name == 'issue_comment' && | |
github.event.issue.pull_request && | |
startsWith(github.event.comment.body, '/test') | |
name: Retrieve command | |
runs-on: ubuntu-24.04 | |
outputs: | |
github_ref: ${{ steps.refs.outputs.head_sha }} | |
depth: ${{ env.DEPTH }} | |
limit: ${{ env.LIMIT }} | |
test_level: ${{ env.TEST_LEVEL }} | |
feature_type: ${{ env.FEATURE_TYPE }} | |
log_level: ${{ env.LOG_LEVEL }} | |
steps: | |
- name: Check for Command | |
id: command | |
uses: xt0rted/slash-command-action@v2 | |
continue-on-error: false | |
with: | |
command: test | |
reaction: "true" | |
reaction-type: "eyes" | |
allow-edits: "false" | |
permission-level: write | |
- name: Process arguments | |
id: args | |
run: | | |
ARGS="${{ steps.command.outputs.command-arguments }}" | |
# Set the defaults | |
DEPTH="main" | |
LIMIT="local" | |
TEST_LEVEL="4" | |
FEATURE_TYPE="" | |
LOG_LEVEL="debug" | |
for ARG in $ARGS; do | |
IFS='=' read name value <<< $ARG | |
case "${name}" in | |
"depth"|"d") | |
DEPTH="${value}" | |
;; | |
"limit"|"l") | |
LIMIT="${value}" | |
;; | |
"test_level"|"level"|"tl") | |
TEST_LEVEL="${value}" | |
;; | |
"feature_type"|"type"|"ft") | |
FEATURE_TYPE="${value}" | |
;; | |
"log_level"|"ll") | |
LOG_LEVEL="${value}" | |
;; | |
*) | |
;; | |
esac | |
done | |
echo "DEPTH=${DEPTH}" >> $GITHUB_ENV | |
echo "LIMIT=${LIMIT}" >> $GITHUB_ENV | |
echo "TEST_LEVEL=${TEST_LEVEL}" >> $GITHUB_ENV | |
echo "FEATURE_TYPE=${FEATURE_TYPE}" >> $GITHUB_ENV | |
echo "LOG_LEVEL=${LOG_LEVEL}" >> $GITHUB_ENV | |
- name: Resolve Git reference | |
uses: xt0rted/pull-request-comment-branch@v2 | |
id: refs | |
- name: Create comment | |
uses: peter-evans/create-or-update-comment@v4 | |
with: | |
token: ${{ secrets.GITHUB_TOKEN }} | |
repository: ${{ github.repository }} | |
issue-number: ${{ github.event.issue.number }} | |
body: | | |
@${{ github.actor }}, here's the link to the E2E on CNPG workflow run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
test_arguments: | |
name: Parse arguments | |
if: | | |
github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' | |
runs-on: ubuntu-24.04 | |
outputs: | |
github_ref: ${{ github.ref }} | |
depth: ${{ env.DEPTH }} | |
limit: ${{ env.LIMIT }} | |
test_level: ${{ env.TEST_LEVEL }} | |
feature_type: ${{ env.FEATURE_TYPE }} | |
log_level: ${{ env.LOG_LEVEL }} | |
steps: | |
- name: Parse input to env | |
run: | | |
# Set the defaults for workflow dispatch | |
if [[ ${{ github.event_name }} == 'workflow_dispatch' ]]; then | |
DEPTH=${{ github.event.inputs.depth }} | |
LIMIT=${{ github.event.inputs.limit }} | |
TEST_LEVEL=${{ github.event.inputs.test_level }} | |
FEATURE_TYPE="${{ github.event.inputs.feature_type }}" | |
LOG_LEVEL="${{ github.event.inputs.log_level }}" | |
fi | |
# Set the defaults for schedule dispatch | |
if [[ ${{ github.event_name }} == 'schedule' ]]; then | |
DEPTH="schedule" | |
LIMIT="" | |
TEST_LEVEL="4" | |
FEATURE_TYPE="" | |
LOG_LEVEL="debug" | |
fi | |
echo "DEPTH=${DEPTH}" >> $GITHUB_ENV | |
echo "LIMIT=${LIMIT}" >> $GITHUB_ENV | |
echo "TEST_LEVEL=${TEST_LEVEL}" >> $GITHUB_ENV | |
echo "FEATURE_TYPE=${FEATURE_TYPE}" >> $GITHUB_ENV | |
echo "LOG_LEVEL=${LOG_LEVEL}" >> $GITHUB_ENV | |
evaluate_options: | |
name: Evaluate workflow options | |
needs: | |
- check_commenter | |
- test_arguments | |
runs-on: ubuntu-24.04 | |
if: | | |
( | |
needs.check_commenter.result == 'success' || | |
needs.test_arguments.result == 'success' | |
) && | |
!cancelled() | |
outputs: | |
git_ref: ${{ env.GITHUB_REF }} | |
depth: ${{ env.DEPTH }} | |
limit: ${{ env.LIMIT }} | |
test_level: ${{ env.TEST_LEVEL }} | |
feature_type: ${{ env.FEATURE_TYPE }} | |
log_level: ${{ env.LOG_LEVEL }} | |
steps: | |
- name: From command | |
run: | | |
if [[ ${{ github.event_name }} == 'workflow_dispatch' ]] || [[ ${{ github.event_name }} == 'schedule' ]]; then | |
echo 'GITHUB_REF=${{ needs.test_arguments.outputs.github_ref }}' >> $GITHUB_ENV | |
echo 'DEPTH=${{ needs.test_arguments.outputs.depth }}' >> $GITHUB_ENV | |
echo 'LIMIT=${{ needs.test_arguments.outputs.limit }}' >> $GITHUB_ENV | |
echo 'TEST_LEVEL=${{ needs.test_arguments.outputs.test_level }}' >> $GITHUB_ENV | |
echo 'FEATURE_TYPE=${{ needs.test_arguments.outputs.feature_type }}' >> $GITHUB_ENV | |
echo 'LOG_LEVEL=${{ needs.test_arguments.outputs.log_level }}' >> $GITHUB_ENV | |
fi | |
if [[ ${{ github.event_name }} == 'issue_comment' ]]; then | |
echo 'GITHUB_REF=${{ needs.check_commenter.outputs.github_ref }}' >> $GITHUB_ENV | |
echo 'DEPTH=${{ needs.check_commenter.outputs.depth }}' >> $GITHUB_ENV | |
echo 'LIMIT=${{ needs.check_commenter.outputs.limit }}' >> $GITHUB_ENV | |
echo 'TEST_LEVEL=${{ needs.check_commenter.outputs.test_level }}' >> $GITHUB_ENV | |
echo 'FEATURE_TYPE=${{ needs.check_commenter.outputs.feature_type }}' >> $GITHUB_ENV | |
echo 'LOG_LEVEL=${{ needs.check_commenter.outputs.log_level }}' >> $GITHUB_ENV | |
fi | |
buildx: | |
name: Build containers | |
needs: | |
- check_commenter | |
- test_arguments | |
- evaluate_options | |
if: | | |
always() && !cancelled() && | |
needs.evaluate_options.result == 'success' | |
runs-on: ubuntu-24.04 | |
permissions: | |
contents: read | |
packages: write | |
pull-requests: read | |
outputs: | |
image: ${{ steps.image-meta.outputs.image }} | |
# 'branch_name' is used in 'GetMostRecentReleaseTag' in the Go code | |
branch_name: ${{ steps.build-meta.outputs.branch_name }} | |
upload_artifacts: ${{ steps.build-meta.outputs.upload_artifacts }} | |
commit_msg: ${{ steps.build-meta.outputs.commit_msg }} | |
commit_sha: ${{ steps.build-meta.outputs.commit_sha }} | |
author_name: ${{ steps.build-meta.outputs.author_name }} | |
author_email: ${{ steps.build-meta.outputs.author_email }} | |
controller_img: ${{ env.CONTROLLER_IMG }} | |
controller_img_ubi8: ${{ env.CONTROLLER_IMG_UBI8 }} | |
bundle_img: ${{ env.BUNDLE_IMG }} | |
catalog_img: ${{ env.CATALOG_IMG }} | |
steps: | |
- | |
name: Checkout | |
uses: actions/checkout@v4 | |
with: | |
ref: ${{ needs.evaluate_options.outputs.git_ref }} | |
# To identify the commit we need the history and all the tags. | |
fetch-depth: 0 | |
- | |
name: Install Go | |
uses: actions/setup-go@v5 | |
with: | |
go-version: ${{ env.GOLANG_VERSION }} | |
check-latest: true | |
- | |
name: Build meta | |
id: build-meta | |
env: | |
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
run: | | |
images='${{ env.OPERATOR_IMAGE_NAME }}' | |
tags='' | |
labels='' | |
commit_sha=${{ needs.evaluate_options.outputs.git_ref }} | |
commit_date=$(git log -1 --pretty=format:'%ad' --date short "${commit_sha}" || : ) | |
# use git describe to get the nearest tag and use that to build the version (e.g. 1.4.0-dev24 or 1.4.0) | |
commit_version=$(git describe --tags --match 'v*' "${commit_sha}"| sed -e 's/^v//; s/-g[0-9a-f]\+$//; s/-\([0-9]\+\)$/-dev\1/') | |
# shortened commit sha | |
commit_short=$(git rev-parse --short "${commit_sha}") | |
# multiline strings are weird | |
commit_message=$(git show -s --format=%B "${commit_sha}") | |
commit_message=${commit_message//$'%'/'%25'} | |
commit_message=${commit_message//$'\n'/'%0A'} | |
commit_message=${commit_message//$'\r'/'%0D'} | |
# get git user and email | |
author_name=$(git show -s --format='%an' "${commit_sha}") | |
author_email=$(git show -s --format='%ae' "${commit_sha}") | |
# extract branch name | |
if [[ ${{ github.event_name }} == 'workflow_dispatch' ]] || [[ ${{ github.event_name }} == 'schedule' ]] | |
then | |
branch_name=${GITHUB_REF#refs/heads/} | |
fi | |
if [[ ${{ github.event_name }} == 'issue_comment' ]] | |
then | |
branch_name=$(gh pr view "${{ github.event.issue.number }}" --json headRefName -q '.headRefName' 2>/dev/null) | |
fi | |
# extract tag from branch name | |
tag_name=$(echo "$branch_name" | sed 's/[^a-zA-Z0-9]/-/g') | |
upload_artifacts=false | |
if [[ ${branch_name} == main || ${branch_name} =~ ^release- ]]; then | |
upload_artifacts=true | |
fi | |
echo "IMAGES=${images}" >> $GITHUB_ENV | |
echo "TAGS=${tags}" >> $GITHUB_ENV | |
echo "LABELS=${labels}" >> $GITHUB_ENV | |
echo "DATE=${commit_date}" >> $GITHUB_ENV | |
echo "VERSION=${commit_version}" >> $GITHUB_ENV | |
echo "COMMIT=${commit_short}" >> $GITHUB_ENV | |
echo "commit_sha=${commit_sha}" >> $GITHUB_OUTPUT | |
echo "commit_msg=${commit_message}" >> $GITHUB_OUTPUT | |
echo "author_name=${author_name}" >> $GITHUB_OUTPUT | |
echo "author_email=${author_email}" >> $GITHUB_OUTPUT | |
echo "branch_name=${branch_name}" >> $GITHUB_OUTPUT | |
echo "tag_name=${tag_name,,}" >> $GITHUB_OUTPUT | |
echo "upload_artifacts=${upload_artifacts}" >> $GITHUB_OUTPUT | |
- | |
name: Set GoReleaser environment | |
run: | | |
echo GOPATH=$(go env GOPATH) >> $GITHUB_ENV | |
echo PWD=$(pwd) >> $GITHUB_ENV | |
- | |
name: Run GoReleaser | |
uses: goreleaser/goreleaser-action@v6 | |
with: | |
distribution: goreleaser | |
version: v2 | |
args: ${{ env.BUILD_MANAGER_RELEASE_ARGS }} | |
env: | |
DATE: ${{ env.DATE }} | |
COMMIT: ${{ env.COMMIT }} | |
VERSION: ${{ env.VERSION }} | |
- | |
name: Docker meta | |
id: docker-meta | |
uses: docker/metadata-action@v5 | |
with: | |
images: ${{ env.IMAGES }} | |
tags: | | |
type=raw,value=${{ steps.build-meta.outputs.tag_name }} | |
- | |
name: Docker meta UBI8 | |
id: docker-meta-ubi8 | |
uses: docker/metadata-action@v5 | |
with: | |
images: ${{ env.IMAGES }} | |
flavor: | | |
suffix=-ubi8 | |
tags: | | |
type=raw,value=${{ steps.build-meta.outputs.tag_name }} | |
- | |
name: Set up QEMU | |
uses: docker/setup-qemu-action@v3 | |
with: | |
platforms: ${{ env.PLATFORMS }} | |
- | |
name: Set up Docker Buildx | |
uses: docker/setup-buildx-action@v3 | |
- | |
name: Login into docker registry | |
uses: docker/login-action@v3 | |
with: | |
registry: ${{ env.REGISTRY }} | |
username: ${{ env.REGISTRY_USER }} | |
password: ${{ env.REGISTRY_PASSWORD }} | |
- | |
name: Build and push | |
uses: docker/build-push-action@v6 | |
with: | |
platforms: ${{ env.PLATFORMS }} | |
context: . | |
file: Dockerfile | |
push: true | |
build-args: | | |
VERSION=${{ env.VERSION }} | |
tags: ${{ steps.docker-meta.outputs.tags }} | |
labels: ${{ env.LABELS }} | |
provenance: ${{ env.BUILD_PUSH_PROVENANCE }} | |
cache-from: ${{ env.BUILD_PUSH_CACHE_FROM }} | |
cache-to: ${{ env.BUILD_PUSH_CACHE_TO }} | |
- | |
name: Build and push UBI8 | |
uses: docker/build-push-action@v6 | |
with: | |
platforms: ${{ env.PLATFORMS }} | |
context: . | |
file: Dockerfile-ubi8 | |
push: true | |
build-args: | | |
VERSION=${{ env.VERSION }} | |
tags: ${{ steps.docker-meta-ubi8.outputs.tags }} | |
labels: ${{ env.LABELS }} | |
provenance: ${{ env.BUILD_PUSH_PROVENANCE }} | |
cache-from: ${{ env.BUILD_PUSH_CACHE_FROM }} | |
cache-to: ${{ env.BUILD_PUSH_CACHE_TO }} | |
- | |
name: Image Meta | |
id: image-meta | |
env: | |
TAGS: ${{ steps.docker-meta.outputs.tags }} | |
run: | | |
# If there is more than one tag, take the first one | |
# TAGS could be separated by newlines or commas | |
image=$(sed -n '1{s/,.*//; p}' <<< "$TAGS") | |
echo "image=${image}" >> $GITHUB_OUTPUT | |
- | |
name: Output images | |
env: | |
TAGS: ${{ steps.docker-meta.outputs.tags }} | |
TAGS_UBI8: ${{ steps.docker-meta-ubi8.outputs.tags }} | |
run: | | |
LOWERCASE_OPERATOR_IMAGE_NAME=${OPERATOR_IMAGE_NAME,,} | |
TAG=${TAGS#*:} | |
TAG_UBI=${TAGS_UBI8#*:} | |
echo "CONTROLLER_IMG=${LOWERCASE_OPERATOR_IMAGE_NAME}:${TAG}" >> $GITHUB_ENV | |
echo "CONTROLLER_IMG_UBI8=${LOWERCASE_OPERATOR_IMAGE_NAME}:${TAG_UBI}" >> $GITHUB_ENV | |
echo "BUNDLE_IMG=${LOWERCASE_OPERATOR_IMAGE_NAME}:bundle-${TAG}" >> $GITHUB_ENV | |
echo "CATALOG_IMG=${LOWERCASE_OPERATOR_IMAGE_NAME}:catalog-${TAG}" >> $GITHUB_ENV | |
- | |
name: Generate manifest for operator deployment | |
id: generate-manifest | |
env: | |
CONTROLLER_IMG: ${{ steps.image-meta.outputs.image }} | |
run: | | |
make generate-manifest | |
- | |
name: Upload the operator manifest as artifact in workflow | |
uses: actions/upload-artifact@v4 | |
with: | |
name: operator-manifest.yaml | |
path: dist/operator-manifest.yaml | |
retention-days: 7 | |
- | |
# In order to test the case of upgrading from the current operator | |
# to a future one, we build and push an image with a different VERSION | |
# to force a different hash for the manager binary. | |
# (Otherwise the ONLINE upgrade won't trigger) | |
# | |
# NOTE: we only fire this in TEST DEPTH = 4, as that is the level of the | |
# upgrade test | |
name: Build binary for upgrade test | |
uses: goreleaser/goreleaser-action@v6 | |
if: | | |
always() && !cancelled() && | |
needs.evaluate_options.outputs.test_level == '4' | |
with: | |
distribution: goreleaser | |
version: v2 | |
args: ${{ env.BUILD_MANAGER_RELEASE_ARGS }} | |
env: | |
DATE: ${{ env.DATE }} | |
COMMIT: ${{ env.COMMIT }} | |
VERSION: ${{ env.VERSION }}-prime | |
- | |
# In order to test the case of upgrading from the current operator | |
# to a future one, we build and push an image with a different VERSION | |
# to force a different hash for the manager binary. | |
# (Otherwise the ONLINE upgrade won't trigger) | |
# | |
# We push the "prime" binary using a tag with the suffix "-prime" | |
# NOTE: we only fire this in TEST DEPTH = 4, as that is the level of the | |
# upgrade test | |
name: Build and push image for upgrade test | |
uses: docker/build-push-action@v6 | |
if: | | |
always() && !cancelled() && | |
needs.evaluate_options.outputs.test_level == '4' | |
with: | |
platforms: ${{ env.PLATFORMS }} | |
context: . | |
file: Dockerfile | |
push: true | |
build-args: | | |
VERSION=${{ env.VERSION }}-prime | |
tags: ${{ steps.docker-meta.outputs.tags }}-prime | |
labels: ${{ env.LABELS }} | |
provenance: ${{ env.BUILD_PUSH_PROVENANCE }} | |
cache-from: ${{ env.BUILD_PUSH_CACHE_FROM }} | |
cache-to: ${{ env.BUILD_PUSH_CACHE_TO }} | |
# This will only execute in cloudnative-pg org | |
publish-artifacts: | |
name: Publish artifacts | |
needs: | |
- buildx | |
if: | | |
(always() && !cancelled()) && | |
needs.buildx.result == 'success' && | |
needs.buildx.outputs.upload_artifacts == 'true' && | |
github.repository_owner == 'cloudnative-pg' | |
runs-on: ubuntu-24.04 | |
steps: | |
- | |
name: Checkout artifact | |
uses: actions/checkout@v4 | |
with: | |
repository: cloudnative-pg/artifacts | |
token: ${{ secrets.REPO_GHA_PAT }} | |
ref: main | |
fetch-depth: 0 | |
- | |
name: Configure git user | |
run: | | |
git config user.email "${{ needs.buildx.outputs.author_email }}" | |
git config user.name "${{ needs.buildx.outputs.author_name }}" | |
- | |
name: Switch to or create the right branch | |
env: | |
BRANCH: ${{ needs.buildx.outputs.branch_name }} | |
run: | | |
git checkout "${BRANCH}" 2>/dev/null || git checkout -b "${BRANCH}" | |
# Remove the previous operator manifest if present because the next | |
# step doesn't overwrite existing files | |
rm -fr manifests/operator-manifest.yaml | |
- | |
name: Prepare the operator manifest | |
uses: actions/download-artifact@v4 | |
with: | |
name: operator-manifest.yaml | |
path: manifests | |
- | |
name: Prepare the commit | |
env: | |
COMMIT_MESSAGE: | | |
${{ needs.buildx.outputs.commit_msg }} | |
https://github.com/cloudnative-pg/cloudnative-pg/commit/${{ needs.buildx.outputs.commit_sha }} | |
run: | | |
# Skip creating the commit if there are no changes | |
[ -n "$(git status -s)" ] || exit 0 | |
git add . | |
git commit -m "${COMMIT_MESSAGE}" | |
- | |
name: Push changes | |
uses: ad-m/[email protected] | |
with: | |
github_token: ${{ secrets.REPO_GHA_PAT }} | |
repository: cloudnative-pg/artifacts | |
branch: ${{ needs.buildx.outputs.branch_name }} | |
generate-jobs: | |
name: Generate jobs for E2E tests | |
needs: | |
- buildx | |
- evaluate_options | |
# We try to avoid running the E2E Test Suite in general, to reduce load on | |
# GitHub resources. | |
# Currently, it's executed in the following cases: | |
# - When dispatched via chatops commands | |
# - On a push in main and release branches | |
# - On scheduled executions | |
if: | | |
(always() && !cancelled()) && | |
needs.buildx.result == 'success' | |
runs-on: ubuntu-24.04 | |
outputs: | |
image: ${{ needs.buildx.outputs.image }} | |
localMatrix: ${{ steps.generate-jobs.outputs.localMatrix }} | |
localEnabled: ${{ steps.generate-jobs.outputs.localEnabled }} | |
localTimeout: ${{ steps.generate-jobs.outputs.localE2ETimeout }} | |
eksMatrix: ${{ steps.generate-jobs.outputs.eksMatrix }} | |
eksEnabled: ${{ steps.generate-jobs.outputs.eksEnabled }} | |
eksTimeout: ${{ steps.generate-jobs.outputs.eksE2ETimeout }} | |
aksMatrix: ${{ steps.generate-jobs.outputs.aksMatrix }} | |
aksEnabled: ${{ steps.generate-jobs.outputs.aksEnabled }} | |
aksTimeout: ${{ steps.generate-jobs.outputs.aksE2ETimeout }} | |
gkeMatrix: ${{ steps.generate-jobs.outputs.gkeMatrix }} | |
gkeEnabled: ${{ steps.generate-jobs.outputs.gkeEnabled }} | |
gkeTimeout: ${{ steps.generate-jobs.outputs.gkeE2ETimeout }} | |
openshiftMatrix: ${{ steps.generate-jobs.outputs.openshiftMatrix }} | |
openshiftEnabled: ${{ steps.generate-jobs.outputs.openshiftEnabled }} | |
openshiftTimeout: ${{ steps.generate-jobs.outputs.openshiftE2ETimeout }} | |
steps: | |
- | |
name: Checkout code | |
uses: actions/checkout@v4 | |
with: | |
ref: ${{ needs.evaluate_options.outputs.git_ref }} | |
- | |
id: generate-jobs | |
# Generates the jobs that will become different matrix branches, | |
# according to the event, or to the "depth" parameter if set manually | |
name: Generate Jobs | |
shell: bash | |
run: | | |
python .github/e2e-matrix-generator.py \ | |
-m '${{ needs.evaluate_options.outputs.depth }}' \ | |
-l '${{ needs.evaluate_options.outputs.limit }}' | |
e2e-local: | |
name: Run E2E on local executors | |
if: | | |
(always() && !cancelled()) && | |
needs.generate-jobs.outputs.localEnabled == 'true' && | |
needs.generate-jobs.result == 'success' | |
needs: | |
- buildx | |
- generate-jobs | |
- evaluate_options | |
strategy: | |
fail-fast: false | |
matrix: ${{ fromJSON(needs.generate-jobs.outputs.localMatrix) }} | |
runs-on: ubuntu-24.04 | |
env: | |
# TEST_DEPTH determines the maximum test level the suite should be running | |
TEST_DEPTH: ${{ needs.evaluate_options.outputs.test_level }} | |
# FEATURE_TYPE, when defined, determines the subset of E2E tests that will be executed, divided by feature type | |
FEATURE_TYPE: ${{ needs.evaluate_options.outputs.feature_type }} | |
K8S_VERSION: "${{ matrix.k8s_version }}" | |
POSTGRES_VERSION: ${{ matrix.postgres_version }} | |
POSTGRES_KIND: ${{ matrix.postgres_kind }} | |
MATRIX: ${{ matrix.id }} | |
POSTGRES_IMG: "${{ matrix.postgres_img }}" | |
# The version of operator to upgrade FROM, in the rolling upgrade E2E test | |
E2E_PRE_ROLLING_UPDATE_IMG: "${{ matrix.postgres_pre_img }}" | |
TEST_TIMEOUTS: ${{ needs.generate-jobs.outputs.localTimeout }} | |
BRANCH_NAME: ${{ needs.buildx.outputs.branch_name }} | |
DEBUG: "true" | |
BUILD_IMAGE: "false" | |
CONTROLLER_IMG: ${{ needs.generate-jobs.outputs.image }} | |
E2E_DEFAULT_STORAGE_CLASS: standard | |
E2E_CSI_STORAGE_CLASS: csi-hostpath-sc | |
E2E_DEFAULT_VOLUMESNAPSHOT_CLASS: csi-hostpath-snapclass | |
LOG_DIR: ${{ github.workspace }}/kind-logs/ | |
DOCKER_REGISTRY_MIRROR: https://mirror.gcr.io | |
TEST_CLOUD_VENDOR: "local" | |
steps: | |
- | |
name: Cleanup Disk | |
uses: jlumbroso/free-disk-space@main | |
with: | |
android: true | |
dotnet: true | |
haskell: true | |
tool-cache: true | |
large-packages: false | |
swap-storage: false | |
- | |
name: Cleanup docker cache | |
run: | | |
echo "-------------Disk info before cleanup----------------" | |
df -h | |
echo "-----------------------------------------------------" | |
docker system prune -a -f | |
echo "-------------Disk info after cleanup----------------" | |
df -h | |
echo "-----------------------------------------------------" | |
- | |
name: Checkout code | |
uses: actions/checkout@v4 | |
with: | |
ref: ${{ needs.evaluate_options.outputs.git_ref }} | |
- | |
name: Install Go | |
uses: actions/setup-go@v5 | |
with: | |
go-version: ${{ env.GOLANG_VERSION }} | |
check-latest: true | |
- | |
## In case hack/setup-cluster.sh need pull operand image from registry | |
name: Login into docker registry | |
uses: docker/login-action@v3 | |
with: | |
registry: ${{ env.REGISTRY }} | |
username: ${{ env.REGISTRY_USER }} | |
password: ${{ env.REGISTRY_PASSWORD }} | |
- | |
# 'Retry' preparing the E2E test ENV | |
name: Prepare the environment | |
uses: nick-fields/retry@v3 | |
with: | |
timeout_seconds: 300 | |
max_attempts: 3 | |
on_retry_command: | | |
# Clear-ups before retries | |
sudo rm -rf /usr/local/bin/kind /usr/local/bin/kubectl | |
command: | | |
sudo apt-get update | |
sudo apt-get install -y gettext-base | |
sudo hack/setup-cluster.sh prepare /usr/local/bin | |
- | |
name: Prepare patch for customization | |
env: | |
## the following variable all need be set if we use env_override_customized.yaml.template | |
## this is customization for local kind | |
LEADER_ELECTION: "true" | |
LEADER_LEASE_DURATION: 15 | |
LEADER_RENEW_DEADLINE: 10 | |
LIVENESS_PROBE_THRESHOLD: 3 | |
LOG_LEVEL: ${{ needs.evaluate_options.outputs.log_level }} | |
run: | | |
LOG_LEVEL=${LOG_LEVEL:-info} | |
envsubst < hack/e2e/env_override_customized.yaml.template > config/manager/env_override.yaml | |
cat config/manager/env_override.yaml | |
- | |
name: Run Kind End-to-End tests | |
run: | |
make e2e-test-kind | |
- | |
# Summarize the failed E2E test cases if there are any | |
name: Report failed E2E tests | |
if: failure() | |
run: | | |
set +x | |
chmod +x .github/report-failed-test.sh | |
./.github/report-failed-test.sh | |
- | |
# Create an individual artifact for each E2E test, which will be used to | |
# generate E2E test summary in the follow-up job 'summarize-e2e-tests' | |
name: Create individual artifact for each E2E test | |
if: (always() && !cancelled()) | |
env: | |
RUNNER: "local" | |
RUN_ID: ${{ github.run_id }} | |
REPOSITORY: ${{ github.repository }} | |
GIT_REF: ${{ needs.evaluate_options.outputs.git_ref }} | |
run: | | |
set +x | |
python .github/generate-test-artifacts.py \ | |
-o testartifacts-${{ env.MATRIX }} \ | |
-f tests/e2e/out/report.json \ | |
--environment=true | |
if [ -f tests/e2e/out/upgrade_report.json ]; then | |
python .github/generate-test-artifacts.py \ | |
-o testartifacts-${{ env.MATRIX }} \ | |
-f tests/e2e/out/upgrade_report.json \ | |
--environment=true | |
fi | |
- | |
name: Archive test artifacts | |
if: (always() && !cancelled()) | |
uses: actions/upload-artifact@v4 | |
with: | |
name: testartifacts-${{ env.MATRIX }} | |
path: testartifacts-${{ env.MATRIX }}/ | |
retention-days: 7 | |
- | |
name: Cleanup test artifacts | |
if: always() | |
run: | |
rm -rf testartifacts-${{ env.MATRIX }}/ | |
- | |
name: Cleanup ginkgo JSON report | |
# Delete report.json after the analysis. File should always exist. | |
# Delete upgrade_report.json. It may not exist depending on test level. | |
if: always() | |
run: | | |
if [ -f tests/e2e/out/upgrade_report.json ]; then | |
rm tests/e2e/out/upgrade_report.json | |
fi | |
if [ -f tests/e2e/out/report.json ]; then | |
rm tests/e2e/out/report.json | |
fi | |
- | |
# Archive logs for failed test cases if there are any | |
name: Archive Kind logs | |
if: failure() | |
uses: actions/upload-artifact@v4 | |
with: | |
name: kind-logs-${{ matrix.id }} | |
path: kind-logs/ | |
retention-days: 7 | |
- | |
name: Archive e2e failure contexts | |
if: failure() | |
uses: actions/upload-artifact@v4 | |
with: | |
name: test-failure-contexts-${{ matrix.id }} | |
path: | | |
tests/*/out/ | |
retention-days: 7 | |
if-no-files-found: ignore | |
- | |
name: Archive e2e logs | |
if: failure() | |
uses: actions/upload-artifact@v4 | |
with: | |
name: cluster-logs-${{ matrix.id }} | |
path: | | |
tests/e2e/cluster_logs/** | |
retention-days: 7 | |
if-no-files-found: ignore | |
# AKS Secrets required | |
# secrets.AZURE_CREDENTIALS | |
# secrets.AZURE_SUBSCRIPTION | |
# secrets.AZURE_RESOURCEGROUP | |
# secrets.AZURE_RESOURCENAME | |
# secrets.AZURE_WORKSPACE_RESOURCE_ID | |
e2e-aks-setup: | |
name: Setup shared resources for Microsoft AKS E2Es | |
if: | | |
(always() && !cancelled()) && | |
vars.AKS_ENABLED == 'true' && | |
needs.generate-jobs.outputs.aksEnabled == 'true' && | |
needs.generate-jobs.result == 'success' | |
needs: | |
- buildx | |
- generate-jobs | |
- evaluate_options | |
runs-on: ubuntu-24.04 | |
outputs: | |
azure_storage_account: ${{ steps.setup.outputs.azure_storage_account }} | |
steps: | |
- | |
name: Azure Login | |
uses: azure/[email protected] | |
with: | |
creds: ${{ secrets.AZURE_CREDENTIALS }} | |
- | |
name: Create AKS shared resources | |
uses: nick-fields/retry@v3 | |
id: setup | |
with: | |
timeout_minutes: 10 | |
max_attempts: 3 | |
command: | | |
az extension add --allow-preview true --name aks-preview | |
az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION }} | |
AZURE_STORAGE_ACCOUNT="${{ github.run_number }}${{ env.E2E_SUFFIX }}" | |
az storage account create \ | |
--resource-group ${{ secrets.AZURE_RESOURCEGROUP }} \ | |
--name ${AZURE_STORAGE_ACCOUNT} \ | |
--sku Standard_LRS -o none | |
# Output storage account name | |
echo "azure_storage_account=${AZURE_STORAGE_ACCOUNT}" >> $GITHUB_OUTPUT | |
e2e-aks: | |
name: Run E2E on Microsoft AKS | |
if: | | |
(always() && !cancelled()) && | |
vars.AKS_ENABLED == 'true' && | |
needs.generate-jobs.outputs.aksEnabled == 'true' && | |
needs.generate-jobs.result == 'success' && | |
needs.e2e-aks-setup.result == 'success' | |
needs: | |
- buildx | |
- generate-jobs | |
- evaluate_options | |
- e2e-aks-setup | |
strategy: | |
fail-fast: false | |
max-parallel: 8 | |
matrix: ${{ fromJSON(needs.generate-jobs.outputs.aksMatrix) }} | |
runs-on: ubuntu-24.04 | |
env: | |
# TEST_DEPTH determines the maximum test level the suite should be running | |
TEST_DEPTH: ${{ needs.evaluate_options.outputs.test_level }} | |
# FEATURE_TYPE, when defined, determines the subset of E2E tests that will be executed, divided by feature type | |
FEATURE_TYPE: ${{ needs.evaluate_options.outputs.feature_type }} | |
K8S_VERSION: "${{ matrix.k8s_version }}" | |
POSTGRES_VERSION: ${{ matrix.postgres_version }} | |
POSTGRES_KIND: ${{ matrix.postgres_kind }} | |
MATRIX: ${{ matrix.id }} | |
POSTGRES_IMG: "${{ matrix.postgres_img }}" | |
# The version of operator to upgrade FROM, in the rolling upgrade E2E test | |
E2E_PRE_ROLLING_UPDATE_IMG: "${{ matrix.postgres_pre_img }}" | |
TEST_TIMEOUTS: ${{ needs.generate-jobs.outputs.aksTimeout }} | |
BRANCH_NAME: ${{ needs.buildx.outputs.branch_name }} | |
AZURE_STORAGE_ACCOUNT: ${{ needs.e2e-aks-setup.outputs.azure_storage_account }} | |
# AZURE_STORAGE_KEY: this one is gathered during a subsequent step | |
DEBUG: "true" | |
BUILD_IMAGE: "false" | |
CONTROLLER_IMG: ${{ needs.generate-jobs.outputs.image }} | |
E2E_DEFAULT_STORAGE_CLASS: rook-ceph-block | |
E2E_CSI_STORAGE_CLASS: rook-ceph-block | |
E2E_DEFAULT_VOLUMESNAPSHOT_CLASS: csi-rbdplugin-snapclass | |
TEST_CLOUD_VENDOR: "aks" | |
steps: | |
- | |
name: Checkout code | |
uses: actions/checkout@v4 | |
with: | |
ref: ${{ needs.evaluate_options.outputs.git_ref }} | |
- | |
name: Install Go | |
uses: actions/setup-go@v5 | |
with: | |
go-version: ${{ env.GOLANG_VERSION }} | |
check-latest: true | |
- | |
name: Prepare the environment | |
uses: nick-fields/retry@v3 | |
with: | |
timeout_seconds: 300 | |
max_attempts: 3 | |
command: | | |
sudo apt-get update | |
sudo apt-get install -y gettext-base | |
- | |
name: Install ginkgo | |
uses: nick-fields/retry@v3 | |
with: | |
timeout_minutes: 1 | |
max_attempts: 3 | |
command: | | |
go install github.com/onsi/ginkgo/v2/ginkgo | |
- | |
## In case hack/setup-cluster.sh need pull operand image from registry | |
name: Login into docker registry | |
uses: docker/login-action@v3 | |
with: | |
registry: ${{ env.REGISTRY }} | |
username: ${{ env.REGISTRY_USER }} | |
password: ${{ env.REGISTRY_PASSWORD }} | |
- | |
name: Azure Login | |
uses: azure/[email protected] | |
with: | |
creds: ${{ secrets.AZURE_CREDENTIALS }} | |
- | |
name: Install kubectl | |
uses: azure/setup-kubectl@v4 | |
with: | |
version: v${{ env.K8S_VERSION }} | |
- | |
name: Create AKS cluster | |
uses: nick-fields/retry@v3 | |
with: | |
timeout_minutes: 10 | |
max_attempts: 3 | |
command: | | |
az extension add --allow-preview true --name aks-preview | |
az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION }} | |
# name of the AKS cluster | |
AZURE_AKS="${{ secrets.AZURE_RESOURCENAME }}-${{ github.run_number }}-$( echo ${{ matrix.id }} | tr -d '_.-' )" | |
echo "AZURE_AKS=${AZURE_AKS}" >> $GITHUB_ENV | |
# gather the storage account Key | |
AZURE_STORAGE_KEY=$(az storage account keys list -g "${{ secrets.AZURE_RESOURCEGROUP }}" -n "${{ env.AZURE_STORAGE_ACCOUNT }}" --query "[0].value" -o tsv) | |
echo "::add-mask::$AZURE_STORAGE_KEY" | |
echo "AZURE_STORAGE_KEY=${AZURE_STORAGE_KEY}" >> $GITHUB_ENV | |
# name of the cluster's blob container in the storage account | |
AZURE_BLOB_CONTAINER="$( echo ${{ matrix.id }} | tr -d '_.-' | tr '[:upper:]' '[:lower:]' )" | |
echo "AZURE_BLOB_CONTAINER=${AZURE_BLOB_CONTAINER}" >> $GITHUB_ENV | |
# create and login to the AKS cluster | |
az aks create --resource-group ${{ secrets.AZURE_RESOURCEGROUP }} \ | |
--name ${AZURE_AKS} \ | |
--tier standard \ | |
--node-count 3 -k v${K8S_VERSION} --generate-ssh-keys --enable-addons monitoring \ | |
--workspace-resource-id ${{ secrets.AZURE_WORKSPACE_RESOURCE_ID }} \ | |
--aks-custom-headers EnableAzureDiskFileCSIDriver=true | |
az aks get-credentials --resource-group ${{ secrets.AZURE_RESOURCEGROUP }} \ | |
--name ${AZURE_AKS} | |
# create diagnostic settings for monitoring kube-apiserver logs | |
AKS_CLUSTER_RESOURCE_ID=$(az aks show --resource-group ${{ secrets.AZURE_RESOURCEGROUP }} --name ${AZURE_AKS} --query id -o tsv --only-show-errors) | |
az monitor diagnostic-settings create \ | |
--resource-group ${{ secrets.AZURE_RESOURCEGROUP }} \ | |
--resource ${AKS_CLUSTER_RESOURCE_ID} \ | |
--name diagnostic-kube-apiserver-logs \ | |
--workspace ${{ secrets.AZURE_WORKSPACE_RESOURCE_ID }} \ | |
--logs '[ { "category": "kube-apiserver", "enabled": true } ]' | |
- | |
# Azure is slow in provisioning disks, and we can't wait two minutes | |
# every time we create a pod, otherwise all the tests will time out. | |
# We set up a few large disks now, we run Rook on top of them and we | |
# use rook to get the small PV we use in the tests. | |
# It can still take a while to deploy rook. | |
name: Set up Rook | |
uses: nick-fields/retry@v3 | |
with: | |
timeout_minutes: 27 | |
max_attempts: 1 | |
command: | | |
STORAGECLASSNAME=default | |
go install github.com/mikefarah/yq/v4@v4 | |
ROOK_BASE_URL=https://raw.githubusercontent.com/rook/rook/${{ env.ROOK_VERSION }}/deploy/examples | |
kubectl apply -f ${ROOK_BASE_URL}/crds.yaml | |
kubectl apply -f ${ROOK_BASE_URL}/common.yaml | |
kubectl apply -f ${ROOK_BASE_URL}/operator.yaml | |
curl ${ROOK_BASE_URL}/cluster-on-pvc.yaml | \ | |
sed '/^ *#/d;/^ *$/d' | \ | |
yq e ".spec.storage.storageClassDeviceSets[].volumeClaimTemplates[].spec.resources.requests.storage = \"50Gi\" | | |
.spec.storage.storageClassDeviceSets[].volumeClaimTemplates[].spec.storageClassName = \"${STORAGECLASSNAME}\" | | |
.spec.mon.volumeClaimTemplate.spec.storageClassName = \"${STORAGECLASSNAME}\" " - | \ | |
kubectl apply -f - | |
while true; do | |
output=$( kubectl get deploy -n rook-ceph -l app=rook-ceph-osd --no-headers -o name ) | |
if [[ $(wc -w <<< $output) == 3 ]]; then | |
break | |
fi | |
done | |
echo "Waiting for Rook OSDs to be available" | |
kubectl wait deploy -n rook-ceph --for condition=available --timeout 480s -l app=rook-ceph-osd | |
kubectl apply -f ${ROOK_BASE_URL}/csi/rbd/storageclass.yaml | |
kubectl apply -f ${ROOK_BASE_URL}/csi/rbd/snapshotclass.yaml | |
kubectl annotate storageclass ${{env.E2E_DEFAULT_STORAGE_CLASS}} storage.kubernetes.io/default-snapshot-class=${{env.E2E_DEFAULT_VOLUMESNAPSHOT_CLASS}} --overwrite | |
- | |
name: Prepare patch for customization | |
env: | |
## the following variable all need be set if we use env_override_customized.yaml.template | |
## this is customization for aks | |
LEADER_ELECTION: "true" | |
LEADER_LEASE_DURATION: 15 | |
LEADER_RENEW_DEADLINE: 10 | |
LIVENESS_PROBE_THRESHOLD: 3 | |
LOG_LEVEL: ${{ needs.evaluate_options.outputs.log_level }} | |
run: | | |
LOG_LEVEL=${LOG_LEVEL:-info} | |
envsubst < hack/e2e/env_override_customized.yaml.template > config/manager/env_override.yaml | |
cat config/manager/env_override.yaml | |
- | |
name: Run E2E tests | |
run: hack/e2e/run-e2e.sh | |
- | |
# Summarize the failed E2E test cases if there are any | |
name: Report failed E2E tests | |
if: failure() | |
run: | | |
set +x | |
chmod +x .github/report-failed-test.sh | |
./.github/report-failed-test.sh | |
- | |
# Create an individual artifact for each E2E test, which will be used to | |
# generate E2E test summary in the follow-up job 'summarize-e2e-tests' | |
name: Create individual artifact for each E2E test | |
if: (always() && !cancelled()) | |
env: | |
RUNNER: "aks" | |
RUN_ID: ${{ github.run_id }} | |
REPOSITORY: ${{ github.repository }} | |
GIT_REF: ${{ needs.evaluate_options.outputs.git_ref }} | |
run: | | |
set +x | |
python .github/generate-test-artifacts.py \ | |
-o testartifacts-${{ env.MATRIX }} \ | |
-f tests/e2e/out/report.json \ | |
--environment=true | |
if [ -f tests/e2e/out/upgrade_report.json ]; then | |
python .github/generate-test-artifacts.py \ | |
-o testartifacts-${{ env.MATRIX }} \ | |
-f tests/e2e/out/upgrade_report.json \ | |
--environment=true | |
fi | |
- | |
name: Archive test artifacts | |
if: (always() && !cancelled()) | |
uses: actions/upload-artifact@v4 | |
with: | |
name: testartifacts-${{ env.MATRIX }} | |
path: testartifacts-${{ env.MATRIX }}/ | |
retention-days: 7 | |
- | |
name: Cleanup test artifacts | |
if: always() | |
run: | |
rm -rf testartifacts-${{ env.MATRIX }}/ | |
- | |
name: Cleanup ginkgo JSON report | |
# Delete report.json after the analysis. File should always exist. | |
# Delete upgrade_report.json. It may not exist depending on test level. | |
if: always() | |
run: | | |
if [ -f tests/e2e/out/upgrade_report.json ]; then | |
rm tests/e2e/out/upgrade_report.json | |
fi | |
if [ -f tests/e2e/out/report.json ]; then | |
rm tests/e2e/out/report.json | |
fi | |
- | |
name: Archive e2e failure contexts | |
if: failure() | |
uses: actions/upload-artifact@v4 | |
with: | |
name: test-failure-contexts-${{ matrix.id }} | |
path: | | |
tests/*/out/ | |
retention-days: 7 | |
if-no-files-found: ignore | |
- | |
name: Archive e2e logs | |
if: failure() | |
uses: actions/upload-artifact@v4 | |
with: | |
name: cluster-logs-${{ matrix.id }} | |
path: | | |
tests/e2e/cluster_logs/** | |
retention-days: 7 | |
if-no-files-found: ignore | |
- | |
name: Clean up | |
if: always() | |
run: | | |
set +e | |
az extension add --allow-preview true --name monitor-control-service | |
az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION }} | |
attempt=1 | |
max_attempts=3 | |
while [ "${attempt}" -le "${max_attempts}" ]; do | |
echo "Deleting cluster. Attempt ${attempt} of ${max_attempts}" | |
az aks delete --resource-group ${{ secrets.AZURE_RESOURCEGROUP }} -y --name ${{ env.AZURE_AKS }} | |
status=$? | |
if [[ $status == 0 ]]; then | |
echo "AKS cluster deleted" | |
break | |
fi | |
echo "Failed deleting cluster ${{ env.AZURE_AKS }}, retrying" | |
sleep 5 | |
attempt=$((attempt+1)) | |
done | |
attempt=1 | |
AZURE_RESOURCEGROUP_LOCATION="$( az group show --resource-group ${{ secrets.AZURE_RESOURCEGROUP }} --query location -o tsv --only-show-errors )" | |
DATA_COLL_RULE_NAME="MSCI-${AZURE_RESOURCEGROUP_LOCATION}-${{ env.AZURE_AKS }}" | |
while [ "${attempt}" -le "${max_attempts}" ]; do | |
echo "Deleting data-collection rule ${DATA_COLL_RULE_NAME}. Attempt ${attempt} of ${max_attempts}" | |
az monitor data-collection rule show --name ${DATA_COLL_RULE_NAME} --resource-group ${{ secrets.AZURE_RESOURCEGROUP }} --query name | |
# if not found, let it go | |
status=$? | |
if [[ $status != 0 ]]; then | |
echo "AKS data-collection rule not found" | |
break | |
fi | |
az monitor data-collection rule delete -y --name ${DATA_COLL_RULE_NAME} --resource-group ${{ secrets.AZURE_RESOURCEGROUP }} | |
status=$? | |
if [[ $status == 0 ]]; then | |
echo "AKS data-collection rule deleted" | |
break | |
fi | |
echo "Failed deleting data-collection rule ${DATA_COLL_RULE_NAME}, retrying" | |
sleep 5 | |
attempt=$((attempt+1)) | |
done | |
e2e-aks-teardown: | |
name: Teardown Microsoft AKS shared resources | |
if: | | |
always() && | |
vars.AKS_ENABLED == 'true' && | |
needs.generate-jobs.outputs.aksEnabled == 'true' && | |
needs.generate-jobs.result == 'success' && | |
needs.e2e-aks-setup.result == 'success' | |
needs: | |
- buildx | |
- generate-jobs | |
- e2e-aks-setup | |
- e2e-aks | |
runs-on: ubuntu-24.04 | |
env: | |
AZURE_STORAGE_ACCOUNT: ${{ needs.e2e-aks-setup.outputs.azure_storage_account }} | |
steps: | |
- | |
name: Azure Login | |
if: always() | |
uses: azure/[email protected] | |
with: | |
creds: ${{ secrets.AZURE_CREDENTIALS }} | |
- | |
name: Teardown AKS shared resources | |
if: always() | |
uses: nick-fields/retry@v3 | |
with: | |
timeout_minutes: 5 | |
max_attempts: 3 | |
command: | | |
az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION }} | |
az storage account delete -y --resource-group ${{ secrets.AZURE_RESOURCEGROUP }} --name ${{ env.AZURE_STORAGE_ACCOUNT }} | |
# EKS Secrets required | |
# secrets.AWS_EKS_ADMIN_IAM_ROLES | |
# secrets.AWS_ACCESS_KEY_ID | |
# secrets.AWS_SECRET_ACCESS_KEY | |
e2e-eks: | |
name: Run E2E on Amazon EKS | |
if: | | |
(always() && !cancelled()) && | |
vars.EKS_ENABLED == 'true' && | |
needs.generate-jobs.outputs.eksEnabled == 'true' && | |
needs.generate-jobs.result == 'success' | |
needs: | |
- buildx | |
- generate-jobs | |
- evaluate_options | |
strategy: | |
fail-fast: false | |
max-parallel: 6 | |
matrix: ${{ fromJSON(needs.generate-jobs.outputs.eksMatrix) }} | |
runs-on: ubuntu-24.04 | |
env: | |
# TEST_DEPTH determines the maximum test level the suite should be running | |
TEST_DEPTH: ${{ needs.evaluate_options.outputs.test_level }} | |
# FEATURE_TYPE, when defined, determines the subset of E2E tests that will be executed, divided by feature type | |
FEATURE_TYPE: ${{ needs.evaluate_options.outputs.feature_type }} | |
K8S_VERSION: "${{ matrix.k8s_version }}" | |
POSTGRES_VERSION: ${{ matrix.postgres_version }} | |
POSTGRES_KIND: ${{ matrix.postgres_kind }} | |
MATRIX: ${{ matrix.id }} | |
POSTGRES_IMG: "${{ matrix.postgres_img }}" | |
# The version of operator to upgrade FROM, in the rolling upgrade E2E test | |
E2E_PRE_ROLLING_UPDATE_IMG: "${{ matrix.postgres_pre_img }}" | |
TEST_TIMEOUTS: ${{ needs.generate-jobs.outputs.eksTimeout }} | |
BRANCH_NAME: ${{ needs.buildx.outputs.branch_name }} | |
DEBUG: "true" | |
BUILD_IMAGE: "false" | |
CONTROLLER_IMG: ${{ needs.generate-jobs.outputs.image }} | |
E2E_DEFAULT_STORAGE_CLASS: gp3 | |
E2E_CSI_STORAGE_CLASS: gp3 | |
E2E_DEFAULT_VOLUMESNAPSHOT_CLASS: ebs-csi-snapclass | |
AWS_REGION: eu-central-1 | |
AWS_EKS_ADMIN_IAM_ROLES: ${{ secrets.AWS_EKS_ADMIN_IAM_ROLES }} | |
TEST_CLOUD_VENDOR: "eks" | |
steps: | |
- | |
name: Set cluster name | |
run: | | |
echo "CLUSTER_NAME=${{ env.E2E_SUFFIX }}-test-${{ github.run_number }}-$( echo ${{ matrix.id }} | tr -d '_.-' )" >> $GITHUB_ENV | |
- | |
name: Checkout code | |
uses: actions/checkout@v4 | |
with: | |
ref: ${{ needs.evaluate_options.outputs.git_ref }} | |
- | |
name: Install Go | |
uses: actions/setup-go@v5 | |
with: | |
go-version: ${{ env.GOLANG_VERSION }} | |
check-latest: true | |
- | |
## In case hack/setup-cluster.sh need pull operand image from registry | |
name: Login into docker registry | |
uses: docker/login-action@v3 | |
with: | |
registry: ${{ env.REGISTRY }} | |
username: ${{ env.REGISTRY_USER }} | |
password: ${{ env.REGISTRY_PASSWORD }} | |
- | |
name: Prepare the environment | |
uses: nick-fields/retry@v3 | |
with: | |
timeout_seconds: 300 | |
max_attempts: 3 | |
command: | | |
sudo apt-get update | |
sudo apt-get install -y gettext-base | |
- | |
name: Install ginkgo | |
uses: nick-fields/retry@v3 | |
with: | |
timeout_minutes: 1 | |
max_attempts: 3 | |
command: | | |
go install github.com/onsi/ginkgo/v2/ginkgo | |
- | |
name: Configure AWS credentials | |
uses: aws-actions/configure-aws-credentials@v4 | |
with: | |
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
aws-region: ${{ env.AWS_REGION }} | |
- | |
name: Install eksctl | |
uses: nick-fields/retry@v3 | |
with: | |
timeout_minutes: 1 | |
max_attempts: 3 | |
command: | | |
mkdir -p "$HOME/.local/bin" | |
curl -sL "https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_$(uname -s)_amd64.tar.gz" \ | |
| tar xz -C $HOME/.local/bin | |
echo "$HOME/.local/bin" >> $GITHUB_PATH | |
- | |
name: Configure EKS setup | |
run: | | |
envsubst < hack/e2e/eks-cluster.yaml.template > hack/e2e/eks-cluster.yaml | |
- | |
name: Setup EKS | |
run: | | |
# Setting up EKS cluster | |
echo "create cluster" | |
eksctl create cluster -f hack/e2e/eks-cluster.yaml | |
# Create iamidentitymapping | |
echo "$AWS_EKS_ADMIN_IAM_ROLES" | while read role | |
do | |
# Masking variables to hide values | |
echo "::add-mask::$role" | |
eksctl create iamidentitymapping --cluster "${CLUSTER_NAME}" --region="${AWS_REGION}" --arn "${role}" --group system:masters --username admin | |
done | |
# Updating .kubeconfig to use the correct version of client.authentication.k8s.io API | |
aws eks update-kubeconfig --name ${CLUSTER_NAME} --region ${AWS_REGION} | |
# Installing CRD for support volumeSnapshot | |
SNAPSHOTTER_BASE_URL=https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/${{env.EXTERNAL_SNAPSHOTTER_VERSION}} | |
kubectl apply -f ${SNAPSHOTTER_BASE_URL}/client/config/crd/snapshot.storage.k8s.io_volumesnapshotclasses.yaml | |
kubectl apply -f ${SNAPSHOTTER_BASE_URL}/client/config/crd/snapshot.storage.k8s.io_volumesnapshotcontents.yaml | |
kubectl apply -f ${SNAPSHOTTER_BASE_URL}/client/config/crd/snapshot.storage.k8s.io_volumesnapshots.yaml | |
## Controller | |
kubectl apply -f ${SNAPSHOTTER_BASE_URL}/deploy/kubernetes/snapshot-controller/rbac-snapshot-controller.yaml | |
kubectl apply -f ${SNAPSHOTTER_BASE_URL}/deploy/kubernetes/snapshot-controller/setup-snapshot-controller.yaml | |
# Install volume snapshot class | |
kubectl apply -f hack/e2e/volumesnapshotclass-ebs-csi.yaml | |
kubectl get volumesnapshotclass | |
# Change to use gp3 as default storage account | |
kubectl annotate storageclass gp2 storageclass.kubernetes.io/is-default-class=false --overwrite | |
kubectl apply -f hack/e2e/storage-class-gp3.yaml | |
kubectl annotate storageclass ${{env.E2E_DEFAULT_STORAGE_CLASS}} storage.kubernetes.io/default-snapshot-class=${{env.E2E_DEFAULT_VOLUMESNAPSHOT_CLASS}} --overwrite | |
kubectl get storageclass | |
- | |
name: Setup Velero | |
uses: nick-fields/retry@v3 | |
env: | |
VELERO_VERSION: "v1.14.1" | |
VELERO_AWS_PLUGIN_VERSION: "v1.10.1" | |
with: | |
timeout_minutes: 10 | |
max_attempts: 3 | |
on_retry_command: | | |
# Clean up buckets | |
output=$( aws s3api delete-bucket --bucket "${VELERO_BUCKET_NAME}" --region "${AWS_REGION}" 2>&1 ) | |
status=$? | |
if [[ $status == 0 ]]; then | |
echo "S3 Bucket deleted" | |
break | |
fi | |
if ( grep "NoSuchBucket" <<< "$output" ); then | |
echo "S3 Bucket doesn't exist, nothing to remove" | |
break | |
fi | |
# Uninstall Velero | |
kubectl delete namespace/velero clusterrolebinding/velero | |
kubectl delete crds -l component=velero | |
command: | | |
VELERO_BUCKET_NAME="${CLUSTER_NAME,,}-velero" | |
echo "VELERO_BUCKET_NAME=${VELERO_BUCKET_NAME}" >> $GITHUB_ENV | |
# Create S3 bucket | |
aws s3api create-bucket \ | |
--bucket "${VELERO_BUCKET_NAME}" \ | |
--region "${AWS_REGION}" \ | |
--create-bucket-configuration LocationConstraint="${AWS_REGION}" | |
# Download Velero, extract and place it in $PATH | |
curl -sL "https://github.com/vmware-tanzu/velero/releases/download/${VELERO_VERSION}/velero-${VELERO_VERSION}-linux-amd64.tar.gz" | tar xz | |
mv velero-${VELERO_VERSION}-linux-amd64/velero $HOME/.local/bin | |
# Set Velero-specific credentials | |
echo -e "[default]\naws_access_key_id=${{ secrets.AWS_ACCESS_KEY_ID }}\naws_secret_access_key=${{ secrets.AWS_SECRET_ACCESS_KEY }}" >> credentials-velero | |
# Install Velero | |
velero install \ | |
--provider aws \ | |
--plugins velero/velero-plugin-for-aws:${VELERO_AWS_PLUGIN_VERSION} \ | |
--bucket "${VELERO_BUCKET_NAME}" \ | |
--backup-location-config region="${AWS_REGION}" \ | |
--snapshot-location-config region="${AWS_REGION}" \ | |
--secret-file ./credentials-velero \ | |
--wait | |
- | |
name: Prepare patch for customization | |
env: | |
## the following variable all need be set if we use env_override_customized.yaml.template | |
## this is customization for eks | |
LEADER_ELECTION: "true" | |
LEADER_LEASE_DURATION: 15 | |
LEADER_RENEW_DEADLINE: 10 | |
LIVENESS_PROBE_THRESHOLD: 3 | |
LOG_LEVEL: ${{ needs.evaluate_options.outputs.log_level }} | |
run: | | |
LOG_LEVEL=${LOG_LEVEL:-info} | |
envsubst < hack/e2e/env_override_customized.yaml.template > config/manager/env_override.yaml | |
cat config/manager/env_override.yaml | |
- | |
name: Run E2E tests | |
run: hack/e2e/run-e2e.sh | |
- | |
# Summarize the failed E2E test cases if there are any | |
name: Report failed E2E tests | |
if: failure() | |
run: | | |
set +x | |
chmod +x .github/report-failed-test.sh | |
./.github/report-failed-test.sh | |
- | |
# Create an individual artifact for each E2E test, which will be used to | |
# generate E2E test summary in the follow-up job 'summarize-e2e-tests' | |
name: Create individual artifact for each E2E test | |
if: (always() && !cancelled()) | |
env: | |
RUNNER: "eks" | |
RUN_ID: ${{ github.run_id }} | |
REPOSITORY: ${{ github.repository }} | |
GIT_REF: ${{ needs.evaluate_options.outputs.git_ref }} | |
run: | | |
set +x | |
python .github/generate-test-artifacts.py \ | |
-o testartifacts-${{ env.MATRIX }} \ | |
-f tests/e2e/out/report.json \ | |
--environment=true | |
if [ -f tests/e2e/out/upgrade_report.json ]; then | |
python .github/generate-test-artifacts.py \ | |
-o testartifacts-${{ env.MATRIX }} \ | |
-f tests/e2e/out/upgrade_report.json \ | |
--environment=true | |
fi | |
- | |
name: Archive test artifacts | |
if: (always() && !cancelled()) | |
uses: actions/upload-artifact@v4 | |
with: | |
name: testartifacts-${{ env.MATRIX }} | |
path: testartifacts-${{ env.MATRIX }}/ | |
retention-days: 7 | |
- | |
name: Cleanup test artifacts | |
if: always() | |
run: | |
rm -rf testartifacts-${{ env.MATRIX }}/ | |
- | |
name: Cleanup ginkgo JSON report | |
# Delete report.json after the analysis. File should always exist. | |
# Delete upgrade_report.json. It may not exist depending on test level. | |
if: always() | |
run: | | |
if [ -f tests/e2e/out/upgrade_report.json ]; then | |
rm tests/e2e/out/upgrade_report.json | |
fi | |
if [ -f tests/e2e/out/report.json ]; then | |
rm tests/e2e/out/report.json | |
fi | |
- | |
name: Archive e2e failure contexts | |
if: failure() | |
uses: actions/upload-artifact@v4 | |
with: | |
name: test-failure-contexts-${{ matrix.id }} | |
path: | | |
tests/*/out/ | |
retention-days: 7 | |
if-no-files-found: ignore | |
- | |
name: Archive e2e logs | |
if: failure() | |
uses: actions/upload-artifact@v4 | |
with: | |
name: cluster-logs-${{ matrix.id }} | |
path: | | |
tests/e2e/cluster_logs/** | |
retention-days: 7 | |
if-no-files-found: ignore | |
- | |
name: Clean up | |
if: always() | |
run: | | |
set +e | |
CLUSTER_NAME="${{ env.CLUSTER_NAME }}" | |
REGION_NAME="${{ env.AWS_REGION }}" | |
STACK_NAME="eksctl-${CLUSTER_NAME}-cluster" | |
CLOUDFORMATION_STATUS_CURRENT=$(aws cloudformation describe-stacks --stack-name "${STACK_NAME}" | jq -r '.Stacks[].StackStatus') | |
if [[ -z "${CLOUDFORMATION_STATUS_CURRENT}" ]]; then | |
echo "CloudFormation stack not found. Nothing to cleanup." | |
exit 0 | |
fi | |
# Attempt to remove any leftover PDB (and Cluster that would recreate it) | |
# that could prevent the EKS cluster deletion | |
kubectl delete cluster --all --all-namespaces --now --timeout=30s || true | |
kubectl delete pdb --all --all-namespaces --now --timeout=30s || true | |
kubectl delete pvc --all --all-namespaces --now --timeout=30s || true | |
# Remove any LoadBalancer service | |
kubectl get service --all-namespaces -o json | jq -r '.items[] | select(.spec.type=="LoadBalancer") | .metadata | "kubectl delete service --now --timeout=30s -n " + .namespace + " " + .name' | xargs -rI X bash -c X || true | |
NODEGROUP_STACK_NAMES=$(eksctl get nodegroup --cluster "${CLUSTER_NAME}" -o json | jq -r '.[].StackName' || true) | |
attempt=1 | |
bucket_attempt=1 | |
max_attempts=3 | |
# Attempting three times to remove the Velero S3 bucket | |
VELERO_BUCKET_NAME=${VELERO_BUCKET_NAME:-"${CLUSTER_NAME,,}-velero"} | |
while [ "${bucket_attempt}" -le "${max_attempts}" ]; do | |
echo "Deleting S3 Bucket. Attempt ${bucket_attempt} of ${max_attempts}" | |
output=$( aws s3api delete-bucket --bucket "${VELERO_BUCKET_NAME}" --region "${AWS_REGION}" 2>&1 ) | |
status=$? | |
if [[ $status == 0 ]]; then | |
echo "S3 Bucket deleted" | |
break | |
fi | |
if ( grep "NoSuchBucket" <<< "$output" ); then | |
echo "S3 Bucket doesn't exist, nothing to remove" | |
break | |
fi | |
echo "Failed deleting S3 Bucket ${VELERO_BUCKET_NAME}, retrying" | |
sleep 5 | |
bucket_attempt=$((bucket_attempt+1)) | |
done | |
# Attempting three times to cleanly remove the cluster via eksctl | |
while [ "${attempt}" -le "${max_attempts}" ]; do | |
echo "Deleting cluster. Attempt ${attempt} of ${max_attempts}" | |
output=$( eksctl delete cluster -n "${CLUSTER_NAME}" -r "${REGION_NAME}" --wait --force 2>&1 ) | |
status=$? | |
if [[ $status == 0 ]]; then | |
echo "EKS cluster deleted" | |
break | |
fi | |
if ( grep "ResourceNotFoundException: No cluster found for name: ${CLUSTER_NAME}" <<< "$output" ); then | |
echo "EKS cluster doesn't exist, nothing to remove" | |
break | |
fi | |
echo "Failed deleting cluster ${CLUSTER_NAME}, retrying" | |
sleep 5 | |
attempt=$((attempt+1)) | |
done | |
# Recheck if something got stuck, and use harder methods to clean up | |
CLOUDFORMATION_STATUS_CURRENT=$(aws cloudformation describe-stacks --stack-name "${STACK_NAME}" | jq -r '.Stacks[].StackStatus') | |
if [ -n "${CLOUDFORMATION_STATUS_CURRENT}" ] ; then | |
echo "::warning file=continuous-delivery.yml::eksctl failed deleting a cluster cleanly" | |
# When the status of CloudFormation stack managed by eksctl reports an error, try to delete resources directly with AWS CLI | |
pip install boto3 | |
for vpc_id in $(aws ec2 describe-vpcs | jq -r '.Vpcs[] | select(.Tags?[]? | .Key == "Name" and (.Value | contains("'"${STACK_NAME}"'"))).VpcId'); do | |
python .github/vpc_destroy.py --vpc_id "${vpc_id}" --region "${REGION_NAME}" --services ec2 | |
done | |
# Then we try to delete the cluster cleanly and the cloudformation | |
if aws eks describe-cluster --name "${CLUSTER_NAME}" --region "${REGION_NAME}" ; then | |
eksctl delete cluster -n "${CLUSTER_NAME}" -r "${REGION_NAME}" --wait --force | |
fi | |
if [ -n "${NODEGROUP_STACK_NAMES}" ] ; then | |
for NODEGROUP_STACK_NAME in ${NODEGROUP_STACK_NAMES}; do | |
if aws cloudformation describe-stacks --stack-name "${NODEGROUP_STACK_NAME}" --region "${REGION_NAME}" ; then | |
aws cloudformation delete-stack --stack-name "${NODEGROUP_STACK_NAME}" --region "${REGION_NAME}" | |
fi | |
done | |
fi | |
if aws cloudformation describe-stacks --stack-name "${STACK_NAME}" --region "${REGION_NAME}" ; then | |
aws cloudformation delete-stack --stack-name "${STACK_NAME}" --region "${REGION_NAME}" | |
fi | |
fi | |
# Clear up leftover volumes | |
while read -r volume; do | |
echo "Deleting $volume of cluster $CLUSTER_NAME ..." | |
if ! aws ec2 delete-volume --region "${REGION_NAME}" --volume-id "$volume" ; then | |
echo "::warning file=continuous-delivery.yml::Failed deleting $volume of cluster $CLUSTER_NAME" | |
fi | |
done < <(aws ec2 describe-volumes --region "${REGION_NAME}" --query 'Volumes[?not_null(Tags[?Key == `kubernetes.io/cluster/'"$CLUSTER_NAME"'` && Value == `owned`].Value)].VolumeId' | jq -r '.[]' || true) | |
# GKE Secrets required | |
# secrets.GCP_SERVICE_ACCOUNT | |
# secrets.GCP_PROJECT_ID | |
e2e-gke: | |
name: Run E2E on Google GKE | |
if: | | |
(always() && !cancelled()) && | |
vars.GKE_ENABLED == 'true' && | |
needs.generate-jobs.outputs.gkeEnabled == 'true' && | |
needs.generate-jobs.result == 'success' | |
needs: | |
- buildx | |
- generate-jobs | |
- evaluate_options | |
strategy: | |
fail-fast: false | |
max-parallel: 6 | |
matrix: ${{ fromJSON(needs.generate-jobs.outputs.gkeMatrix) }} | |
runs-on: ubuntu-24.04 | |
env: | |
# TEST_DEPTH determines the maximum test level the suite should be running | |
TEST_DEPTH: ${{ needs.evaluate_options.outputs.test_level }} | |
# FEATURE_TYPE, when defined, determines the subset of E2E tests that will be executed, divided by feature type | |
FEATURE_TYPE: ${{ needs.evaluate_options.outputs.feature_type }} | |
K8S_VERSION: "${{ matrix.k8s_version }}" | |
POSTGRES_VERSION: ${{ matrix.postgres_version }} | |
POSTGRES_KIND: ${{ matrix.postgres_kind }} | |
MATRIX: ${{ matrix.id }} | |
POSTGRES_IMG: "${{ matrix.postgres_img }}" | |
# The version of operator to upgrade FROM, in the rolling upgrade E2E test | |
E2E_PRE_ROLLING_UPDATE_IMG: "${{ matrix.postgres_pre_img }}" | |
TEST_TIMEOUTS: ${{ needs.generate-jobs.outputs.gkeTimeout }} | |
BRANCH_NAME: ${{ needs.buildx.outputs.branch_name }} | |
DEBUG: "true" | |
BUILD_IMAGE: "false" | |
CONTROLLER_IMG: ${{ needs.generate-jobs.outputs.image }} | |
E2E_DEFAULT_STORAGE_CLASS: standard-rwo | |
E2E_CSI_STORAGE_CLASS: standard-rwo | |
E2E_DEFAULT_VOLUMESNAPSHOT_CLASS: pd-csi-snapclass | |
REGION: europe-west3 | |
TEST_CLOUD_VENDOR: "gke" | |
steps: | |
- | |
name: Checkout code | |
uses: actions/checkout@v4 | |
with: | |
ref: ${{ needs.evaluate_options.outputs.git_ref }} | |
- | |
name: Install Go | |
uses: actions/setup-go@v5 | |
with: | |
go-version: ${{ env.GOLANG_VERSION }} | |
check-latest: true | |
- | |
## In case hack/setup-cluster.sh need pull operand image from registry | |
name: Login into docker registry | |
uses: docker/login-action@v3 | |
with: | |
registry: ${{ env.REGISTRY }} | |
username: ${{ env.REGISTRY_USER }} | |
password: ${{ env.REGISTRY_PASSWORD }} | |
- | |
name: Prepare the environment | |
uses: nick-fields/retry@v3 | |
with: | |
timeout_seconds: 300 | |
max_attempts: 3 | |
command: | | |
sudo apt-get update | |
sudo apt-get install -y gettext-base | |
- | |
name: Install ginkgo | |
uses: nick-fields/retry@v3 | |
with: | |
timeout_seconds: 120 | |
max_attempts: 3 | |
command: | | |
go install github.com/onsi/ginkgo/v2/ginkgo | |
- | |
name: Set cluster name | |
run: | | |
# GKE cluster names rules: | |
# only lowercase alphanumerics and '-' allowed, must start with a letter and end with an alphanumeric, | |
# and must be no longer than 40 characters | |
# We need to shorten the name and lower the case | |
SHORT_ID=$( echo ${{ matrix.id }} | tr -d '_.-' | tr '[:upper:]' '[:lower:]') | |
echo "CLUSTER_NAME=${{ env.E2E_SUFFIX }}-test-${{ github.run_number }}-${SHORT_ID}" >> $GITHUB_ENV | |
- | |
name: Authenticate to Google Cloud | |
id: 'auth' | |
uses: google-github-actions/auth@v2 | |
with: | |
credentials_json: '${{ secrets.GCP_SERVICE_ACCOUNT }}' | |
- | |
name: Set up Cloud SDK and kubectl | |
uses: google-github-actions/setup-gcloud@v2 | |
with: | |
project_id: ${{ secrets.GCP_PROJECT_ID }} | |
install_components: 'kubectl,gke-gcloud-auth-plugin' | |
- | |
name: Create GKE cluster | |
run: | | |
set +e | |
# We may go over the amount of API requests allowed | |
# by Google when creating all the clusters at the same time. | |
# We give a few attempts at creating the cluster before giving up. | |
# The following command will create a 3 nodes cluster, with each | |
# node deployed in its own availability zone. | |
for i in `seq 1 5`; do | |
if gcloud container clusters create ${{ env.CLUSTER_NAME }} \ | |
--num-nodes=1 \ | |
--cluster-version=${{ env.K8S_VERSION }} \ | |
--region=${{ env.REGION }} \ | |
--disk-size=20 \ | |
--machine-type=e2-standard-2 \ | |
--labels=cluster=${{ env.CLUSTER_NAME }} | |
then | |
exit 0 | |
fi | |
echo "Couldn't create the cluster. Retrying in 100s." | |
sleep 100 | |
done | |
echo "Couldn't create the cluster. Failing." | |
exit 1 | |
- | |
name: Get GKE kubeconfig credentials | |
env: | |
USE_GKE_GCLOUD_AUTH_PLUGIN: "True" | |
run: | | |
gcloud container clusters get-credentials ${{ env.CLUSTER_NAME }} --region ${{ env.REGION }} --project ${{ secrets.GCP_PROJECT_ID }} | |
- | |
name: Configure Storage | |
run: | | |
# Install volume snapshot class | |
kubectl apply -f hack/e2e/volumesnapshotclass-pd-csi.yaml | |
# Change to use standard-rwo as default storage account | |
kubectl annotate storageclass ${{env.E2E_DEFAULT_STORAGE_CLASS}} storage.kubernetes.io/default-snapshot-class=${{env.E2E_DEFAULT_VOLUMESNAPSHOT_CLASS}} --overwrite | |
kubectl get storageclass | |
- | |
name: Prepare patch for customization | |
env: | |
## the following variable all need be set if we use env_override_customized.yaml.template | |
## this is customization for gke | |
LEADER_ELECTION: "false" | |
LEADER_LEASE_DURATION: 240 | |
LEADER_RENEW_DEADLINE: 230 | |
LIVENESS_PROBE_THRESHOLD: 9 | |
LOG_LEVEL: ${{ needs.evaluate_options.outputs.log_level }} | |
run: | | |
LOG_LEVEL=${LOG_LEVEL:-info} | |
envsubst < hack/e2e/env_override_customized.yaml.template > config/manager/env_override.yaml | |
cat config/manager/env_override.yaml | |
- | |
name: Run E2E tests | |
run: hack/e2e/run-e2e.sh | |
- | |
name: Report failed E2E tests | |
if: failure() | |
run: | | |
set +x | |
chmod +x .github/report-failed-test.sh | |
./.github/report-failed-test.sh | |
- | |
# Create an individual artifact for each E2E test, which will be used to | |
# generate E2E test summary in the follow-up job 'summarize-e2e-tests' | |
name: Create individual artifact for each E2E test | |
if: (always() && !cancelled()) | |
env: | |
RUNNER: "gke" | |
RUN_ID: ${{ github.run_id }} | |
REPOSITORY: ${{ github.repository }} | |
GIT_REF: ${{ needs.evaluate_options.outputs.git_ref }} | |
run: | | |
set +x | |
python .github/generate-test-artifacts.py \ | |
-o testartifacts-${{ env.MATRIX }} \ | |
-f tests/e2e/out/report.json \ | |
--environment=true | |
if [ -f tests/e2e/out/upgrade_report.json ]; then | |
python .github/generate-test-artifacts.py \ | |
-o testartifacts-${{ env.MATRIX }} \ | |
-f tests/e2e/out/upgrade_report.json \ | |
--environment=true | |
fi | |
- | |
name: Archive test artifacts | |
if: (always() && !cancelled()) | |
uses: actions/upload-artifact@v4 | |
with: | |
name: testartifacts-${{ env.MATRIX }} | |
path: testartifacts-${{ env.MATRIX }}/ | |
retention-days: 7 | |
- | |
name: Cleanup test artifacts | |
if: always() | |
run: | |
rm -rf testartifacts-${{ env.MATRIX }}/ | |
- | |
name: Cleanup ginkgo JSON report | |
# Delete report.json after the analysis. File should always exist. | |
# Delete upgrade_report.json. It may not exist depending on test level. | |
if: always() | |
run: | | |
if [ -f tests/e2e/out/upgrade_report.json ]; then | |
rm tests/e2e/out/upgrade_report.json | |
fi | |
if [ -f tests/e2e/out/report.json ]; then | |
rm tests/e2e/out/report.json | |
fi | |
- | |
name: Archive e2e failure contexts | |
if: failure() | |
uses: actions/upload-artifact@v4 | |
with: | |
name: test-failure-contexts-${{ matrix.id }} | |
path: | | |
tests/*/out/ | |
retention-days: 7 | |
if-no-files-found: ignore | |
- | |
name: Archive e2e logs | |
if: failure() | |
uses: actions/upload-artifact@v4 | |
with: | |
name: cluster-logs-${{ matrix.id }} | |
path: | | |
tests/e2e/cluster_logs/** | |
retention-days: 7 | |
if-no-files-found: ignore | |
- | |
name: Clean up | |
if: always() | |
run: | | |
set +e | |
# Attempt to remove any leftover resource | |
kubectl delete cluster --all --all-namespaces --now --timeout=30s || true | |
kubectl delete pdb --all --all-namespaces --now --timeout=30s || true | |
kubectl delete pvc --all --all-namespaces --now --timeout=30s || true | |
# Wait until all the PVs provisioned are actually reclaimed | |
kubectl wait --for delete --all pv --timeout=60s || true | |
attempt=1 | |
max_attempts=3 | |
while [ "${attempt}" -le "${max_attempts}" ]; do | |
gcloud container clusters delete ${{ env.CLUSTER_NAME }} --region=${{ env.REGION }} --quiet | |
status=$? | |
if [[ $status == 0 ]]; then | |
echo "GKS cluster ${{ env.CLUSTER_NAME }} deleted from region ${{ env.REGION }}" | |
break | |
fi | |
echo "Failed deleting cluster ${{ env.CLUSTER_NAME }} from region ${{ env.REGION }}, retrying" | |
sleep 5 | |
attempt=$((attempt+1)) | |
done | |
# The node's disks are not automatically deleted when the cluster is removed. | |
# We delete all the disks tagged with the name of the cluster that are not | |
# owned by anyone. | |
attempt=1 | |
max_attempts=3 | |
while [ "${attempt}" -le "${max_attempts}" ]; do | |
IDS=$(gcloud compute disks list --filter="labels.cluster=${{ env.CLUSTER_NAME }} AND region:${{ env.REGION }} AND -users:*" --format="value(id)") | |
amount="$(echo $IDS | awk '{print NF}')" | |
if [[ "$amount" == 3 ]]; then | |
echo -e "Found the 3 disks to be removed:\n$IDS" | |
break | |
fi | |
echo "Expected 3 disks to delete but found $amount, waiting and retrying" | |
sleep 20 | |
attempt=$((attempt+1)) | |
done | |
for ID in ${IDS} | |
do | |
attempt=1 | |
max_attempts=3 | |
while [ "${attempt}" -le "${max_attempts}" ]; do | |
gcloud compute disks delete --region "${{ env.REGION }}" --quiet "${ID}" | |
status=$? | |
if [[ $status == 0 ]]; then | |
echo "computer disk ${ID} deleted" | |
break | |
fi | |
echo "Failed deleting disk ${ID} from region ${{ env.REGION }}, retrying" | |
sleep 5 | |
attempt=$((attempt+1)) | |
done | |
done | |
# OpenShift Secrets required | |
# secrets.AWS_EKS_ADMIN_IAM_ROLES | |
# secrets.AWS_ACCESS_KEY_ID | |
# secrets.AWS_SECRET_ACCESS_KEY | |
e2e-openshift: | |
name: Run E2E on OpenShift | |
if: | | |
always() && !cancelled() && | |
vars.OPENSHIFT_ENABLED == 'true' && | |
needs.generate-jobs.outputs.openshiftEnabled == 'true' && | |
needs.generate-jobs.result == 'success' | |
needs: | |
- buildx | |
- generate-jobs | |
- evaluate_options | |
strategy: | |
fail-fast: false | |
max-parallel: 6 | |
matrix: ${{ fromJSON(needs.generate-jobs.outputs.openshiftMatrix) }} | |
runs-on: ubuntu-24.04 | |
env: | |
# TEST_DEPTH determines the maximum test level the suite should be running | |
TEST_DEPTH: ${{ needs.evaluate_options.outputs.test_level }} | |
# FEATURE_TYPE, when defined, determines the subset of E2E tests that will be executed, divided by feature type | |
FEATURE_TYPE: ${{ needs.evaluate_options.outputs.feature_type }} | |
K8S_VERSION: "${{ matrix.k8s_version }}" | |
POSTGRES_VERSION: ${{ matrix.postgres_version }} | |
POSTGRES_KIND: ${{ matrix.postgres_kind }} | |
MATRIX: ${{ matrix.id }} | |
POSTGRES_IMG: "${{ matrix.postgres_img }}" | |
# The version of operator to upgrade FROM, in the rolling upgrade E2E test | |
E2E_PRE_ROLLING_UPDATE_IMG: "${{ matrix.postgres_pre_img }}" | |
TEST_TIMEOUTS: ${{ needs.generate-jobs.outputs.openshiftTimeout }} | |
BRANCH_NAME: ${{ needs.buildx.outputs.branch_name }} | |
DEBUG: "true" | |
BUILD_IMAGE: "false" | |
CONTROLLER_IMG: ${{ needs.generate-jobs.outputs.image }} | |
E2E_DEFAULT_STORAGE_CLASS: gp3-csi | |
E2E_CSI_STORAGE_CLASS: gp3-csi | |
E2E_DEFAULT_VOLUMESNAPSHOT_CLASS: csi-aws-vsc | |
TEST_CLOUD_VENDOR: "ocp" | |
# AWS configuration | |
AWS_BASE_DOMAIN: ${{ secrets.AWS_BASE_DOMAIN }} | |
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
AWS_REGION: eu-central-1 | |
AWS_EKS_ADMIN_IAM_ROLES: ${{ secrets.AWS_EKS_ADMIN_IAM_ROLES }} | |
REDHAT_PULL: ${{ secrets.REDHAT_PULL }} | |
SSH_PUBLIC_KEY: ${{ secrets.SSH_PUBLIC_KEY }} | |
steps: | |
- | |
name: Set cluster name | |
run: | | |
echo "CLUSTER_NAME=${{ env.E2E_SUFFIX }}-ocp-${{ github.run_number}}-$( echo ${{ matrix.k8s_version }} | tr -d '.' )" >> $GITHUB_ENV | |
- | |
name: Checkout code | |
uses: actions/checkout@v4 | |
with: | |
ref: ${{ needs.evaluate_options.outputs.git_ref }} | |
fetch-depth: 0 | |
- | |
name: Install Go | |
uses: actions/setup-go@v5 | |
with: | |
go-version: ${{ env.GOLANG_VERSION }} | |
check-latest: true | |
- | |
name: Set up QEMU | |
uses: docker/setup-qemu-action@v3 | |
with: | |
platforms: ${{ env.PLATFORMS }} | |
- | |
name: Set up Docker Buildx | |
uses: docker/setup-buildx-action@v3 | |
- | |
## In case hack/setup-cluster.sh need pull operand image from registry | |
name: Login into docker registry | |
uses: docker/login-action@v3 | |
with: | |
registry: ${{ env.REGISTRY }} | |
username: ${{ env.REGISTRY_USER }} | |
password: ${{ env.REGISTRY_PASSWORD }} | |
- | |
name: Build and push the operator and catalog | |
env: | |
CONTROLLER_IMG: ${{ needs.buildx.outputs.controller_img_ubi8 }} | |
BUNDLE_IMG: ${{ needs.buildx.outputs.bundle_img }} | |
CATALOG_IMG: ${{ needs.buildx.outputs.catalog_img }} | |
run: | | |
make olm-catalog | |
- | |
name: Install OC Installer and client | |
uses: redhat-actions/openshift-tools-installer@v1 | |
with: | |
source: "mirror" | |
openshift-install: ${{ matrix.k8s_version }} | |
oc: ${{ matrix.k8s_version }} | |
- | |
name: Install OpenShift Cluster ${{ matrix.k8s_version }} | |
run: | | |
envsubst < hack/install-config.yaml.template > hack/install-config.yaml | |
openshift-install create cluster --dir hack/ --log-level warn | |
- | |
name: Run E2E tests | |
if: (always() && !cancelled()) | |
run: | | |
# Before running on OpenShift we make sure that the catalog is created | |
# in the openshift-marketplace namespace | |
sed -i -e 's/namespace: operators/namespace: openshift-marketplace/' cloudnative-pg-catalog.yaml | |
find -type f -name "cloudnative-pg-catalog.yaml" | |
cat cloudnative-pg-catalog.yaml | |
KUBECONFIG=$(pwd)/hack/auth/kubeconfig bash -x hack/e2e/run-e2e-ocp.sh | |
- | |
# Summarize the failed E2E tests cases if there are any | |
name: Report failed E2E tests | |
if: failure() | |
run: | | |
set +x | |
chmod +x .github/report-failed-test.sh | |
./.github/report-failed-test.sh | |
- | |
# Create an individual artifact for each E2E test, which will be used to | |
# generate E2E test summary in the follow-up job 'summarize-e2e-tests' | |
name: Create individual artifact for each E2E test | |
if: (always() && !cancelled()) | |
env: | |
RUNNER: "openshift" | |
RUN_ID: ${{ github.run_id }} | |
REPOSITORY: ${{ github.repository }} | |
GIT_REF: ${{ needs.evaluate_options.outputs.git_ref }} | |
run: | | |
set +x | |
python .github/generate-test-artifacts.py \ | |
-o testartifacts-${{ env.MATRIX }} \ | |
-f tests/e2e/out/report.json \ | |
--environment=true | |
if [ -f tests/e2e/out/upgrade_report.json ]; then | |
python .github/generate-test-artifacts.py \ | |
-o testartifacts-${{ env.MATRIX }} \ | |
-f tests/e2e/out/upgrade_report.json \ | |
--environment=true | |
fi | |
- | |
name: Archive test artifacts | |
if: (always() && !cancelled()) | |
uses: actions/upload-artifact@v4 | |
with: | |
name: testartifacts-${{ env.MATRIX }} | |
path: testartifacts-${{ env.MATRIX }}/ | |
retention-days: 7 | |
- | |
name: Cleanup test artifacts | |
if: always() | |
run: | |
rm -rf testartifacts-${{ env.MATRIX }}/ | |
- | |
name: Cleanup ginkgo JSON report | |
# Delete report.json after the analysis. File should always exist. | |
# Delete upgrade_report.json. It may not exist depending on test level. | |
if: always() | |
run: | | |
if [ -f tests/e2e/out/upgrade_report.json ]; then | |
rm tests/e2e/out/upgrade_report.json | |
fi | |
if [ -f tests/e2e/out/report.json ]; then | |
rm tests/e2e/out/report.json | |
fi | |
- | |
name: Archive e2e failure contexts | |
if: failure() | |
uses: actions/upload-artifact@v4 | |
with: | |
name: test-failure-contexts-${{ matrix.id }} | |
path: | | |
tests/*/out/ | |
retention-days: 7 | |
if-no-files-found: ignore | |
- | |
name: Archive e2e logs | |
if: failure() | |
uses: actions/upload-artifact@v4 | |
with: | |
name: cluster-logs-${{ matrix.id }} | |
path: | | |
tests/e2e/cluster_logs/** | |
retention-days: 7 | |
if-no-files-found: ignore | |
- | |
name: Destroy OpenShift Cluster ${{ matrix.k8s_version }} | |
if: always() | |
run: | | |
openshift-install destroy cluster --dir hack/ | |
# Summarize E2E test results, display in the GitHub 'summary' view | |
summarize-e2e-tests: | |
name: E2E test suite | |
needs: | |
- evaluate_options | |
- e2e-local | |
- e2e-eks | |
- e2e-aks | |
- e2e-gke | |
- e2e-openshift | |
if: | | |
(always() && !cancelled()) && | |
(( | |
needs.e2e-local.result == 'success' || | |
needs.e2e-local.result == 'failure' | |
) || | |
( | |
needs.e2e-eks.result == 'success' || | |
needs.e2e-eks.result == 'failure' | |
) || | |
( | |
needs.e2e-aks.result == 'success' || | |
needs.e2e-aks.result == 'failure' | |
) || | |
( | |
needs.e2e-gke.result == 'success' || | |
needs.e2e-gke.result == 'failure' | |
) || | |
( | |
needs.e2e-openshift.result == 'success' || | |
needs.e2e-openshift.result == 'failure' | |
)) | |
runs-on: ubuntu-24.04 | |
steps: | |
- name: Create a directory for the artifacts | |
run: mkdir test-artifacts | |
- name: Download all artifacts to the directory | |
uses: actions/download-artifact@v4 | |
with: | |
path: test-artifacts | |
pattern: testartifacts-* | |
- name: Flatten all artifacts onto directory | |
# The download-artifact action, since we did not give it a name, | |
# downloads all artifacts and creates a new folder for each. | |
# In this step we bring all the JSONs to a single folder | |
run: | | |
mkdir test-artifacts/data | |
mv test-artifacts/*/*.json test-artifacts/data || true | |
- name: Display the structure of the artifact folder | |
run: ls -R test-artifacts/data | |
- name: Compute the E2E test summary | |
id: generate-summary | |
uses: cloudnative-pg/[email protected] | |
with: | |
artifact_directory: test-artifacts/data | |
- name: If there is an overflow summary, archive it | |
if: steps.generate-summary.outputs.Overflow | |
uses: actions/upload-artifact@v4 | |
with: | |
name: ${{ steps.generate-summary.outputs.Overflow }} | |
path: ${{ steps.generate-summary.outputs.Overflow }} | |
retention-days: 7 | |
- name: Send the Ciclops view over Slack | |
# Send the Ciclops thermometer on every scheduled run on `main`. | |
# or when there are systematic failures in release branches | |
uses: rtCamp/action-slack-notify@v2 | |
if: | | |
github.repository_owner == env.REPOSITORY_OWNER && | |
( | |
github.event_name == 'schedule' || | |
( | |
steps.generate-summary.outputs.alerts && | |
startsWith(needs.evaluate_options.outputs.git_ref, 'refs/heads/release-') | |
) | |
) | |
env: | |
# SLACK_COLOR is where we distinguish a run with/without alerts. It's where the | |
# action has hooks for conditionality in the message body (yeah, weird) | |
SLACK_COLOR: ${{ steps.generate-summary.outputs.alerts && 'failure' || 'success' }} | |
SLACK_ICON: https://avatars.githubusercontent.com/u/85171364?size=48 | |
SLACK_USERNAME: ${{ env.SLACK_USERNAME }} | |
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} | |
SLACK_TITLE: CICLOPS view for ${{ github.repository }} | |
SLACK_MESSAGE_ON_SUCCESS: | | |
${{ steps.generate-summary.outputs.thermometer }} | |
SLACK_MESSAGE_ON_FAILURE: | | |
${{ steps.generate-summary.outputs.thermometer }} | |
:warning: *Systematic failures!* | |
${{ steps.generate-summary.outputs.alerts }} | |
SLACK_FOOTER: | | |
<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|*See full CI run*> | |
- name: Delete the downloaded files | |
run: rm -rf test-artifacts | |
# Adds the 'ok-to-merge' label to workflows that have run successfully and | |
# have adequate test and matrix coverage. | |
# This label is a prerequisite to be able to merge a PR. | |
# Also see to 'require-labels.yml' | |
ok-to-merge: | |
name: Label the PR as "ok to merge :ok_hand:" | |
needs: | |
- evaluate_options | |
- e2e-local | |
if: | | |
always() && | |
needs.e2e-local.result == 'success' && | |
github.event_name == 'issue_comment' && | |
needs.evaluate_options.outputs.test_level == '4' | |
runs-on: ubuntu-24.04 | |
steps: | |
- name: Check preconditions | |
id: get_pr_number_and_labels | |
env: | |
GITHUB_TOKEN: ${{ secrets.REPO_GHA_PAT }} | |
run: | | |
ok_label=$(gh pr view "${{ github.event.issue.number }}" --json labels -q ".labels.[].name" 2>/dev/null | grep "ok to merge :ok_hand:" || :) | |
echo "OK_LABEL=${ok_label}" >> $GITHUB_ENV | |
- name: Label the PR as "ok to merge :ok_hand:" | |
if: | | |
env.OK_LABEL == '' | |
uses: actions-ecosystem/[email protected] | |
with: | |
github_token: ${{ secrets.REPO_GHA_PAT }} | |
number: ${{ github.event.issue.number }} | |
labels: "ok to merge :ok_hand:" | |
# Remove the "ok to merge :ok_hand:" label if the E2E tests or previous steps failed | |
unlabel-ok-to-merge: | |
name: Remove the "ok to merge :ok_hand:" label from the PR | |
needs: | |
- evaluate_options | |
- e2e-local | |
if: | | |
always() && | |
needs.e2e-local.result == 'failure' && | |
github.event_name == 'issue_comment' | |
runs-on: ubuntu-24.04 | |
steps: | |
- name: Check preconditions | |
id: get_pr_number_and_labels | |
env: | |
GITHUB_TOKEN: ${{ secrets.REPO_GHA_PAT }} | |
run: | | |
ok_label=$(gh pr view "${{ github.event.issue.number }}" --json labels -q ".labels.[].name" 2>/dev/null | grep "ok to merge :ok_hand:" || :) | |
echo "OK_LABEL=${ok_label}" >> $GITHUB_ENV | |
- name: Remove "ok to merge :ok_hand:" label from PR | |
if: | | |
env.OK_LABEL != '' | |
uses: actions-ecosystem/[email protected] | |
with: | |
github_token: ${{ secrets.REPO_GHA_PAT }} | |
number: ${{ github.event.issue.number }} | |
labels: "ok to merge :ok_hand:" |