#!/usr/bin/env bash # T38 (re-run 2): Envoy는 NACK한 설정을 버리지 않고 직전 good config로 계속 서빙한다(fail-safe) # # RUN-1 LESSON: an EnvoyFilter whose typed_config @type is completely unresolvable # ("this_type_does_not_exist.v3.NoSuchFilter") is rejected by istiod's validating # admission webhook at `kubectl apply` time -- it never reaches Envoy, so the # Envoy-NACK/fail-safe scenario can't be exercised that way (verdict was inconclusive). # # RUN-2 FIX: use a *structurally valid* TypedStruct for a real, registered Envoy # extension (envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit) but with # a semantically-invalid field value (token_bucket.max_tokens = 0, which violates the # proto's "(validate.rules).uint32 = {gt: 0}" constraint). Istio's admission webhook only # checks that the type URL resolves and the JSON unmarshals into the message (a structural # check) -- it does NOT run protoc-gen-validate (PGV) semantic constraint checks, so this # passes admission. Envoy's own config ingestion DOES run PGV validation, so it NACKs the # LDS update at that point -- giving us a genuine Envoy-side NACK to observe. # # Also applies the corrected FQDN rule from harness-notes.md CORRECTION (2026-07-05): # curl uses the SHORT k8s service name (http://echo/), not the *.svc.homelab.local form # used in the original spec text (cluster DNS domain != istiod's registry domain). set -euo pipefail NS="istio-vt-t38-r2" 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} (istio-injection=enabled) ---" kubectl create namespace "${NS}" --dry-run=client -o yaml | kubectl apply -f - kubectl label namespace "${NS}" istio-injection=enabled --overwrite cd "${DIR}" echo "--- apply client+echo (manifest.yaml) ---" kubectl apply -f manifest.yaml kubectl -n "${NS}" wait --for=condition=Ready pod/client --timeout=90s kubectl -n "${NS}" rollout status deploy/echo --timeout=120s echo "--- before_nack curl (SHORT service name) ---" kubectl -n "${NS}" exec client -c curl -- curl -s -o /dev/null -w 'before_nack=%{http_code}\n' \ "http://echo/" echo "--- apply broken EnvoyFilter (client-only): LocalRateLimit max_tokens=0 ---" kubectl apply -f broken-filter-client-only.yaml echo "--- sleep 8 ---" sleep 8 echo "--- istioctl proxy-status (client row, summary) ---" istioctl proxy-status | grep -E "NAME|client\.${NS}" || true echo "--- istioctl proxy-status (per-proxy sync diff: activeState vs errorState) ---" istioctl proxy-status "client.${NS}" | head -3 || true echo "(full diff shows 'Listeners Don'\''t Match'; listener 0.0.0.0_80 has an errorState.failedConfiguration" echo " with the rejected filter + PGV error, while activeState keeps serving the pre-EnvoyFilter listener)" echo "--- istiod logs grep for filter/type name / ACK ERROR ---" kubectl -n istio-system logs deploy/istiod --since=2m \ | grep -i 'broken-filter-client-only\|local_ratelimit\|LocalRateLimit\|ACK ERROR' | tail -15 || true echo "--- after_nack curl (traffic should still be 200: Envoy kept serving last-good config) ---" kubectl -n "${NS}" exec client -c curl -- curl -s -o /dev/null -w 'after_nack=%{http_code}\n' \ "http://echo/" echo "--- delete broken EnvoyFilter ---" kubectl delete -f broken-filter-client-only.yaml --ignore-not-found echo "--- done ---"