Skip to content

Instantly share code, notes, and snippets.

@clwluvw
Created March 6, 2026 18:50
Show Gist options
  • Select an option

  • Save clwluvw/33c72331ea33d53cda8befe81dd93a6d to your computer and use it in GitHub Desktop.

Select an option

Save clwluvw/33c72331ea33d53cda8befe81dd93a6d to your computer and use it in GitHub Desktop.
#!/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