From f8fe522dcf150fc3b9f656021a1377325921b3c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20B=C4=9Bh=C3=A1vka?= Date: Thu, 13 Aug 2026 13:46:39 +0200 Subject: [PATCH 1/5] test: Introduce integration tests that run against real STACKIT DNS API --- Makefile | 69 ++++++++++++++++ tests/e2e/deploy/external-dns.yaml | 81 +++++++++++++++++++ tests/e2e/kuttl-test.yaml | 5 ++ .../record-lifecycle/00-assert.yaml | 9 +++ .../record-lifecycle/00-issuer-cert.yaml | 32 ++++++++ .../record-lifecycle/01-assert.yaml | 24 ++++++ .../record-lifecycle/01-delete.yaml | 7 ++ 7 files changed, 227 insertions(+) create mode 100644 tests/e2e/deploy/external-dns.yaml create mode 100644 tests/e2e/kuttl-test.yaml create mode 100644 tests/e2e/test-suite/record-lifecycle/00-assert.yaml create mode 100644 tests/e2e/test-suite/record-lifecycle/00-issuer-cert.yaml create mode 100644 tests/e2e/test-suite/record-lifecycle/01-assert.yaml create mode 100644 tests/e2e/test-suite/record-lifecycle/01-delete.yaml diff --git a/Makefile b/Makefile index d695f7e4..7f2dcbad 100644 --- a/Makefile +++ b/Makefile @@ -92,3 +92,72 @@ license-check: $(GO_LICENSES) reports ## Check licenses against code. .PHONY: license-report license-report: $(GO_LICENSES) reports ## Create licenses report against code. $(GO_LICENSES) report --include_tests --ignore $(LICENCES_IGNORE_LIST) ./... > ./reports/licenses/licenses-list.csv + +# ============================================================================== +# E2E Local Testing +# ============================================================================== + +E2E_TMP_DIR = tests/e2e-tmp + +.PHONY: build-linux +build-linux: + CGO_ENABLED=0 GOOS=linux GOARCH=$(shell go env GOARCH) go build -ldflags "-s -w" -o ./stackit-cert-manager-webhook -v cmd/webhook/main.go + +.PHONY: docker-build-e2e +docker-build-e2e: build-linux + docker build -t stackitcloud/stackit-cert-manager-webhook:e2e -f Dockerfile . + rm ./stackit-cert-manager-webhook # Clean up the binary after build + +# Run this to test the webhook locally +test-e2e-local: docker-build-e2e + @if [ -z "$(PROJECT_ID)" ] || [ -z "$(ZONE_NAME)" ] || [ -z "$(AUTH_KEY_PATH)" ]; then \ + echo "Error: Missing PROJECT_ID, ZONE_NAME, or AUTH_KEY_PATH environment variables."; \ + exit 1; \ + fi + @echo "=> Creating Kind cluster..." + kind create cluster --name stackit-e2e || true + @echo "=> Loading image into Kind..." + kind load docker-image stackitcloud/stackit-cert-manager-webhook:e2e --name stackit-e2e + + @echo "=> Installing cert-manager..." + kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.15.2/cert-manager.yaml + kubectl wait --for=condition=Available --timeout=300s deployment/cert-manager -n cert-manager + kubectl wait --for=condition=Available --timeout=300s deployment/cert-manager-cainjector -n cert-manager + kubectl wait --for=condition=Available --timeout=300s deployment/cert-manager-webhook -n cert-manager + + @echo "=> Setting up STACKIT credentials..." + kubectl create secret generic stackit-sa-authentication -n cert-manager \ + --from-file=sa.json=$(AUTH_KEY_PATH) \ + --dry-run=client -o yaml | kubectl apply -f - + + @echo "=> Preparing test manifests..." + rm -rf $(E2E_TMP_DIR) + cp -r tests/e2e $(E2E_TMP_DIR) + find $(E2E_TMP_DIR) -type f -name "*.yaml" -exec sed -i.bak "s/\$${PROJECT_ID}/$(PROJECT_ID)/g" {} + + find $(E2E_TMP_DIR) -type f -name "*.yaml" -exec sed -i.bak "s/\$${ZONE_NAME}/$(ZONE_NAME)/g" {} + + find $(E2E_TMP_DIR) -type f -name "*.bak" -delete + + @echo "=> Deploying cert-manager-webhook..." + helm upgrade --install stackit-cert-manager-webhook ./deploy/stackit \ + --namespace cert-manager \ + --set image.repository=stackitcloud/stackit-cert-manager-webhook \ + --set image.tag=e2e \ + --set image.pullPolicy=Never \ + --set stackitSaAuthentication.enabled=true \ + --set stackitSaAuthentication.secretName=stackit-sa-authentication + + @echo "=> Waiting for webhook to be ready..." + kubectl wait --for=condition=available --timeout=120s deployment/stackit-cert-manager-webhook -n cert-manager + + @echo "=> Running Kuttl Tests..." + cd $(E2E_TMP_DIR) && \ + kubectl kuttl test; \ + RET=$$?; \ + echo "=> Cleaning up local test environment..."; \ + cd ../.. && rm -rf $(E2E_TMP_DIR); \ + exit $$RET + +.PHONY: clean-e2e-local +clean-e2e-local: + kind delete cluster --name stackit-e2e + rm -rf $(E2E_TMP_DIR) diff --git a/tests/e2e/deploy/external-dns.yaml b/tests/e2e/deploy/external-dns.yaml new file mode 100644 index 00000000..e6a40a37 --- /dev/null +++ b/tests/e2e/deploy/external-dns.yaml @@ -0,0 +1,81 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: external-dns + namespace: default +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: external-dns +rules: + - apiGroups: [""] + resources: ["services","endpoints","pods","nodes"] + verbs: ["get","watch","list"] + - apiGroups: ["extensions","networking.k8s.io"] + resources: ["ingresses"] + verbs: ["get","watch","list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: external-dns-viewer +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: external-dns +subjects: + - kind: ServiceAccount + name: external-dns + namespace: default +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: external-dns + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: external-dns + template: + metadata: + labels: + app: external-dns + spec: + serviceAccountName: external-dns + volumes: + - name: stackit-sa-key + secret: + secretName: external-dns-stackit-webhook + items: + - key: sa.json + path: sa.json + containers: + - name: external-dns + image: registry.k8s.io/external-dns/external-dns:v0.14.0 + args: + - --log-level=info + - --interval=10s + - --source=service + - --policy=sync + - --provider=webhook + - --domain-filter=${ZONE_NAME} + - --txt-prefix=txt- + - name: webhook + image: stackitcloud/external-dns-stackit-webhook:e2e + imagePullPolicy: Never # Forces the use of our sideloaded local build + args: + - --project-id=${PROJECT_ID} + - --log-level=debug + ports: + - name: http + containerPort: 8888 + env: + - name: AUTH_KEY_PATH + value: /var/run/secrets/stackit/sa.json + volumeMounts: + - name: stackit-sa-key + mountPath: /var/run/secrets/stackit + readOnly: true \ No newline at end of file diff --git a/tests/e2e/kuttl-test.yaml b/tests/e2e/kuttl-test.yaml new file mode 100644 index 00000000..0a66d9fc --- /dev/null +++ b/tests/e2e/kuttl-test.yaml @@ -0,0 +1,5 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestSuite +testDirs: + - test-suite +timeout: 300 \ No newline at end of file diff --git a/tests/e2e/test-suite/record-lifecycle/00-assert.yaml b/tests/e2e/test-suite/record-lifecycle/00-assert.yaml new file mode 100644 index 00000000..bff66d7f --- /dev/null +++ b/tests/e2e/test-suite/record-lifecycle/00-assert.yaml @@ -0,0 +1,9 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: e2e-cert + namespace: default +status: + conditions: + - type: Ready + status: "True" \ No newline at end of file diff --git a/tests/e2e/test-suite/record-lifecycle/00-issuer-cert.yaml b/tests/e2e/test-suite/record-lifecycle/00-issuer-cert.yaml new file mode 100644 index 00000000..69def97f --- /dev/null +++ b/tests/e2e/test-suite/record-lifecycle/00-issuer-cert.yaml @@ -0,0 +1,32 @@ +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: stackit-e2e-issuer + namespace: default +spec: + acme: + server: https://acme-staging-v02.api.letsencrypt.org/directory + email: e2e-test@stackit.de + privateKeySecretRef: + name: stackit-e2e-issuer-key + solvers: + - dns01: + webhook: + solverName: stackit + groupName: acme.stackit.de + config: + projectId: ${PROJECT_ID} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: e2e-cert + namespace: default +spec: + secretName: e2e-cert-tls + issuerRef: + name: stackit-e2e-issuer + kind: Issuer + commonName: "e2e-cert.${ZONE_NAME}" + dnsNames: + - "e2e-cert.${ZONE_NAME}" \ No newline at end of file diff --git a/tests/e2e/test-suite/record-lifecycle/01-assert.yaml b/tests/e2e/test-suite/record-lifecycle/01-assert.yaml new file mode 100644 index 00000000..bc6ce0ab --- /dev/null +++ b/tests/e2e/test-suite/record-lifecycle/01-assert.yaml @@ -0,0 +1,24 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 120 +commands: + - script: | + RECORD_NAME="_acme-challenge.e2e-cert.${ZONE_NAME}" + ZONE="${ZONE_NAME}" + + AUTH_NS=$(dig +short NS "$ZONE" | head -n 1) + if [ -z "$AUTH_NS" ]; then + echo "ERROR: Could not determine authoritative nameserver for $ZONE" + return 1 + fi + + RESULT=$(dig "@$AUTH_NS" -t TXT +short "$RECORD_NAME") + echo "DEBUG: Dig Result for $RECORD_NAME: '$RESULT'" + + if [ -z "$RESULT" ]; then + echo "SUCCESS: TXT record $RECORD_NAME successfully deleted by the webhook!" + exit 0 + fi + + echo "FAILED: $RECORD_NAME still exists." + exit 1 \ No newline at end of file diff --git a/tests/e2e/test-suite/record-lifecycle/01-delete.yaml b/tests/e2e/test-suite/record-lifecycle/01-delete.yaml new file mode 100644 index 00000000..29b2a209 --- /dev/null +++ b/tests/e2e/test-suite/record-lifecycle/01-delete.yaml @@ -0,0 +1,7 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +delete: + - apiVersion: cert-manager.io/v1 + kind: Certificate + name: e2e-cert + namespace: default \ No newline at end of file From ff393ce2cd386ac2ba52cf9e9af07c6d4c71b34d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20B=C4=9Bh=C3=A1vka?= Date: Thu, 13 Aug 2026 14:13:21 +0000 Subject: [PATCH 2/5] Add pipeline, prettify makefile, add more tests --- .github/workflows/e2e.yml | 49 +++++++++++++++ Makefile | 60 +++++++++++-------- .../wildcard-certificate/00-assert.yaml | 9 +++ .../wildcard-certificate/00-issuer-cert.yaml | 33 ++++++++++ .../wildcard-certificate/01-assert.yaml | 26 ++++++++ .../wildcard-certificate/01-delete.yaml | 7 +++ 6 files changed, 160 insertions(+), 24 deletions(-) create mode 100644 .github/workflows/e2e.yml create mode 100644 tests/e2e/test-suite/wildcard-certificate/00-assert.yaml create mode 100644 tests/e2e/test-suite/wildcard-certificate/00-issuer-cert.yaml create mode 100644 tests/e2e/test-suite/wildcard-certificate/01-assert.yaml create mode 100644 tests/e2e/test-suite/wildcard-certificate/01-delete.yaml diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 00000000..74f84abf --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,49 @@ +name: E2E Integration Tests + +on: + pull_request: + branches: ["main", "release/**"] + schedule: + - cron: '0 2 * * *' # Runs daily at 2:00 AM UTC + +jobs: + e2e-tests: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up Go + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: 'go.mod' + + - name: Install Helm + uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1 + + - name: Install Dependencies (Kind, Kuttl, Dig) + run: | + # Install dig for DNS assertions + sudo apt-get update && sudo apt-get install -y dnsutils + + # Install the latest Kind (v0.32.0) + curl -sSLo ./kind https://kind.sigs.k8s.io/dl/v0.32.0/kind-linux-amd64 + chmod +x ./kind + sudo mv ./kind /usr/local/bin/kind + + # Install the latest Kuttl (v0.26.0) + curl -sSLo kubectl-kuttl https://github.com/kudobuilder/kuttl/releases/download/v0.26.0/kubectl-kuttl_0.26.0_linux_x86_64 + chmod +x kubectl-kuttl + sudo mv kubectl-kuttl /usr/local/bin/ + + - name: Prepare STACKIT Service Account Key + run: | + echo '${{ secrets.STACKIT_SERVICE_ACCOUNT_KEY }}' > /tmp/sa.json + + - name: Run E2E Tests + env: + PROJECT_ID: ${{ secrets.STACKIT_PROJECT_ID }} + ZONE_NAME: ${{ secrets.STACKIT_ZONE_NAME }} + AUTH_KEY_PATH: /tmp/sa.json + run: | + make test-e2e-local \ No newline at end of file diff --git a/Makefile b/Makefile index 7f2dcbad..139f9110 100644 --- a/Makefile +++ b/Makefile @@ -106,38 +106,40 @@ build-linux: .PHONY: docker-build-e2e docker-build-e2e: build-linux docker build -t stackitcloud/stackit-cert-manager-webhook:e2e -f Dockerfile . - rm ./stackit-cert-manager-webhook # Clean up the binary after build + rm ./stackit-cert-manager-webhook -# Run this to test the webhook locally -test-e2e-local: docker-build-e2e +.PHONY: e2e-check-env +e2e-check-env: @if [ -z "$(PROJECT_ID)" ] || [ -z "$(ZONE_NAME)" ] || [ -z "$(AUTH_KEY_PATH)" ]; then \ echo "Error: Missing PROJECT_ID, ZONE_NAME, or AUTH_KEY_PATH environment variables."; \ exit 1; \ fi - @echo "=> Creating Kind cluster..." + +.PHONY: e2e-cluster +e2e-cluster: docker-build-e2e + @echo "=> Creating Kind cluster and loading image..." kind create cluster --name stackit-e2e || true - @echo "=> Loading image into Kind..." kind load docker-image stackitcloud/stackit-cert-manager-webhook:e2e --name stackit-e2e - @echo "=> Installing cert-manager..." - kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.15.2/cert-manager.yaml +.PHONY: e2e-cert-manager +e2e-cert-manager: + @echo "=> Installing cert-manager via Helm..." + helm repo add jetstack https://charts.jetstack.io --force-update + helm upgrade --install cert-manager jetstack/cert-manager \ + --namespace cert-manager \ + --create-namespace \ + --set crds.enabled=true + kubectl wait --for=condition=Available --timeout=300s deployment/cert-manager -n cert-manager - kubectl wait --for=condition=Available --timeout=300s deployment/cert-manager-cainjector -n cert-manager kubectl wait --for=condition=Available --timeout=300s deployment/cert-manager-webhook -n cert-manager - @echo "=> Setting up STACKIT credentials..." +.PHONY: e2e-deploy-webhook +e2e-deploy-webhook: + @echo "=> Deploying stackit-cert-manager-webhook..." kubectl create secret generic stackit-sa-authentication -n cert-manager \ --from-file=sa.json=$(AUTH_KEY_PATH) \ --dry-run=client -o yaml | kubectl apply -f - - @echo "=> Preparing test manifests..." - rm -rf $(E2E_TMP_DIR) - cp -r tests/e2e $(E2E_TMP_DIR) - find $(E2E_TMP_DIR) -type f -name "*.yaml" -exec sed -i.bak "s/\$${PROJECT_ID}/$(PROJECT_ID)/g" {} + - find $(E2E_TMP_DIR) -type f -name "*.yaml" -exec sed -i.bak "s/\$${ZONE_NAME}/$(ZONE_NAME)/g" {} + - find $(E2E_TMP_DIR) -type f -name "*.bak" -delete - - @echo "=> Deploying cert-manager-webhook..." helm upgrade --install stackit-cert-manager-webhook ./deploy/stackit \ --namespace cert-manager \ --set image.repository=stackitcloud/stackit-cert-manager-webhook \ @@ -146,18 +148,28 @@ test-e2e-local: docker-build-e2e --set stackitSaAuthentication.enabled=true \ --set stackitSaAuthentication.secretName=stackit-sa-authentication - @echo "=> Waiting for webhook to be ready..." kubectl wait --for=condition=available --timeout=120s deployment/stackit-cert-manager-webhook -n cert-manager +.PHONY: e2e-run-kuttl +e2e-run-kuttl: + @echo "=> Preparing test manifests..." + rm -rf $(E2E_TMP_DIR) + cp -r tests/e2e $(E2E_TMP_DIR) + find $(E2E_TMP_DIR) -type f -name "*.yaml" -exec sed -i.bak "s/\$${PROJECT_ID}/$(PROJECT_ID)/g" {} + + find $(E2E_TMP_DIR) -type f -name "*.yaml" -exec sed -i.bak "s/\$${ZONE_NAME}/$(ZONE_NAME)/g" {} + + find $(E2E_TMP_DIR) -type f -name "*.bak" -delete + @echo "=> Running Kuttl Tests..." - cd $(E2E_TMP_DIR) && \ - kubectl kuttl test; \ - RET=$$?; \ - echo "=> Cleaning up local test environment..."; \ - cd ../.. && rm -rf $(E2E_TMP_DIR); \ - exit $$RET + cd $(E2E_TMP_DIR) && kubectl kuttl test .PHONY: clean-e2e-local clean-e2e-local: + @echo "=> Cleaning up local test environment..." kind delete cluster --name stackit-e2e rm -rf $(E2E_TMP_DIR) + +# The main target chains the dependencies together +.PHONY: test-e2e-local +test-e2e-local: e2e-check-env e2e-cluster e2e-cert-manager e2e-deploy-webhook + @$(MAKE) e2e-run-kuttl || ( $(MAKE) clean-e2e-local && exit 1 ) + @$(MAKE) clean-e2e-local \ No newline at end of file diff --git a/tests/e2e/test-suite/wildcard-certificate/00-assert.yaml b/tests/e2e/test-suite/wildcard-certificate/00-assert.yaml new file mode 100644 index 00000000..9827f134 --- /dev/null +++ b/tests/e2e/test-suite/wildcard-certificate/00-assert.yaml @@ -0,0 +1,9 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: e2e-wildcard-cert + namespace: default +status: + conditions: + - type: Ready + status: "True" \ No newline at end of file diff --git a/tests/e2e/test-suite/wildcard-certificate/00-issuer-cert.yaml b/tests/e2e/test-suite/wildcard-certificate/00-issuer-cert.yaml new file mode 100644 index 00000000..3aa13734 --- /dev/null +++ b/tests/e2e/test-suite/wildcard-certificate/00-issuer-cert.yaml @@ -0,0 +1,33 @@ +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: stackit-e2e-wildcard-issuer + namespace: default +spec: + acme: + server: https://acme-staging-v02.api.letsencrypt.org/directory + email: e2e-wildcard-test@stackit.de + privateKeySecretRef: + name: stackit-e2e-wildcard-issuer-key + solvers: + - dns01: + webhook: + solverName: stackit + groupName: acme.stackit.de + config: + projectId: ${PROJECT_ID} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: e2e-wildcard-cert + namespace: default +spec: + secretName: e2e-wildcard-cert-tls + issuerRef: + name: stackit-e2e-wildcard-issuer + kind: Issuer + commonName: "*.e2e-wildcard.${ZONE_NAME}" + dnsNames: + - "e2e-wildcard.${ZONE_NAME}" + - "*.e2e-wildcard.${ZONE_NAME}" \ No newline at end of file diff --git a/tests/e2e/test-suite/wildcard-certificate/01-assert.yaml b/tests/e2e/test-suite/wildcard-certificate/01-assert.yaml new file mode 100644 index 00000000..0dd6d483 --- /dev/null +++ b/tests/e2e/test-suite/wildcard-certificate/01-assert.yaml @@ -0,0 +1,26 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 120 +commands: + - script: | + # For both e2e-wildcard.YOUR_ZONE and *.e2e-wildcard.YOUR_ZONE, + # the challenge record is placed at the exact same location. + RECORD_NAME="_acme-challenge.e2e-wildcard.${ZONE_NAME}" + ZONE="${ZONE_NAME}" + + AUTH_NS=$(dig +short NS "$ZONE" | head -n 1) + if [ -z "$AUTH_NS" ]; then + echo "ERROR: Could not determine authoritative nameserver for $ZONE" + return 1 + fi + + RESULT=$(dig "@$AUTH_NS" -t TXT +short "$RECORD_NAME") + echo "DEBUG: Dig Result for $RECORD_NAME: '$RESULT'" + + if [ -z "$RESULT" ]; then + echo "SUCCESS: TXT record $RECORD_NAME successfully deleted!" + exit 0 + fi + + echo "FAILED: $RECORD_NAME still exists." + exit 1 \ No newline at end of file diff --git a/tests/e2e/test-suite/wildcard-certificate/01-delete.yaml b/tests/e2e/test-suite/wildcard-certificate/01-delete.yaml new file mode 100644 index 00000000..2a5cb1bd --- /dev/null +++ b/tests/e2e/test-suite/wildcard-certificate/01-delete.yaml @@ -0,0 +1,7 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +delete: + - apiVersion: cert-manager.io/v1 + kind: Certificate + name: e2e-wildcard-cert + namespace: default \ No newline at end of file From fe51966e5e681036494344e8c8f61086bbf76c5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20B=C4=9Bh=C3=A1vka?= Date: Fri, 14 Aug 2026 09:02:16 +0200 Subject: [PATCH 3/5] test: Force tests to be sequential --- tests/e2e/kuttl-test.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/e2e/kuttl-test.yaml b/tests/e2e/kuttl-test.yaml index 0a66d9fc..0fda5f7d 100644 --- a/tests/e2e/kuttl-test.yaml +++ b/tests/e2e/kuttl-test.yaml @@ -2,4 +2,5 @@ apiVersion: kuttl.dev/v1beta1 kind: TestSuite testDirs: - test-suite -timeout: 300 \ No newline at end of file +timeout: 300 +parallel: 1 \ No newline at end of file From c53d56a75c112cf331fd3448ed3be66033b3dabb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20B=C4=9Bh=C3=A1vka?= Date: Tue, 18 Aug 2026 10:24:05 +0000 Subject: [PATCH 4/5] Update/add READMEs --- Makefile | 3 ++- README.md | 66 +++++++++++++++++++++++++++------------------ e2e_test/README.md | 2 +- tests/e2e/README.md | 49 +++++++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 28 deletions(-) create mode 100644 tests/e2e/README.md diff --git a/Makefile b/Makefile index 139f9110..535c7e48 100644 --- a/Makefile +++ b/Makefile @@ -62,7 +62,8 @@ html-coverage: out/report.json out/report.json: go test -race ./... -coverprofile=out/cover.out --json | tee "$(@)" -test-e2e: +.PHONY: test-e2e-conformance +test-e2e-conformance: @STACKIT_TOKEN=$(STACKIT_TOKEN) TEST_ZONE_NAME=$(TEST_ZONE_NAME) go test -race -tags=e2e ./... -coverprofile out/cover.out run: diff --git a/README.md b/README.md index b3e0de23..d10a1ffe 100644 --- a/README.md +++ b/README.md @@ -96,18 +96,18 @@ helm install stackit-cert-manager-webhook --namespace cert-manager stackit-cert- projectId: ``` *Note on service accounts and namespaces:* - - Issuer-per-namespace (recommended for isolation): create a STACKIT service-account key (sa.json) for each STACKIT project you need to manage and place that key in a Kubernetes Secret in the same namespace as the Issuer. This means one sa.json (one SA key) per Issuer/namespace when the Issuers target different STACKIT projects. - Example (create a secret in the Issuer namespace): - ```bash - kubectl create secret generic stackit-sa-authentication \ - -n \ - --from-literal=sa.json='{"id":"...","credentials":{...}}' - ``` - Ensure the webhook can read the secret in that namespace (create the secret where the Issuer lives). - - Alternative (single SA key for multiple projects): you can grant the service account broader permissions at folder or organization level so one sa.json can manage zones across multiple projects. This is more convenient but grants wider access — evaluate security and follow least-privilege principles. - - Tradeoffs: - - Per-namespace/per-project SA keys: better isolation and least privilege, easier to rotate keys per project. - - Folder/org-level SA key: lower operational overhead (single key), but larger blast radius if compromised. + - Issuer-per-namespace (recommended for isolation): create a STACKIT service-account key (sa.json) for each STACKIT project you need to manage and place that key in a Kubernetes Secret in the same namespace as the Issuer. This means one sa.json (one SA key) per Issuer/namespace when the Issuers target different STACKIT projects. + Example (create a secret in the Issuer namespace): + ```bash + kubectl create secret generic stackit-sa-authentication \ + -n \ + --from-literal=sa.json='{"id":"...","credentials":{...}}' + ``` + Ensure the webhook can read the secret in that namespace (create the secret where the Issuer lives). + - Alternative (single SA key for multiple projects): you can grant the service account broader permissions at folder or organization level so one sa.json can manage zones across multiple projects. This is more convenient but grants wider access — evaluate security and follow least-privilege principles. + - Tradeoffs: + - Per-namespace/per-project SA keys: better isolation and least privilege, easier to rotate keys per project. + - Folder/org-level SA key: lower operational overhead (single key), but larger blast radius if compromised. 3. ***Demonstration of Ingress Integration with Wildcard SSL/TLS Certificate Generation*** Given the preceding configuration, it is possible to exploit the capabilities of the Issuer or ClusterIssuer to @@ -192,23 +192,37 @@ spec: ## Test Procedures -- Unit Testing: - ```bash - make test - ``` +### Unit Testing: +```bash +make test +``` -- Unit Testing with Coverage Analysis: - ```bash - make coverage - ``` +### Unit Testing with Coverage Analysis: +```bash +make coverage +``` -- Linting: - ```bash - make lint - ``` +### Linting: +```bash +make lint +``` + +### Go Conformance Testing: +Runs the official cert-manager Go solver test suite in memory against the STACKIT API: +```bash +STACKIT_TOKEN="" TEST_ZONE_NAME="example.com" make test-e2e-conformance +``` +Follow the comprehensive guide available [here](e2e_test/README.md). -- End-to-End Testing Workflow: - Follow the comprehensive guide available [here](e2e_test/README.md). +### Kubernetes Integration (E2E) Testing: +Spins up a local Kind cluster, installs cert-manager, builds and deploys the webhook, and executes Kuttl integration tests (testing single-record lifecycle and wildcard certificates against Let's Encrypt Staging): +```bash +make test-e2e-local \ + PROJECT_ID="" \ + ZONE_NAME="" \ + AUTH_KEY_PATH="" +``` +Follow the comprehensive guide available [here](tests/e2e/README.md). ## Release Process Overview diff --git a/e2e_test/README.md b/e2e_test/README.md index 23af708f..7fd38cc2 100644 --- a/e2e_test/README.md +++ b/e2e_test/README.md @@ -45,5 +45,5 @@ Declare the testing DNS zone. Ensure project_id consistency: ### Environmental Prerequisite Configuration: With prerequisites addressed, proceed to run the entire E2E test suite: ```bash -make test-e2e +make test-e2e-conformance ``` diff --git a/tests/e2e/README.md b/tests/e2e/README.md new file mode 100644 index 00000000..5b38da10 --- /dev/null +++ b/tests/e2e/README.md @@ -0,0 +1,49 @@ +# End-to-End (E2E) Integration Testing + +This directory contains the Kuttl-based integration test suite for the STACKIT `cert-manager` webhook. + +The E2E pipeline creates a local [Kind](https://kind.sigs.k8s.io/) cluster, installs `cert-manager`, deploys the webhook image built from source, and executes assertions against real Let's Encrypt Staging ACME challenges. + +--- + +## Test Cases + +The test suite consists of the following sequential test scenarios: + +1. **`record-lifecycle`**: Verifies the creation, propagation check, certificate issuance, and cleanup of a standard single domain certificate (`e2e-cert.`). +2. **`wildcard-certificate`**: Verifies concurrent challenge handling for a wildcard certificate request covering both base and wildcard domains (`*.e2e-wildcard.` and `e2e-wildcard.`). + +--- + +## Prerequisites + +To run local E2E tests, ensure the following tools are installed: + +- **Docker** +- **Go** +- **Kind** +- **Kubectl** +- **Helm** +- **Kuttl** (`kubectl-kuttl`) +- **Dig** (`dnsutils` package) + +--- + +## Running E2E Tests Locally + +1. Prepare a STACKIT Service Account key JSON file with permissions to manage DNS records in your target test zone. +2. Export required parameters and execute the Makefile target: + +```bash +make test-e2e-local \ + PROJECT_ID="" \ + ZONE_NAME="" \ + AUTH_KEY_PATH="" +``` + +## Note on Corporate Proxies & VPNs + +When running tests locally on a corporate machine behind proxies or VPNs, cert-manager's propagation self-checks may timeout due to DNS interception or blocked outbound UDP port 53 traffic. + +### Solution +If you encounter `dial tcp ...:53 i/o timeout` errors during cert-manager propagation checks, run `make test-e2e-local` in an environment that is unaffected by these network interceptions. From ae9bf3dcfb4a54dea11d9cc79d400ce6647e3d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20B=C4=9Bh=C3=A1vka?= Date: Tue, 18 Aug 2026 13:01:49 +0000 Subject: [PATCH 5/5] Remove leftover external dns deployment. --- CONTRIBUTING.md | 2 +- tests/e2e/deploy/external-dns.yaml | 81 ------------------------------ 2 files changed, 1 insertion(+), 82 deletions(-) delete mode 100644 tests/e2e/deploy/external-dns.yaml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 07da1761..326ce96d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Contributing to External DNS STACKIT Webhook +# Contributing to Cert-Manager STACKIT Webhook Welcome and thank you for making it this far and considering contributing to stackit-cert-manager-webhook. We always appreciate any contributions by raising issues, improving the documentation, fixing bugs or adding new features. diff --git a/tests/e2e/deploy/external-dns.yaml b/tests/e2e/deploy/external-dns.yaml deleted file mode 100644 index e6a40a37..00000000 --- a/tests/e2e/deploy/external-dns.yaml +++ /dev/null @@ -1,81 +0,0 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - name: external-dns - namespace: default ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: external-dns -rules: - - apiGroups: [""] - resources: ["services","endpoints","pods","nodes"] - verbs: ["get","watch","list"] - - apiGroups: ["extensions","networking.k8s.io"] - resources: ["ingresses"] - verbs: ["get","watch","list"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: external-dns-viewer -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: external-dns -subjects: - - kind: ServiceAccount - name: external-dns - namespace: default ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: external-dns - namespace: default -spec: - replicas: 1 - selector: - matchLabels: - app: external-dns - template: - metadata: - labels: - app: external-dns - spec: - serviceAccountName: external-dns - volumes: - - name: stackit-sa-key - secret: - secretName: external-dns-stackit-webhook - items: - - key: sa.json - path: sa.json - containers: - - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.14.0 - args: - - --log-level=info - - --interval=10s - - --source=service - - --policy=sync - - --provider=webhook - - --domain-filter=${ZONE_NAME} - - --txt-prefix=txt- - - name: webhook - image: stackitcloud/external-dns-stackit-webhook:e2e - imagePullPolicy: Never # Forces the use of our sideloaded local build - args: - - --project-id=${PROJECT_ID} - - --log-level=debug - ports: - - name: http - containerPort: 8888 - env: - - name: AUTH_KEY_PATH - value: /var/run/secrets/stackit/sa.json - volumeMounts: - - name: stackit-sa-key - mountPath: /var/run/secrets/stackit - readOnly: true \ No newline at end of file