Skip to content

Commit c58d58e

Browse files
committed
Merge remote-tracking branch 'origin/main' into codex/fix-release-workflow
# Conflicts: # scripts/ci/ci_assets_test.go # test/e2e/smoke/run.sh
2 parents 7904295 + e3df1ff commit c58d58e

5 files changed

Lines changed: 144 additions & 41 deletions

File tree

deploy/README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,11 @@ The two most important dynamic Services currently are:
145145

146146
Notes:
147147

148-
- `nantian-gw-dataplane` is the shared frontend entry; current Kind smoke defaults to using it.
148+
- `nantian-gw-dataplane` is the shared frontend entry, mainly useful for shared-node exposure patterns such as Kind NodePort access.
149149
- `nantian-gw-<gatewayName>` is a dedicated Service derived from the Gateway name; for example, if the `Gateway` is named `edge`, the corresponding Service is typically `nantian-gw-edge`.
150150
- per-Gateway Services default to `ClusterIP`, but can be lowered to `NodePort` or `LoadBalancer` via `Gateway.spec.infrastructure.parametersRef`, making them the more natural north-south exposure point in long-term environments.
151151
- These objects are maintained by the control plane reconcile loop and are not static install assets from `deploy/kubernetes/base/`.
152+
- Because these derived frontend Services are backed by controller-managed EndpointSlices rather than a pod selector, `kubectl port-forward service/...` is not the right validation path for smoke tests; use real Service traffic from inside the cluster or a normal client entry point instead.
152153

153154
## Traffic Entry Relationships
154155

@@ -179,7 +180,8 @@ The most easily confused aspect of the current repository is that the entries se
179180

180181
| Scenario | Default Entry | Notes |
181182
| --- | --- | --- |
182-
| Kind / smoke | `nantian-gw-dataplane` | Shared dataplane Service uses `NodePort`; Kind overlay maps HTTP `18080`, HTTPS/TLS `18443`, UDP `5300` / `5301`, TCPRoute `19000` / `19001` by default |
183+
| Kind shared-node access | `nantian-gw-dataplane` | Shared dataplane Service uses `NodePort`; Kind overlay maps HTTP `18080`, HTTPS/TLS `18443`, UDP `5300` / `5301`, TCPRoute `19000` / `19001` by default |
184+
| Smoke validation | `nantian-gw-<gatewayName>` | Current smoke validates the derived per-Gateway Service from inside the cluster so selectorless frontend Services are exercised through normal Service routing |
183185
| Long-term / production | `nantian-gw-<gatewayName>` | Better to expose per Gateway individually, then control `ClusterIP` / `NodePort` / `LoadBalancer` via `parametersRef` |
184186

185187
So if you see locally:

internal/grpcserver/snapshot_projection.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ func projectListeners(listeners []ir.Listener, snapshot *ir.Snapshot) []ir.Liste
157157
attachedRoutes = append(attachedRoutes, routeKey)
158158
}
159159
}
160-
if len(attachedRoutes) == 0 {
160+
if len(attachedRoutes) == 0 && !isGatewayProjectedListener(listener) {
161161
continue
162162
}
163163
listener.AttachedRoutes = attachedRoutes
@@ -166,6 +166,13 @@ func projectListeners(listeners []ir.Listener, snapshot *ir.Snapshot) []ir.Liste
166166
return out
167167
}
168168

169+
func isGatewayProjectedListener(listener ir.Listener) bool {
170+
if listener.Metadata == nil {
171+
return false
172+
}
173+
return listener.Metadata["gateway"] != "" && listener.Metadata["namespace"] != ""
174+
}
175+
169176
func filterBackendRefs(routeNamespace string, refs []ir.BackendRef, survivingBackends map[string]struct{}) []ir.BackendRef {
170177
out := make([]ir.BackendRef, 0, len(refs))
171178
for _, ref := range refs {

internal/grpcserver/snapshot_projection_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,44 @@ func TestProjectedSnapshotFullProfilePreservesAIServiceTokenPolicyWasmAndLabels(
135135
}
136136
}
137137

138+
func TestProjectedSnapshotPreservesListenerWhenAllAttachedRoutesArePruned(t *testing.T) {
139+
t.Parallel()
140+
141+
projected := buildProjectedProtoSnapshot(
142+
&ir.Snapshot{
143+
Listeners: []ir.Listener{{
144+
Name: "listener-empty-after-projection",
145+
Address: "0.0.0.0",
146+
Port: 80,
147+
Protocol: "HTTP",
148+
AttachedRoutes: []string{"default/http-missing"},
149+
Metadata: map[string]string{
150+
"gateway": "edge",
151+
"namespace": "default",
152+
},
153+
}},
154+
HTTPRoutes: []ir.HTTPRoute{{
155+
Name: "http-missing",
156+
Namespace: "default",
157+
Rules: []ir.HTTPRule{{
158+
BackendRefs: []ir.BackendRef{{
159+
Name: "missing-backend",
160+
Namespace: "default",
161+
Port: 8080,
162+
}},
163+
}},
164+
}},
165+
},
166+
effectiveProjectionProfile([]string{featureCoreV1}),
167+
slog.New(slog.NewTextHandler(io.Discard, nil)),
168+
)
169+
170+
listener := findProjectedListener(t, projected, "listener-empty-after-projection")
171+
if got := listener.GetAttachedRoutes(); len(got) != 0 {
172+
t.Fatalf("attached routes = %#v, want empty list", got)
173+
}
174+
}
175+
138176
func projectionTestSnapshot() *ir.Snapshot {
139177
return &ir.Snapshot{
140178
ID: "projection-snapshot",

scripts/ci/ci_assets_test.go

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -252,14 +252,18 @@ func TestCollectKindDiagnosticsCapturesFrontendTopology(t *testing.T) {
252252
}
253253
}
254254

255-
func TestSmokeScriptForwardsToProgrammedGatewayListener(t *testing.T) {
255+
func TestSmokeScriptProbesDerivedGatewayServiceFromInsideCluster(t *testing.T) {
256256
contents := string(readFile(t, repoPath("test", "e2e", "smoke", "run.sh")))
257257

258258
for _, want := range []string{
259-
`GATEWAY_HOST="${GATEWAY_HOST:-127.0.0.1}"`,
260-
`GATEWAY_HTTP_PORT="${GATEWAY_HTTP_PORT:-80}"`,
261-
`wait_for_gateway_programmed`,
262-
`http://${GATEWAY_HOST}:${GATEWAY_HTTP_PORT}/echo`,
259+
`GATEWAY_SERVICE="nantian-gw-$GATEWAY_NAME"`,
260+
`SMOKE_CLIENT_POD="smoke-client"`,
261+
`SMOKE_URL="http://${GATEWAY_SERVICE}.${CONTROL_PLANE_NS}.svc.cluster.local/echo"`,
262+
`kubectl get service -n "$CONTROL_PLANE_NS" "$GATEWAY_SERVICE"`,
263+
`kubectl get endpointslice -n "$CONTROL_PLANE_NS"`,
264+
`kubernetes.io/service-name=$GATEWAY_SERVICE`,
265+
`wget -q -T "$request_timeout" -O - "$SMOKE_URL"`,
266+
`last_request_error`,
263267
`request_deadline=`,
264268
} {
265269
if !strings.Contains(contents, want) {
@@ -268,16 +272,15 @@ func TestSmokeScriptForwardsToProgrammedGatewayListener(t *testing.T) {
268272
}
269273

270274
for _, unwanted := range []string{
271-
`LOCAL_HTTP_PORT="${LOCAL_HTTP_PORT:-10080}"`,
272-
`service/$DATA_PLANE_SVC`,
273275
`kubectl port-forward`,
276+
`service/$DATA_PLANE_SVC`,
274277
`pod/$dataplane_pod`,
275278
`dataplane_pod=$(kubectl get pod`,
276-
`port-forward to $dataplane_pod exited before request succeeded`,
277-
`10080:10080`,
279+
`curl -s -o /dev/null -w "%{http_code}"`,
280+
`wget -q -O - "$SMOKE_URL" >/dev/null 2>&1`,
278281
} {
279282
if strings.Contains(contents, unwanted) {
280-
t.Fatalf("smoke script still contains stale pod-forward pattern %q", unwanted)
283+
t.Fatalf("smoke script still contains stale host-probe pattern %q", unwanted)
281284
}
282285
}
283286
}
@@ -295,6 +298,7 @@ func TestCIEntrypointsUseCurrentDeployResourceNames(t *testing.T) {
295298
for _, want := range []string{
296299
`GATEWAY_CLASS_NAME="${GATEWAY_CLASS_NAME:-nantian-gw}"`,
297300
`CONTROL_PLANE_DEPLOYMENT="nantian-gw-controlplane"`,
301+
`DATA_PLANE_SELECTOR="app=nantian-gw-dataplane"`,
298302
`gatewayClassName: $GATEWAY_CLASS_NAME`,
299303
} {
300304
if !strings.Contains(smokeScript, want) {

test/e2e/smoke/run.sh

Lines changed: 80 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@ CLUSTER_NAME="${CLUSTER_NAME:-nantian-e2e}"
1010
CONTROL_PLANE_NS="nantian-gw"
1111
TEST_NS="nantian-e2e"
1212
CONTROL_PLANE_DEPLOYMENT="nantian-gw-controlplane"
13+
DATA_PLANE_SELECTOR="app=nantian-gw-dataplane"
1314
GATEWAY_CLASS_NAME="${GATEWAY_CLASS_NAME:-nantian-gw}"
14-
GATEWAY_HOST="${GATEWAY_HOST:-127.0.0.1}"
15-
GATEWAY_HTTP_PORT="${GATEWAY_HTTP_PORT:-80}"
15+
GATEWAY_NAME="${GATEWAY_NAME:-nantian-gw}"
16+
GATEWAY_SERVICE="nantian-gw-$GATEWAY_NAME"
17+
SMOKE_CLIENT_POD="smoke-client"
18+
SMOKE_CLIENT_IMAGE="${SMOKE_CLIENT_IMAGE:-docker.io/busybox:1.36.1}"
19+
SMOKE_URL="http://${GATEWAY_SERVICE}.${CONTROL_PLANE_NS}.svc.cluster.local/echo"
20+
ECHO_PORT=8080
1621
TIMEOUT="${TIMEOUT:-180}"
1722
KIND_CONFIG="${KIND_CONFIG:-$GATEWAY_ROOT/scripts/ci/kind-ci-config.yaml}"
1823
CLEANUP="true"
@@ -143,7 +148,7 @@ create_gateway() {
143148
apiVersion: gateway.networking.k8s.io/v1
144149
kind: Gateway
145150
metadata:
146-
name: nantian-gw
151+
name: $GATEWAY_NAME
147152
spec:
148153
gatewayClassName: $GATEWAY_CLASS_NAME
149154
listeners:
@@ -190,7 +195,7 @@ metadata:
190195
name: echo
191196
spec:
192197
parentRefs:
193-
- name: nantian-gw
198+
- name: $GATEWAY_NAME
194199
rules:
195200
- matches:
196201
- path:
@@ -207,53 +212,101 @@ YAML
207212
green " HTTPRoute created"
208213
}
209214

210-
wait_for_gateway_programmed() {
211-
local gateway_name="${1:-nantian-gw}"
215+
ensure_smoke_client() {
216+
kubectl apply -n "$TEST_NS" -f - <<YAML
217+
apiVersion: v1
218+
kind: Pod
219+
metadata:
220+
name: $SMOKE_CLIENT_POD
221+
spec:
222+
restartPolicy: Always
223+
containers:
224+
- name: client
225+
image: $SMOKE_CLIENT_IMAGE
226+
command:
227+
- sh
228+
- -c
229+
- sleep 3600
230+
YAML
212231

213-
echo "=== Waiting for Gateway ${gateway_name} to be Programmed ==="
214-
local request_deadline=$((SECONDS + TIMEOUT))
215-
while (( SECONDS < request_deadline )); do
216-
local programmed
217-
programmed="$(
218-
kubectl get gateway "$gateway_name" -n "$CONTROL_PLANE_NS" \
219-
-o jsonpath='{.status.conditions[?(@.type=="Programmed")].status}' 2>/dev/null || true
220-
)"
221-
if [[ "$programmed" == "True" ]]; then
222-
green " Gateway ${gateway_name} is Programmed"
232+
kubectl wait --for=condition=ready pod/$SMOKE_CLIENT_POD -n "$TEST_NS" --timeout="${TIMEOUT}s"
233+
}
234+
235+
gateway_service_exists() {
236+
kubectl get service -n "$CONTROL_PLANE_NS" "$GATEWAY_SERVICE" >/dev/null 2>&1
237+
}
238+
239+
gateway_frontend_endpoints_ready() {
240+
kubectl get endpointslice -n "$CONTROL_PLANE_NS" \
241+
-l "kubernetes.io/service-name=$GATEWAY_SERVICE" \
242+
-o jsonpath='{range .items[*].endpoints[*]}{range .addresses[*]}{.}{" "}{end}{.conditions.ready}{"\n"}{end}' \
243+
2>/dev/null \
244+
| awk 'NF >= 1 && $NF != "false" {found=1} END {exit(found ? 0 : 1)}'
245+
}
246+
247+
wait_for_gateway_frontend_endpoints() {
248+
local deadline="$1"
249+
250+
while (( SECONDS < deadline )); do
251+
if ! gateway_service_exists; then
252+
sleep 2
253+
continue
254+
fi
255+
256+
if gateway_frontend_endpoints_ready; then
223257
return 0
224258
fi
259+
225260
sleep 2
226261
done
227262

228-
fail "Gateway ${gateway_name} did not become Programmed=True within ${TIMEOUT}s"
229263
return 1
230264
}
231265

232-
# ── Step 6: send request through the Kind host port entry ──
266+
# ── Step 6: probe the derived Gateway Service from inside the cluster ──
233267
send_request() {
234-
local endpoint="http://${GATEWAY_HOST}:${GATEWAY_HTTP_PORT}/echo"
268+
echo "=== Sending test request via derived Gateway Service ($SMOKE_URL) ==="
269+
270+
ensure_smoke_client
235271

236-
echo "=== Sending test request (${endpoint}) ==="
237272
local request_deadline=$((SECONDS + TIMEOUT))
238-
local response="000"
273+
local request_timeout="${SMOKE_REQUEST_TIMEOUT_SEC:-5}"
274+
local last_request_error=""
275+
local output=""
276+
277+
if ! wait_for_gateway_frontend_endpoints "$request_deadline"; then
278+
if ! gateway_service_exists; then
279+
fail "derived Gateway Service $GATEWAY_SERVICE was not created within ${TIMEOUT}s"
280+
return 1
281+
fi
282+
283+
fail "derived Gateway Service $GATEWAY_SERVICE did not expose ready frontend endpoints within ${TIMEOUT}s"
284+
return 1
285+
fi
286+
239287
while (( SECONDS < request_deadline )); do
240-
response=$(curl -s -o /dev/null -w "%{http_code}" "$endpoint" 2>/dev/null || echo "000")
241-
if [[ "$response" == "200" ]]; then
242-
green " PASS: GET /echo -> HTTP $response"
288+
if output="$(kubectl exec -n "$TEST_NS" "$SMOKE_CLIENT_POD" -- \
289+
wget -q -T "$request_timeout" -O - "$SMOKE_URL" 2>&1)"; then
290+
green " PASS: GET /echo via $GATEWAY_SERVICE -> HTTP 200"
243291
return 0
244292
fi
245293

294+
last_request_error="$output"
246295
sleep 2
247296
done
248297

249-
fail "GET /echo via ${endpoint} -> HTTP $response (expected 200 within ${TIMEOUT}s)"
298+
local detail=""
299+
if [[ -n "$last_request_error" ]]; then
300+
detail=$'\nlast request error: '"$last_request_error"
301+
fi
302+
303+
fail "GET /echo via $GATEWAY_SERVICE did not succeed within ${TIMEOUT}s${detail}"
250304
return 1
251305
}
252306

253307
# ── Main ──
254308
main() {
255-
trap 'FAILED=true' ERR
256-
trap 'cleanup_cluster; if $FAILED; then red "✗ Smoke test FAILED"; else green "✓ Smoke test PASSED"; fi' EXIT
309+
trap 'exit_code=$?; if [[ "$exit_code" -ne 0 ]]; then FAILED=true; fi; cleanup_cluster; if $FAILED; then red "✗ Smoke test FAILED"; else green "✓ Smoke test PASSED"; fi; exit "$exit_code"' EXIT
257310

258311
if [[ "$BOOTSTRAP" == "true" ]]; then
259312
ensure_cluster
@@ -264,7 +317,6 @@ main() {
264317
create_gateway
265318
create_reference_grant
266319
create_route
267-
wait_for_gateway_programmed
268320
send_request
269321
}
270322

0 commit comments

Comments
 (0)