diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..74f84ab --- /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 d695f7e..139f911 100644 --- a/Makefile +++ b/Makefile @@ -92,3 +92,84 @@ 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 + +.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 + +.PHONY: e2e-cluster +e2e-cluster: docker-build-e2e + @echo "=> Creating Kind cluster and loading image..." + kind create cluster --name stackit-e2e || true + kind load docker-image stackitcloud/stackit-cert-manager-webhook:e2e --name stackit-e2e + +.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-webhook -n cert-manager + +.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 - + + 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 + + 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 + +.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/deploy/external-dns.yaml b/tests/e2e/deploy/external-dns.yaml new file mode 100644 index 0000000..e6a40a3 --- /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 0000000..0a66d9f --- /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 0000000..bff66d7 --- /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 0000000..69def97 --- /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 0000000..bc6ce0a --- /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 0000000..29b2a20 --- /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 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 0000000..9827f13 --- /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 0000000..3aa1373 --- /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 0000000..0dd6d48 --- /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 0000000..2a5cb1b --- /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