feat(keycloak): Gateway-API as an alternative to the two Ingresses

Group 3 of the #114 sweep, first chart. keycloak has TWO Ingress objects -
the public hostname and the admin hostname - so it gets TWO HTTPRoutes, with
httpRoute.admin.parentRefs falling back to httpRoute.parentRefs exactly as
ingress.admin.className falls back to ingress.className: "which data path" is
a property of the Gateway in this API, not of the route, so an internal
Gateway here is what an internal ingress class is in the other mode.

The admin route stays gated on ingress.admin.enabled, because that value is
not an Ingress switch - it also decides whether KC_HOSTNAME_ADMIN is set on
the Deployment, i.e. whether the admin hostname exists at all. Switching to
Gateway mode therefore must not require turning it off.

FIXED ALONG THE WAY: the admin Ingress did not hang off ingress.enabled.
Measured on origin/main, `helm template -f ci/keycloak/test-values.yaml --set
ingress.enabled=false` emitted kc-keycloak-admin-ingress - so
"ingress.enabled: false" did not mean "no Ingress", and ingress.clusterIssuer
stayed required for an object nobody asked for. It also made Gateway mode
impossible: the admin Ingress would have gone on claiming the admin host next
to the admin HTTPRoute, one host over two data paths, which is the exact
conflict the mutual exclusion exists to prevent. The default render is
unchanged; what changed is a combination that could not be coherent.

WHAT THIS MODE DROPS, MEASURED ON THE LIVE CLUSTER rather than inferred. On
2026-10-12 BOTH of this chart's Ingress objects in jk-cluster carried

    nginx.ingress.kubernetes.io/proxy-buffer-size:    16k
    nginx.ingress.kubernetes.io/proxy-buffers-number: 4

set by the OPERATOR through ingress.annotations and ingress.admin.annotations
- the chart sets neither, which is why reading the chart alone does not reveal
them. The Gateway API has no annotation channel and no counterpart for proxy
buffer sizing; it is implementation policy, not routing. Per decision 1 of
#114 the chart renders no policy object, so this is NAMED and not rebuilt, in
values.yaml, in the schema and in the Gateway branch of NOTES.txt, with the
consequence spelled out in Keycloak's own terms: large headers (JWTs, SAML
assertions, long Set-Cookie chains on the login flow) make a too-small buffer
answer 502 at the PROXY while Keycloak logs the request as successful.

Named and deliberately not touched: four further Ingress objects in the same
namespace (keycloak-realm-*-ingress, with app-root) belong to the Helm release
jk-cluster-config-fleet-cd-keycloak-realm-ingress, i.e. to another repository.
They point at this release's Service under four further hostnames. This chart
renders exactly the two routes its own two Ingresses cover.

Minor: additive, defaults unchanged. Empty helm template diff against
origin/main with ci/keycloak/test-values.yaml.

Part of #114
This commit is contained in:
jklein committed 2026-10-12 20:44:49 +00:00
1 parent f202fdc3fc
commit f97e1e6d52
7 files changed
+470 -13

No files matched your search

+85
View File
@@ -642,6 +642,11 @@ ingress:
# Extra annotations for the public Ingress; merged with the cert-manager
# annotation the chart always sets.
#
# INERT IN GATEWAY MODE (httpRoute.enabled) - the Gateway API has no
# annotation channel for controller behaviour. That matters for THIS chart in
# particular: see the httpRoute block below for the proxy-buffer annotations
# this installation actually relies on and what their loss looks like.
annotations: {}
admin:
@@ -684,4 +689,84 @@ ingress:
# annotation. This is where an IP allow-list would go
# (nginx.ingress.kubernetes.io/whitelist-source-range) - with the caveat
# above firmly in mind.
#
# INERT IN GATEWAY MODE, like ingress.annotations - see the httpRoute block
# below for what this installation actually has in here today and what
# losing it costs.
annotations: {}
# THE GATEWAY-API ALTERNATIVE TO THE TWO INGRESSES (Gitea #114).
#
# Mutually exclusive with ingress.enabled - the render aborts when both are
# true, and the message names both value paths. The chart renders ONLY
# HTTPRoutes with parentRefs: no Gateway, no class, no Certificate, and no
# implementation-specific policy object. Class and certificate sit on the
# Gateway and its listeners and belong to the platform (cluster-config), which
# is what makes the Gateway implementation replaceable without touching this
# chart again.
#
# TWO ROUTES, mirroring the two Ingresses: the public hostname and the admin
# hostname. ingress.admin.enabled keeps gating BOTH forms, because it is not an
# Ingress switch - it decides whether KC_HOSTNAME_ADMIN is set on the
# Deployment, i.e. whether the admin hostname exists at all.
#
# WHAT THIS MODE COSTS THIS CHART, MEASURED AND NOT HYPOTHETICAL. On
# 2026-10-12 both of this chart's live Ingress objects carried
#
# nginx.ingress.kubernetes.io/proxy-buffer-size: 16k
# nginx.ingress.kubernetes.io/proxy-buffers-number: 4
#
# set by the OPERATOR through the two annotations blocks above - this chart
# sets neither, which is why reading the chart alone does not reveal them. The
# Gateway API has no annotation channel and no counterpart for proxy buffer
# sizing: it is implementation policy, not routing, so there is nothing here to
# render and nothing this chart could promise. The consequence is specific:
# Keycloak's responses carry large headers (JWTs, SAML assertions, long
# Set-Cookie chains during the login flow), and a proxy whose buffer is too
# small answers 502 ITSELF, while Keycloak logs a successful request. The
# symptom therefore points at the application and the cause is at the proxy.
# Set the equivalent next to the Gateway, in that implementation's own object,
# BEFORE moving the host. NOTES.txt prints this in Gateway mode.
#
# THE GATEWAY-API CRDs MUST BE PRESENT. The chart cannot detect them
# (.Capabilities.APIVersions is unreliable at template time) and an HTTPRoute
# without them fails at install, so this is a value and not a guess.
httpRoute:
enabled: false
# The Gateway(s) the PUBLIC route attaches to - a platform object, usually in
# another namespace, whose listener must allow this namespace in
# allowedRoutes. Entries are the Gateway API's ParentReference (name, and
# optionally namespace, sectionName, port, group, kind), passed through
# unchanged:
#
# parentRefs:
# - name: public
# namespace: gateway
# sectionName: https
#
# REQUIRED while httpRoute.enabled is true - the render aborts on an empty
# list, because a route without a parent is created, is valid and routes
# nothing, with no event to say so.
parentRefs: []
# Empty means [ingress.domain], so the public hostname has one source of
# truth. ingress.domain is required in every mode in this chart anyway (the
# Deployment reads it as KC_HOSTNAME), so setting this list is only for
# publishing under a different or an ADDITIONAL name - which is how a
# migration from the Ingress runs: the HTTPRoute takes a second hostname
# while the Ingress keeps ingress.domain.
hostnames: []
admin:
# The admin route's own parent. null/empty falls back to
# httpRoute.parentRefs - the exact counterpart of ingress.admin.className
# falling back to ingress.className. "Which data path" is a property of the
# Gateway in this API rather than of the route, so an INTERNAL Gateway here
# is what an internal ingress class is in the other mode. Still not the
# access control the ingress.admin paragraph above is about.
parentRefs: []
# Empty means [ingress.admin.domain]. Required (via that value) while
# ingress.admin.enabled is true and httpRoute.enabled is true.
hostnames: []