From ff8ffed7918520a04431bbb810326047dd2ac774 Mon Sep 17 00:00:00 2001 From: Evan Vetere Date: Wed, 24 Jun 2026 15:07:59 -0400 Subject: [PATCH] test(e2e): reproduce #219 hostname-less listener collision Add a chainsaw e2e test that reproduces issue #219: a tenant Gateway whose listeners use 80/HTTP or 443/HTTPS without a hostname is rejected at admission with the misleading upstream CEL message "Combination of port, protocol and hostname must be unique for each listener". The test is a pure admission-time reproduction on the control-plane cluster (no downstream wiring), driving the failure exactly as the issue did via kubectl apply --dry-run=server. Probes mirror the issue matrix: B (decisive) 1 listener 80/HTTP no hostname -> rejected A 2 listeners 80+443 no hostname -> rejected D (control) 1 listener 80/HTTP hostname -> accepted Probe B is decisive: a single listener cannot collide with itself, so its rejection isolates the injected default-listener tuple collision as the cause. Probe D confirms a distinguishing hostname is admitted. Once #220 merges, NSO's defaulting webhook rejects A/B first with a clear hostname-required message, so the expected message here will change. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../chainsaw-test.yaml | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 test/e2e/gateway-listener-hostname-collision/chainsaw-test.yaml diff --git a/test/e2e/gateway-listener-hostname-collision/chainsaw-test.yaml b/test/e2e/gateway-listener-hostname-collision/chainsaw-test.yaml new file mode 100644 index 0000000..2ef818e --- /dev/null +++ b/test/e2e/gateway-listener-hostname-collision/chainsaw-test.yaml @@ -0,0 +1,183 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json +# +# Issue #219 — a tenant Gateway whose listeners use the default ports/protocols +# (80/HTTP, 443/HTTPS) WITHOUT a hostname is rejected at admission with the +# misleading upstream message: +# +# Combination of port, protocol and hostname must be unique for each listener +# +# The spec is valid per upstream Gateway API (listener hostname is optional). +# The rejection comes from NSO's default-listener injection: the mutating +# webhook unconditionally appends hostname-less default-http (80/HTTP) and +# default-https (443/HTTPS) listeners. When a tenant defines their own 80/HTTP +# or 443/HTTPS listener and omits the hostname, defaulting produces duplicate +# (port, protocol, hostname=empty) tuples, and the upstream CRD CEL rejects the +# whole object before NSO's own validating webhook can surface a clear reason. +# +# This is a pure admission-time reproduction: the rejection happens on the +# control-plane (nso-standard) cluster, so no downstream/edge wiring is needed. +# We drive it exactly the way the issue did — kubectl apply --dry-run=server — +# which runs the mutating webhook + CRD CEL without persisting anything. +# +# The probes mirror the issue's investigation matrix: +# B (decisive) — 1 listener, 80/HTTP, no hostname -> rejected +# A — 2 listeners, 80/HTTP + 443/HTTPS, no hostname -> rejected +# D (control) — 1 listener, 80/HTTP, WITH a hostname -> accepted +# +# Probe B is decisive: a single tenant listener cannot collide with itself, yet +# it is rejected — proving the failure is the injected-default collision, not +# tenant-side duplication. Probe D is the positive control: adding a hostname +# makes the tuple distinct from the injected defaults and the object is admitted. +# +# Run against unfixed code (main / this branch), all three probes pass: the bug +# reproduces. Once datum-cloud/network-services-operator#220 merges, probes A/B +# are rejected by NSO's defaulting webhook FIRST, with a clear hostname-required +# message instead of "must be unique" — so this test's expected message will +# change with the fix (see the grep in each probe). +# +# concurrent: false — creating the shared managed GatewayClass coexists with the +# other gateway e2e tests via apply (not create), but keep it serialized to +# avoid GatewayClass churn racing their admission paths. +apiVersion: chainsaw.kyverno.io/v1alpha1 +kind: Test +metadata: + name: gateway-listener-hostname-collision +spec: + concurrent: false + bindings: + - name: gatewayClassName + value: (join('-', ['e2e', $namespace])) + cluster: nso-standard + steps: + - name: Create a managed GatewayClass + description: | + The Gateway webhooks only fire for a GatewayClass whose controllerName + matches the operator's managed controller. Without this, shouldProcess() + short-circuits and neither defaulting nor validation runs. + try: + - apply: + cluster: nso-standard + resource: + apiVersion: gateway.networking.k8s.io/v1 + kind: GatewayClass + metadata: + name: ($gatewayClassName) + spec: + controllerName: gateway.networking.datumapis.com/external-global-proxy-controller + + - name: Probe B (decisive) — single hostname-less 80/HTTP listener is rejected + description: | + A single tenant listener cannot collide with itself. Its rejection with + "must be unique" proves the injected default-http (80/HTTP, no hostname) + is what duplicates the tuple. + try: + - script: + cluster: nso-standard + env: + - name: GATEWAY_CLASS + value: ($gatewayClassName) + content: | + set -u + out=$(kubectl apply --dry-run=server -f - 2>&1 <&1 <&1 <