The gateway controller lets application charts open L4 (TCP/TLS) listeners on the shared Contour Gateway — and on the cluster VIP — purely by declaring a route in their own chart. No operator has to edit the central Contour configuration for each new port.
Foundry installs Contour with a single, statically-provisioned Envoy that holds
the cluster VIP (via externalIPs on the contour-envoy Service). Opening a new
port on that shared data plane normally requires three coordinated changes that
only an operator can make:
- add a listener to the
contourGateway, - add the port to the
contour-envoyService (so the VIP answers on it), and - allow the port through the Envoy
NetworkPolicy.
Gateway API splits ownership on purpose — the operator owns the Gateway (listeners/ports), and the app owns the route (hostname → service). The controller automates the operator half: it derives the needed listeners from the routes themselves, so an app team only writes a route.
For UDP, Contour has no support — expose it with a plain Service that lists the VIP in
externalIPs. See the "Alternatives" section.
An app declares intent with a TLSRoute or TCPRoute whose parentRefs target
the Contour Gateway. The listener port can be declared just once, on the
backendRefs (the natural place — it's the Service port); parentRefs[].port is
optional and overrides the backend port when set:
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TLSRoute
metadata:
name: linkkeys
namespace: linkkeys
spec:
parentRefs:
- name: contour
namespace: projectcontour
# port: 4987 # optional — overrides the backend port when present
hostnames:
- linkkeys.example.com # routed by SNI (TLS passthrough)
rules:
- backendRefs:
- name: linkkeys
port: 4987 # the port the controller opensThe controller derives the listener port from parentRefs[].port when set,
otherwise from the route's rules[].backendRefs[].port; the protocol comes from
the route kind:
| Route kind | Listener protocol | Routing |
|---|---|---|
TLSRoute |
TLS (Passthrough) |
by SNI — TLS terminates at the backend |
TCPRoute |
TCP |
by port only (no hostname) |
Multiple TLSRoutes can share one listener/port and are disambiguated by SNI, so
two domains on port 4987 each terminate TLS at their own backend.
For every desired (port, protocol), the controller converges three resources in
the Gateway namespace:
- Gateway listener — named
gw-<proto>-<port>(e.g.gw-tls-4987), withallowedRoutesfor the matching route kind andfrom: All. contour-envoyService port —port→targetPort = port + 8000. Contour binds Envoy to the listener port plus 8000 (80→8080, 443→8443, 4987→12987); the port is reachable on the VIP automatically through the existingexternalIPs.- Envoy
NetworkPolicyingress — admits the remapped target port (the default policy only allows 8080/8443/8002).
It owns only what it creates: gw-prefixed listeners/ports, and NetworkPolicy
ports recorded in the gateway.foundry.dev/managed-target-ports annotation. The
built-in HTTP/HTTPS listeners and any operator-pinned static listeners are never
touched. Deleting a route prunes its port. Because it reconciles continuously, it
also re-applies itself after a foundry stack install/upgrade resets the Service.
Ports 80 and 443 are reserved for the built-in listeners and are refused.
A port requested as both TLS and TCP is skipped with a logged conflict. A route
that targets the Gateway but yields no listener port — neither a parentRefs[].port
nor a single backendRefs[].port (none set, or backends declaring different
ports) — is skipped and named in the resync log so it's clear what was ignored
and why, instead of vanishing silently.
foundry gateway controller # watch loop, 15s resync (Ctrl-C to stop)
foundry gateway controller --once # single reconcile pass, then exitFlags: --gateway-name, --gateway-namespace, --envoy-service,
--network-policy (empty to skip), --interval, --kubeconfig.
Client config resolves in order: --kubeconfig → in-cluster service account (when
running as a pod) → ~/.foundry/kubeconfig.
foundry ships the chart embedded in the binary and installs it as the
gateway-controller component (single-replica Deployment + ServiceAccount +
ClusterRole/Binding in foundry-system, pointing at the published image).
As part of foundry stack install — opt-in. The component is in the install
order but is skipped unless explicitly enabled, so a default stack install
never depends on the controller image being present. Enable it in the stack
config:
components:
gateway-controller:
enabled: true
# optional typed shortcuts:
# image_tag: "0.2.0"
# interval: "30s"
# replica_count: 1
# raw chart values overlay (values.yaml equivalent) — deep-merged on top of
# the typed fields and the chart defaults; this wins:
values:
resources:
limits:
cpu: 200m
podAnnotations:
team: ingress
controller:
interval: 60s # overrides just this leaf; siblings keep their defaultsThen foundry stack install installs (or upgrades) it; flip enabled back to
false/remove it and it's simply skipped on the next run.
The values map is passed through to the Helm chart exactly like a
-f values.yaml: anything the chart's values.yaml exposes (resources,
nodeSelector, tolerations, affinity, podAnnotations, securityContext,
controller.extraArgs, …) can be set there. It is deep-merged on top of the
typed fields, so values always wins and you can override a single nested leaf
without restating its siblings.
On demand — any time:
foundry component install gateway-controllerThe same chart can be installed with helm (it lives in the module at
v1/charts/foundry-gateway-controller):
helm install gateway-controller \
v1/charts/foundry-gateway-controller \
--namespace foundry-system --create-namespace \
--set image.tag=<version>See v1/charts/foundry-gateway-controller/README.md for all values.
CI builds and publishes both the image and the chart on merge to main via
reactorcide (see .reactorcide/jobs/); because the chart is embedded in the
binary, a chart change also produces a new image.
The controller reads routes cluster-wide and writes only to the Gateway namespace:
gateway.networking.k8s.io:tlsroutes,tcproutes— get/list/watchgateway.networking.k8s.io:gateways— get/list/watch/update/patch- core
services— get/list/watch/update/patch networking.k8s.ionetworkpolicies— get/list/watch/update/patch
- Static operator-pinned listeners. If a port should always be open regardless
of routes, declare it once under
components.contour.listenersin the stack config — each entry hasname,protocol(TCP/TLS),port, and optionaltls_mode/hostname/certificate_ref. Foundry opens it duringstack install(Gateway listener + Envoy service port + NetworkPolicy). The controller leaves these untouched — the two coexist. - UDP. Contour does not implement
UDPRoute. Expose UDP with a plain Service that lists the VIP inexternalIPsand aprotocol: UDPport, owned by the app's chart. It bypasses the Gateway entirely.