This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is a load-testing framework for comparing Kubernetes ingress controllers (nginx via Ingress, Istio via Gateway API). It orchestrates: spin up a Kind cluster → install a traffic controller → deploy a backend server via Helm → run vegeta load tests → collect JSON results → tear down.
# Ingress (nginx) + basic RPS
./scripts/master.sh --traffic ingress --scenario basic-rps --rate 50 --duration 30s --output-file ./results/basic_rps.json
# Gateway (istio) + basic RPS
./scripts/master.sh --traffic gateway --scenario basic-rps --rate 50 --duration 30s --output-file ./results/gateway_basic_rps.json
# Restarting backend scenario
./scripts/master.sh --traffic ingress --scenario restarting-backend-rps --rate 50 --duration 90s --output-file ./results/restarting_backend_rps.json
# With scheduling constraints (multi-node Kind cluster)
./scripts/master.sh --traffic ingress --scenario basic-rps --kind-topology scheduling-e2e --node-selector scheduling=enabled --tolerations-file ./charts/server/ci-scheduling-values.yaml --rate 50 --duration 30s --output-file ./results/scheduling_test.jsonmaster.sh handles everything including cleanup (via EXIT trap). Requires kubectl, helm, jq, and curl on the host.
./modules/kind/test/test.sh # Creates/destroys a real Kind cluster
./modules/vegeta/test/test.sh # Starts a Docker container, runs a short vegeta attack
./modules/jplot/test/test.sh # Installs jplot (requires Go)cd server && go build -o serverhelm template server ./charts/server
helm template server ./charts/server --set ingress.enabled=true --set ingress.className=nginx
helm template server ./charts/server --set gateway.enabled=true --set gateway.className=istiobash -n scripts/master.sh
bash -n scripts/scenarios/basic_rps.sh
bash -n scripts/scenarios/restarting_backend_rps.shscripts/master.sh is the single entry point that orchestrates these steps in order:
- Kind cluster —
modules/kind/install/install.sh+modules/kind/run/run.shcreate a cluster withextraPortMappings(port 8080→80). The--kind-topologyflag controls the cluster shape:default— single control-plane node (current behavior)scheduling-e2e— control-plane + one worker node labeledscheduling=enabledand taintedscheduling=enabled:NoSchedule
- Traffic controller —
scripts/install/nginx.sh(Ingress) orscripts/install/istio.sh(Gateway API) - Server deployment —
scripts/setup/ingress.shorscripts/setup/gateway.shdeployscharts/servervia Helm with full readiness polling - URL + readiness — reads
host_portfrom Kind's statefile, constructs the URL, then curl-probes until HTTP 200 - Scenario —
scripts/scenarios/basic_rps.shorscripts/scenarios/restarting_backend_rps.shinstalls vegeta and runs load tests via the vegeta module
Each module under modules/ follows the same interface:
install/install.sh— installs the toolrun/run.sh— runs the tool (can be sourced as a library or executed directly)output/output.sh— reads from../statefile.jsonand emits resultsmerge/merge.sh— (vegeta only) merges multiple.binfiles into per-second JSON output using timestamp-based bucketingtest/test.sh— integration test
Modules communicate via statefile.json (one per module, gitignored in practice). The kind module writes {"cluster_name": "...", "host_port": "..."}. The vegeta module writes streaming jaggr JSON (one line per second of the test).
The Dockerfile entrypoint routes commands to scripts via prefix matching:
master [args...]→scripts/master.shscenario/<name> [args...]→scripts/scenarios/<name>.shinstall/<name> [args...]→scripts/install/<name>.shsetup/<name> [args...]→scripts/setup/<name>.shmodule/<name>/<action> [args...]→modules/<name>/<action>/<action>.shmerge [args...]→modules/vegeta/merge/merge.shserver→ starts the Go HTTP server
Scenarios accept CLI arguments (--ingress-url, --rate, --duration, --workers, --output-file, --request-headers). Environment variables with the same names (uppercased, underscored) are read as defaults for backward compatibility. Each scenario installs its own dependencies (vegeta).
The vegeta run pipeline streams results in real time: vegeta attack | tee statefile.bin | vegeta encode | jaggr | tee statefile.json. This saves raw binary results to .bin alongside the per-second jaggr JSON. The scenario script then copies statefile.json content to the --output-file. The result file contains one JSON line per second of the test. The CI validation sums code.hist["200"] across all lines and fails only if the total is zero (i.e., no connection was ever successfully routed).
For multi-pod tests, modules/vegeta/merge/merge.sh combines multiple .bin files into unified per-second JSON. It uses actual request timestamps for bucketing (not wall-clock time), so it correctly interleaves results from pods that started at slightly different times. The first and last second-buckets may be partial.
Single chart with two mutually exclusive traffic modes controlled by values:
ingress.enabled=true+ingress.className=nginx— creates an Ingress resourcegateway.enabled=true+gateway.className=istio— creates Gateway + HTTPRoute resourcesnodeSelector: {}+tolerations: []— optional pod scheduling values rendered only when non-empty
scripts/setup/ingress.sh and scripts/setup/gateway.sh accept --node-selector <key=value> and --tolerations-file <path> to wire these values into the Helm release. master.sh also accepts these flags and passes them through to the appropriate setup script.
--tolerations-file must point to a Helm values fragment with a top-level tolerations: key, not a bare YAML list, because the chart reads .Values.tolerations.
The server image is ghcr.io/azure/aks-traffic-ingress-competitive-testing (a Go HTTP server returning "hello world!" on /).
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.
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. Setup scripts accept --existing-n <N> to offset the index range (objects are numbered (existing-n+1)..(existing-n+count)) so additional batches can be appended without colliding with existing hostnames. Setup scripts stream the generated multi-document YAML to a unique mktemp file under $TMPDIR (path printed at the top of the run) and apply it with a single kubectl apply --server-side -f. Cleanup uses label selector dns-test=true with --wait=false --ignore-not-found and leaves the parent Gateway intact.
- All scripts expect to be run from the repository root.
- All shell scripts use
set -ex(orset -efor library-style scripts). Scripts with data-processing pipelines also useset -o pipefailso that failures in any pipeline stage propagate correctly. - The
--trafficflag acceptsingressorgateway. The--scenarioflag acceptsbasic-rpsorrestarting-backend-rps. - Releases are driven by CHANGELOG.md — the Release workflow reads it to create GitHub releases. Follow Keep a Changelog format.