#!/usr/bin/env bash # T27 -- Envoy admin POST /drain_listeners?graceful: "suppressor not blocker" verification. # Re-runnable end-to-end script. Namespace: istio-vt-t27 (created and torn down by this script). set -euo pipefail NS=istio-vt-t27 DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cleanup() { echo "=== cleanup: deleting namespace ${NS} ===" kubectl delete namespace "${NS}" --wait=false --ignore-not-found } trap cleanup EXIT echo "=== create namespace ${NS} with istio-injection=enabled ===" kubectl create namespace "${NS}" kubectl label namespace "${NS}" istio-injection=enabled --overwrite echo "=== apply manifest.yaml ===" kubectl apply -f "${DIR}/manifest.yaml" echo "=== wait for workloads ready ===" kubectl -n "${NS}" wait --for=condition=Ready pod/client --timeout=90s kubectl -n "${NS}" wait --for=condition=available deploy/drainbackend deploy/drainbackend-noskipexit deploy/drainbackend-inboundonly --timeout=120s echo "=== confirm 2/2 sidecar injection ===" kubectl -n "${NS}" get pods -o wide ######################################## # Phase A: in-flight protection + new-conn discourage (not block) + rq_active gauge ######################################## echo "=== PHASE A ===" ( kubectl -n "${NS}" exec client -c curl -- curl -s -o /dev/null -w 'inflight_result=%{http_code} elapsed=%{time_total}\n' \ "http://drainbackend.${NS}.svc.homelab.local/delay/8" > /tmp/t27_inflight.log 2>&1 & ) sleep 1 kubectl -n "${NS}" exec deploy/drainbackend -c istio-proxy -- curl -s localhost:15000/stats | grep 'http.inbound.*downstream_rq_active' kubectl -n "${NS}" exec deploy/drainbackend -c istio-proxy -- curl -s -X POST 'localhost:15000/drain_listeners?graceful&skip_exit' echo "--- new conn during drain (expect success; connection:close header NOT guaranteed -- see notes) ---" kubectl -n "${NS}" exec client -c curl -- curl -sv -o /dev/null "http://drainbackend.${NS}.svc.homelab.local/get" 2>&1 | grep -iE '^< |^\* Connection' || true sleep 8 echo "--- in-flight result (expect 200, elapsed ~8s) ---" cat /tmp/t27_inflight.log kubectl -n "${NS}" exec deploy/drainbackend -c istio-proxy -- curl -s localhost:15000/stats | grep 'http.inbound.*downstream_rq_active' # Supplementary: reused pre-existing keep-alive connection across the drain boundary (raw HTTP/1.1 over nc) echo "--- supplementary: pre-existing connection reused across drain boundary ---" ( kubectl -n "${NS}" exec client -c curl -- sh -c " (printf 'GET /get HTTP/1.1\r\nHost: drainbackend.${NS}.svc.homelab.local\r\nConnection: keep-alive\r\n\r\n'; sleep 6; \ printf 'GET /get HTTP/1.1\r\nHost: drainbackend.${NS}.svc.homelab.local\r\nConnection: keep-alive\r\n\r\n'; sleep 3) | nc drainbackend.${NS}.svc.homelab.local 80 " > /tmp/t27_nc.log 2>&1 & ) sleep 2 kubectl -n "${NS}" exec deploy/drainbackend -c istio-proxy -- curl -s -X POST 'localhost:15000/drain_listeners?graceful&skip_exit' sleep 9 cat /tmp/t27_nc.log ######################################## # Phase B: long-lived response -- rq_active must NOT drop to 0 merely from time passing. # NOTE: kennethreitz/httpbin's /stream/50 completes in ~8ms (not genuinely slow); use /drip instead. ######################################## echo "=== PHASE B (using /drip, not /stream/50 -- see notes in result.txt) ===" ( kubectl -n "${NS}" exec client -c curl -- curl -s -o /dev/null -w 'drip_result=%{http_code} elapsed=%{time_total}\n' \ "http://drainbackend.${NS}.svc.homelab.local/drip?duration=15&numbytes=10" > /tmp/t27_drip.log 2>&1 & ) sleep 3 kubectl -n "${NS}" exec deploy/drainbackend -c istio-proxy -- curl -s localhost:15000/stats | grep 'http.inbound.*downstream_rq_active' # expect 1 sleep 8 kubectl -n "${NS}" exec deploy/drainbackend -c istio-proxy -- curl -s localhost:15000/stats | grep 'http.inbound.*downstream_rq_active' # expect still 1 (t+11s, drip not done) sleep 8 kubectl -n "${NS}" exec deploy/drainbackend -c istio-proxy -- curl -s localhost:15000/stats | grep 'http.inbound.*downstream_rq_active' # expect 0 (t+19s, drip done) cat /tmp/t27_drip.log ######################################## # Phase C: skip_exit keeps the process alive after the drain window elapses ######################################## echo "=== PHASE C ===" kubectl -n "${NS}" get pod -l app=drainbackend -o jsonpath='{.items[0].metadata.name} restarts={.items[0].status.containerStatuses[?(@.name=="istio-proxy")].restartCount}{"\n"}' sleep 15 kubectl -n "${NS}" get pod -l app=drainbackend -o jsonpath='{.items[0].status.containerStatuses[?(@.name=="istio-proxy")].restartCount}{"\n"}' # expect unchanged (0) ######################################## # Phase D: WITHOUT skip_exit -- check whether Envoy exits itself once drain completes # (drain_time is 45s; empirically this did NOT happen even ~13 min later in the original run -- see result.txt) ######################################## echo "=== PHASE D ===" kubectl -n "${NS}" exec deploy/drainbackend-noskipexit -c istio-proxy -- curl -s -X POST 'localhost:15000/drain_listeners?graceful' sleep 50 kubectl -n "${NS}" get pod -l app=drainbackend-noskipexit -o jsonpath='{.items[0].status.containerStatuses[?(@.name=="istio-proxy")].restartCount}{"\n"}' kubectl -n "${NS}" exec deploy/drainbackend-noskipexit -c istio-proxy -- ps -ef || true ######################################## # Phase E: inboundonly semantics -- drain WITHOUT inboundonly also drains this pod's own outbound listener # (use the shared, untouched echo.istio-verify service as a clean outbound target) ######################################## echo "=== PHASE E ===" kubectl -n "${NS}" exec deploy/drainbackend-inboundonly -c istio-proxy -- curl -sv -o /dev/null --max-time 4 http://echo.istio-verify.svc.homelab.local/ 2>&1 | grep -iE '< HTTP|connection:' || true kubectl -n "${NS}" exec deploy/drainbackend-inboundonly -c istio-proxy -- curl -s -X POST 'localhost:15000/drain_listeners?graceful&skip_exit' kubectl -n "${NS}" exec deploy/drainbackend-inboundonly -c istio-proxy -- curl -sv -o /dev/null --max-time 4 http://echo.istio-verify.svc.homelab.local/ 2>&1 | grep -iE '< HTTP|connection:' || true echo "=== done ==="