diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index df5cd41..63c5708 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -25,7 +25,7 @@ jobs: name: End Status if: always() runs-on: ubuntu-latest - needs: [discover-modules, test-modules, test-scenarios, validate-structure, test-charts, test-merge] + needs: [discover-modules, test-modules, test-scenarios, validate-structure, test-charts, test-merge, test-dns-resources] permissions: contents: read statuses: write @@ -320,6 +320,125 @@ jobs: docker stop merge-test-server > /dev/null 2>&1 || true docker rm merge-test-server > /dev/null 2>&1 || true + test-dns-resources: + name: Test DNS ${{ matrix.resource }} scripts + runs-on: ubuntu-latest + needs: [test-modules, start-status] + strategy: + matrix: + include: + - resource: ingresses + traffic: ingress + setup_script: ./scripts/setup/ingress.sh + install_script: ./scripts/install/nginx.sh + kind: Ingress + - resource: httproutes + traffic: gateway + setup_script: ./scripts/setup/gateway.sh + install_script: ./scripts/install/istio.sh + kind: HTTPRoute + fail-fast: false + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + chmod +x kubectl + sudo mv kubectl /usr/local/bin/ + + curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + + - name: Syntax check DNS scripts + run: | + bash -n scripts/setup/dns-ingresses.sh + bash -n scripts/cleanup/dns-ingresses.sh + bash -n scripts/setup/dns-httproutes.sh + bash -n scripts/cleanup/dns-httproutes.sh + + - name: Setup Kind cluster + run: | + chmod +x ./modules/kind/install/install.sh ./modules/kind/run/run.sh + ./modules/kind/install/install.sh + ./modules/kind/run/run.sh + + - name: Install ${{ matrix.traffic }} controller + run: | + chmod +x ${{ matrix.install_script }} + ${{ matrix.install_script }} + + - name: Deploy server chart + run: | + chmod +x ${{ matrix.setup_script }} + ${{ matrix.setup_script }} + + - name: Run setup/dns-${{ matrix.resource }} (count=5) + run: | + chmod +x scripts/setup/dns-${{ matrix.resource }}.sh + scripts/setup/dns-${{ matrix.resource }}.sh --namespace server --count 5 --domain extdns.telescope.test + + - name: Assert 5 ${{ matrix.kind }} resources exist with dns-test=true + run: | + if [ "${{ matrix.resource }}" = "ingresses" ]; then + COUNT=$(kubectl get ingress -n server -l dns-test=true -o name | wc -l) + else + COUNT=$(kubectl get httproute.gateway.networking.k8s.io -n server -l dns-test=true -o name | wc -l) + fi + echo "Found ${COUNT} ${{ matrix.kind }} resources with dns-test=true" + if [ "${COUNT}" -ne 5 ]; then + echo "ERROR: Expected 5, got ${COUNT}" + exit 1 + fi + + - name: Run cleanup/dns-${{ matrix.resource }} + run: | + chmod +x scripts/cleanup/dns-${{ matrix.resource }}.sh + scripts/cleanup/dns-${{ matrix.resource }}.sh --namespace server + + - name: Assert all dns-test resources are gone + run: | + # --wait=false means deletes are async; poll briefly + for i in {1..15}; do + if [ "${{ matrix.resource }}" = "ingresses" ]; then + COUNT=$(kubectl get ingress -n server -l dns-test=true -o name 2>/dev/null | wc -l) + else + COUNT=$(kubectl get httproute.gateway.networking.k8s.io -n server -l dns-test=true -o name 2>/dev/null | wc -l) + fi + [ "${COUNT}" -eq 0 ] && break + echo "Still ${COUNT} resources present, waiting... (attempt $i/15)" + sleep 2 + done + if [ "${COUNT}" -ne 0 ]; then + echo "ERROR: Expected 0, got ${COUNT}" + exit 1 + fi + echo "✓ All dns-test ${{ matrix.kind }} resources removed" + + - name: Negative path - missing service + run: | + if scripts/setup/dns-${{ matrix.resource }}.sh --namespace server --count 1 --domain extdns.telescope.test --service-name nonexistent 2>/dev/null; then + echo "ERROR: Expected non-zero exit for missing service" + exit 1 + fi + echo "✓ Correctly failed on missing service" + + - name: Negative path - invalid domain + run: | + if scripts/setup/dns-${{ matrix.resource }}.sh --namespace server --count 1 --domain "Bad_Domain!" 2>/dev/null; then + echo "ERROR: Expected non-zero exit for invalid domain" + exit 1 + fi + echo "✓ Correctly failed on invalid domain" + + - name: Negative path - count=0 + run: | + if scripts/setup/dns-${{ matrix.resource }}.sh --namespace server --count 0 --domain extdns.telescope.test 2>/dev/null; then + echo "ERROR: Expected non-zero exit for count=0" + exit 1 + fi + echo "✓ Correctly failed on count=0" + validate-structure: name: Validate Project Structure runs-on: ubuntu-latest diff --git a/CLAUDE.md b/CLAUDE.md index b6ffcc6..eff20b5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -117,7 +117,11 @@ The server image is `ghcr.io/azure/aks-traffic-ingress-competitive-testing` (a G ### CI (validate.yaml) -The validation workflow runs on PRs to main. The `test-scenarios` job uses a matrix over `traffic: [ingress, gateway]` × `scenario: [basic-rps, restarting-backend-rps]` × `variant: [default]` — each combination gets its own runner and Kind cluster. An additional `scheduling-e2e` variant tests pod placement with node selectors and tolerations on a multi-node Kind cluster. The `test-merge` job validates multi-pod merge by running 4 simultaneous vegeta attacks against a Docker server and verifying the merged output (RPS totals, code histograms, JSON structure). Other jobs: module tests (matrix over discovered modules), chart validation (including scheduling render checks), and project structure validation. +The validation workflow runs on PRs to main. The `test-scenarios` job uses a matrix over `traffic: [ingress, gateway]` × `scenario: [basic-rps, restarting-backend-rps]` × `variant: [default]` — each combination gets its own runner and Kind cluster. An additional `scheduling-e2e` variant tests pod placement with node selectors and tolerations on a multi-node Kind cluster. The `test-merge` job validates multi-pod merge by running 4 simultaneous vegeta attacks against a Docker server and verifying the merged output (RPS totals, code histograms, JSON structure). The `test-dns-resources` job runs a matrix over `resource: [ingresses, httproutes]`, deploys the server chart to namespace `server`, and exercises the setup/cleanup DNS scripts with positive (count=5, asserting created/deleted via `kubectl get -n server`) and negative paths (missing service, invalid domain, count=0). Because the chart deploys into namespace `server`, all DNS script invocations must pass `--namespace server`. Other jobs: module tests (matrix over discovered modules), chart validation (including scheduling render checks), and project structure validation. + +### DNS test scripts + +`scripts/setup/dns-ingresses.sh`, `scripts/setup/dns-httproutes.sh`, `scripts/cleanup/dns-ingresses.sh`, and `scripts/cleanup/dns-httproutes.sh` bulk-create or delete N `Ingress` / `HTTPRoute` resources (each with a unique `test-{i}.{domain}` hostname, all labeled `dns-test=true`) for external-DNS reconciliation testing in downstream telescope pipelines (app-routing-nginx, app-routing-istio). They are intentionally **not** wired into `master.sh` — that script remains RPS-focused. The httproutes setup script requires an existing parent `Gateway`; the ingresses setup script requires an existing backend `Service`. Defaults: namespace `default`, service/gateway `server`, port `8080`. Cleanup uses label selector `dns-test=true` with `--wait=false --ignore-not-found` and leaves the parent Gateway intact. ## Conventions diff --git a/Dockerfile b/Dockerfile index 09bdf18..7489698 100644 --- a/Dockerfile +++ b/Dockerfile @@ -103,6 +103,7 @@ RUN printf '%s\n' \ ' echo " scenario/ [args...] Run a scenario (e.g. scenario/basic_rps)"' \ ' echo " install/ [args...] Run an install script (e.g. install/nginx)"' \ ' echo " setup/ [args...] Run a setup script (e.g. setup/ingress)"' \ + ' echo " cleanup/ [args...] Run a cleanup script (e.g. cleanup/dns-ingresses)"' \ ' echo " module// [args...] Run a module script (e.g. module/vegeta/run)"' \ ' echo " merge [args...] Merge vegeta .bin files (modules/vegeta/merge/merge.sh)"' \ ' echo " server Start the HTTP server"' \ @@ -155,6 +156,17 @@ RUN printf '%s\n' \ ' ls /app/scripts/setup/*.sh 2>/dev/null | sed "s|/app/scripts/setup/||;s|\.sh||" | sed "s|^| |"' \ ' exit 1' \ ' fi' \ + 'elif [[ "$1" == cleanup/* ]]; then' \ + ' name="${1#cleanup/}"' \ + ' shift' \ + ' if [ -f "/app/scripts/cleanup/${name}.sh" ]; then' \ + ' exec bash "/app/scripts/cleanup/${name}.sh" "$@"' \ + ' else' \ + ' echo "ERROR: Unknown cleanup script: ${name}"' \ + ' echo "Available cleanup scripts:"' \ + ' ls /app/scripts/cleanup/*.sh 2>/dev/null | sed "s|/app/scripts/cleanup/||;s|\.sh||" | sed "s|^| |"' \ + ' exit 1' \ + ' fi' \ 'elif [[ "$1" == module/* ]]; then' \ ' path="${1#module/}"' \ ' module_name="${path%%/*}"' \ diff --git a/README.md b/README.md index 924f365..70c4032 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,12 @@ docker run scenario/basic_rps --ingress-url http://localhost:8080 --rate docker run install/nginx docker run setup/ingress --ingress-class nginx --replica-count 3 +# Bulk-create dns-test Ingresses or HTTPRoutes (for external-DNS testing) +docker run setup/dns-ingresses --count 100 --domain extdns.telescope.test +docker run setup/dns-httproutes --count 100 --domain extdns.telescope.test +docker run cleanup/dns-ingresses +docker run cleanup/dns-httproutes + # Run module scripts docker run module/vegeta/install docker run module/vegeta/run --target-url http://localhost:8080 --rate 50 --duration 30s @@ -153,6 +159,7 @@ Note: all modules expect to be **run from the root directory of this project**. - `/install` — traffic controller install scripts (`nginx.sh`, `istio.sh`) - `/setup` — server deployment scripts with readiness checks (`ingress.sh`, `gateway.sh`) - `/scenarios` — load test scenario scripts. These assume the cluster, traffic controller, and server are already running. Their output is JSON so that consumers can decide on the final display format themselves. +- `/setup/dns-ingresses.sh`, `/setup/dns-httproutes.sh` — bulk-create N `Ingress` or Gateway API `HTTPRoute` resources (each with a unique `test-{i}.{domain}` hostname, all labeled `dns-test=true`) for external-DNS reconciliation testing. Paired with `/cleanup/dns-ingresses.sh` and `/cleanup/dns-httproutes.sh`, which delete by label. ### /server diff --git a/scripts/cleanup/dns-httproutes.sh b/scripts/cleanup/dns-httproutes.sh new file mode 100755 index 0000000..5f1d34e --- /dev/null +++ b/scripts/cleanup/dns-httproutes.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +# Script to delete all HTTPRoute resources created by setup/dns-httproutes. +# Matches by label: dns-test=true. Does NOT touch the parent Gateway. +# +# This script expects to be run from the root directory of this project. + +set -eo pipefail + +show_usage() { + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Deletes all dns-test HTTPRoutes (label dns-test=true) in the namespace." + echo "The parent Gateway is left intact." + echo "" + echo "Options:" + echo " --namespace Kubernetes namespace (default: default)" + echo " -h, --help Show this help message" + exit 1 +} + +NAMESPACE="default" + +while [[ $# -gt 0 ]]; do + case "$1" in + --namespace) NAMESPACE="$2"; shift 2 ;; + -h|--help) show_usage ;; + *) + echo "Error: Unknown option: $1" + show_usage + ;; + esac +done + +LABEL="dns-test=true" + +echo "Deleting dns-test HTTPRoutes in namespace $NAMESPACE..." +kubectl delete httproute.gateway.networking.k8s.io -n "$NAMESPACE" -l "$LABEL" --wait=false --ignore-not-found + +echo "Delete request submitted (--wait=false). Gateway left intact." diff --git a/scripts/cleanup/dns-ingresses.sh b/scripts/cleanup/dns-ingresses.sh new file mode 100755 index 0000000..37e13c9 --- /dev/null +++ b/scripts/cleanup/dns-ingresses.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# Script to delete all Ingress resources created by setup/dns-ingresses. +# Matches by label: dns-test=true +# +# This script expects to be run from the root directory of this project. + +set -eo pipefail + +show_usage() { + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Deletes all dns-test ingresses (label dns-test=true) in the namespace." + echo "" + echo "Options:" + echo " --namespace Kubernetes namespace (default: default)" + echo " -h, --help Show this help message" + exit 1 +} + +NAMESPACE="default" + +while [[ $# -gt 0 ]]; do + case "$1" in + --namespace) NAMESPACE="$2"; shift 2 ;; + -h|--help) show_usage ;; + *) + echo "Error: Unknown option: $1" + show_usage + ;; + esac +done + +LABEL="dns-test=true" + +echo "Deleting dns-test ingresses in namespace $NAMESPACE..." +kubectl delete ingress -n "$NAMESPACE" -l "$LABEL" --wait=false --ignore-not-found + +echo "Delete request submitted (--wait=false)." diff --git a/scripts/setup/dns-httproutes.sh b/scripts/setup/dns-httproutes.sh new file mode 100755 index 0000000..60b5451 --- /dev/null +++ b/scripts/setup/dns-httproutes.sh @@ -0,0 +1,120 @@ +#!/bin/bash + +# Script to create N Gateway API HTTPRoute resources, each with a unique hostname, +# all attached to an existing Gateway and routed to the existing backend service. +# Used to test external-dns record reconciliation at scale. +# +# This script expects to be run from the root directory of this project. + +set -eo pipefail + +show_usage() { + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Creates N HTTPRoute resources with hostnames test-{i}.{domain} attached" + echo "to an existing Gateway. All resources are labeled dns-test=true." + echo "" + echo "Options:" + echo " --count (required) Number of HTTPRoutes to create" + echo " --domain (required) DNS zone domain (e.g. extdns.telescope.test)" + echo " --namespace Kubernetes namespace (default: default)" + echo " --gateway Parent Gateway name (default: server)" + echo " --gateway-section-name Parent Gateway listener section name (default: http)" + echo " --service-name Backend service name (default: server)" + echo " --service-port Backend service port (default: 8080)" + echo " -h, --help Show this help message" + exit 1 +} + +COUNT="" +DOMAIN="" +NAMESPACE="default" +GATEWAY="server" +GATEWAY_SECTION_NAME="http" +SERVICE_NAME="server" +SERVICE_PORT="8080" + +while [[ $# -gt 0 ]]; do + case "$1" in + --count) COUNT="$2"; shift 2 ;; + --domain) DOMAIN="$2"; shift 2 ;; + --namespace) NAMESPACE="$2"; shift 2 ;; + --gateway) GATEWAY="$2"; shift 2 ;; + --gateway-section-name) GATEWAY_SECTION_NAME="$2"; shift 2 ;; + --service-name) SERVICE_NAME="$2"; shift 2 ;; + --service-port) SERVICE_PORT="$2"; shift 2 ;; + -h|--help) show_usage ;; + *) + echo "Error: Unknown option: $1" + show_usage + ;; + esac +done + +if [ -z "$COUNT" ] || [ -z "$DOMAIN" ]; then + echo "Error: --count and --domain are required" + show_usage +fi + +if ! [[ "$COUNT" =~ ^[1-9][0-9]*$ ]]; then + echo "Error: --count must be a positive integer (>= 1)" + exit 1 +fi + +DNS_LABEL='[a-z0-9]([-a-z0-9]*[a-z0-9])?' +if ! [[ "$DOMAIN" =~ ^${DNS_LABEL}(\.${DNS_LABEL})*$ ]]; then + echo "Error: --domain '$DOMAIN' is not a valid DNS subdomain (RFC 1123)" + exit 1 +fi + +if ! kubectl get gateway.gateway.networking.k8s.io "$GATEWAY" -n "$NAMESPACE" >/dev/null 2>&1; then + echo "Error: Gateway '$GATEWAY' not found in namespace '$NAMESPACE'" + exit 1 +fi + +if ! kubectl get service "$SERVICE_NAME" -n "$NAMESPACE" >/dev/null 2>&1; then + echo "Error: Service '$SERVICE_NAME' not found in namespace '$NAMESPACE'" + exit 1 +fi + +echo "Creating $COUNT dns-test HTTPRoutes:" +echo " Domain: $DOMAIN" +echo " Namespace: $NAMESPACE" +echo " Gateway: $GATEWAY (section: $GATEWAY_SECTION_NAME)" +echo " Service: $SERVICE_NAME:$SERVICE_PORT" + +MANIFEST=$( + for i in $(seq 1 "$COUNT"); do + if [ "$i" -gt 1 ]; then + printf '%s\n' '---' + fi + cat < (required) Number of ingresses to create" + echo " --domain (required) DNS zone domain (e.g. extdns.telescope.test)" + echo " --namespace Kubernetes namespace (default: default)" + echo " --ingress-class Ingress class (default: webapprouting.kubernetes.azure.com)" + echo " --service-name Backend service name (default: server)" + echo " --service-port Backend service port (default: 8080)" + echo " -h, --help Show this help message" + exit 1 +} + +COUNT="" +DOMAIN="" +NAMESPACE="default" +INGRESS_CLASS="webapprouting.kubernetes.azure.com" +SERVICE_NAME="server" +SERVICE_PORT="8080" + +while [[ $# -gt 0 ]]; do + case "$1" in + --count) COUNT="$2"; shift 2 ;; + --domain) DOMAIN="$2"; shift 2 ;; + --namespace) NAMESPACE="$2"; shift 2 ;; + --ingress-class) INGRESS_CLASS="$2"; shift 2 ;; + --service-name) SERVICE_NAME="$2"; shift 2 ;; + --service-port) SERVICE_PORT="$2"; shift 2 ;; + -h|--help) show_usage ;; + *) + echo "Error: Unknown option: $1" + show_usage + ;; + esac +done + +if [ -z "$COUNT" ] || [ -z "$DOMAIN" ]; then + echo "Error: --count and --domain are required" + show_usage +fi + +if ! [[ "$COUNT" =~ ^[1-9][0-9]*$ ]]; then + echo "Error: --count must be a positive integer (>= 1)" + exit 1 +fi + +# RFC 1123 DNS subdomain: labels of [a-z0-9]([a-z0-9-]*[a-z0-9])?, separated by dots. +DNS_LABEL='[a-z0-9]([-a-z0-9]*[a-z0-9])?' +if ! [[ "$DOMAIN" =~ ^${DNS_LABEL}(\.${DNS_LABEL})*$ ]]; then + echo "Error: --domain '$DOMAIN' is not a valid DNS subdomain (RFC 1123)" + exit 1 +fi + +if ! kubectl get service "$SERVICE_NAME" -n "$NAMESPACE" >/dev/null 2>&1; then + echo "Error: Service '$SERVICE_NAME' not found in namespace '$NAMESPACE'" + exit 1 +fi + +echo "Creating $COUNT dns-test ingresses:" +echo " Domain: $DOMAIN" +echo " Namespace: $NAMESPACE" +echo " Ingress Class: $INGRESS_CLASS" +echo " Service: $SERVICE_NAME:$SERVICE_PORT" + +MANIFEST=$( + for i in $(seq 1 "$COUNT"); do + if [ "$i" -gt 1 ]; then + printf '%s\n' '---' + fi + cat <