feat(keycloak): one hostname per realm, in both routing modes
Validate Helm Charts / Detect changed charts (pull_request) Successful in 4s
Validate Helm Charts / Resource quantities (pull_request) Successful in 14s
Validate Helm Charts / Selector match (pull_request) Successful in 14s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 14s
Validate Helm Charts / Build keycloak (pull_request) Successful in 8s
Validate Helm Charts / Trivy config keycloak (pull_request) Successful in 24s
Install-test / Install-test keycloak (pull_request) Successful in 3m29s
Validate Helm Charts / Install-test (pull_request) Successful in 3m29s

keycloak.realmHosts gives a realm a hostname of its own: this release's
Service published under a further name, with that name's bare / sent to
/realms/<realm>/account/. It renders in BOTH modes - one Ingress per entry
carrying nginx.ingress.kubernetes.io/app-root, or one HTTPRoute per entry
whose Exact match on / carries a RequestRedirect filter - because a routing value in
this repository is mode-independent by design - ingress.domain is read in
Gateway mode too - so a feature rendering in one mode only would silently
disappear when an installation switched.

Default []: nothing renders, and the render is byte-identical to 1.2.0 for
the fixture's values in both modes. Minor, additive, no value becomes
required.

ONE OBJECT PER ENTRY, not one with several hostnames. spec.hostnames applies
to the whole object and a rule's matches are path-based, so inside a shared
route every entry would match Exact: / and the realm could not be told
apart. Names therefore carry the LIST INDEX, as homeassistant's per-host
routes do and for the same reason - a realm name is not a dependable
object-name fragment. The consequence is written down rather than left to be
discovered: reordering the list renames and recreates the objects.

THE WORK IS IN THE INGRESS HALF, NOT THE ROUTE. Two convention rules do not
survive these objects, and both deviations are required rather than
tolerated: tlsSecretName is a per-entry value with NO default, because these
hostnames already have certificates and several entries typically share one,
so a derived name would ask cert-manager for a second certificate for a name
a wildcard already covers; and the cert-manager annotation is OPTIONAL per
entry, because cert-manager keys a Certificate by the secret it writes and
only one object per secret may request it - the same deliberate omission the
qBittorrent object in freesailarr makes. Annotations are per entry too: of
the four hostnames this was written for, three carry the proxy-buffer pair
and one does not, which a chart-global block could not express.

statusCode: 302 is written out at the field's own default. 301 and 302 are
both Support: Core, so the portability trap of #119 - an implementation
declining an Extended code sets Accepted: False on the WHOLE route - does
not apply here; ingress-nginx emits 302 for app-root, and a value that is
only accidentally right documents nothing.

Five render aborts, each against a failure that reports nothing: an entry
without a host (ingress-nginx reads an empty host as a catch-all for the
whole controller), without a realm (/realms//account/, a 404 behind a
healthy hostname and certificate), without a tlsSecretName in Ingress mode
(the controller's default certificate, a browser-only failure), a repeated
host or one the release already publishes (webhook rejection in Ingress
mode, two merged routes decided by object age in Gateway mode), and a filled
list with both modes off (no object at all, and nothing in the release that
reveals the list was ignored). The entry-shape checks sit in a shared helper
so they exist in both modes, and the Ingress template skips them while
httpRoute.enabled is also true so that the specific message cannot hide the
mutual-exclusion one.

CI: the two entries live in ci/keycloak/test-values.yaml, which
ci/httproute/keycloak.yaml is applied on top of, so one definition covers
both modes - two Ingress objects in the install-test, two HTTPRoutes in the
render test. They cover both Ingress forms, with and without a clusterIssuer,
and the render test holds their inertness in Gateway mode. The httproute
match test goes from 56 assertions over 21 charts to 60, keycloak's share
from 4 to 8 plus two "not checked" lines for the backend-less redirect rules.
The #118 prefix/redirect pairing correctly does not fire: it is scoped to
rules that strip a prefix, and a realm route strips nothing.

NOT IN THIS CHANGE: the migration. Nothing in jk-cluster-config is touched.
With the default [] no running system changes, so the order a takeover has to
run in is documented instead - remove the objects in the owning repository
first, then install with the list filled, because the reverse or both at once
is rejected by ingress-nginx's admission webhook and leaves a Fleet bundle in
ErrApplied until forceSyncGeneration is raised. Whether anything there
depends on those objects BY NAME is unchecked and named as unchecked.

Fixes #122
This commit is contained in:
jklein committed 2026-10-13 10:13:08 +00:00
1 parent 6517643cd6
commit 7a2eb29e9e
10 files changed
+629 -16

No files matched your search

+20 -2
View File
@@ -30,11 +30,29 @@
# parentRefs fall back to httpRoute.parentRefs below - the empty-admin
# branch, which is the one an installation hits unless it wants an
# internal Gateway.
# - both routes' backendRef is checked against the serving Service of the
# same render. Both point at the SAME Service, by PORT NUMBER
# - every route's backendRef is checked against the serving Service of the
# same render. They all point at the SAME Service, by PORT NUMBER
# (keycloak.ports.http) rather than by the name "http" the Ingress uses,
# because backendRefs takes a PortNumber - so this overlay is what holds
# that number against the Service's.
# - THE TWO REALM ROUTES (Gitea #122), which come from keycloak.realmHosts in
# the fixture rather than from this file. Nothing is repeated here on
# purpose: the fixture installs them as Ingress objects and this overlay
# turns the same two entries into HTTPRoutes, so one definition covers both
# modes and the two halves cannot drift into describing different hostnames.
# What the routes add to the two above is a SECOND RULE SHAPE - an Exact
# match on / carrying a RequestRedirect filter and no backendRefs at all.
# That rule is reported as "not checked" by this test, correctly and by
# design: a filter-only rule has no backend to resolve. The assertion it
# does earn is on its sibling rule, the PathPrefix one that points at the
# Service. The prefix/redirect pairing check of #118 does NOT fire for these
# routes and must not: it is scoped to rules that STRIP a prefix with a
# ReplacePrefixMatch rewrite, and these strip nothing - they redirect the
# bare root of a hostname that serves one realm.
# The entries also cover the two Ingress-mode forms (one with a
# clusterIssuer, one without) and what this test holds about them is their
# INERTNESS: neither tlsSecretName nor clusterIssuer nor the per-entry
# annotation may appear anywhere in a rendered route.
#
# NOT covered here, and it cannot be: that either hostname answers. No Gateway
# controller runs in CI, no route reaches "Accepted: True", and the
+37
View File
@@ -203,6 +203,43 @@ keycloak:
mountPath: /opt/keycloak/themes/ci-theme
readOnly: true
# TWO REALM HOSTNAMES (Gitea #122), and they are set HERE rather than in the
# Gateway overlay on purpose: ci/httproute/keycloak.yaml is applied on top of
# this file, so one definition covers BOTH modes - two Ingress objects in this
# install, two HTTPRoutes in the render-only test, from the same two entries.
# Defining them in the overlay instead would leave the Ingress half of the
# feature with no coverage at all, and it is the half that carries the
# required value and the convention deviation.
#
# WHAT THIS INSTALL ADDS TO THE RENDER TEST, which is the part a cluster-less
# check cannot reach: the API server accepts both objects, including the
# Service port named BY NAME ("http") - a typo there is invisible until
# somebody opens the URL - and the second entry's cert-manager annotation does
# not make the apply fail in a cluster without cert-manager. No ingress
# controller answers in CI, so neither app-root nor the proxy buffers are
# exercised; that is the same gap the two objects above have.
#
# THE TWO ENTRIES ARE THE TWO FORMS, deliberately:
# [0] NO clusterIssuer - the normal case, where the certificate comes from
# elsewhere (a wildcard, another release) and this object only consumes
# the secret. It also carries a per-entry annotation, which is what
# makes the per-entry (rather than chart-global) annotations block a
# tested shape rather than an intended one.
# [1] WITH clusterIssuer - the case where the certificate is issued for this
# entry's own secret.
# Both are inert in Gateway mode, and the render test is what holds that: the
# routes it renders from these entries must show no trace of either field.
realmHosts:
- host: realm-one.ci.example.com
realm: ci-realm-one
tlsSecretName: tls-ci-realm-shared-ingress
annotations:
nginx.ingress.kubernetes.io/proxy-buffer-size: 16k
- host: realm-two.ci.example.com
realm: ci-realm-two
tlsSecretName: tls-ci-realm-two-ingress
clusterIssuer: ci-selfsigned
ingress:
# Left ON although no ingress controller answers in CI: the point is that
# both objects are accepted and that the Service port they name BY NAME