Skip to content

Gateway: hostname-less tenant listeners rejected with misleading 'must be unique' (default-listener injection collision) #219

Description

@ecv

Summary

A tenant Gateway (class datum-external-global-proxy) whose listeners use the default ports/protocols (80/HTTP, 443/HTTPS) without a hostname is rejected at admission with a misleading upstream message:

Gateway.gateway.networking.k8s.io "http-gateway" is invalid: spec.listeners:
  Invalid value: 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 colliding with the tenant's listeners.

Reproduction (live, kubectl apply --dry-run=server)

# Listeners Hostname(s) Result
A 2 — 80/HTTP, 443/HTTPS none ❌ "must be unique"
B 180/HTTP none ❌ "must be unique"
C 2 — 80/HTTP, 443/HTTPS distinct
D 180/HTTP set
E 2 — 80/HTTP, 443/HTTPS same hostname

Probe B is decisive: a single tenant listener cannot collide with itself, yet it is rejected — so the failure is not tenant-side tuple duplication.

Root cause

SetDefaultListeners (internal/util/gateway/listeners.go) unconditionally appends two listeners, both without a hostname at creation:

  • default-http80 / HTTP
  • default-https443 / HTTPS

IsDefaultListener keys off the listener name (default-http / default-https) only. When a tenant defines their own 80/HTTP and/or 443/HTTPS listeners under different names and omits hostname, defaulting produces duplicate (port, protocol, hostname=∅) tuples — e.g. tenant http (80,HTTP,∅) + injected default-http (80,HTTP,∅). The upstream Gateway API CRD CEL then rejects the whole object with Combination of port, protocol and hostname must be unique for each listener.

Probes C/D/E pass because giving the tenant listeners a hostname makes their tuple distinct from the hostname-less injected defaults.

Separately, validateListeners (internal/validation/gateway_validation.go:72) already requires a hostname on non-default listeners (must be set to "…" or a custom hostname). So hostname-less tenant listeners are unsupported by design — but the tenant never sees that clear message; the injected-default collision trips the CEL first and surfaces the confusing "must be unique" error instead.

Impact

  • Misleading error: tenants see a uniqueness complaint about listeners they didn't duplicate, with no indication that defaults were injected.
  • Blocks datum-cloud/infra integration test tests/networking/http-gateway (datum-cloud/infra#2804), whose Gateway defines plain http/https listeners with no hostname.

Suggested fixes (pick per design intent)

  1. Make defaulting idempotent against tenant listeners — skip injecting default-http/default-https when a tenant listener already covers that port/protocol (dedupe by (port, protocol), not just name).
  2. Set the expected DNS hostname on the injected default listeners at creation so their tuples are always distinct.
  3. If hostname-less listeners are simply unsupported, fail fast in validateListeners with the clear hostname is required message before defaulting can produce a colliding tuple — so the surfaced error is actionable.

Refs

  • datum-cloud/infra#2804 (test failure + full investigation matrix)

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions