Created
March 6, 2026 18:50
-
-
Save clwluvw/33c72331ea33d53cda8befe81dd93a6d to your computer and use it in GitHub Desktop.
This file contains hidden or 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
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| # ============================================================================= | |
| # ExternalDNS + PowerDNS (SQLite backend) on Minikube | |
| # ============================================================================= | |
| # Test for PR #6234: GetDomainFilter interface | |
| # | |
| # Strategy: | |
| # - Create TWO forward zones in PowerDNS: example.local + other.local | |
| # - Create ONE reverse zone: 168.192.in-addr.arpa | |
| # - Pass only "example.local" via --domain-filter | |
| # - Create DNSEndpoints for BOTH zones | |
| # - Verify only example.local records are created (other.local ignored) | |
| # - Verify PTR records are created in the reverse zone | |
| # ============================================================================= | |
| DOMAIN_A="example.local" | |
| DOMAIN_B="other.local" | |
| PDNS_API_KEY="supersecretapikey" | |
| EXTERNALDNS_IMAGE="clwluvw/external-dns" | |
| EXTERNALDNS_TAG="v0.20.0-128-g7be6d366" | |
| PDNS_NAMESPACE="powerdns" | |
| EXTERNALDNS_NAMESPACE="external-dns" | |
| PDNS_SQLITE_PATH="/var/lib/powerdns/pdns.sqlite3" | |
| # --- Colors --- | |
| RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' | |
| info() { echo -e "${GREEN}[INFO]${NC} $*"; } | |
| warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } | |
| err() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; } | |
| pass() { echo -e "${GREEN}[PASS]${NC} $*"; } | |
| fail() { echo -e "${RED}[FAIL]${NC} $*"; } | |
| # --- Pre-flight checks --- | |
| for cmd in minikube kubectl helm; do | |
| command -v "$cmd" &>/dev/null || err "$cmd is required but not installed." | |
| done | |
| # ============================================================================= | |
| # 1. Start Minikube | |
| # ============================================================================= | |
| info "Starting Minikube..." | |
| minikube status &>/dev/null && info "Minikube already running" || minikube start --cpus=4 --memory=4096 --addons=ingress | |
| info "Waiting for ingress controller to be ready..." | |
| kubectl wait --namespace ingress-nginx \ | |
| --for=condition=ready pod \ | |
| --selector=app.kubernetes.io/component=controller \ | |
| --timeout=120s 2>/dev/null || warn "Ingress controller not ready yet, continuing..." | |
| # ============================================================================= | |
| # 2. Deploy PowerDNS with SQLite backend (3 zones) | |
| # ============================================================================= | |
| info "Creating namespace: $PDNS_NAMESPACE" | |
| kubectl create namespace "$PDNS_NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - | |
| info "Deploying PowerDNS Authoritative Server (SQLite)..." | |
| cat <<EOF | kubectl apply -f - | |
| --- | |
| apiVersion: v1 | |
| kind: ConfigMap | |
| metadata: | |
| name: pdns-config | |
| namespace: $PDNS_NAMESPACE | |
| data: | |
| pdns.conf: | | |
| launch=gsqlite3 | |
| gsqlite3-database=$PDNS_SQLITE_PATH | |
| api=yes | |
| api-key=$PDNS_API_KEY | |
| webserver=yes | |
| webserver-address=0.0.0.0 | |
| webserver-port=8081 | |
| webserver-allow-from=0.0.0.0/0 | |
| default-soa-content=ns1.$DOMAIN_A hostmaster.$DOMAIN_A 1 10800 3600 604800 3600 | |
| default-soa-edit=INCEPTION-INCREMENT | |
| log-dns-details=yes | |
| loglevel=6 | |
| --- | |
| apiVersion: v1 | |
| kind: ConfigMap | |
| metadata: | |
| name: pdns-init | |
| namespace: $PDNS_NAMESPACE | |
| data: | |
| init.sh: | | |
| #!/bin/bash | |
| set -e | |
| DB_PATH="$PDNS_SQLITE_PATH" | |
| SCHEMA_FILE="/usr/local/share/doc/pdns/schema.sqlite3.sql" | |
| # Create database directory if it doesn't exist | |
| mkdir -p "\$(dirname "\${DB_PATH}")" | |
| # Initialize database if it doesn't exist | |
| if [ ! -f "\$DB_PATH" ]; then | |
| echo "Initializing SQLite3 database..." | |
| if [ -f "\$SCHEMA_FILE" ]; then | |
| sqlite3 "\$DB_PATH" < "\$SCHEMA_FILE" | |
| echo "Database initialized successfully." | |
| else | |
| echo "ERROR: Schema file not found at \$SCHEMA_FILE" | |
| echo "Searching for schema files..." | |
| find / -name "schema.sqlite3.sql" 2>/dev/null || true | |
| exit 1 | |
| fi | |
| else | |
| echo "Database already exists at \$DB_PATH" | |
| fi | |
| # Create temporary config for pdnsutil | |
| TEMP_CONFIG_DIR="/tmp/pdns-init" | |
| TEMP_CONFIG_FILE="\${TEMP_CONFIG_DIR}/pdns.conf" | |
| mkdir -p "\${TEMP_CONFIG_DIR}" | |
| cat > "\${TEMP_CONFIG_FILE}" <<PDNSCONF | |
| launch=gsqlite3 | |
| gsqlite3-database=\${DB_PATH} | |
| default-soa-content=ns1.$DOMAIN_A. hostmaster.$DOMAIN_A. 0 10800 3600 604800 3600 | |
| default-soa-edit=INCEPTION-INCREMENT | |
| PDNSCONF | |
| # --- Zone A: $DOMAIN_A (will be in domain-filter) --- | |
| if ! pdnsutil --config-dir="\${TEMP_CONFIG_DIR}" list-zone "$DOMAIN_A" &>/dev/null; then | |
| echo "Creating zone $DOMAIN_A..." | |
| pdnsutil --config-dir="\${TEMP_CONFIG_DIR}" create-zone "$DOMAIN_A" ns1.$DOMAIN_A | |
| pdnsutil --config-dir="\${TEMP_CONFIG_DIR}" set-kind "$DOMAIN_A" native | |
| echo "Zone $DOMAIN_A created." | |
| else | |
| echo "Zone $DOMAIN_A already exists." | |
| fi | |
| # --- Zone B: $DOMAIN_B (NOT in domain-filter — should be ignored) --- | |
| if ! pdnsutil --config-dir="\${TEMP_CONFIG_DIR}" list-zone "$DOMAIN_B" &>/dev/null; then | |
| echo "Creating zone $DOMAIN_B..." | |
| pdnsutil --config-dir="\${TEMP_CONFIG_DIR}" create-zone "$DOMAIN_B" ns1.$DOMAIN_B | |
| pdnsutil --config-dir="\${TEMP_CONFIG_DIR}" set-kind "$DOMAIN_B" native | |
| echo "Zone $DOMAIN_B created." | |
| else | |
| echo "Zone $DOMAIN_B already exists." | |
| fi | |
| echo "" | |
| echo "=== Zone listing ===" | |
| pdnsutil --config-dir="\${TEMP_CONFIG_DIR}" list-zone "$DOMAIN_A" || true | |
| echo "---" | |
| pdnsutil --config-dir="\${TEMP_CONFIG_DIR}" list-zone "$DOMAIN_B" || true | |
| # Ensure correct permissions | |
| chown -R 953:953 "\$(dirname "\${DB_PATH}")" || true | |
| chmod 664 "\$DB_PATH" || true | |
| --- | |
| apiVersion: apps/v1 | |
| kind: Deployment | |
| metadata: | |
| name: powerdns | |
| namespace: $PDNS_NAMESPACE | |
| spec: | |
| replicas: 1 | |
| selector: | |
| matchLabels: | |
| app: powerdns | |
| template: | |
| metadata: | |
| labels: | |
| app: powerdns | |
| spec: | |
| initContainers: | |
| - name: init-db | |
| image: powerdns/pdns-auth-50:5.0.2 | |
| command: ["/bin/bash", "/scripts/init.sh"] | |
| volumeMounts: | |
| - name: init-script | |
| mountPath: /scripts | |
| - name: pdns-data | |
| mountPath: /var/lib/powerdns | |
| containers: | |
| - name: powerdns | |
| image: powerdns/pdns-auth-50:5.0.2 | |
| ports: | |
| - containerPort: 53 | |
| protocol: TCP | |
| - containerPort: 53 | |
| protocol: UDP | |
| - containerPort: 8081 | |
| protocol: TCP | |
| volumeMounts: | |
| - name: pdns-config | |
| mountPath: /etc/powerdns/pdns.d/ | |
| - name: pdns-data | |
| mountPath: /var/lib/powerdns | |
| readinessProbe: | |
| httpGet: | |
| path: /api/v1/servers/localhost | |
| port: 8081 | |
| httpHeaders: | |
| - name: X-API-Key | |
| value: "$PDNS_API_KEY" | |
| initialDelaySeconds: 5 | |
| periodSeconds: 5 | |
| volumes: | |
| - name: pdns-config | |
| configMap: | |
| name: pdns-config | |
| items: | |
| - key: pdns.conf | |
| path: pdns.conf | |
| - name: init-script | |
| configMap: | |
| name: pdns-init | |
| defaultMode: 0755 | |
| - name: pdns-data | |
| emptyDir: {} | |
| --- | |
| apiVersion: v1 | |
| kind: Service | |
| metadata: | |
| name: powerdns | |
| namespace: $PDNS_NAMESPACE | |
| spec: | |
| selector: | |
| app: powerdns | |
| ports: | |
| - name: dns-tcp | |
| port: 53 | |
| targetPort: 53 | |
| protocol: TCP | |
| - name: dns-udp | |
| port: 53 | |
| targetPort: 53 | |
| protocol: UDP | |
| - name: api | |
| port: 8081 | |
| targetPort: 8081 | |
| protocol: TCP | |
| EOF | |
| info "Waiting for PowerDNS to be ready..." | |
| kubectl wait -n "$PDNS_NAMESPACE" --for=condition=available deployment/powerdns --timeout=180s | |
| info "Checking PowerDNS init logs..." | |
| kubectl logs -n "$PDNS_NAMESPACE" -l app=powerdns -c init-db || true | |
| # ============================================================================= | |
| # 3. Deploy ExternalDNS via Helm — only DOMAIN_A in domain-filter | |
| # ============================================================================= | |
| info "Cleaning up any previous manual ExternalDNS resources..." | |
| kubectl delete clusterrole external-dns --ignore-not-found | |
| kubectl delete clusterrolebinding external-dns-viewer --ignore-not-found | |
| kubectl delete serviceaccount external-dns -n "$EXTERNALDNS_NAMESPACE" --ignore-not-found | |
| kubectl delete deployment external-dns -n "$EXTERNALDNS_NAMESPACE" --ignore-not-found | |
| info "Adding external-dns Helm repo..." | |
| helm repo add external-dns https://kubernetes-sigs.github.io/external-dns/ 2>/dev/null | |
| helm repo update | |
| info "Installing ExternalDNS via Helm..." | |
| info " domain-filter: $DOMAIN_A (only this zone should be managed)" | |
| info " NOT filtered: $DOMAIN_B (should be ignored)" | |
| helm upgrade --install external-dns external-dns/external-dns \ | |
| --namespace "$EXTERNALDNS_NAMESPACE" \ | |
| --create-namespace \ | |
| --set "image.repository=${EXTERNALDNS_IMAGE}" \ | |
| --set "image.tag=${EXTERNALDNS_TAG}" \ | |
| --set "image.pullPolicy=Always" \ | |
| --set "provider.name=pdns" \ | |
| --set "extraArgs[0]=--pdns-server=http://powerdns.${PDNS_NAMESPACE}.svc.cluster.local:8081" \ | |
| --set "extraArgs[1]=--pdns-api-key=${PDNS_API_KEY}" \ | |
| --set "domainFilters[0]=${DOMAIN_A}" \ | |
| --set "policy=sync" \ | |
| --set "logLevel=debug" \ | |
| --set "interval=10s" \ | |
| --set "txtOwnerId=external-dns" \ | |
| --set "sources[0]=service" \ | |
| --set "sources[1]=ingress" \ | |
| --set "sources[2]=crd" \ | |
| --wait --timeout 120s | |
| info "ExternalDNS deployed via Helm." | |
| # ============================================================================= | |
| # 4. Install DNSEndpoint CRD and create test records in BOTH zones | |
| # ============================================================================= | |
| info "Creating DNSEndpoint for $DOMAIN_A (SHOULD be processed)..." | |
| cat <<EOF | kubectl apply -f - | |
| apiVersion: externaldns.k8s.io/v1alpha1 | |
| kind: DNSEndpoint | |
| metadata: | |
| name: test-domain-a | |
| namespace: default | |
| spec: | |
| endpoints: | |
| - dnsName: app.$DOMAIN_A | |
| recordTTL: 300 | |
| recordType: A | |
| targets: | |
| - 192.168.1.10 | |
| - dnsName: api.$DOMAIN_A | |
| recordTTL: 300 | |
| recordType: A | |
| targets: | |
| - 192.168.1.20 | |
| EOF | |
| info "Creating DNSEndpoint for $DOMAIN_B (should NOT be processed)..." | |
| cat <<EOF | kubectl apply -f - | |
| apiVersion: externaldns.k8s.io/v1alpha1 | |
| kind: DNSEndpoint | |
| metadata: | |
| name: test-domain-b | |
| namespace: default | |
| spec: | |
| endpoints: | |
| - dnsName: app.$DOMAIN_B | |
| recordTTL: 300 | |
| recordType: A | |
| targets: | |
| - 192.168.2.10 | |
| - dnsName: api.$DOMAIN_B | |
| recordTTL: 300 | |
| recordType: A | |
| targets: | |
| - 192.168.2.20 | |
| EOF | |
| # ============================================================================= | |
| # 5. Verification — test GetDomainFilter (PR #6234) | |
| # ============================================================================= | |
| info "Waiting 30s for ExternalDNS to sync records..." | |
| sleep 30 | |
| info "=== ExternalDNS logs ===" | |
| kubectl logs -n "$EXTERNALDNS_NAMESPACE" -l app.kubernetes.io/name=external-dns --tail=60 | |
| echo "" | |
| info "=== Querying PowerDNS API for records ===" | |
| kubectl port-forward -n "$PDNS_NAMESPACE" svc/powerdns 8081:8081 & | |
| PF_PID=$! | |
| sleep 2 | |
| echo "" | |
| info "--- Forward zone: $DOMAIN_A (filtered — expect A + TXT records) ---" | |
| ZONE_A_RECORDS=$(curl -s "http://127.0.0.1:8081/api/v1/servers/localhost/zones/${DOMAIN_A}." \ | |
| -H "X-API-Key: $PDNS_API_KEY") | |
| echo "$ZONE_A_RECORDS" | python3 -m json.tool 2>/dev/null || echo "(Could not fetch records)" | |
| echo "" | |
| info "--- Forward zone: $DOMAIN_B (NOT filtered — expect NO extra records) ---" | |
| ZONE_B_RECORDS=$(curl -s "http://127.0.0.1:8081/api/v1/servers/localhost/zones/${DOMAIN_B}." \ | |
| -H "X-API-Key: $PDNS_API_KEY") | |
| echo "$ZONE_B_RECORDS" | python3 -m json.tool 2>/dev/null || echo "(Could not fetch records)" | |
| kill $PF_PID 2>/dev/null || true | |
| wait $PF_PID 2>/dev/null || true | |
| # ============================================================================= | |
| # 6. Automated test assertions | |
| # ============================================================================= | |
| echo "" | |
| info "=============================================" | |
| info " PR #6234 Test Results: GetDomainFilter" | |
| info "=============================================" | |
| echo "" | |
| TESTS_PASSED=0 | |
| TESTS_FAILED=0 | |
| # Test 1: A records in DOMAIN_A | |
| if echo "$ZONE_A_RECORDS" | grep -q "app.${DOMAIN_A}"; then | |
| pass "app.$DOMAIN_A A record exists in zone $DOMAIN_A" | |
| TESTS_PASSED=$((TESTS_PASSED + 1)) | |
| else | |
| fail "app.$DOMAIN_A A record NOT found in zone $DOMAIN_A" | |
| TESTS_FAILED=$((TESTS_FAILED + 1)) | |
| fi | |
| if echo "$ZONE_A_RECORDS" | grep -q "api.${DOMAIN_A}"; then | |
| pass "api.$DOMAIN_A A record exists in zone $DOMAIN_A" | |
| TESTS_PASSED=$((TESTS_PASSED + 1)) | |
| else | |
| fail "api.$DOMAIN_A A record NOT found in zone $DOMAIN_A" | |
| TESTS_FAILED=$((TESTS_FAILED + 1)) | |
| fi | |
| # Test 2: NO A records in DOMAIN_B (the key assertion for PR #6234) | |
| if echo "$ZONE_B_RECORDS" | grep -q "app.${DOMAIN_B}.*192.168.2.10"; then | |
| fail "app.$DOMAIN_B A record FOUND in zone $DOMAIN_B — domain filter NOT working!" | |
| TESTS_FAILED=$((TESTS_FAILED + 1)) | |
| else | |
| pass "app.$DOMAIN_B A record correctly absent from zone $DOMAIN_B" | |
| TESTS_PASSED=$((TESTS_PASSED + 1)) | |
| fi | |
| if echo "$ZONE_B_RECORDS" | grep -q "api.${DOMAIN_B}.*192.168.2.20"; then | |
| fail "api.$DOMAIN_B A record FOUND in zone $DOMAIN_B — domain filter NOT working!" | |
| TESTS_FAILED=$((TESTS_FAILED + 1)) | |
| else | |
| pass "api.$DOMAIN_B A record correctly absent from zone $DOMAIN_B" | |
| TESTS_PASSED=$((TESTS_PASSED + 1)) | |
| fi | |
| # Test 3: Done | |
| echo "" | |
| info "=============================================" | |
| info " Results: $TESTS_PASSED passed, $TESTS_FAILED failed" | |
| info "=============================================" | |
| if [ "$TESTS_FAILED" -gt 0 ]; then | |
| err "Some tests failed! Check output above." | |
| else | |
| info "All tests passed!" | |
| fi | |
| echo "" | |
| info "Useful commands:" | |
| info " # Check ExternalDNS logs" | |
| info " kubectl logs -n $EXTERNALDNS_NAMESPACE -l app.kubernetes.io/name=external-dns -f" | |
| info "" | |
| info " # Query PowerDNS API for zone records" | |
| info " kubectl port-forward -n $PDNS_NAMESPACE svc/powerdns 8081:8081" | |
| info " curl -s 'http://127.0.0.1:8081/api/v1/servers/localhost/zones/${DOMAIN_A}.' -H 'X-API-Key: $PDNS_API_KEY' | jq" | |
| info " curl -s 'http://127.0.0.1:8081/api/v1/servers/localhost/zones/${DOMAIN_B}.' -H 'X-API-Key: $PDNS_API_KEY' | jq" | |
| info "" | |
| info " # Test DNS resolution directly against PowerDNS" | |
| info " kubectl run -it --rm dnstest --image=busybox:1.36 --restart=Never -- nslookup app.$DOMAIN_A powerdns.$PDNS_NAMESPACE.svc.cluster.local" | |
| info "" | |
| info " # Cleanup everything" | |
| info " minikube delete" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment