feat(keycloak): new chart for Keycloak (1.0.0+up26.8.0)
Two replicas with cache=ispn, clustered through Keycloak's default jdbc-ping stack: discovery runs over the PostgreSQL that is there anyway, so no headless Service and no JGroups wiring. Deployment rather than StatefulSet - Keycloak 26 keeps everything in the database, user sessions included. The chart's defining property is that it has TWO INIT CONTAINERS and needs both. db, health-enabled and metrics-enabled are Keycloak BUILD options (verified in the 26.8.0 option table), the published image is augmented with their defaults, and supplying different ones at runtime makes Keycloak rewrite /opt/keycloak/lib/quarkus at start - which readOnlyRootFilesystem: true forbids and which upstream describes as a start that "creates an image that is mutable". Not setting db is no way out: the probes alone force it, because health-enabled is a build option too. An empty emptyDir over that path fails as well, because QuarkusEntryPoint loads quarkus/quarkus-application.dat before it dispatches any command, `build` included. So one init container seeds the directory into a volume, the next runs `kc.sh build` into it, and the serving container runs `start --optimized` - every container keeping the full hardening baseline, with no documented deviation to carry. Build options therefore exist in exactly one place, and the asymmetry is enforced rather than trusted: the serving container passes no build option at all, and a build option's variable name in keycloak.env fails the render with a message naming it. Hostname handling is the security-relevant part. KC_HOSTNAME is derived from ingress.domain as a full URL (which upstream requires alongside hostname-admin), hostname-strict stays at its default true, and proxy-headers is set rather than optional. The admin hostname is NOT presented as an access control: upstream states that hostname-admin "does not prevent accessing the Administration REST API endpoints via the frontend URL specified by the hostname option", so the chart renders no /admin deny rule and values.yaml, NOTES.txt and the wiki all say in the same words that restricting that API is reverse-proxy work. The management port 9000 is a containerPort and a ClusterIP Service for in-cluster scraping, and is in no Ingress: it serves /health/* and optionally /metrics without authentication. Liveness uses /health/live and deliberately not /health or /health/ready - those include the database, and a liveness probe on them would restart the whole cluster for as long as the database is away. Also in this change: - First PodDisruptionBudget in this repository, which makes it a convention (README section 11): minAvailable as a value, and rendered only while the computed replica count is >= 2, because minAvailable: 1 with one replica blocks every node drain forever with a message that names the PDB rather than the replica count. - First quay.io image, so quay.io joins the trusted registries in ci/trivy/data/ksv0125.yaml - configured rather than suppressed, so AVD-KSV-0125 keeps being able to say something. - Renovate block with NO version constraint, which is the counter-case to garage and stalwart and is measured rather than assumed: docker versioning already excludes the -N rebuild tags, the floating minors and latest/nightly. Three dry-run fixtures are recorded at the block, and against the committed config 26.7.4 offers exactly 26.7.5 (patch) and 26.8.0 (minor). - No automerge exception, by decision (Gitea #83): the global automerge applies, and the minor-upgrade procedure plus `kc.sh update-compatibility` is documented in NOTES.txt instead. - Two upstream defaults deviated from, each with its reason at the value: db-pool-max-size 20 instead of 100 (two replicas at 100 ask a default PostgreSQL for twice its max_connections), and uid 1000 / gid 0 instead of this repo's 20000/1000, because the image declares keycloak:x:1000:0 and chowns its install tree to that pair. Part of #83
This commit is contained in:
1 parent
a23d4977c5
commit
4c1cff1744
19 files changed
+2563
No files matched your search
@@ -0,0 +1,96 @@
|
||||
# Fixtures for the keycloak install-test: a throwaway PostgreSQL plus the two
|
||||
# Secrets the 1Password operator would otherwise materialize from the
|
||||
# OnePasswordItems the chart renders (release name: ci, stub CRD in ci/common/).
|
||||
#
|
||||
# WHY A REAL DATABASE AND NOT A STUB: Keycloak does nothing without one. It
|
||||
# creates its entire schema through Liquibase on first start, and - the reason
|
||||
# this fixture is the valuable half of the test - jdbc-ping discovers the second
|
||||
# replica THROUGH this database. A stub would turn a cluster test into a
|
||||
# templating test.
|
||||
#
|
||||
# The image is pinned to a major line like every other fixture here
|
||||
# (postgres:16-alpine); ci/** is excluded from Renovate on purpose, because
|
||||
# this is test scaffolding and churning it only risks breaking the
|
||||
# install-test.
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: ci-keycloak-db-secret
|
||||
stringData:
|
||||
# The field NAMES are what matters here, not the values: the operator
|
||||
# materializes one Secret key per item field, and the Deployment references
|
||||
# these two by name. A rename on either side leaves the pod in
|
||||
# CreateContainerConfigError, which is a state no render can show.
|
||||
#
|
||||
# They must agree with the POSTGRES_USER / POSTGRES_PASSWORD below, because
|
||||
# Keycloak really connects.
|
||||
username: keycloak
|
||||
password: ci-keycloak-db-password
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: ci-keycloak-bootstrap-admin-secret
|
||||
stringData:
|
||||
# The temporary bootstrap administrator. Keycloak creates it while the master
|
||||
# realm is created, so these values are exercised on the very first start -
|
||||
# and the variables are named KC_BOOTSTRAP_ADMIN_* only since Keycloak 26.
|
||||
#
|
||||
# A visibly throwaway password: nothing here survives the run, the cluster is
|
||||
# deleted with the job, and the only thing it protects is a realm that exists
|
||||
# for a few minutes.
|
||||
username: ci-temp-admin
|
||||
password: ci-bootstrap-admin-password
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: postgres
|
||||
labels:
|
||||
app: postgres
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: postgres
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: postgres
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
image: public.ecr.aws/docker/library/postgres:16-alpine
|
||||
env:
|
||||
- name: POSTGRES_USER
|
||||
value: keycloak
|
||||
- name: POSTGRES_PASSWORD
|
||||
value: ci-keycloak-db-password
|
||||
- name: POSTGRES_DB
|
||||
value: keycloak
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["pg_isready", "-U", "keycloak"]
|
||||
periodSeconds: 5
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/postgresql/data
|
||||
volumes:
|
||||
# emptyDir: the database exists for the length of one job. Nothing here
|
||||
# is meant to survive, and a PVC would only add a provisioner to the
|
||||
# list of things that can fail a chart's test.
|
||||
- name: data
|
||||
emptyDir: {}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: postgres
|
||||
spec:
|
||||
selector:
|
||||
app: postgres
|
||||
ports:
|
||||
- port: 5432
|
||||
targetPort: 5432
|
||||
@@ -0,0 +1,17 @@
|
||||
# The default 300s is not enough here, and the three parts that fill the budget
|
||||
# are worth naming so the number can be lowered again if one of them changes:
|
||||
#
|
||||
# 1. TWO INIT CONTAINERS PER POD. The build one runs a full Quarkus
|
||||
# augmentation - upstream's own issue reports measure "Quarkus
|
||||
# augmentation completed in" at roughly 13-23 seconds, and a CI runner is
|
||||
# not a fast machine.
|
||||
# 2. THE FIRST START CREATES THE WHOLE SCHEMA through Liquibase, and with two
|
||||
# replicas the second pod waits on the first's migration lock. So the two
|
||||
# pods do not become Ready in parallel.
|
||||
# 3. The rollout is only complete when BOTH pods are Ready, which is
|
||||
# deliberate (it is what makes cluster formation part of the assertion)
|
||||
# and which means the slow parts add up rather than overlap.
|
||||
#
|
||||
# The chart's own startupProbe budget is 600s (10s x 60); this timeout has to
|
||||
# be at least that, or the test gives up before the probe would have.
|
||||
INSTALL_TIMEOUT=900
|
||||
@@ -0,0 +1,127 @@
|
||||
# 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. jdbc-ping FINDS THE SECOND REPLICA from cache=ispn and the database
|
||||
# configuration alone - which is why this test runs TWO replicas (see
|
||||
# below) and why that is the single most valuable line in this file.
|
||||
# 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 no install-test here could:
|
||||
# - that hostname-admin serves the console cleanly on a second hostname. No
|
||||
# ingress controller answers in CI and nothing drives a browser, so the
|
||||
# chart's own measurement point 5 stays open. What IS checked is that both
|
||||
# Ingress objects are accepted and that the Service port they name exists.
|
||||
# - 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
|
||||
|
||||
# ON against the chart's default of off, because it is a BUILD option: with
|
||||
# this true the init container has to build an installation that differs from
|
||||
# the published image in TWO options rather than one, and /metrics has to
|
||||
# appear on the management port. A green run therefore proves the build
|
||||
# arguments really reach `kc.sh build` - with metrics off, a build that
|
||||
# silently did nothing would look 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
|
||||
|
||||
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
|
||||
@@ -20,3 +20,9 @@ ksv0125:
|
||||
- cr.jk-dev.de
|
||||
- ghcr.io
|
||||
- docker.io
|
||||
# quay.io, added with the keycloak chart (Gitea #83) - Keycloak publishes
|
||||
# its official image only there (quay.io/keycloak/keycloak), and it is so
|
||||
# far the only chart in this repo using that registry. Listed rather than
|
||||
# suppressed, for the reason the comment above gives: the check keeps being
|
||||
# able to say something about an image from a genuinely foreign source.
|
||||
- quay.io
|
||||
Reference in new issue
Block a user