Files
jklein 7a2eb29e9e
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
feat(keycloak): one hostname per realm, in both routing modes
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
2026-10-13 10:13:08 +00:00

260 lines
14 KiB
YAML

# CI values for the keycloak install-test.
#
# WHY THIS CHART IS TESTED RATHER THAN EXCLUDED, and what a green run is worth.
# Keycloak has no volume, needs one throwaway PostgreSQL and reaches Ready on
# its own, so an exclusion would be hard to justify. More importantly, this is
# the only place where the four riskiest assumptions of the chart are exercised
# at all - none of them is observable in a render.
#
# WHAT A GREEN RUN PROVES, point by point (the measurement points of Gitea #83):
#
# 1. THE TWO INIT CONTAINERS WORK AT ALL. The seed container copies the
# image's own augmentation output into the volume, the build container
# mounts that volume at /opt/keycloak/lib/quarkus and runs `kc.sh build`
# into it - both with readOnlyRootFilesystem: true. If the seeding
# assumption were wrong (QuarkusEntryPoint loads
# quarkus/quarkus-application.dat before it dispatches any command,
# `build` included), the build container could not start, and the pod
# would sit in Init with the reason only in its log.
# 2. readOnlyRootFilesystem: true HOLDS FOR THE SERVING CONTAINER, with
# emptyDirs only at /opt/keycloak/lib/quarkus, /opt/keycloak/data and
# /tmp - and NOT at /opt/keycloak/conf, which would hide the
# cache-ispn.xml that cache=ispn reads.
# 3. `start --optimized` ACCEPTS THE PRE-BUILT INSTALLATION and does not
# re-augment. A disagreement between the build options and the serving
# container would show up here as a crash loop rather than as a slow
# start, because the path it would want to rewrite is read-only.
# 4. TWO REPLICAS BOTH COME UP AND STAY UP against one database - the
# rollout is complete only when BOTH pods are Ready, so a second replica
# that crashed, deadlocked on the schema migration lock or fought the
# first over the database would fail the run. Measured in Gitea PR #87:
# 71.8s from `helm install` to both Ready, 0 restarts - three runs, all
# three 72s and 0 restarts.
# THAT WAS LUCK, OR A FAST WEEK, AND NOT AN INVARIANT - which took an A/B
# inside Gitea PR #92 to establish, because the first five runs there made
# it look like the new theme mount had destabilized the start:
# WITH the theme mount (spec of #88)
# run 585: 101s, ONE restart on one pod
# run 590: 82s, 0
# run 593: 191s, 0
# run 595: 101s, ONE
# run 597: 111s, ONE -> 3 of 5
# WITHOUT it (the same branch, the pair commented out; pod template
# hash 5844db56b6, i.e. BYTE-IDENTICAL to the spec PR #87 measured)
# run 598: 81s, 0
# run 599: 171s, ONE -> 1 of 2
# So the restart happens WITHOUT the mount as well, and #88 is not its
# cause. It is pre-existing, intermittent behaviour of THIS chart's first
# install: always exactly one restart on one pod, 35-80s before that pod
# reports Ready. It does not track the runner's speed either - the fastest
# (81s) and the slowest (191s) run of the whole series restarted nothing.
# WHAT THE RESTART IS HAS NOT BEEN ESTABLISHED, and that is the finding
# worth keeping: the harness dumps container logs only when a run FAILS
# (diagnose() hangs off the EXIT trap behind "RC -ne 0") and all seven runs
# PASSED - A GREEN RUN CANNOT EXPLAIN A RESTART IT REPORTS. Answering it
# means making the final-state step print the restart reasons whenever a
# counter is above zero; that is a change to ci/run-install-test.sh with a
# CI-CD wiki obligation of its own, and it was deliberately NOT made inside
# #88. A first install that serializes two replicas on the Liquibase
# migration lock is a plausible place for it - stated as the hypothesis it
# is, because nothing here measured it.
# THE METHOD IS THE PART TO REUSE: a counter that moved in the same round
# as a change is not evidence against the change until the control case has
# run on the SAME machine. Three clean runs two days earlier were not that
# control, and treating them as one would have cost this PR its conclusion
# in both directions.
# 5. A MOUNTED LOGIN THEME SURVIVES ALL OF THAT (Gitea #88) - the full
# argument sits at keycloak.extraVolumes further down, including what it
# still does not prove.
# Plus the mechanical half: the Secret key names match what the Deployment
# references, /health/started, /health/live and /health/ready all answer on
# the management port (so health-enabled really was baked in by the init
# container), the resource quantities are valid, and the schema accepts a
# realistic non-default values file.
#
# WHAT IT DOES NOT PROVE, and the first entry is the one not to misread:
# - THAT jdbc-ping ACTUALLY FOUND THE PEER. Two replicas both reaching Ready
# is NOT that proof, and believing it was is the mistake this paragraph
# exists to prevent: readiness reports this node's own health, so a node
# that never found its peer is Ready, healthy and alone - and two such
# nodes are two islands that each believe they are the cluster. The
# Infinispan view has to be read out of the log, which is what step 2 of
# NOTES.txt tells the operator to do. So measurement point 4 of the plan
# stays open after this test; what the test does rule out is the louder
# half (a second replica that cannot start or cannot share the database).
# - that hostname-admin serves the console cleanly on a second hostname. No
# ingress controller answers in CI and nothing drives a browser, so
# measurement point 5 stays open too. What IS checked is that both Ingress
# objects are accepted, that the Service port they name exists, and that
# Keycloak accepts both hostname values as full URLs at start.
# - anything about an upgrade. The rolling-update restriction between minor
# versions is a property of two versions meeting, and this test installs
# one.
# - that the PostgreSQL here resembles production. It is one pod with an
# emptyDir, so nothing about durability, connection limits or latency
# transfers.
secrets:
# Materialized by ci/keycloak/fixtures.yaml as ci-keycloak-db-secret and
# ci-keycloak-bootstrap-admin-secret (the OnePasswordItem stub CRD comes from
# ci/common/).
databaseSecretRef: op://ci/keycloak-db
bootstrapAdminSecretRef: op://ci/keycloak-bootstrap-admin
keycloak:
# TWO, i.e. the chart's default, and the whole reason this fixture is not a
# one-replica shortcut. With one replica the install-test would be green
# without ever showing whether the second node is found - and "a path the
# test does not walk is a path we have not proven" is the lesson Gitea #80
# paid for. The install-test script waits for the Deployment's rollout, and a
# rollout is only complete when BOTH pods are Ready, so cluster formation is
# inside the assertion rather than next to it.
replicas: 2
db:
# The throwaway PostgreSQL from fixtures.yaml. Plain Service name: both are
# in the same namespace.
host: postgres
database: keycloak
# Small but not degenerate: 2 replicas x 5 = 10 connections against
# postgres:16-alpine's default max_connections of 100, which leaves the
# fixture comfortable while still exercising the pool options.
pool:
initialSize: 1
minSize: 1
maxSize: 5
# The chart default, spelled out because it is load-bearing for what this
# test means: metrics-enabled is what puts the DATABASE check into
# /health/ready (upstream's check table, "Requires Metrics: Yes"). With it
# off, readiness would pass without ever touching the database, and a green
# run would say nothing about the connection at all.
#
# It is also a BUILD option, so with it true the init container has to build
# an installation differing from the published image in three options rather
# than two - which is what makes a build that silently did nothing fail the
# run instead of looking identical.
metrics:
enabled: true
# Set on purpose, although one release cannot collide with itself: it
# exercises the KC_CACHE_EMBEDDED_CLUSTER_NAME path, and a name Keycloak
# rejected would fail the start rather than pass unnoticed.
cache:
clusterName: ci-keycloak
resources:
# Below the chart's defaults to stay inside the runner's budget, and the
# memory number is chosen rather than rounded: the image sizes the heap at
# 70% of the LIMIT, so 600Mi x limitFactor 3 = 1800Mi gives a ~1260Mi heap.
# Upstream's floor is "at least 750 MB" for the limit; going much below
# this makes the JVM the reason a run fails.
requests:
cpu: 250m
memory: 600Mi
ephemeral-storage: 256Mi
build:
resources:
requests:
cpu: 250m
memory: 512Mi
ephemeral-storage: 256Mi
# THE EXTRA VOLUME PAIR, WALKED RATHER THAN RENDERED (Gitea #88). The theme
# ConfigMap comes from fixtures.yaml. Three things this puts inside the
# assertion that no render reaches:
#
# 1. A MOUNT ON A SUBPATH OF A DIRECTORY THAT EXISTS IN THE IMAGE IS
# ACCEPTED ALONGSIDE readOnlyRootFilesystem: true.
# /opt/keycloak/themes/ci-theme does not exist in the image, so the
# runtime has to create the mount point inside a root filesystem that is
# about to be read-only. If that did not work, every container of this
# pod would fail to start.
# 2. `kc.sh build` STILL SUCCEEDS WITH THE EXTRA MOUNT PRESENT. The mounts
# are rendered on the build init container too (that is what makes SPI
# providers work at all), so this fixture is also the test that the extra
# mount does not disturb the augmentation - a failure would leave the pod
# in Init with the reason only in a log.
# 3. `start --optimized` COMES UP WITH A RUNTIME THEME IN PLACE. The plan of
# #88 called that "a reasoned expectation, not verified"; a Ready pod with
# this mounted is the verification, and it is the one that matters for the
# unusual two-init-container construction of this chart.
#
# WHAT IT STILL DOES NOT PROVE, and it would be easy to overclaim here: that
# the LOGIN PAGE renders and that base/keycloak/keycloak.v2 are still listed.
# The health endpoints answer regardless of the themes directory, and nothing
# in this harness makes an HTTP request to / or runs a command in the pod. That
# the subpath hides nothing is a property of the mount path rather than a
# measurement - and the kcLogoIdP line needs a browser plus a realm setting,
# neither of which exists in CI.
extraVolumes:
- name: login-theme
configMap:
name: ci-keycloak-theme
items:
- key: theme.properties
path: login/theme.properties
extraVolumeMounts:
- name: login-theme
# THE SUBPATH, AND THE WHOLE POINT OF THE EXAMPLE: a volume on
# /opt/keycloak/themes would hide the image's own three themes and break
# the login page while the pod stayed Ready.
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
# ("http") actually exists - a typo there is otherwise invisible until
# somebody opens the URL.
enabled: true
domain: keycloak.ci.example.com
clusterIssuer: ci-selfsigned
admin:
# The second hostname, on as well, so the chart renders KC_HOSTNAME_ADMIN
# and Keycloak has to accept it as a full URL. That is the half of
# measurement point 5 a cluster-less test can reach: Keycloak validates
# both hostname values at start ("verified that full URL is used, incl.
# scheme and hostname"), so a chart that built a bare host here would fail
# the start instead of serving a broken console later.
enabled: true
domain: keycloak-admin.ci.example.com