Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 183 additions & 0 deletions test/e2e/gateway-listener-hostname-collision/chainsaw-test.yaml
Original file line number Diff line number Diff line change
@@ -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 <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: probe-b
namespace: $NAMESPACE
spec:
gatewayClassName: $GATEWAY_CLASS
listeners:
- name: http
protocol: HTTP
port: 80
EOF
) && rc=0 || rc=$?
echo "--- apply output (rc=$rc) ---"
echo "$out"
if [ "$rc" -eq 0 ]; then
echo "FAIL: hostname-less listener was admitted; issue #219 not reproduced"
exit 1
fi
echo "$out" | grep -q "Combination of port, protocol and hostname must be unique for each listener" || {
echo "FAIL: rejected, but not with the misleading 'must be unique' CEL message"
exit 1
}
echo "OK: probe B reproduced the misleading rejection"

- name: Probe A — two hostname-less listeners (80/HTTP + 443/HTTPS) are rejected
try:
- script:
cluster: nso-standard
env:
- name: GATEWAY_CLASS
value: ($gatewayClassName)
content: |
set -u
out=$(kubectl apply --dry-run=server -f - 2>&1 <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: probe-a
namespace: $NAMESPACE
spec:
gatewayClassName: $GATEWAY_CLASS
listeners:
- name: http
protocol: HTTP
port: 80
- name: https
protocol: HTTPS
port: 443
tls:
mode: Terminate
options:
gateway.networking.datumapis.com/certificate-issuer: e2e
EOF
) && rc=0 || rc=$?
echo "--- apply output (rc=$rc) ---"
echo "$out"
if [ "$rc" -eq 0 ]; then
echo "FAIL: hostname-less listeners were admitted; issue #219 not reproduced"
exit 1
fi
echo "$out" | grep -q "Combination of port, protocol and hostname must be unique for each listener" || {
echo "FAIL: rejected, but not with the misleading 'must be unique' CEL message"
exit 1
}
echo "OK: probe A reproduced the misleading rejection"

- name: Probe D (control) — single 80/HTTP listener WITH a hostname is accepted
description: |
Positive control. Giving the tenant listener a hostname makes its tuple
distinct from the hostname-less injected defaults, so the object is
admitted. This isolates the missing hostname as the cause.
try:
- script:
cluster: nso-standard
env:
- name: GATEWAY_CLASS
value: ($gatewayClassName)
content: |
set -u
out=$(kubectl apply --dry-run=server -f - 2>&1 <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: probe-d
namespace: $NAMESPACE
spec:
gatewayClassName: $GATEWAY_CLASS
listeners:
- name: http
protocol: HTTP
port: 80
hostname: probe-d.e2e.env.datum.net
EOF
) && rc=0 || rc=$?
echo "--- apply output (rc=$rc) ---"
echo "$out"
if [ "$rc" -ne 0 ]; then
echo "FAIL: a listener with a distinguishing hostname should be admitted"
exit 1
fi
echo "OK: probe D admitted — hostname makes the tuple unique"
Loading