Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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@v7
Comment thread
ondbeh marked this conversation as resolved.
Dismissed

- name: Set up Go
uses: actions/setup-go@v6
Comment thread
ondbeh marked this conversation as resolved.
Dismissed
with:
go-version-file: 'go.mod'

- name: Install Dependencies (Kind, Kuttl, Dig)
run: |
# Install dig
sudo apt-get update && sudo apt-get install -y dnsutils

# Install Kind
curl -sSLo ./kind https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-amd64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind

# Install Kuttl
curl -sSLo kubectl-kuttl https://github.com/kudobuilder/kuttl/releases/download/v0.15.0/kubectl-kuttl_0.15.0_linux_x86_64
chmod +x kubectl-kuttl
sudo mv kubectl-kuttl /usr/local/bin/

- name: Prepare STACKIT Service Account Key
run: |
# Write the secret from GitHub Actions into a temporary file so the Makefile can mount it
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
54 changes: 54 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,57 @@ 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 ./external-dns-stackit-webhook -v cmd/webhook/main.go

.PHONY: docker-build-e2e
docker-build-e2e: build-linux
docker build -t stackitcloud/external-dns-stackit-webhook:e2e -f Dockerfile .
rm ./external-dns-stackit-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/external-dns-stackit-webhook:e2e --name stackit-e2e
@echo "=> Setting up STACKIT credentials..."
kubectl create secret generic external-dns-stackit-webhook \
--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 ExternalDNS and Webhook..."
kubectl apply -f $(E2E_TMP_DIR)/deploy/external-dns.yaml
kubectl wait --for=condition=available --timeout=60s deployment/external-dns

@echo "=> Running Kuttl Tests..."
cd $(E2E_TMP_DIR) && \
kubectl kuttl test; \
RET=$$?; \
echo "=> Cleaning up local test environment..."; \
kind delete cluster --name stackit-e2e; \
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)
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,3 +361,16 @@ Test the code:
```bash
make test
```

### E2E Testing

End-to-end integration tests are orchestrated using [Kuttl](https://kuttl.dev/) and run against a dynamically generated [Kind](https://kind.sigs.k8s.io/) cluster. The test suite builds the webhook locally, deploys it alongside ExternalDNS, and directly verifies real DNS record propagation (A, AAAA, CNAME) against the STACKIT authoritative nameservers.

To run the E2E test suite locally, ensure you have Docker and `kind` installed, then execute:

```bash
make test-e2e-local \
PROJECT_ID="your-project-id" \
ZONE_NAME="your.test.zone.cloud" \
AUTH_KEY_PATH="/absolute/path/to/your/sa.json"
```
81 changes: 81 additions & 0 deletions tests/e2e/deploy/external-dns.yaml
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions tests/e2e/kuttl-test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
apiVersion: kuttl.dev/v1beta1
kind: TestSuite
testDirs:
- test-suite
timeout: 120
39 changes: 39 additions & 0 deletions tests/e2e/test-suite/record-lifecycle/00-assert.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
apiVersion: kuttl.dev/v1beta1
kind: TestAssert
timeout: 120
commands:
- script: |
check_record() {
RECORD_TYPE=$1
RECORD_NAME=$2
EXPECTED_RESULT=$3
ZONE=$4

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 "$RECORD_TYPE" +short "$RECORD_NAME")
echo "DEBUG: Dig Result for $RECORD_NAME: '$RESULT'"

if echo "$RESULT" | grep -q "$EXPECTED_RESULT"; then
echo "SUCCESS: $RECORD_TYPE record $RECORD_NAME verified!"
return 0
fi

echo "FAILED: Condition not met for $RECORD_NAME."
return 1
}

# Run all checks. If any fail, the chain short-circuits and we exit 1 to trigger a Kuttl retry.
check_record A "e2e-a.${ZONE_NAME}" "10.0.0.1" "${ZONE_NAME}" && \
check_record AAAA "e2e-aaaa.${ZONE_NAME}" "2001:db8::1" "${ZONE_NAME}" && \
check_record CNAME "e2e-cname.${ZONE_NAME}" "e2e-a.${ZONE_NAME}." "${ZONE_NAME}"

if [ $? -ne 0 ]; then
sleep 5
exit 1
fi
exit 0
38 changes: 38 additions & 0 deletions tests/e2e/test-suite/record-lifecycle/00-service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# 1. Triggers an A Record (IPv4)
apiVersion: v1
kind: Service
metadata:
name: test-service-a
annotations:
external-dns.alpha.kubernetes.io/hostname: "e2e-a.${ZONE_NAME}"
external-dns.alpha.kubernetes.io/target: "10.0.0.1"
spec:
type: LoadBalancer
ports:
- port: 80
---
# 2. Triggers an AAAA Record (IPv6)
apiVersion: v1
kind: Service
metadata:
name: test-service-aaaa
annotations:
external-dns.alpha.kubernetes.io/hostname: "e2e-aaaa.${ZONE_NAME}"
external-dns.alpha.kubernetes.io/target: "2001:db8::1"
spec:
type: LoadBalancer
ports:
- port: 80
---
# 3. Triggers a CNAME Record (Hostname)
apiVersion: v1
kind: Service
metadata:
name: test-service-cname
annotations:
external-dns.alpha.kubernetes.io/hostname: "e2e-cname.${ZONE_NAME}"
external-dns.alpha.kubernetes.io/target: "e2e-a.${ZONE_NAME}"
spec:
type: LoadBalancer
ports:
- port: 80
37 changes: 37 additions & 0 deletions tests/e2e/test-suite/record-lifecycle/01-assert.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
apiVersion: kuttl.dev/v1beta1
kind: TestAssert
timeout: 120
commands:
- script: |
check_deleted() {
RECORD_TYPE=$1
RECORD_NAME=$2
ZONE=$3

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 "$RECORD_TYPE" +short "$RECORD_NAME")
echo "DEBUG: Dig Result for $RECORD_NAME: '$RESULT'"

if [ -z "$RESULT" ]; then
echo "SUCCESS: $RECORD_TYPE record $RECORD_NAME successfully deleted!"
return 0
fi

echo "FAILED: $RECORD_NAME still exists."
return 1
}

check_deleted A "e2e-a.${ZONE_NAME}" "${ZONE_NAME}" && \
check_deleted AAAA "e2e-aaaa.${ZONE_NAME}" "${ZONE_NAME}" && \
check_deleted CNAME "e2e-cname.${ZONE_NAME}" "${ZONE_NAME}"

if [ $? -ne 0 ]; then
sleep 5
exit 1
fi
exit 0
12 changes: 12 additions & 0 deletions tests/e2e/test-suite/record-lifecycle/01-delete.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
delete:
- apiVersion: v1
kind: Service
name: test-service-a
- apiVersion: v1
kind: Service
name: test-service-aaaa
- apiVersion: v1
kind: Service
name: test-service-cname