reweave/test/gateway: end-to-end test of the agent's Gateways
images / images (push) Failing after 4m14s

On test ports of a held address, against a live cluster: the class and the
Gateway accepted and programmed with their routes attached, route statuses
(including an unresolved backend), a TCP listener forwarding in the kernel
to a pod on another node that sees the client's own address, HTTPS with the
listener's certificate by SNI over HTTP/2 routed by exact path, prefix with a
rewrite and default, X-Forwarded-For equal to that same client address, the
HTTP-to-HTTPS redirect, 500 for the unresolved backend and no certificate
for an unknown name, a pod inside the cluster reaching it, a renewed
certificate served without a restart, failing closed on an address nobody
holds and recovering, and the holder's rules gone with the Gateway.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHXzGrsxfkGk8LmwrBbHNZ
This commit is contained in:
levaliciousandClaude Opus 5.5 committed 2026-10-14 00:06:47 -04:00
1 parent 7405b51e53
commit e7583c4177
2 files changed
+253

No files matched your search

+112
View File
@@ -0,0 +1,112 @@
# Objects for the Gateway e2e (run.sh): two echo backends (agnhost netexec: /hostname, /clientip, /header), one on
# the holder and one elsewhere, and a Gateway of class reweave on test ports of ADDR (HTTPS with the test
# certificate, plain HTTP that redirects, and TCP straight to a backend), with its routes.
apiVersion: v1
kind: Namespace
metadata:
name: gwtest
---
apiVersion: v1
kind: Pod
metadata: {name: echo-a, namespace: gwtest, labels: {app: echo-a}}
spec:
nodeName: HOLDER
containers:
- {name: c, image: "registry.k8s.io/e2e-test-images/agnhost:2.59", args: ["netexec", "--http-port=8080"]}
---
apiVersion: v1
kind: Pod
metadata: {name: echo-b, namespace: gwtest, labels: {app: echo-b}}
spec:
nodeName: OTHER
containers:
- {name: c, image: "registry.k8s.io/e2e-test-images/agnhost:2.59", args: ["netexec", "--http-port=8080"]}
---
apiVersion: v1
kind: Pod
metadata: {name: client, namespace: gwtest}
spec:
nodeName: OTHER
containers:
- {name: c, image: "curlimages/curl:8.10.1", command: ["sleep", "3600"]}
---
apiVersion: v1
kind: Service
metadata: {name: echo-a, namespace: gwtest}
spec:
selector: {app: echo-a}
ports: [{name: http, port: 80, targetPort: 8080}]
---
apiVersion: v1
kind: Service
metadata: {name: echo-b, namespace: gwtest}
spec:
selector: {app: echo-b}
ports: [{name: http, port: 80, targetPort: 8080}]
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata: {name: gwtest, namespace: gateway-system}
spec:
gatewayClassName: reweave
addresses: [{type: IPAddress, value: ADDR}]
listeners:
- name: https
port: 18443
protocol: HTTPS
hostname: gw.test
tls: {certificateRefs: [{name: gwtest-cert}]}
allowedRoutes: {namespaces: {from: Selector, selector: {matchLabels: {kubernetes.io/metadata.name: gwtest}}}}
- name: http
port: 18080
protocol: HTTP
allowedRoutes: {namespaces: {from: All}}
- name: tcp
port: 18022
protocol: TCP
allowedRoutes: {namespaces: {from: All}}
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata: {name: main, namespace: gwtest}
spec:
parentRefs: [{name: gwtest, namespace: gateway-system, sectionName: https}]
hostnames: [gw.test]
rules:
# exact beats the default prefix "/"; /a/... reaches echo-a with /a cut off; the rest goes to echo-a as is
- matches: [{path: {type: Exact, value: /hostname}}]
backendRefs: [{name: echo-b, port: 80}]
- matches: [{path: {type: PathPrefix, value: /a}}]
backendRefs: [{name: echo-a, port: 80}]
filters:
- type: URLRewrite
urlRewrite: {path: {type: ReplacePrefixMatch, replacePrefixMatch: /}}
- backendRefs: [{name: echo-a, port: 80}]
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata: {name: broken, namespace: gwtest}
spec:
parentRefs: [{name: gwtest, namespace: gateway-system, sectionName: https}]
hostnames: [gw.test]
rules:
- matches: [{path: {type: PathPrefix, value: /broken}}]
backendRefs: [{name: missing, port: 80}]
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata: {name: redirect, namespace: gwtest}
spec:
parentRefs: [{name: gwtest, namespace: gateway-system, sectionName: http}]
rules:
- filters:
- type: RequestRedirect
requestRedirect: {scheme: https, port: 18443, statusCode: 301}
---
apiVersion: gateway.networking.k8s.io/v1
kind: TCPRoute
metadata: {name: raw, namespace: gwtest}
spec:
parentRefs: [{name: gwtest, namespace: gateway-system, sectionName: tcp}]
rules:
- backendRefs: [{name: echo-b, port: 80}]
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env bash
# End-to-end test of the agent's Gateways (class reweave) against a live cluster, on test ports of ADDR.
#
# CP="ssh ... root@<cp>" HOLDER_SSH="ssh ... root@<holder>" ADDR=<address HOLDER holds> HOLDER=<node> \
# OTHER=<node> reweave/test/gateway/run.sh
#
# The machine running this script is the outside client. Asserts, in order:
# G0 GatewayClass reweave is accepted
# G1 the Gateway is accepted and programmed at ADDR, its listeners with their routes attached
# G2 route statuses: accepted and resolved, the broken one ResolvedRefs False (BackendNotFound)
# G3 TCP listener: forwarded in the kernel to a pod on another node, which sees this client's own address
# G4 HTTPS from outside: the listener's certificate by SNI, HTTP/2, routing by exact path, by prefix with a
# rewrite, and by default; the backend sees this client (the same address as G3) in X-Forwarded-For
# G5 plain HTTP redirects to HTTPS on the listener's port, path and query kept
# G6 an unresolved backend answers 500; a name no listener has gets no certificate
# G7 a pod inside the cluster reaches the Gateway too
# G8 a renewed certificate is served without a restart
# G9 fail closed: an address nobody holds is not programmed and nothing is served; back at ADDR, it is again
# G10 deleting the Gateway removes its accept and forward rules from the holder
# Leaves nothing behind (namespace gwtest, the Gateway and its Secret are deleted, also on failure).
set -euo pipefail
: "${CP:?}" "${HOLDER_SSH:?}" "${ADDR:?}" "${HOLDER:?}" "${OTHER:?}"
here=$(cd "$(dirname "$0")" && pwd)
k() { $CP "KUBECONFIG=/etc/kubernetes/admin.conf kubectl $*"; }
kin() { $CP "KUBECONFIG=/etc/kubernetes/admin.conf kubectl $* -f -"; }
fail() { echo "FAIL: $*" >&2; exit 1; }
ok() { echo "ok: $*"; }
work=$(mktemp -d)
cleanup() {
k -n gateway-system delete gateway gwtest --ignore-not-found >/dev/null 2>&1 || true
k -n gateway-system delete secret gwtest-cert --ignore-not-found >/dev/null 2>&1 || true
k delete namespace gwtest --ignore-not-found --wait=false >/dev/null 2>&1 || true
rm -rf "$work"
}
trap cleanup EXIT
eventually() {
local n=$1; shift
for _ in $(seq "$n"); do
if "$@" >/dev/null 2>&1; then return 0; fi
sleep 1
done
return 1
}
# a test certificate for gw.test, as a TLS Secret in gateway-system
cert() {
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -days 1 -subj "/CN=gw.test" \
-addext "subjectAltName=DNS:gw.test" -keyout "$work/$1.key" -out "$work/$1.crt" 2>/dev/null
printf 'apiVersion: v1\nkind: Secret\nmetadata: {name: gwtest-cert, namespace: gateway-system}\ntype: kubernetes.io/tls\ndata:\n tls.crt: %s\n tls.key: %s\n' \
"$(base64 -w0 < "$work/$1.crt")" "$(base64 -w0 < "$work/$1.key")" | kin apply >/dev/null
}
fingerprint() { openssl x509 -noout -fingerprint -sha256 -in "$1" | cut -d= -f2; }
served() {
echo | openssl s_client -connect "$ADDR:18443" -servername gw.test 2>/dev/null |
openssl x509 -noout -fingerprint -sha256 2>/dev/null | cut -d= -f2
}
https() { curl -sk --max-time 10 --resolve "gw.test:18443:$ADDR" "$@"; }
cert v1
sed -e "s/\bADDR\b/$ADDR/g; s/\bHOLDER\b/$HOLDER/g; s/\bOTHER\b/$OTHER/g" "$here/gateway-test.yaml" | kin apply >/dev/null
k -n gwtest wait --for=condition=Ready pod --all --timeout=300s >/dev/null || fail "test pods not ready"
cond() { k get $1 -o "jsonpath='{.status.conditions[?(@.type==\"$2\")].status}/{.status.conditions[?(@.type==\"$2\")].reason}'"; }
is() { [ "$(cond "$1" "$2")" = "$3" ]; }
eventually 60 is "gatewayclass reweave" Accepted "True/Accepted" || fail "G0: GatewayClass: $(cond "gatewayclass reweave" Accepted)"
ok "G0. GatewayClass reweave accepted"
gw="-n gateway-system gateway gwtest"
eventually 90 is "$gw" Programmed "True/Programmed" || fail "G1: programmed: $(cond "$gw" Programmed)"
is "$gw" Accepted "True/Accepted" || fail "G1: accepted: $(cond "$gw" Accepted)"
[ "$(k get $gw -o "jsonpath='{.status.addresses[0].value}'")" = "$ADDR" ] || fail "G1: status address"
attached=$(k get $gw -o "jsonpath='{range .status.listeners[*]}{.name}={.attachedRoutes} {end}'")
[ "$attached" = "https=2 http=1 tcp=1 " ] || fail "G1: attached routes: '$attached'"
ok "G1. Gateway accepted and programmed at $ADDR; https=2 http=1 tcp=1 routes attached"
route() { k -n gwtest get "$1" "$2" -o "jsonpath='{.status.parents[0].conditions[?(@.type==\"$3\")].status}/{.status.parents[0].conditions[?(@.type==\"$3\")].reason}'"; }
route_is() { [ "$(route "$1" "$2" "$3")" = "$4" ]; }
for r in "httproute main" "httproute redirect" "tcproute raw" "httproute broken"; do
eventually 30 route_is $r Accepted "True/Accepted" || fail "G2: $r accepted: $(route $r Accepted)"
done
for r in "httproute main" "httproute redirect" "tcproute raw"; do
route_is $r ResolvedRefs "True/ResolvedRefs" || fail "G2: $r resolved: $(route $r ResolvedRefs)"
done
route_is httproute broken ResolvedRefs "False/BackendNotFound" || fail "G2: broken: $(route httproute broken ResolvedRefs)"
ok "G2. routes accepted and resolved; the broken one ResolvedRefs False (BackendNotFound)"
tcp_hostname() { [ "$(curl -s --max-time 5 "http://$ADDR:18022/hostname")" = echo-b ]; }
eventually 30 tcp_hostname || fail "G3: TCP listener did not reach echo-b"
me=$(curl -s --max-time 5 "http://$ADDR:18022/clientip" | sed 's/:[0-9]*$//')
case "$me" in
10.*|"") fail "G3: the backend saw '$me', not this client's own address" ;;
esac
ok "G3. TCP listener forwards to echo-b on $OTHER, which sees this client as $me"
[ "$(served)" = "$(fingerprint "$work/v1.crt")" ] || fail "G4: wrong certificate for gw.test"
[ "$(https -o /dev/null -w '%{http_version}' https://gw.test:18443/hostname)" = 2 ] || fail "G4: no HTTP/2"
[ "$(https https://gw.test:18443/hostname)" = echo-b ] || fail "G4: exact /hostname did not reach echo-b"
[ "$(https https://gw.test:18443/a/hostname)" = echo-a ] || fail "G4: prefix /a (rewritten to /) did not reach echo-a"
xff=$(https "https://gw.test:18443/header?key=X-Forwarded-For")
[ "$xff" = "$me" ] || fail "G4: X-Forwarded-For '$xff', want $me"
proto=$(https "https://gw.test:18443/header?key=X-Forwarded-Proto")
[ "$proto" = https ] || fail "G4: X-Forwarded-Proto '$proto'"
ok "G4. HTTPS: gw.test's certificate, HTTP/2, exact/prefix+rewrite/default routing; X-Forwarded-For $me"
loc=$(curl -s -o /dev/null --max-time 5 -w '%{http_code} %{redirect_url}' -H 'Host: gw.test' "http://$ADDR:18080/a/x?y=1")
[ "$loc" = "301 https://gw.test:18443/a/x?y=1" ] || fail "G5: redirect: '$loc'"
ok "G5. HTTP redirects to https://gw.test:18443, path and query kept"
[ "$(https -o /dev/null -w '%{http_code}' https://gw.test:18443/broken)" = 500 ] || fail "G6: broken backend did not answer 500"
if curl -sk --max-time 5 --resolve "other.test:18443:$ADDR" https://other.test:18443/ >/dev/null 2>&1; then
fail "G6: a name no listener has got a certificate"
fi
ok "G6. unresolved backend answers 500; a name no listener has gets no certificate"
inside=$(k -n gwtest exec client -- curl -sk --max-time 10 --resolve "gw.test:18443:$ADDR" https://gw.test:18443/hostname)
[ "$inside" = echo-b ] || fail "G7: from a pod: '$inside'"
ok "G7. a pod on $OTHER reaches the Gateway"
cert v2
renewed() { [ "$(served)" = "$(fingerprint "$work/v2.crt")" ]; }
eventually 60 renewed || fail "G8: the renewed certificate is not served"
ok "G8. renewed certificate served without a restart"
k patch $gw --type=merge -p "'{\"spec\":{\"addresses\":[{\"type\":\"IPAddress\",\"value\":\"198.51.100.7\"}]}}'" >/dev/null
eventually 60 is "$gw" Programmed "False/AddressNotUsable" || fail "G9: unheld address: $(cond "$gw" Programmed)"
gone() { ! https -o /dev/null https://gw.test:18443/hostname; }
eventually 30 gone || fail "G9: still served from $ADDR after the Gateway moved off it"
k patch $gw --type=merge -p "'{\"spec\":{\"addresses\":[{\"type\":\"IPAddress\",\"value\":\"$ADDR\"}]}}'" >/dev/null
back() { [ "$(https https://gw.test:18443/hostname)" = echo-b ]; }
eventually 60 back || fail "G9: not served again at $ADDR"
ok "G9. an address nobody holds: Programmed False (AddressNotUsable), nothing served; back at $ADDR: served"
k -n gateway-system delete gateway gwtest >/dev/null
rules_gone() {
! $HOLDER_SSH "iptables -S REWEAVE-LB-IN; iptables -t nat -S REWEAVE-LB" | grep -q 'gateway-system/gwtest\|gateway gateway-system/gwtest'
}
eventually 60 rules_gone || fail "G10: the holder still has the Gateway's rules"
ok "G10. Gateway deleted: its accept and forward rules are gone from $HOLDER"
echo PASS