Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6df877224
|
No files matched your search
@@ -42,7 +42,6 @@ on:
|
||||
- valkey
|
||||
- stalwart
|
||||
- garage
|
||||
- keycloak
|
||||
# freesailarr is deliberately absent: none of its ten images takes
|
||||
# its tag from appVersion (they are all pinned in its values.yaml),
|
||||
# so this workflow would rewrite appVersion and republish the chart
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
name: Publish Keycloak Helm Chart
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- keycloak/**
|
||||
jobs:
|
||||
publish-keycloak:
|
||||
name: Publish Keycloak Helm Chart
|
||||
uses: workflow-util/pipelines/.gitea/workflows/publish-helm-chart.yml@v3
|
||||
with:
|
||||
chart-name: 'keycloak'
|
||||
registry-owner: ${{ github.repository_owner }}
|
||||
secrets: inherit
|
||||
@@ -22,7 +22,6 @@ Every chart has a wiki page of its own — `https://git.jk-dev.de/jlab-cluster/h
|
||||
| `garage` | Garage (S3-compatible object store) | **One** node, `replication_factor = 1` — a deliberate trade with its price named (Gitea #81): the volume layer underneath already replicates and encrypts, so Garage replication on top would be nine times the raw data across two layers that know nothing about each other. What that costs is that the factor applies to the **metadata** too, where a corrupted database is upstream-documented as "generally not recoverable" and a replicated volume replicates the corruption. Hence three settings that are mandatory rather than optional: `metadata_fsync = true` (**off** in Garage's defaults), the automatic metadata snapshots, and those snapshots on a **second PVC** — a file-system snapshot of a live metadata database is not a restore point, so the volume backup covers the blocks and not the index. The first PVC carries `metadata_dir` **and** `data_dir` as two subdirectories, because volume snapshots are atomic per volume: that is what keeps index and blocks consistent with each other. Two further properties shape the whole chart: the image is **`FROM scratch`** and contains exactly one file, `/garage` — no shell, so **exec probes are impossible here** and an init container would need a second image — and the RPC secret has to arrive as an **environment variable**, because `rpc_secret_file` rejects any file with a group bit set while the kubelet sets one (the reasoning sits at the value). Started with `garage server --single-node`, which creates the cluster layout in-process before any listener opens and is idempotent on the layout version, and with `--default-access-key`/`--default-bucket`, which **import** key and bucket from the 1Password item — so the vault is the source of truth instead of a copy of what the server generated. Readiness is the admin API's `/health`, liveness and startup deliberately are **not**: `/health` answers 503 until a layout exists, so a node waiting for one would be restarted forever |
|
||||
| `gotenberg` | Gotenberg | Deployment + Service only (no ingress) |
|
||||
| `homeassistant` | Home Assistant | StatefulSet; the only chart carrying Helm tests (`templates/tests/`) |
|
||||
| `keycloak` | Keycloak (OpenID Connect / SAML identity provider) | **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, **no PVC** (Keycloak 26 keeps everything in the database, user sessions included). **The chart has two init containers and needs both** (Gitea #83): `db`, `health-enabled` and `metrics-enabled` are Keycloak *build* options, the published image is built with their defaults, and a runtime change 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". An empty `emptyDir` over that path does not work either, 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. Build options therefore exist in exactly **one** place and a build option in `keycloak.env` fails the render. First chart with a `PodDisruptionBudget` (§11) and first `quay.io` image (hence the entry in `ci/trivy/data/ksv0125.yaml`). Two Services (8080, and the management port 9000 **never** published — it serves `/health/*` and optionally `/metrics` unauthenticated) and two Ingresses. **The admin hostname is not an access control**: `hostname-admin` only decides which URLs the server generates, and upstream states that it "does not prevent accessing the Administration REST API endpoints via the frontend URL" — restricting that API is reverse-proxy work, and the chart renders no `/admin` deny rule. `ingress.domain` is read as `KC_HOSTNAME` (as a full URL, which upstream requires alongside `hostname-admin`), so it stays required with the Ingress off. Liveness is `/health/live` and deliberately not `/health` or `/health/ready` — those include the database, and a liveness probe on them restarts the whole cluster for as long as the database is away. **`keycloak.metrics.enabled` defaults to `true` for the readiness probe, not for the metrics**: upstream's table of health checks carries a "Requires Metrics" column and the *database* check is the row that says yes, so with it off `/health/ready` answers 200 while the database is gone — the `urlaubsverwaltung` shape (#218) with the dependency hidden behind an option whose name says observability. Two traps at the values: upstream's `db-pool-max-size` default of 100 would ask a default PostgreSQL for 200 connections across two replicas (the chart ships 20), and two releases sharing one database **merge into one Infinispan cluster** unless the cluster names differ |
|
||||
| `matrix-synapse` | Matrix Synapse | StatefulSet plus an nginx delegation Deployment/Service |
|
||||
| `minecraft-router` | itzg/mc-router | Deployment + routing ConfigMap + NodePort Service (TCP only, no ingress); `mappings` is a required value |
|
||||
| `minecraft-server` | itzg/minecraft-server | StatefulSet + PVC + Service + init-script ConfigMap (no ingress); service type is a value (`ClusterIP` by default, routed via `minecraft-router`). **Only the newest Java line is maintained** (chart 5.0.0, #158): the directory carries one line, older ones stay published as frozen artifacts (java8/java17 at `4.0.0`, java21 at `4.1.0`) and receive no further chart changes — publishing once per line out of one directory is what made the lines drift. To run an older JVM with the current chart, set `image.tagOverride`; the chart then checks that `minecraft.version` fits the Java line of the **effective** tag and fails the render with both values named. Two cases pass unchecked on purpose: a tag without a `java<N>` part and a `minecraft.version` that is not a plain numeric `x[.y[.z]]`. `image.tagOverride` is deliberately outside Renovate's reach — `renovate.json5` tracks `appVersion` only. Since chart 2.0.0 the tag is an **immutable dated upstream release** (`appVersion: <release>-java8` / `-java17` / `-java21` / `-java25`, e.g. `2026.8.2-java8`) instead of the continuously rebuilt rolling line tag. Pilot for the strict `values.schema.json` (#68), which is now binding for every chart |
|
||||
@@ -386,8 +385,6 @@ These guidelines are **binding for every current and future chart** in this repo
|
||||
- Every Deployment/StatefulSet has a `replicas` value (default `1`, the values path per workload matches its section) that also accepts an explicit `0` for per-workload fine control.
|
||||
- Every chart has a global `scaleDown` flag (default `false`): `scaleDown: true` takes precedence over all `replicas` values, renders every Deployment/StatefulSet with `replicas: 0` and suspends CronJobs. It **stops** the app but does not uninstall it — PVCs, Secrets, Services and Ingress stay in place; setting it back to `false` brings the workloads back up.
|
||||
- Implemented via a per-chart `<chart>.replicas` helper in `_helpers.tpl` (reference in `template-chart`); the helper uses an explicit nil check (`kindIs "invalid"`) because Helm's `default` would swallow an explicit `0`.
|
||||
- **A chart whose default is more than one replica ships a `PodDisruptionBudget`** — `policy/v1`, named `<release>-<chart>-pdb`, `minAvailable` as a value (default `1`), selector matching the workload's `app` label. `keycloak` (Gitea #83) is the first and sets the convention. The reason it is a rule and not a per-chart taste: a node drain is the most common reason a pod moves — it is what a node upgrade does — and without a budget the drain is free to take every replica at once, so the second replica buys nothing against the failure it was added for.
|
||||
- **And it is rendered only while the effective replica count is `>= 2`.** `minAvailable: 1` with exactly one replica forbids every voluntary eviction, so `kubectl drain` blocks forever on a pod nothing may move — and the message names the PDB, not the replica count, which sends the reader to the wrong file. The same applies to `scaleDown: true`, where there are no pods and the object would describe a promise about nothing. So the condition is on the **computed** replica count (the `<chart>.replicas` helper), not on the raw value, and `NOTES.txt` prints which of the two happened rather than leaving it to be inferred from `kubectl get pdb`.
|
||||
|
||||
### 12. Values that must not lie (#68, #93, #99, #215)
|
||||
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
# 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
|
||||
@@ -1,17 +0,0 @@
|
||||
# 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
|
||||
@@ -1,143 +0,0 @@
|
||||
# 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.
|
||||
# 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
|
||||
|
||||
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,9 +20,3 @@ 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
|
||||
@@ -1,50 +0,0 @@
|
||||
apiVersion: v2
|
||||
name: keycloak
|
||||
description: A Helm chart to deploy Keycloak, an OpenID Connect and SAML identity provider, as a two-node cluster on Kubernetes
|
||||
icon: https://www.keycloak.org/resources/images/icon.svg
|
||||
|
||||
type: application
|
||||
|
||||
# appVersion is the exact image tag of quay.io/keycloak/keycloak. No "v"
|
||||
# prefix here - upstream tags are plain three-part versions (26.8.0).
|
||||
#
|
||||
# THE TAG LANDSCAPE, AND WHY THIS CHART NEEDS NO VERSION CONSTRAINT. Measured
|
||||
# on 2026-10-04 against quay.io's tag API, this registry carries four shapes
|
||||
# side by side:
|
||||
#
|
||||
# 1. release tags 26.8.0, 26.7.5, ... <- what we track
|
||||
# 2. REBUILD tags 26.8.0-0, 26.7.3-1, 26.7.2-2 - the same app
|
||||
# version rebuilt (base image or CVE fix) without an app-version bump.
|
||||
# 26.8.0-0 resolves to the SAME manifest digest as 26.8.0.
|
||||
# 3. floating minor tags 26.8, 26.7, 26.6, ... - they move
|
||||
# 4. floating names latest, nightly - they move daily
|
||||
#
|
||||
# This is the same GENUS of trap as garage's commit-SHA tags and stalwart's
|
||||
# moving names, and it is the one case in this repo where the answer came out
|
||||
# the other way: plain `docker` versioning already excludes all three unwanted
|
||||
# shapes, so the renovate.json5 block carries NO versioningTemplate. Docker
|
||||
# versioning splits a tag at the first dash and only ever offers candidates
|
||||
# with the SAME suffix - our value has none, so the -N rebuilds are invisible
|
||||
# to it - and 26.8 fails on the missing third part while latest/nightly parse
|
||||
# as no version at all.
|
||||
#
|
||||
# THAT IS MEASURED, NOT ASSUMED, and the measurement is written down because
|
||||
# the next round will otherwise add a constraint "to be safe" (renovate 44.132.5,
|
||||
# --platform=local --dry-run=lookup against the real registry):
|
||||
# appVersion 26.8.0 -> updates [], warnings []
|
||||
# appVersion 26.7.0 -> exactly one: 26.8.0 (minor)
|
||||
# appVersion 26.7.4 -> exactly two: 26.7.5 (patch), 26.8.0 (minor)
|
||||
# No run offered a rebuild tag, a floating minor, latest or nightly.
|
||||
#
|
||||
# AND THE OTHER HALF, WHICH THE VERSION NUMBER CANNOT CARRY: Keycloak supports
|
||||
# a rolling update only WITHIN a minor line. A minor jump needs the cluster
|
||||
# shut down. The global `automerge: true` in renovate.json5 applies here
|
||||
# anyway - a deliberate acceptance of that risk, not an oversight - so the
|
||||
# procedure lives in NOTES.txt and on the wiki page instead of in a
|
||||
# packageRules entry. Read the "MINOR UPGRADES" section of NOTES.txt before
|
||||
# merging an app-version PR that changes the minor.
|
||||
version: 1.0.0+up26.8.0
|
||||
appVersion: "26.8.0"
|
||||
|
||||
maintainers:
|
||||
- name: jklein
|
||||
@@ -1,204 +0,0 @@
|
||||
{{- $settings := fromYaml (include "keycloak.settings" .) -}}
|
||||
{{- $replicas := int (include "keycloak.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.keycloak.replicas)) -}}
|
||||
{{- $adminHostname := eq (include "keycloak.adminHostnameEnabled" .) "true" -}}
|
||||
{{- $deploy := printf "%s-%s-deployment" .Release.Name .Chart.Name -}}
|
||||
{{- $svc := printf "%s-%s-service" .Release.Name .Chart.Name -}}
|
||||
Keycloak {{ .Chart.AppVersion }} installed as release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
|
||||
|
||||
{{ $replicas }} replica(s), cache=ispn with jdbc-ping discovery over the database,
|
||||
external PostgreSQL at {{ $settings.db.host | default "<keycloak.db.host NOT SET>" }}:{{ $settings.db.port | int64 }}/{{ $settings.db.database }}.
|
||||
|
||||
ENDPOINTS
|
||||
{{ if .Values.ingress.enabled }} Public: {{ include "keycloak.frontendUrl" . }}
|
||||
{{ else }} Public: no Ingress (ingress.enabled: false) - in-cluster only
|
||||
{{ end }}{{ if $adminHostname }} Admin: {{ include "keycloak.adminUrl" . }}/admin
|
||||
{{ else }} Admin: no admin hostname (ingress.admin.enabled: false) - the console is
|
||||
served on the public host and Keycloak generates its URLs for it
|
||||
{{ end }} In-cluster: http://{{ $svc }}.{{ .Release.Namespace }}.svc.cluster.local:{{ $settings.ports.http | int64 }}
|
||||
|
||||
Management port {{ $settings.ports.management | int64 }} is NOT published and must never be: it serves
|
||||
/health, /health/live, /health/ready, /health/started{{ if $settings.metrics.enabled }} and /metrics{{ end }}
|
||||
WITHOUT authentication. It has a ClusterIP Service
|
||||
({{ .Release.Name }}-{{ .Chart.Name }}-management-service) for in-cluster scraping only.
|
||||
|
||||
THE 1PASSWORD ITEMS, AND WHICH FIELDS THEY MUST CARRY
|
||||
The operator materializes one Secret key per item FIELD, so a missing or
|
||||
misnamed field leaves the pod in CreateContainerConfigError - with no message
|
||||
naming the field.
|
||||
|
||||
{{ .Values.secrets.databaseSecretRef | default "<secrets.databaseSecretRef NOT SET>" }}
|
||||
username the PostgreSQL role
|
||||
password that role's password
|
||||
The role must be able to CREATE its schema: Keycloak builds and migrates
|
||||
the whole schema itself on first start.
|
||||
{{ if $settings.bootstrapAdmin.enabled }}
|
||||
{{ .Values.secrets.bootstrapAdminSecretRef | default "<secrets.bootstrapAdminSecretRef NOT SET>" }}
|
||||
username the TEMPORARY bootstrap administrator (upstream default name:
|
||||
temp-admin)
|
||||
password its password
|
||||
{{ else }}
|
||||
(no bootstrap admin item - keycloak.bootstrapAdmin.enabled is false, which is
|
||||
the correct state once a permanent administrator exists)
|
||||
{{ end }}
|
||||
Both arrive as ENVIRONMENT VARIABLES, not as files, and that is forced:
|
||||
Keycloak 26 has no _FILE form for an option's value. The price: the password
|
||||
is in the container's process environment.
|
||||
|
||||
FIRST INSTALL - AND STEP 4 IS MANDATORY, NOT ADVICE
|
||||
|
||||
1. kubectl -n {{ .Release.Namespace }} rollout status deployment/{{ $deploy }}
|
||||
Ready means /health/ready answered 200, which means the database is
|
||||
reachable and the schema is in place. The first start is the slow one: it
|
||||
creates the entire schema through Liquibase, and with {{ $replicas }} replica(s)
|
||||
the second waits on the first's migration lock.
|
||||
|
||||
2. Check that the cluster really formed. THIS STEP IS NOT OPTIONAL AND NO
|
||||
PROBE REPLACES IT: 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 install-test cannot catch it either.
|
||||
kubectl -n {{ .Release.Namespace }} logs deployment/{{ $deploy }} | grep -i "ISPN000094\|view accepted\|members"
|
||||
Look for a view with {{ $replicas }} members. One member per pod means
|
||||
jdbc-ping did not find the peer - check that both pods can reach each
|
||||
other on port {{ $settings.ports.jgroups | int64 }} and that they really share one database.
|
||||
|
||||
3. Log in at {{ if $adminHostname }}{{ include "keycloak.adminUrl" . }}/admin{{ else if .Values.ingress.enabled }}{{ include "keycloak.frontendUrl" . }}/admin{{ else }}the in-cluster address, port-forwarded{{ end }}
|
||||
with the bootstrap credentials from the vault item above.
|
||||
|
||||
4. CREATE A PERMANENT ADMINISTRATOR, then remove the temporary one.
|
||||
The bootstrap account is TEMPORARY by design - upstream: "the account
|
||||
should exist only for the duration necessary to perform operations needed
|
||||
to gain permanent and more secure admin access. After that, the account
|
||||
needs to be removed manually." This is different from Keycloak before 26,
|
||||
where KEYCLOAK_ADMIN created a regular administrator and there was nothing
|
||||
to clean up.
|
||||
a) In the master realm, create a user, set a password, assign the
|
||||
realm-admin / admin role.
|
||||
b) Verify you can log in as that user.
|
||||
c) Delete the temp-admin user.
|
||||
d) helm upgrade with keycloak.bootstrapAdmin.enabled: false
|
||||
|
||||
5. Until step 4d, EVERY START LOGS a line about the initial administrator not
|
||||
being created, because the master realm already exists and the bootstrap
|
||||
path is skipped. It is harmless. It is also the kind of message somebody
|
||||
eventually investigates, which is why 4d is part of the install and not a
|
||||
tidy-up.
|
||||
|
||||
MINOR UPGRADES NEED THE CLUSTER DOWN - AND THE AUTOMERGE DOES NOT KNOW THAT
|
||||
|
||||
Keycloak supports a rolling update only WITHIN a minor line. A minor jump
|
||||
(26.8 -> 26.9) requires every node shut down before any new node starts;
|
||||
mixing two minor versions in one Infinispan cluster is not a supported
|
||||
configuration, and nothing in the cluster reports that it happened.
|
||||
|
||||
THIS REPOSITORY'S RENOVATE AUTOMERGES EVERY UPDATE TYPE, MAJORS INCLUDED.
|
||||
That is a deliberate, accepted risk and not an oversight (Gitea #83), but it
|
||||
means an app-version PR that moves the minor can reach main, publish the
|
||||
chart and be rolled out as a rolling update without anybody deciding to.
|
||||
Watch app-version PRs for a changed minor, and if one has landed, take this
|
||||
path rather than letting the rollout happen:
|
||||
|
||||
1. Decide it with the tool instead of the version number. With the OLD
|
||||
version and the OLD configuration:
|
||||
kc.sh update-compatibility metadata --file=/tmp/kc-compat.json
|
||||
then with the NEW version and the NEW configuration:
|
||||
kc.sh update-compatibility check --file=/tmp/kc-compat.json
|
||||
Pass EVERY configuration option on both calls - upstream: "Omitting any
|
||||
configuration options results in incomplete metadata, and could lead to
|
||||
a wrong reported result".
|
||||
Exit codes, which is what to automate on: 0 rolling update possible,
|
||||
1 unexpected error, 2 invalid CLI option, 3 and 4 not possible - the
|
||||
deployment must be shut down.
|
||||
|
||||
2. If it says shut down:
|
||||
helm upgrade ... --set scaleDown=true # every pod stops
|
||||
# back up the database AND the configuration before anything else
|
||||
helm upgrade ... --set scaleDown=false # with the new version
|
||||
|
||||
A NOTE ON WHAT IS NOT SETTLED: upstream's 26.6 release notes call patch-level
|
||||
rolling updates supported and active by default, while the 26.8.0 page for
|
||||
update-compatibility says a rolling update is possible "when the Keycloak
|
||||
version is the same" and uses a PATCH pair (26.2.0 -> 26.2.1) as its example
|
||||
of an incompatible one. Those two cannot both be current. Until it is clear,
|
||||
run the check for patch updates too and trust its exit code rather than the
|
||||
size of the version step.
|
||||
|
||||
TRAPS WORTH KNOWING BEFORE YOU NEED THEM
|
||||
|
||||
- THE ADMIN HOSTNAME IS NOT AN ACCESS CONTROL. hostname-admin decides which
|
||||
URLs the server generates, nothing more. Upstream: "Using the
|
||||
hostname-admin option does not prevent accessing the Administration REST
|
||||
API endpoints via the frontend URL specified by the hostname option. If you
|
||||
want to restrict access to the Administration REST API, you need to do it
|
||||
on the reverse proxy level." So /admin/realms/... is reachable through the
|
||||
public host whatever you put on the admin host, and an IP allow-list on the
|
||||
admin Ingress restricts nothing on its own. This chart renders no deny rule
|
||||
for /admin - see values.yaml for why not.
|
||||
|
||||
- A SHARED DATABASE MERGES TWO CLUSTERS. Node discovery runs through the
|
||||
database, so - upstream - "clusters that share the same database but should
|
||||
remain separate will merge unless they have distinct cluster names. The
|
||||
default cluster name is ISPN." Two releases of this chart on one database
|
||||
therefore form ONE cluster, silently.
|
||||
{{ with $settings.cache.clusterName }}This release sets cache-embedded-cluster-name={{ . }}.{{ else }}This release uses the default name (ISPN) - keycloak.cache.clusterName is
|
||||
unset. Read its comment in values.yaml before setting it: upstream also
|
||||
warns about cache invalidation between differently-named clusters.{{ end }}
|
||||
|
||||
- READINESS ONLY CHECKS THE DATABASE WHILE metrics-enabled IS TRUE. Upstream's
|
||||
table of health checks has a "Requires Metrics" column and the DATABASE
|
||||
check is the row that says yes. So with keycloak.metrics.enabled false,
|
||||
/health/ready answers 200 with the database gone and the pod keeps taking
|
||||
logins it cannot serve. This release has it {{ if $settings.metrics.enabled }}ON, which is the chart default
|
||||
and the reason the default is what it is.{{ else }}OFF, so the readiness probe
|
||||
cannot currently detect a database outage.{{ end }}
|
||||
|
||||
- THERE IS NO ConfigMap IN THIS CHART, so no checksum/config annotation. That
|
||||
is not an omission: Keycloak is configured entirely through options and
|
||||
KC_* variables, so there is no config file whose change could fail to
|
||||
restart anything.
|
||||
|
||||
- A CHANGE INSIDE A VAULT ITEM RESTARTS NOTHING. Helm never sees it. After
|
||||
rotating a field:
|
||||
kubectl -n {{ .Release.Namespace }} rollout restart deployment/{{ $deploy }}
|
||||
(Repointing a secrets.*SecretRef at a different item DOES roll the pods -
|
||||
that is covered by checksum/secret-ref.)
|
||||
|
||||
- THE POD HAS TWO INIT CONTAINERS AND NEEDS BOTH. db, health-enabled and
|
||||
metrics-enabled are Keycloak BUILD options, the published image is built
|
||||
with their defaults, and a runtime change makes Keycloak rewrite
|
||||
/opt/keycloak/lib/quarkus at start - which readOnlyRootFilesystem: true
|
||||
forbids. So one init container seeds that directory into a volume and the
|
||||
next runs `kc.sh build` into it; the serving container then runs
|
||||
`start --optimized`. If a pod is stuck in Init, read those logs first:
|
||||
kubectl -n {{ .Release.Namespace }} logs <pod> -c {{ .Release.Name }}-{{ .Chart.Name }}-build
|
||||
Build options are set ONLY through keycloak.db.vendor,
|
||||
keycloak.metrics.enabled and keycloak.features. Putting one into
|
||||
keycloak.env fails the render on purpose.
|
||||
|
||||
- POOL SIZES ARE PER REPLICA. keycloak.db.pool.maxSize is {{ $settings.db.pool.maxSize | int64 }}, so with the
|
||||
current {{ $replicas }} replica(s) this release can open up to {{ mul ($settings.db.pool.maxSize | int64) $replicas }} connections.
|
||||
Upstream's default is 100 per node, which with two nodes exceeds a default
|
||||
PostgreSQL's max_connections of 100. Raise it together with the server,
|
||||
never alone.
|
||||
{{ if and $settings.podDisruptionBudget.enabled (ge $replicas 2) }}
|
||||
- A PodDisruptionBudget IS in place ({{ .Release.Name }}-{{ .Chart.Name }}-pdb, minAvailable
|
||||
{{ $settings.podDisruptionBudget.minAvailable | int64 }}), so a node drain cannot take all replicas at once.
|
||||
{{ else if $settings.podDisruptionBudget.enabled }}
|
||||
- NO PodDisruptionBudget was rendered, because the effective replica count is
|
||||
{{ $replicas }}. That is deliberate: minAvailable 1 with a single replica forbids every
|
||||
voluntary eviction, so `kubectl drain` would block forever on a pod nothing
|
||||
may move - and the error would name the PDB rather than the replica count.
|
||||
Run two replicas to get the budget.
|
||||
{{ else }}
|
||||
- NO PodDisruptionBudget (keycloak.podDisruptionBudget.enabled is false). A
|
||||
node drain may take every replica at once.
|
||||
{{ end }}
|
||||
WHAT THIS CHART DOES NOT RENDER
|
||||
|
||||
- No PVC and no StatefulSet: Keycloak 26 keeps everything in the database,
|
||||
user sessions included (persistent sessions are the default; volatile ones
|
||||
are deprecated as of 26.8). There is no disk state to keep.
|
||||
- No NetworkPolicy. jdbc-ping opens direct pod-to-pod connections on port
|
||||
{{ $settings.ports.jgroups | int64 }}, so any policy added later has to allow that between the
|
||||
replicas, or the cluster silently becomes two islands.
|
||||
- No deny rule for /admin on the public Ingress, and no restriction of the
|
||||
administration REST API. That is reverse-proxy work - see the first trap.
|
||||
@@ -1,545 +0,0 @@
|
||||
{{/*
|
||||
Render the body of a container "resources:" block from a values dict of the form
|
||||
|
||||
resources:
|
||||
limitFactor: 3 # optional, default 3
|
||||
requests: { cpu, memory, ephemeral-storage / ephemeralStorage, ... }
|
||||
limits: { ... } # optional, every entry optional
|
||||
|
||||
Rules (variant B, see issue #19):
|
||||
- requests are rendered as given (camelCase "ephemeralStorage" is normalized
|
||||
to the Kubernetes key "ephemeral-storage").
|
||||
- an explicitly set limit always wins.
|
||||
- a missing memory / ephemeral-storage limit is computed as
|
||||
limitFactor x the corresponding request.
|
||||
- a missing cpu limit is NOT set at all (the kubelet then treats CPU as
|
||||
unlimited; CPU is compressible, so this cannot kill the workload).
|
||||
|
||||
NOTE: this helper is duplicated into every chart's templates/_helpers.tpl with
|
||||
the chart's name as define prefix, because sharing it would mean a library
|
||||
chart, and a dependency is admissible but expensive (README, guideline 14) -
|
||||
this chart takes none. The reference implementation is
|
||||
template-chart/templates/_helpers.tpl.
|
||||
*/}}
|
||||
{{- define "keycloak.resources" -}}
|
||||
{{- $resources := . | default dict -}}
|
||||
{{- $factor := 3.0 -}}
|
||||
{{- if not (kindIs "invalid" $resources.limitFactor) -}}
|
||||
{{- $factor = float64 $resources.limitFactor -}}
|
||||
{{- end -}}
|
||||
{{- $requests := dict -}}
|
||||
{{- range $key, $value := ($resources.requests | default dict) -}}
|
||||
{{- if not (kindIs "invalid" $value) -}}
|
||||
{{- $_ := set $requests (include "keycloak.resourceKey" $key) $value -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $limits := dict -}}
|
||||
{{- range $key, $value := ($resources.limits | default dict) -}}
|
||||
{{- if not (kindIs "invalid" $value) -}}
|
||||
{{- $_ := set $limits (include "keycloak.resourceKey" $key) $value -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- range $key, $request := $requests -}}
|
||||
{{- if and (ne $key "cpu") (not (hasKey $limits $key)) -}}
|
||||
{{- $_ := set $limits $key (include "keycloak.multiplyQuantity" (dict "quantity" $request "factor" $factor)) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $out := dict -}}
|
||||
{{- with $requests }}{{- $_ := set $out "requests" . }}{{- end -}}
|
||||
{{- with $limits }}{{- $_ := set $out "limits" . }}{{- end -}}
|
||||
{{- toYaml $out -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Normalize a resource name from values to its Kubernetes key.
|
||||
*/}}
|
||||
{{- define "keycloak.resourceKey" -}}
|
||||
{{- if eq . "ephemeralStorage" -}}ephemeral-storage{{- else -}}{{ . }}{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Multiply a Kubernetes quantity by a factor, keeping its suffix.
|
||||
Input: dict "quantity" <string|number> "factor" <float64>.
|
||||
*/}}
|
||||
{{- define "keycloak.multiplyQuantity" -}}
|
||||
{{- $quantity := include "keycloak.formatNumber" .quantity -}}
|
||||
{{- $number := regexFind "^[0-9]+(\\.[0-9]+)?" $quantity -}}
|
||||
{{- if not $number -}}
|
||||
{{- fail (printf "cannot parse resource quantity %q" $quantity) -}}
|
||||
{{- end -}}
|
||||
{{- $suffix := trimPrefix $number $quantity -}}
|
||||
{{- if not (regexMatch "^(m|k|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$" $suffix) -}}
|
||||
{{- fail (printf "unsupported resource quantity suffix %q in %q" $suffix $quantity) -}}
|
||||
{{- end -}}
|
||||
{{- $product := mulf (float64 $number) .factor -}}
|
||||
{{- if eq $product (floor $product) -}}
|
||||
{{- printf "%d%s" (int64 $product) $suffix -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s%s" (include "keycloak.formatNumber" $product) $suffix -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Render a number as a plain decimal string, never in exponential notation
|
||||
(issue #157): the null-safe fallback helpers pass values through fromYaml,
|
||||
which parses every number as float64, and Go's "%v" switches float64 to
|
||||
exponential notation from about 1e6 upwards - "1.048576e+06" is not a valid
|
||||
Kubernetes quantity. Strings ("700Mi") are passed through untouched.
|
||||
*/}}
|
||||
{{- define "keycloak.formatNumber" -}}
|
||||
{{- if kindIs "float64" . -}}
|
||||
{{- regexReplaceAll "\\.?0+$" (printf "%.10f" .) "" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%v" . -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Merge a user-supplied values sub-tree onto a dict of chart defaults so that a
|
||||
null / empty override falls back to the defaults instead of erasing them
|
||||
(issues #61, #99). Input: dict "defaults" <dict> "given" <dict|nil> "path"
|
||||
<string>. Output: YAML of the merged dict (consume it with fromYaml).
|
||||
|
||||
An override key written without children is YAML null, and Helm treats that
|
||||
null as a deletion of the chart's default. This helper defines the opposite
|
||||
semantics - an empty block means "chart defaults apply" - while an explicitly
|
||||
set value still wins, including false and 0.
|
||||
*/}}
|
||||
{{- define "keycloak.defaultedDict" -}}
|
||||
{{- $defaults := .defaults | default dict -}}
|
||||
{{- $given := .given -}}
|
||||
{{- $path := .path | default "value" -}}
|
||||
{{- if kindIs "invalid" $given -}}
|
||||
{{- $given = dict -}}
|
||||
{{- end -}}
|
||||
{{- if not (kindIs "map" $given) -}}
|
||||
{{- fail (printf "%s must be a mapping or null, got %s (%v)" $path (kindOf $given) $given) -}}
|
||||
{{- end -}}
|
||||
{{- $out := deepCopy $defaults -}}
|
||||
{{- range $key, $value := $given -}}
|
||||
{{- if not (kindIs "invalid" $value) -}}
|
||||
{{- $default := index $defaults $key -}}
|
||||
{{- if and (kindIs "map" $value) (kindIs "map" $default) -}}
|
||||
{{- $_ := set $out $key (fromYaml (include "keycloak.defaultedDict" (dict "defaults" $default "given" $value "path" (printf "%s.%s" $path $key)))) -}}
|
||||
{{- else if kindIs "map" $default -}}
|
||||
{{- fail (printf "%s.%s must be a mapping or null, got %s (%v)" $path $key (kindOf $value) $value) -}}
|
||||
{{- else -}}
|
||||
{{- $_ := set $out $key $value -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- toYaml $out -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Pick one sub-block out of a values block that may itself be null.
|
||||
Input: dict "block" <dict|nil> "key" <string> "path" <string>.
|
||||
Returns YAML of dict "value" <the sub-block, may be nil>.
|
||||
*/}}
|
||||
{{- define "keycloak.subBlock" -}}
|
||||
{{- $block := .block -}}
|
||||
{{- if kindIs "invalid" $block -}}
|
||||
{{- $block = dict -}}
|
||||
{{- end -}}
|
||||
{{- if not (kindIs "map" $block) -}}
|
||||
{{- fail (printf "%s must be a mapping or null, got %s (%v)" .path (kindOf $block) $block) -}}
|
||||
{{- end -}}
|
||||
{{- toYaml (dict "value" (index $block .key)) -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
THE EFFECTIVE KEYCLOAK SETTINGS - everything the Deployment, the Services, the
|
||||
PodDisruptionBudget and NOTES.txt read out of .Values.keycloak, merged onto this
|
||||
chart's defaults.
|
||||
|
||||
Why the whole settings block goes through the null-safe helper and not only the
|
||||
three usual suspects (guideline 12.2: "the rule is about the shape, not about
|
||||
those three block kinds"): every value below is dereferenced by a template, and
|
||||
an erased one would not abort the render - it would render a containerPort with
|
||||
no number, a `--db=` with an empty right-hand side, or a database host that is
|
||||
the empty string. The first is rejected by the API server at apply time, the
|
||||
second fails the BUILD inside the init container, and the third is a pod waiting
|
||||
on a connection that cannot be made. All three arrive after the render looked
|
||||
fine, which is exactly the class #99 is about. satisfactory shipped the same
|
||||
shape with half its blocks null-safe and half not (Gitea #16), and the half that
|
||||
was not took the Service's port with it.
|
||||
|
||||
The defaults here MUST stay in sync with values.yaml, which remains the
|
||||
documented, user-facing place for them; this copy is only the fallback.
|
||||
|
||||
Usage: {{ $settings := fromYaml (include "keycloak.settings" .) }}
|
||||
*/}}
|
||||
{{- define "keycloak.settings" -}}
|
||||
{{- $given := (fromYaml (include "keycloak.subBlock" (dict "block" .Values "key" "keycloak" "path" ".Values"))).value -}}
|
||||
{{- $defaults := dict
|
||||
"replicas" 2
|
||||
"db" (dict
|
||||
"vendor" "postgres"
|
||||
"host" nil
|
||||
"port" 5432
|
||||
"database" "keycloak"
|
||||
"pool" (dict "initialSize" 2 "minSize" 2 "maxSize" 20))
|
||||
"bootstrapAdmin" (dict "enabled" true)
|
||||
"metrics" (dict "enabled" true)
|
||||
"features" (list)
|
||||
"cache" (dict "clusterName" nil)
|
||||
"ports" (dict "http" 8080 "management" 9000 "jgroups" 7800)
|
||||
"podDisruptionBudget" (dict "enabled" true "minAvailable" 1) -}}
|
||||
{{- include "keycloak.defaultedDict" (dict "defaults" $defaults "given" $given "path" "keycloak") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Render the replica count for the Deployment.
|
||||
|
||||
Input: dict "scaleDown" <flag|nil> "replicas" <value|nil>.
|
||||
|
||||
- scaleDown: true takes precedence and always yields 0.
|
||||
- otherwise an explicitly set replicas value is used as given, including an
|
||||
explicit 0 - Helm's "default" would swallow 0, hence the kindIs "invalid"
|
||||
nil check.
|
||||
- a missing replicas value defaults to 2.
|
||||
|
||||
A NOTE SPECIFIC TO THIS CHART: the default is 2, not 1, and that is the point
|
||||
of the chart rather than a tuning choice - an SSO outage takes every dependent
|
||||
login with it. Two replicas are cheap here because node discovery runs over the
|
||||
database that is there anyway (cache=ispn plus Keycloak's default jdbc-ping
|
||||
stack). What two replicas cost is the rolling-update restriction: upstream
|
||||
supports a rolling update only inside a minor line, so a minor jump becomes a
|
||||
procedure instead of a restart. See NOTES.txt.
|
||||
*/}}
|
||||
{{- define "keycloak.replicas" -}}
|
||||
{{- if .scaleDown -}}
|
||||
0
|
||||
{{- else if not (kindIs "invalid" .replicas) -}}
|
||||
{{- .replicas -}}
|
||||
{{- else -}}
|
||||
2
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
THE BUILD OPTIONS, IN ONE PLACE - and this helper being the ONLY place is what
|
||||
keeps the chart startable (Gitea #83).
|
||||
|
||||
Keycloak separates BUILD options from runtime options. Verified against the
|
||||
26.8.0 "All configuration" table, where a build option carries the tool icon:
|
||||
db, health-enabled, metrics-enabled, features and features-disabled are build
|
||||
options; db-url-host, db-username, db-password, the db-pool-* sizes, hostname,
|
||||
hostname-admin, hostname-strict, proxy-headers, http-enabled, http-port,
|
||||
http-management-port, cache, cache-stack and the cache-embedded-* options are
|
||||
not.
|
||||
|
||||
The published image is augmented with the build options' DEFAULTS (db=dev-file,
|
||||
health-enabled=false, metrics-enabled=false). Supplying different ones at
|
||||
runtime makes Keycloak re-augment itself at start, and the augmentation
|
||||
rewrites /opt/keycloak/lib/quarkus - inside the root filesystem, which
|
||||
readOnlyRootFilesystem: true makes read-only. Upstream's container guide calls
|
||||
that start what it is: it "creates an image that is mutable".
|
||||
|
||||
So the BUILD init container receives exactly these options and the serving
|
||||
container runs `start --optimized` and passes NO build option at all. That
|
||||
asymmetry is deliberate: with `--optimized` Keycloak expects a pre-built
|
||||
installation and a build option handed to it alongside is an error, and - more
|
||||
importantly - there is no second list that could drift out of step with this
|
||||
one. If a build option must be added, it is added HERE and nowhere else.
|
||||
|
||||
THEY ARE RENDERED AS ENVIRONMENT VARIABLES, NOT AS CLI ARGUMENTS, and that is a
|
||||
safety decision rather than a style one. `kc.sh build` reads KC_* exactly as it
|
||||
reads flags, and as variables the values are handed over by the kubelet instead
|
||||
of being parsed by a shell - so a feature name containing a space cannot become
|
||||
two arguments, and nothing here has to be shell-quoted.
|
||||
|
||||
health-enabled is hard-coded true rather than exposed as a value: all three
|
||||
probes address /health/* on the management port, so a release with it switched
|
||||
off would have no working probe at all - the same defect teamspeak-server's
|
||||
port name exists to prevent (guideline 12.5).
|
||||
*/}}
|
||||
{{- define "keycloak.buildEnv" -}}
|
||||
{{- $settings := fromYaml (include "keycloak.settings" .) -}}
|
||||
- name: KC_DB
|
||||
value: {{ required "A database vendor must be provided (keycloak.db.vendor)" $settings.db.vendor | quote }}
|
||||
- name: KC_HEALTH_ENABLED
|
||||
value: "true"
|
||||
- name: KC_METRICS_ENABLED
|
||||
value: {{ $settings.metrics.enabled | toString | quote }}
|
||||
{{- with $settings.features }}
|
||||
- name: KC_FEATURES
|
||||
value: {{ join "," . | quote }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The variable names of Keycloak BUILD options, used to refuse them in the
|
||||
additive env block (see keycloak.env below).
|
||||
|
||||
Read off the 26.8.0 option table. It is not claimed to be exhaustive - upstream
|
||||
can add a build option tomorrow and this list would not know - which is why it
|
||||
is the SECOND line of defence and not the first. The first is structural: the
|
||||
serving container is rendered with `start --optimized` and no build option, so
|
||||
there is no list of options on it to disagree with the init container's. This
|
||||
guard only stops somebody re-introducing one through values.
|
||||
*/}}
|
||||
{{- define "keycloak.buildOptionEnvNames" -}}
|
||||
- KC_DB
|
||||
- KC_DB_KIND
|
||||
- KC_DB_DRIVER
|
||||
- KC_HEALTH_ENABLED
|
||||
- KC_HTTP_MANAGEMENT_HEALTH_ENABLED
|
||||
- KC_METRICS_ENABLED
|
||||
- KC_FEATURES
|
||||
- KC_FEATURES_DISABLED
|
||||
- KC_HTTP_RELATIVE_PATH
|
||||
- KC_HTTP_MANAGEMENT_RELATIVE_PATH
|
||||
- KC_HTTPS_CLIENT_AUTH
|
||||
- KC_HTTPS_MANAGEMENT_CLIENT_AUTH
|
||||
- KC_TRANSACTION_XA_ENABLED
|
||||
- KC_LEGACY_OBSERVABILITY_INTERFACE
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The frontend URL (KC_HOSTNAME) and the admin URL (KC_HOSTNAME_ADMIN).
|
||||
|
||||
BOTH ARE FULL URLs, NOT BARE HOSTS, and that is upstream's requirement: "If
|
||||
hostname-admin is configured, hostname must be set to a URL (not just
|
||||
hostname). Otherwise Keycloak would not know what is the correct frontend URL
|
||||
(incl. port etc.)", and both values are "verified that full URL is used, incl.
|
||||
scheme and hostname". https:// is the only scheme this chart renders - the
|
||||
Ingress always terminates TLS, so the browser-facing scheme is https even
|
||||
though the pod itself serves plain HTTP.
|
||||
|
||||
ingress.domain is required HERE and not only in the Ingress template, because
|
||||
this is the read that makes it required even with ingress.enabled: false
|
||||
(guideline 12.1): Keycloak refuses to start in production mode without a
|
||||
hostname, hostname-strict being true by default.
|
||||
*/}}
|
||||
{{- define "keycloak.frontendUrl" -}}
|
||||
https://{{ required "A Domain/Host must be provided (ingress.domain) - Keycloak derives KC_HOSTNAME from it and refuses to start without a hostname, so it stays required even with ingress.enabled: false" .Values.ingress.domain }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "keycloak.adminUrl" -}}
|
||||
https://{{ required "An admin Domain/Host must be provided (ingress.admin.domain) while ingress.admin.enabled is true" (((.Values.ingress | default dict).admin | default dict).domain) }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Is the admin hostname in play? Only then are KC_HOSTNAME_ADMIN and the second
|
||||
Ingress rendered.
|
||||
|
||||
WHAT IT IS NOT: an access control. hostname-admin decides which URLs the server
|
||||
GENERATES; upstream states plainly that it "does not prevent accessing the
|
||||
Administration REST API endpoints via the frontend URL specified by the hostname
|
||||
option" and that restricting that API is reverse-proxy work. The full reasoning
|
||||
sits at ingress.admin.enabled in values.yaml, and the same sentence is in
|
||||
NOTES.txt and on the wiki page - three places on purpose, because the value's
|
||||
name suggests a boundary and the next reader will assume one.
|
||||
*/}}
|
||||
{{- define "keycloak.adminHostnameEnabled" -}}
|
||||
{{- $ingress := .Values.ingress | default dict -}}
|
||||
{{- $admin := $ingress.admin | default dict -}}
|
||||
{{- if kindIs "invalid" $admin.enabled -}}
|
||||
true
|
||||
{{- else -}}
|
||||
{{- $admin.enabled -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The effective security contexts, probes and resources (issue #99).
|
||||
|
||||
The defaults below MUST stay in sync with values.yaml. They reproduce THIS
|
||||
chart's defaults, not the repo standard - see the uid/gid note.
|
||||
*/}}
|
||||
{{- define "keycloak.podSecurityContext" -}}
|
||||
{{- $given := (fromYaml (include "keycloak.subBlock" (dict "block" . "key" "pod" "path" "keycloak.securityContext"))).value -}}
|
||||
{{- $defaults := dict
|
||||
"runAsNonRoot" true
|
||||
"runAsUser" 1000
|
||||
"runAsGroup" 1000
|
||||
"fsGroup" 1000
|
||||
"fsGroupChangePolicy" "Always"
|
||||
"seccompProfile" (dict "type" "RuntimeDefault") -}}
|
||||
{{- include "keycloak.defaultedDict" (dict "defaults" $defaults "given" $given "path" "keycloak.securityContext.pod") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "keycloak.containerSecurityContext" -}}
|
||||
{{- $given := (fromYaml (include "keycloak.subBlock" (dict "block" . "key" "container" "path" "keycloak.securityContext"))).value -}}
|
||||
{{- $defaults := dict
|
||||
"allowPrivilegeEscalation" false
|
||||
"readOnlyRootFilesystem" true
|
||||
"runAsNonRoot" true
|
||||
"capabilities" (dict "drop" (list "ALL") "add" (list)) -}}
|
||||
{{- include "keycloak.defaultedDict" (dict "defaults" $defaults "given" $given "path" "keycloak.securityContext.container") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
LIVENESS: /health/live on the management port, and deliberately NOT /health or
|
||||
/health/ready.
|
||||
|
||||
Guideline 12.3 in the one shape where upstream has already separated the
|
||||
checks: /health/live reports whether this process is alive and contains no
|
||||
database check, while /health/ready and the aggregate /health do. A liveness
|
||||
probe on either of those restarts every pod in the cluster for as long as the
|
||||
database is away - which cannot fix a database, and which turns a degraded login
|
||||
service into no login service. Same lesson as zipline; here it is one path
|
||||
away, which is also why the wrong path would look entirely plausible in a diff.
|
||||
*/}}
|
||||
{{- define "keycloak.livenessProbe" -}}
|
||||
{{- $defaults := dict
|
||||
"httpGet" (dict "path" "/health/live" "port" "management")
|
||||
"periodSeconds" 10
|
||||
"timeoutSeconds" 5
|
||||
"failureThreshold" 3 -}}
|
||||
{{- include "keycloak.defaultedDict" (dict "defaults" $defaults "given" . "path" "keycloak.livenessProbe") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
READINESS: /health/ready - which includes the database, and must. A node that
|
||||
cannot reach its database answers every login with an error, so no Service
|
||||
should route to it. This is the other half of guideline 12.3: a dependency-free
|
||||
liveness path is only correct when readiness really does hang on the
|
||||
dependency.
|
||||
|
||||
AND THE PATH ALONE DOES NOT GUARANTEE THAT, which is the part worth reading
|
||||
before anybody "cleans up" keycloak.metrics.enabled. Upstream's table of
|
||||
available checks carries a "Requires Metrics" column, and the DATABASE check -
|
||||
"Returns the status of the database connection pool" - is the row that says
|
||||
YES; Cluster, Graceful shutdown and Keycloak Initialized say no. So with
|
||||
metrics-enabled=false this endpoint still answers 200 while the database is
|
||||
gone, and the pod keeps taking logins it cannot serve.
|
||||
|
||||
That is exactly the urlaubsverwaltung shape (#218): the probe path was right
|
||||
and the aggregate behind it contained nothing external. Here the dependency
|
||||
hides behind an option whose name says observability, which is why
|
||||
keycloak.metrics.enabled defaults to TRUE and why its comment in values.yaml
|
||||
argues about readiness rather than about scraping.
|
||||
*/}}
|
||||
{{- define "keycloak.readinessProbe" -}}
|
||||
{{- $defaults := dict
|
||||
"httpGet" (dict "path" "/health/ready" "port" "management")
|
||||
"periodSeconds" 10
|
||||
"timeoutSeconds" 5
|
||||
"failureThreshold" 3 -}}
|
||||
{{- include "keycloak.defaultedDict" (dict "defaults" $defaults "given" . "path" "keycloak.readinessProbe") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
STARTUP: /health/started, Keycloak's own "finished booting" endpoint. It gates
|
||||
liveness and readiness, which is why neither carries an initialDelaySeconds.
|
||||
|
||||
Budget = periodSeconds * failureThreshold = 10 * 60 = 600s. What has to fit in
|
||||
it is not only the JVM and Infinispan: on a FIRST install Keycloak creates its
|
||||
whole schema through Liquibase, and with two replicas starting together the
|
||||
second waits on the first's migration lock. Too short a budget there is a
|
||||
CrashLoopBackOff that reads as an application fault and is really a stopwatch.
|
||||
|
||||
The Quarkus augmentation is NOT inside this budget - it runs in the init
|
||||
container, which has no probe and no deadline. That is the second thing the
|
||||
init container buys, after the read-only root filesystem.
|
||||
*/}}
|
||||
{{- define "keycloak.startupProbe" -}}
|
||||
{{- $defaults := dict
|
||||
"httpGet" (dict "path" "/health/started" "port" "management")
|
||||
"periodSeconds" 10
|
||||
"timeoutSeconds" 5
|
||||
"failureThreshold" 60 -}}
|
||||
{{- include "keycloak.defaultedDict" (dict "defaults" $defaults "given" . "path" "keycloak.startupProbe") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The effective resources blocks, fed into the resources helper above so that an
|
||||
erased "resources:" still yields the chart's requests and computed limits.
|
||||
|
||||
Two blocks, because the init container does something different from the
|
||||
serving container: a one-shot, CPU-bound Quarkus augmentation that is gone
|
||||
before the serving container starts.
|
||||
*/}}
|
||||
{{- define "keycloak.effectiveResources" -}}
|
||||
{{- $defaults := dict
|
||||
"limitFactor" 3
|
||||
"requests" (dict "cpu" "500m" "memory" "700Mi" "ephemeral-storage" "256Mi") -}}
|
||||
{{- $merged := fromYaml (include "keycloak.defaultedDict" (dict "defaults" $defaults "given" . "path" "keycloak.resources")) -}}
|
||||
{{- include "keycloak.resources" $merged -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "keycloak.buildResources" -}}
|
||||
{{- $given := (fromYaml (include "keycloak.subBlock" (dict "block" . "key" "resources" "path" "keycloak.build"))).value -}}
|
||||
{{- $defaults := dict
|
||||
"limitFactor" 3
|
||||
"requests" (dict "cpu" "500m" "memory" "512Mi" "ephemeral-storage" "256Mi") -}}
|
||||
{{- $merged := fromYaml (include "keycloak.defaultedDict" (dict "defaults" $defaults "given" $given "path" "keycloak.build.resources")) -}}
|
||||
{{- include "keycloak.resources" $merged -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The additive env block, rendered through tpl with the value, secretKeyRef and
|
||||
configMapKeyRef forms (guideline 10).
|
||||
|
||||
ADDITIVE MEANS ADDITIVE: an entry repeating a name this chart already sets does
|
||||
not replace it. At install time both reach the pod and the later one wins; on an
|
||||
EXISTING release the pair collapses to the chart's own value, because Kubernetes
|
||||
merges env by name - and helm upgrade still reports success. So do not use this
|
||||
block to override KC_HOSTNAME or the database variables; change the values that
|
||||
produce them.
|
||||
|
||||
AND ONE CLASS OF ENTRY IS REFUSED OUTRIGHT: a BUILD option's variable name. A
|
||||
build option in the serving container's environment disagrees with what the init
|
||||
container built, and Keycloak answers a disagreement by re-augmenting - onto a
|
||||
read-only path. The result is a crash loop whose cause is nowhere near the
|
||||
error message, so the render fails here instead, naming the variable. This is a
|
||||
`fail`, not a silent drop, for the reason guideline 12.6 gives: a value that
|
||||
cannot be honoured belongs in an abort, not in an implicit branch.
|
||||
*/}}
|
||||
{{- define "keycloak.env" -}}
|
||||
{{- $root := .root -}}
|
||||
{{- $buildNames := fromYamlArray (include "keycloak.buildOptionEnvNames" .) -}}
|
||||
{{- range $value := .env }}
|
||||
{{- if has (upper ($value.name | toString)) $buildNames }}
|
||||
{{- fail (printf "keycloak.env may not set %q: it is a Keycloak BUILD option, and a build option on the serving container disagrees with what the init container built - Keycloak then re-augments itself onto the read-only /opt/keycloak/lib/quarkus and crash-loops. Build options are set through the chart's own values (keycloak.db.vendor, keycloak.metrics.enabled, keycloak.features), which feed `kc.sh build` in the init container. See values.yaml, point 2." $value.name) }}
|
||||
{{- end }}
|
||||
{{- if $value.value }}
|
||||
- name: {{ $value.name | quote }}
|
||||
# quote is required, not cosmetic: a value like "8080" or "true" renders
|
||||
# unquoted as a YAML number/bool, and the API server then rejects the whole
|
||||
# manifest with "cannot unmarshal number into Go struct field EnvVar.value of
|
||||
# type string".
|
||||
value: {{ tpl $value.value $root | quote }}
|
||||
{{- else if $value.valueFrom }}
|
||||
{{- if $value.valueFrom.secretKeyRef }}
|
||||
- name: {{ $value.name }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ tpl $value.valueFrom.secretKeyRef.name $root | quote }}
|
||||
key: {{ tpl $value.valueFrom.secretKeyRef.key $root | quote }}
|
||||
{{- else if $value.valueFrom.configMapKeyRef }}
|
||||
- name: {{ $value.name }}
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ tpl $value.valueFrom.configMapKeyRef.name $root | quote }}
|
||||
key: {{ tpl $value.valueFrom.configMapKeyRef.key $root | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The three writable paths, defined once because two containers have to agree on
|
||||
them.
|
||||
|
||||
/opt/keycloak/lib/quarkus is the one that matters and the one that is not
|
||||
guessable: it is where the Quarkus augmentation writes, i.e. where `kc.sh
|
||||
build` puts generated-bytecode.jar, transformed-bytecode.jar,
|
||||
quarkus-application.dat and build-system.properties. Read out of the published
|
||||
image's layer, not inferred. The init container writes it, the serving
|
||||
container reads it, and because the directory is also the only place a
|
||||
re-augmentation would write, mounting it writable is what lets the rest of the
|
||||
root filesystem stay read-only.
|
||||
|
||||
/opt/keycloak/data is needed because Quarkus places its temporary directory
|
||||
under the Keycloak home rather than honouring java.io.tmpdir (upstream issue
|
||||
#37533). It is EMPTY in the image, so an emptyDir over it hides nothing.
|
||||
/opt/keycloak/lib/quarkus is NOT empty in the image - which is exactly why it
|
||||
is seeded by the init container rather than merely mounted; an unseeded
|
||||
emptyDir there hides quarkus-application.dat, and QuarkusEntryPoint loads that
|
||||
before any Keycloak code runs.
|
||||
*/}}
|
||||
{{- define "keycloak.quarkusBuildDir" -}}/opt/keycloak/lib/quarkus{{- end -}}
|
||||
{{- define "keycloak.dataDir" -}}/opt/keycloak/data{{- end -}}
|
||||
@@ -1,373 +0,0 @@
|
||||
{{- $settings := fromYaml (include "keycloak.settings" .) }}
|
||||
{{- $adminHostname := eq (include "keycloak.adminHostnameEnabled" .) "true" }}
|
||||
{{- /*
|
||||
WHY THIS WORKLOAD HAS TWO INIT CONTAINERS, read before changing either of them
|
||||
(Gitea #83). Three measured facts force the shape:
|
||||
|
||||
1. db, health-enabled and metrics-enabled are BUILD options (verified in the
|
||||
26.8.0 option table, where each carries the tool icon). The published
|
||||
image is augmented with their DEFAULTS - db=dev-file, health-enabled=false.
|
||||
2. Supplying different build options at runtime makes Keycloak re-augment
|
||||
itself at start, rewriting four files in /opt/keycloak/lib/quarkus. kc.sh
|
||||
says so itself: "only exit code 10 means that implicit reaugmentation
|
||||
occurred and a relaunch is needed". Upstream's container guide calls that
|
||||
start what it is - it "creates an image that is mutable", which is exactly
|
||||
what readOnlyRootFilesystem: true forbids. And not setting db is no way
|
||||
out: THE PROBES ALONE force it, because health-enabled is a build option.
|
||||
3. So the build has to happen before the serving container starts, into a
|
||||
volume. But an EMPTY emptyDir over /opt/keycloak/lib/quarkus does not
|
||||
work either, and this is the part that is not guessable:
|
||||
QuarkusEntryPoint - the main class kc.sh runs for EVERY command, `build`
|
||||
included - loads "quarkus/quarkus-application.dat" (the literal string is
|
||||
in QuarkusEntryPoint.class) before it dispatches anything. With that file
|
||||
hidden by a fresh mount, `kc.sh build` cannot start at all.
|
||||
|
||||
Hence: SEED copies the image's own augmentation output into the volume (reading
|
||||
the real directory, writing only into the volume - so a read-only root
|
||||
filesystem is fine), BUILD then mounts the now-seeded volume at the real path
|
||||
and re-augments INTO it (again writing only into the volume), and the serving
|
||||
container mounts the finished volume and runs `start --optimized`.
|
||||
|
||||
What that buys, and the reason it is worth two containers rather than one with
|
||||
readOnlyRootFilesystem: false: every container in this pod keeps the full
|
||||
hardening baseline. There is no documented deviation to carry, and no Trivy
|
||||
suppression that would also silence the serving container.
|
||||
*/ -}}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
labels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
spec:
|
||||
replicas: {{ include "keycloak.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.keycloak.replicas) }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
template:
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
labels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
annotations:
|
||||
# NO checksum/config HERE, AND THAT IS NOT AN OVERSIGHT. This chart
|
||||
# renders no ConfigMap at all - Keycloak is configured entirely through
|
||||
# options and KC_* variables - so there is no file whose change could
|
||||
# fail to restart the workload. The rule (#82) has nothing to attach
|
||||
# to. values.yaml states this at the top, because an absent annotation
|
||||
# looks identical to a forgotten one.
|
||||
#
|
||||
# Hash of the rendered OnePasswordItems. This covers the SOURCE of the
|
||||
# secrets, not their content: repointing secrets.databaseSecretRef or
|
||||
# secrets.bootstrapAdminSecretRef at a different vault item rolls the
|
||||
# pods instead of leaving the old credentials in the running processes.
|
||||
# A change INSIDE an item cannot be covered - Helm never sees it (see
|
||||
# keycloak.secret.yaml) - and NOTES.txt names the manual restart.
|
||||
checksum/secret-ref: {{ include (print $.Template.BasePath "/keycloak.secret.yaml") . | sha256sum }}
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
securityContext:
|
||||
{{- include "keycloak.podSecurityContext" .Values.keycloak.securityContext | nindent 8 }}
|
||||
initContainers:
|
||||
# --- 1. SEED -------------------------------------------------------
|
||||
# Copy the image's own augmentation output into the shared volume, so
|
||||
# that the BUILD container below can bootstrap at all (fact 3 in the
|
||||
# comment at the top of this file).
|
||||
#
|
||||
# It reads /opt/keycloak/lib/quarkus from the image and writes only
|
||||
# into the volume, which is why readOnlyRootFilesystem: true holds
|
||||
# here without any special handling.
|
||||
#
|
||||
# `command` is overridden because the image's entrypoint is kc.sh and
|
||||
# this step runs no Keycloak command - the only other chart in this
|
||||
# repo that overrides an entrypoint is convertx, and the same
|
||||
# obligation applies: this line has to be re-read whenever the app
|
||||
# version moves, because it depends on the image shipping a shell and
|
||||
# coreutils. Both are present (usr/bin/sh -> bash, usr/bin/cp, verified
|
||||
# in the image layer at 26.8.0).
|
||||
#
|
||||
# `cp -R` and not `cp -a`: -a preserves ownership, which a non-root
|
||||
# process cannot do - it would fail with a diagnostic and a non-zero
|
||||
# exit. Only the content and the directory structure matter here.
|
||||
- name: {{ .Release.Name }}-{{ .Chart.Name }}-seed
|
||||
image: "quay.io/keycloak/keycloak:{{ .Chart.AppVersion }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
args:
|
||||
- |
|
||||
set -eu
|
||||
cp -R /opt/keycloak/lib/quarkus/. /quarkus-build/
|
||||
ls -l /quarkus-build/
|
||||
resources:
|
||||
{{- include "keycloak.buildResources" .Values.keycloak.build | nindent 12 }}
|
||||
securityContext:
|
||||
{{- include "keycloak.containerSecurityContext" .Values.keycloak.securityContext | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: quarkus-build
|
||||
mountPath: /quarkus-build
|
||||
# --- 2. BUILD ------------------------------------------------------
|
||||
# The Quarkus augmentation, i.e. `kc.sh build`. It writes into the
|
||||
# seeded volume, which is mounted at the real path here - so the root
|
||||
# filesystem stays read-only and the output survives into the serving
|
||||
# container.
|
||||
#
|
||||
# THE BUILD OPTIONS ARRIVE AS ENVIRONMENT VARIABLES, NOT AS CLI
|
||||
# ARGUMENTS, and that is a safety decision rather than a style one:
|
||||
# `command` is a fixed shell string here, and interpolating chart
|
||||
# values into a shell string is how a feature name with a space in it
|
||||
# becomes two arguments. As variables they are passed by the kubelet
|
||||
# and never parsed by a shell. `kc.sh build` reads KC_* exactly as it
|
||||
# reads flags.
|
||||
#
|
||||
# This is the ONE place build options exist in this chart. The serving
|
||||
# container passes `start --optimized` and no build option at all, so
|
||||
# there is no second list to drift out of step - and keycloak.env
|
||||
# refuses a build option's variable name outright (see _helpers.tpl).
|
||||
- name: {{ .Release.Name }}-{{ .Chart.Name }}-build
|
||||
image: "quay.io/keycloak/keycloak:{{ .Chart.AppVersion }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- build
|
||||
env:
|
||||
{{- include "keycloak.buildEnv" . | nindent 12 }}
|
||||
resources:
|
||||
{{- include "keycloak.buildResources" .Values.keycloak.build | nindent 12 }}
|
||||
securityContext:
|
||||
{{- include "keycloak.containerSecurityContext" .Values.keycloak.securityContext | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: quarkus-build
|
||||
mountPath: {{ include "keycloak.quarkusBuildDir" . }}
|
||||
# Quarkus puts its temporary directory under the Keycloak home
|
||||
# rather than honouring java.io.tmpdir (upstream issue #37533), so
|
||||
# the augmentation needs both of these writable.
|
||||
- name: data
|
||||
mountPath: {{ include "keycloak.dataDir" . }}
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
containers:
|
||||
- name: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
# Fully qualified: an unqualified reference would resolve to Docker
|
||||
# Hub by convention rather than by rule. quay.io is Keycloak's own
|
||||
# registry and the first quay.io image in this repository - which is
|
||||
# why ci/trivy/data/ksv0125.yaml had to learn about it.
|
||||
image: "quay.io/keycloak/keycloak:{{ .Chart.AppVersion }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
# `start`, not `start-dev` - the development mode has insecure
|
||||
# defaults and a local cache. The image declares no CMD, only
|
||||
# ENTRYPOINT ["/opt/keycloak/bin/kc.sh"], so the subcommand has to
|
||||
# come from here.
|
||||
#
|
||||
# --optimized says "this installation is already built, do not check
|
||||
# and do not augment". It is what makes the read-only root filesystem
|
||||
# possible, and it is also why NO build option may be passed to this
|
||||
# container: with --optimized a build option is an error, and without
|
||||
# it the container would rebuild on a path it cannot write.
|
||||
args:
|
||||
- start
|
||||
- --optimized
|
||||
env:
|
||||
# THE HOSTNAME, DERIVED AND NOT LEFT OPEN - this is the
|
||||
# security-relevant pair of this chart. Upstream on proxy-headers:
|
||||
# "Misconfiguration might leave the server exposed to security
|
||||
# vulnerabilities." The failure mode is Host header injection: with
|
||||
# a hostname resolved from request headers, an attacker controls the
|
||||
# URLs the server generates, which is how tokens and password
|
||||
# resets end up pointed at somebody else's host.
|
||||
#
|
||||
# Two things keep that shut. hostname-strict stays at its default
|
||||
# true, so nothing is taken from the request. And hostname is a
|
||||
# FULL URL rather than a bare host, which upstream requires as soon
|
||||
# as hostname-admin is in play: "If hostname-admin is configured,
|
||||
# hostname must be set to a URL (not just hostname). Otherwise
|
||||
# Keycloak would not know what is the correct frontend URL".
|
||||
- name: KC_HOSTNAME
|
||||
value: {{ include "keycloak.frontendUrl" . | quote }}
|
||||
{{- if $adminHostname }}
|
||||
# THE ADMIN HOSTNAME - and it is NOT an access boundary. It decides
|
||||
# which URLs the server generates for the administration console
|
||||
# (redirects, assets, the API address the console itself calls).
|
||||
# Upstream, verbatim: "Using the hostname-admin option does not
|
||||
# prevent accessing the Administration REST API endpoints via the
|
||||
# frontend URL specified by the hostname option. If you want to
|
||||
# restrict access to the Administration REST API, you need to do it
|
||||
# on the reverse proxy level."
|
||||
#
|
||||
# So /admin/realms/... stays reachable through ingress.domain. This
|
||||
# chart renders no deny rule for it: a path rule on /admin that has
|
||||
# not been tested against a running console breaks redirects and
|
||||
# asset loading, and a measure that only looks like one is worse
|
||||
# than none. The full argument is at ingress.admin in values.yaml.
|
||||
- name: KC_HOSTNAME_ADMIN
|
||||
value: {{ include "keycloak.adminUrl" . | quote }}
|
||||
{{- end }}
|
||||
# Behind the Ingress the TLS ends at the controller, so the pod
|
||||
# itself serves plain HTTP - which Keycloak only does when told to.
|
||||
- name: KC_HTTP_ENABLED
|
||||
value: "true"
|
||||
# xforwarded, because the ingress-nginx controller sets
|
||||
# X-Forwarded-* and not RFC 7239 Forwarded. This is not optional
|
||||
# decoration: without it Keycloak sees the controller's address and
|
||||
# scheme instead of the client's.
|
||||
- name: KC_PROXY_HEADERS
|
||||
value: "xforwarded"
|
||||
- name: KC_HTTP_PORT
|
||||
value: {{ $settings.ports.http | int64 | quote }}
|
||||
- name: KC_HTTP_MANAGEMENT_PORT
|
||||
value: {{ $settings.ports.management | int64 | quote }}
|
||||
# CLUSTERING. ispn is already the default for `start` in production
|
||||
# mode; it is set explicitly because it is the decision that makes
|
||||
# the second replica a cluster member rather than a second island,
|
||||
# and an explicit value cannot be lost to a change of upstream
|
||||
# defaults.
|
||||
#
|
||||
# cache-stack is deliberately NOT set: the default is jdbc-ping,
|
||||
# which discovers peers through the database that is there anyway.
|
||||
# The alternative, cache-stack=kubernetes with DNS_PING, is marked
|
||||
# "(deprecated)" upstream and would need a headless Service and a
|
||||
# jgroups.dns.query - none of which this chart renders.
|
||||
- name: KC_CACHE
|
||||
value: "ispn"
|
||||
{{- with $settings.cache.clusterName }}
|
||||
# Only when set. The trap it addresses: "clusters that share the
|
||||
# same database but should remain separate will merge unless they
|
||||
# have distinct cluster names". See values.yaml - the default is
|
||||
# upstream's ISPN on purpose.
|
||||
- name: KC_CACHE_EMBEDDED_CLUSTER_NAME
|
||||
value: {{ . | quote }}
|
||||
{{- end }}
|
||||
# A stable node name, from the pod name. Upstream's default is "a
|
||||
# random name ... generated on each start, which makes it difficult
|
||||
# to correlate metrics and logs across restarts" - and with two
|
||||
# replicas that correlation is the first thing anybody needs when
|
||||
# the cluster misbehaves.
|
||||
- name: KC_CACHE_EMBEDDED_NODE_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.name
|
||||
# THE DATABASE. Note what is NOT here: KC_DB. The vendor is a build
|
||||
# option and is baked in by the init container above; setting it
|
||||
# here would make this container re-augment on a read-only path.
|
||||
- name: KC_DB_URL_HOST
|
||||
value: {{ required "A database host must be provided (keycloak.db.host)" $settings.db.host | quote }}
|
||||
- name: KC_DB_URL_PORT
|
||||
value: {{ $settings.db.port | int64 | quote }}
|
||||
- name: KC_DB_URL_DATABASE
|
||||
value: {{ required "A database name must be provided (keycloak.db.database)" $settings.db.database | quote }}
|
||||
- name: KC_DB_USERNAME
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-db-secret
|
||||
key: username
|
||||
# AS A VARIABLE, NOT A FILE, and that is forced: Keycloak 26 has no
|
||||
# _FILE indirection for an option's value - no KC_DB_PASSWORD_FILE
|
||||
# and no generic KC_<OPTION>_FILE. Checked against the 26.8.0
|
||||
# option table, where every KC_*_FILE variable is a path option
|
||||
# (certificates, key stores, trust stores, log and export files).
|
||||
# The price is named at secrets.databaseSecretRef in values.yaml.
|
||||
- name: KC_DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-db-secret
|
||||
key: password
|
||||
# POOL SIZES, per replica. Upstream's db-pool-max-size default is
|
||||
# 100, which with two replicas asks a default PostgreSQL
|
||||
# (max_connections = 100) for twice what it has. See values.yaml.
|
||||
- name: KC_DB_POOL_INITIAL_SIZE
|
||||
value: {{ $settings.db.pool.initialSize | int64 | quote }}
|
||||
- name: KC_DB_POOL_MIN_SIZE
|
||||
value: {{ $settings.db.pool.minSize | int64 | quote }}
|
||||
- name: KC_DB_POOL_MAX_SIZE
|
||||
value: {{ $settings.db.pool.maxSize | int64 | quote }}
|
||||
{{- if $settings.bootstrapAdmin.enabled }}
|
||||
# THE TEMPORARY BOOTSTRAP ADMINISTRATOR. Needed for a first
|
||||
# install, because Keycloak "only allows to create the initial
|
||||
# admin user from a local network connection", which a container
|
||||
# never has.
|
||||
#
|
||||
# Two things NOTES.txt repeats because they cost time otherwise:
|
||||
# these variable names exist only since Keycloak 26 (before:
|
||||
# KEYCLOAK_ADMIN / KEYCLOAK_ADMIN_PASSWORD), and the account they
|
||||
# create is TEMPORARY - creating a permanent administrator
|
||||
# afterwards is a mandatory step, not advice.
|
||||
- name: KC_BOOTSTRAP_ADMIN_USERNAME
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-bootstrap-admin-secret
|
||||
key: username
|
||||
- name: KC_BOOTSTRAP_ADMIN_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-bootstrap-admin-secret
|
||||
key: password
|
||||
{{- end }}
|
||||
{{- with .Values.keycloak.env }}
|
||||
{{- include "keycloak.env" (dict "root" $ "env" .) | nindent 12 }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- containerPort: {{ $settings.ports.http | int64 }}
|
||||
protocol: TCP
|
||||
name: http
|
||||
# THE MANAGEMENT PORT. All three probes address it BY NAME, so
|
||||
# renaming it here breaks them. It serves /health/* and - with
|
||||
# keycloak.metrics.enabled - /metrics, both UNAUTHENTICATED, which
|
||||
# is why no Ingress in this chart points at it and none should be
|
||||
# added.
|
||||
- containerPort: {{ $settings.ports.management | int64 }}
|
||||
protocol: TCP
|
||||
name: management
|
||||
# JGroups. Declared for the record rather than for routing: nothing
|
||||
# serves it and no Service exposes it. jdbc-ping finds the peers
|
||||
# through the database and then opens DIRECT pod-to-pod
|
||||
# connections on this port, so the replicas need plain L3
|
||||
# reachability between pod IPs - no Service, headless or otherwise.
|
||||
- containerPort: {{ $settings.ports.jgroups | int64 }}
|
||||
protocol: TCP
|
||||
name: jgroups
|
||||
# Probes, resources and both security contexts go through the
|
||||
# null-safe helpers (#99): an erased block means "chart defaults
|
||||
# apply" instead of a vanished probe or an unhardened container.
|
||||
# Never wrap these in an "if" - a guard hides the erased case
|
||||
# completely instead of rendering a visible null.
|
||||
#
|
||||
# Which probe points where, and why liveness must NOT use /health or
|
||||
# /health/ready, is argued at each helper in _helpers.tpl.
|
||||
livenessProbe:
|
||||
{{- include "keycloak.livenessProbe" .Values.keycloak.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- include "keycloak.readinessProbe" .Values.keycloak.readinessProbe | nindent 12 }}
|
||||
startupProbe:
|
||||
{{- include "keycloak.startupProbe" .Values.keycloak.startupProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- include "keycloak.effectiveResources" .Values.keycloak.resources | nindent 12 }}
|
||||
securityContext:
|
||||
{{- include "keycloak.containerSecurityContext" .Values.keycloak.securityContext | nindent 12 }}
|
||||
volumeMounts:
|
||||
# The finished augmentation, produced by the two init containers.
|
||||
# Writable, although --optimized means nothing should write here:
|
||||
# it is an emptyDir either way, and a writable mount is what keeps
|
||||
# a re-augmentation (should a future version decide it needs one) a
|
||||
# slow start rather than a crash loop.
|
||||
- name: quarkus-build
|
||||
mountPath: {{ include "keycloak.quarkusBuildDir" . }}
|
||||
# Empty in the image, so this hides nothing. Needed because Quarkus
|
||||
# places its temporary directory under the Keycloak home instead of
|
||||
# honouring java.io.tmpdir (upstream issue #37533).
|
||||
- name: data
|
||||
mountPath: {{ include "keycloak.dataDir" . }}
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
# DELIBERATELY NOT MOUNTED: /opt/keycloak/conf. It is not empty in
|
||||
# the image - it carries keycloak.conf and, more importantly,
|
||||
# cache-ispn.xml, which is the Infinispan configuration cache=ispn
|
||||
# reads. An emptyDir there would hide it and the cluster would come up
|
||||
# with no cache configuration.
|
||||
restartPolicy: Always
|
||||
volumes:
|
||||
- name: quarkus-build
|
||||
emptyDir: {}
|
||||
- name: data
|
||||
emptyDir: {}
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
@@ -1,108 +0,0 @@
|
||||
{{- /*
|
||||
TWO INGRESS OBJECTS, AND THE SECOND ONE IS NOT A SECURITY BOUNDARY.
|
||||
|
||||
The public one publishes the login endpoints, the token endpoints and the
|
||||
account console. The admin one publishes the SAME backend under a second
|
||||
hostname, so that the administration console - whose URLs Keycloak generates
|
||||
from hostname-admin - can live on an internal ingress class or in a different
|
||||
DNS zone.
|
||||
|
||||
WHAT IT DOES NOT DO, because the value's name invites the opposite assumption.
|
||||
Upstream, verbatim: "Using the hostname-admin option does not prevent accessing
|
||||
the Administration REST API endpoints via the frontend URL specified by the
|
||||
hostname option. If you want to restrict access to the Administration REST API,
|
||||
you need to do it on the reverse proxy level." So /admin/realms/... stays
|
||||
reachable through ingress.domain, and an IP allow-list on the admin host
|
||||
restricts nothing by itself.
|
||||
|
||||
This chart deliberately renders NO deny rule for /admin on the public Ingress.
|
||||
Two reasons, and the second is the one that decided it: the administration
|
||||
console loads assets and performs redirects across more paths than /admin, so a
|
||||
rule written from the path name breaks the console in ways that only show up in
|
||||
a browser; and a rule nobody has tested against a running console is a measure
|
||||
that looks like one, which is worse than an absent one. Whoever needs the
|
||||
restriction puts it on the proxy in front of the public host, where it can be
|
||||
tested.
|
||||
|
||||
GUIDELINE 12.1, CHECKED IN BOTH DIRECTIONS: ingress.domain is read a second
|
||||
time outside this template - as KC_HOSTNAME on the Deployment - so it stays
|
||||
required even with ingress.enabled: false, and its required() lives in the
|
||||
keycloak.frontendUrl helper rather than here. clusterIssuer is read only in
|
||||
this file, so switching both Ingresses off frees it.
|
||||
*/ -}}
|
||||
{{- $adminEnabled := eq (include "keycloak.adminHostnameEnabled" .) "true" }}
|
||||
{{- if .Values.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-ingress
|
||||
labels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: {{ required "A Cluster-Issuer must be provided (ingress.clusterIssuer)" .Values.ingress.clusterIssuer }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
rules:
|
||||
- host: {{ required "A Domain/Host must be provided (ingress.domain)" .Values.ingress.domain }}
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-service
|
||||
port:
|
||||
name: http
|
||||
path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ required "A Domain/Host must be provided (ingress.domain)" .Values.ingress.domain }}
|
||||
secretName: tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress
|
||||
{{- end }}
|
||||
{{- if $adminEnabled }}
|
||||
{{- if .Values.ingress.enabled }}
|
||||
---
|
||||
{{- end }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-admin-ingress
|
||||
labels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
annotations:
|
||||
# Its own certificate, for its own host. A separate TLS secret
|
||||
# (tls-<release>-keycloak-admin-ingress) rather than a shared one, because
|
||||
# these are two different hostnames that may well sit on two different
|
||||
# ingress classes - the shared-secret form in the style guide is for
|
||||
# several objects on ONE host.
|
||||
cert-manager.io/cluster-issuer: {{ required "A Cluster-Issuer must be provided (ingress.clusterIssuer)" .Values.ingress.clusterIssuer }}
|
||||
{{- with ((.Values.ingress.admin | default dict).annotations) }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
# Falls back to ingress.className when not set. An internal ingress class
|
||||
# here is the useful half of the admin hostname - and still not the access
|
||||
# control the comment at the top of this file is about.
|
||||
ingressClassName: {{ (.Values.ingress.admin | default dict).className | default .Values.ingress.className }}
|
||||
rules:
|
||||
- host: {{ required "An admin Domain/Host must be provided (ingress.admin.domain) while ingress.admin.enabled is true" ((.Values.ingress.admin | default dict).domain) }}
|
||||
http:
|
||||
paths:
|
||||
# The SAME backend and the same path as the public Ingress. The
|
||||
# administration console is not a separate application and not a
|
||||
# separate path - only the hostname differs, and Keycloak generates
|
||||
# its admin URLs for that hostname because of KC_HOSTNAME_ADMIN.
|
||||
- backend:
|
||||
service:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-service
|
||||
port:
|
||||
name: http
|
||||
path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ required "An admin Domain/Host must be provided (ingress.admin.domain) while ingress.admin.enabled is true" ((.Values.ingress.admin | default dict).domain) }}
|
||||
secretName: tls-{{ .Release.Name }}-{{ .Chart.Name }}-admin-ingress
|
||||
{{- end }}
|
||||
@@ -1,40 +0,0 @@
|
||||
{{- $settings := fromYaml (include "keycloak.settings" .) }}
|
||||
{{- /*
|
||||
THE MANAGEMENT SERVICE, AND WHAT IT IS AND IS NOT FOR.
|
||||
|
||||
Keycloak serves /health, /health/live, /health/ready, /health/started and -
|
||||
with keycloak.metrics.enabled - /metrics on the management port, and it serves
|
||||
them WITHOUT AUTHENTICATION. The probes do not need this Service (the kubelet
|
||||
addresses the container port directly); a metrics scraper or an uptime check
|
||||
inside the cluster does, which is the only reason it exists.
|
||||
|
||||
IT IS A SEPARATE OBJECT FROM THE SERVING SERVICE ON PURPOSE. Putting port 9000
|
||||
next to port 8080 on one Service would mean the Service an Ingress already
|
||||
points at also carries the unauthenticated health and metrics surface - and
|
||||
then publishing it is one wrong port name in an Ingress backend away, with
|
||||
nothing in the diff that reads as a mistake. Separated, naming it is a
|
||||
deliberate act.
|
||||
|
||||
AND IT MUST NEVER GET AN INGRESS. There is no value in this chart that would
|
||||
publish it, and none should be added. An unauthenticated readiness endpoint on
|
||||
the internet tells an attacker when the database is down; /metrics tells them
|
||||
the realm and client names. Upstream's own alternative for this - setting
|
||||
legacy-observability-interface=true, which moves health back onto the serving
|
||||
port where the Ingress already is - is explicitly "not recommended for security
|
||||
reasons", and this chart does not offer it.
|
||||
*/ -}}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-management-service
|
||||
labels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
spec:
|
||||
selector:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: {{ $settings.ports.management | int64 }}
|
||||
targetPort: management
|
||||
name: management
|
||||
type: ClusterIP
|
||||
@@ -1,38 +0,0 @@
|
||||
{{- $settings := fromYaml (include "keycloak.settings" .) }}
|
||||
{{- $replicas := int (include "keycloak.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.keycloak.replicas)) }}
|
||||
{{- /*
|
||||
THE FIRST PodDisruptionBudget IN THIS REPOSITORY, and the convention it sets is
|
||||
in README ("Pod disruption budgets") and in the wiki style guide.
|
||||
|
||||
WHY IT EXISTS HERE: with two replicas, a node drain is otherwise free to take
|
||||
both pods at once - and a drain is the most common reason a pod moves, because
|
||||
it is what a node upgrade does. Without this object the second replica buys
|
||||
nothing against the failure it was added for. minAvailable rather than
|
||||
maxUnavailable, because the guarantee wanted is about the service: at least one
|
||||
node answers logins throughout the drain.
|
||||
|
||||
AND WHY IT IS NOT RENDERED BELOW TWO REPLICAS - the trap that makes a PDB worth
|
||||
a condition rather than an unconditional object: minAvailable: 1 with exactly
|
||||
ONE replica forbids every voluntary eviction, so `kubectl drain` blocks forever
|
||||
on a pod nothing is allowed to move. The message then names the PDB and not the
|
||||
replica count, which sends the reader to the wrong file. The same applies to
|
||||
scaleDown: true, where there are no pods at all and the object would only
|
||||
describe a promise about nothing.
|
||||
|
||||
So: rendered when the effective replica count is >= 2, skipped otherwise, and
|
||||
NOTES.txt prints which of the two happened so that an operator does not have to
|
||||
guess from `kubectl get pdb`.
|
||||
*/ -}}
|
||||
{{- if and $settings.podDisruptionBudget.enabled (ge $replicas 2) }}
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-pdb
|
||||
labels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
spec:
|
||||
minAvailable: {{ $settings.podDisruptionBudget.minAvailable | int64 }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
{{- end }}
|
||||
@@ -1,84 +0,0 @@
|
||||
{{- /*
|
||||
The 1Password items this chart materializes. Two of them, because they have
|
||||
different lifetimes rather than different audiences: the database credential
|
||||
lives as long as the release, while the bootstrap administrator is meant to be
|
||||
switched off again after the first install.
|
||||
|
||||
WHAT THE FIELD NAMES ARE AND WHY THEY ARE NOT FREE-FORM: the operator
|
||||
materializes one Secret key per FIELD of the item, and the Deployment
|
||||
references those keys by name. An item without the expected field leaves the
|
||||
container in CreateContainerConfigError - not in a crash loop, and not with a
|
||||
message about the field. NOTES.txt lists all four fields for the installer.
|
||||
|
||||
THEY ARRIVE AS ENVIRONMENT VARIABLES AND THAT IS FORCED, NOT PREFERRED.
|
||||
Keycloak 26 has no _FILE indirection for an option's value: there is no
|
||||
KC_DB_PASSWORD_FILE and no generic KC_<OPTION>_FILE. Checked against the 26.8.0
|
||||
option table - every KC_*_FILE variable there is a PATH option (certificate,
|
||||
key store, trust store, log file, export file), i.e. "point at this file", not
|
||||
"read this option's value from a file". So the file form valkey argues for is
|
||||
simply unavailable here. DO NOT "FIX" THIS to a file mount: there is no option
|
||||
that would read it. (Keycloak's Vault SPI, KC_VAULT=file, is a different and
|
||||
larger mechanism with its own naming scheme, and adopting it would be its own
|
||||
decision.)
|
||||
|
||||
AND THE LIMIT THAT CANNOT BE CLOSED: a change to the CONTENT of either item
|
||||
restarts nothing. Helm never sees a secret the 1Password operator
|
||||
materializes, so there is nothing to hash. What this chart does cover is the
|
||||
op:// PATH, through checksum/secret-ref on the pod template. After rotating a
|
||||
value INSIDE an item the manual step is
|
||||
kubectl -n <ns> rollout restart deployment/<release>-keycloak-deployment
|
||||
|
||||
The 1Password operator's auto-restart annotation is NOT set here. Unlike the
|
||||
StatefulSet charts in this repo it WOULD work - the operator enumerates
|
||||
Deployments - so this is a decision rather than a constraint: its trigger is
|
||||
the item's version COUNTER and not its content, so a re-save with identical
|
||||
values rolls the whole cluster. For an identity provider, an unannounced
|
||||
rolling restart because somebody opened a vault entry is worse than a
|
||||
deliberate one. README guideline 9 has the precedence trap if it is ever set.
|
||||
*/ -}}
|
||||
{{- $settings := fromYaml (include "keycloak.settings" .) -}}
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-db-secret
|
||||
labels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
spec:
|
||||
# Must carry the fields: username, password
|
||||
#
|
||||
# The role needs to own its schema: Keycloak creates and migrates the whole
|
||||
# schema itself through Liquibase on first start, so a read-write grant on
|
||||
# existing tables is not enough.
|
||||
#
|
||||
# Host, port and database name are NOT in this item - they are plain values
|
||||
# (keycloak.db.host / .port / .database), because they are not secret and
|
||||
# because moving a database should not mean editing a vault entry.
|
||||
itemPath: {{ required "You must provide a database secret reference (secrets.databaseSecretRef) - the 1Password item holding the fields 'username' and 'password' of the PostgreSQL role Keycloak uses" .Values.secrets.databaseSecretRef }}
|
||||
{{- if $settings.bootstrapAdmin.enabled }}
|
||||
---
|
||||
apiVersion: onepassword.com/v1
|
||||
kind: OnePasswordItem
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-bootstrap-admin-secret
|
||||
labels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
spec:
|
||||
# Must carry the fields: username, password
|
||||
#
|
||||
# THIS ACCOUNT IS TEMPORARY, and that is a change from Keycloak before 26 -
|
||||
# the old KEYCLOAK_ADMIN / KEYCLOAK_ADMIN_PASSWORD pair created a regular
|
||||
# administrator. Upstream on the current pair: "A user or service admin
|
||||
# account created using one of the methods described below is temporary ...
|
||||
# the account should exist only for the duration necessary to perform
|
||||
# operations needed to gain permanent and more secure admin access. After
|
||||
# that, the account needs to be removed manually."
|
||||
#
|
||||
# So creating a permanent administrator after the first install is a
|
||||
# MANDATORY step, and switching keycloak.bootstrapAdmin.enabled to false
|
||||
# afterwards removes both this item and the per-start log line about the
|
||||
# initial administrator not being created. NOTES.txt has the sequence.
|
||||
#
|
||||
# Upstream's default for the username is "temp-admin"; this chart takes both
|
||||
# fields from the vault so the pair has one source of truth.
|
||||
itemPath: {{ required "You must provide a bootstrap admin secret reference (secrets.bootstrapAdminSecretRef) - the 1Password item holding the fields 'username' and 'password' of the TEMPORARY bootstrap administrator. Required while keycloak.bootstrapAdmin.enabled is true; set that to false once a permanent administrator exists" .Values.secrets.bootstrapAdminSecretRef }}
|
||||
{{- end }}
|
||||
@@ -1,30 +0,0 @@
|
||||
{{- $settings := fromYaml (include "keycloak.settings" .) }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-{{ .Chart.Name }}-service
|
||||
labels:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
spec:
|
||||
selector:
|
||||
app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
||||
ports:
|
||||
# The serving port. Both Ingresses target it BY NAME ("http"), and
|
||||
# in-cluster consumers reach it at
|
||||
# http://<release>-keycloak-service.<namespace>.svc.cluster.local:8080
|
||||
- protocol: TCP
|
||||
port: {{ $settings.ports.http | int64 }}
|
||||
targetPort: http
|
||||
name: http
|
||||
# DELIBERATELY NO MANAGEMENT PORT AND NO JGROUPS PORT HERE. The management
|
||||
# port has a Service of its own (keycloak.management.service.yaml) so that
|
||||
# whatever scrapes it has to name it explicitly rather than inherit it from
|
||||
# the Service an Ingress points at - mixing the two is how an
|
||||
# unauthenticated health endpoint ends up one annotation away from the
|
||||
# internet. JGroups needs no Service at all: jdbc-ping discovers peers
|
||||
# through the database and the nodes then talk pod IP to pod IP.
|
||||
#
|
||||
# ClusterIP, and no value to change it. The way out of the cluster is the
|
||||
# Ingress with a certificate, not a NodePort that puts an identity provider
|
||||
# on a routable address by accident.
|
||||
type: ClusterIP
|
||||
@@ -1,270 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft-07/schema#",
|
||||
"title": "keycloak values",
|
||||
"description": "Strict values schema (issue #68). additionalProperties is false on every chart-owned object level, so a key this chart does not read is rejected at render time instead of being silently ignored; the objects that go verbatim into the Kubernetes pod spec (probes, security contexts, global, free-form annotation maps) stay open, because the chart does not interpret their contents. The blocks the chart dereferences additionally accept null, because an erased block means 'chart defaults apply' (issue #99) rather than a vanished setting. Note what this schema CANNOT enforce: secrets.databaseSecretRef, keycloak.db.host, ingress.domain and ingress.admin.domain all ship as null and JSON Schema's 'required' tests presence, not null-ness, so a 'required' array would never fire - the enforcement is required() in the templates (issue #18).",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"global": {
|
||||
"description": "Helm's conventional cross-chart values. Unused by this chart, kept open so an umbrella install cannot break on it.",
|
||||
"type": "object"
|
||||
},
|
||||
"scaleDown": {
|
||||
"description": "true renders the Deployment with replicas: 0 and takes precedence over keycloak.replicas. Also the first half of the minor-upgrade procedure, because Keycloak supports a rolling update only inside a minor line.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"secrets": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"databaseSecretRef": {
|
||||
"description": "op:// item path. The item must carry the fields 'username' and 'password' of the PostgreSQL role, which has to be able to create its own schema. Always required - the chart has no default database.",
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"bootstrapAdminSecretRef": {
|
||||
"description": "op:// item path. The item must carry the fields 'username' and 'password' of the TEMPORARY bootstrap administrator. Required while keycloak.bootstrapAdmin.enabled is true.",
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"keycloak": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"replicas": {
|
||||
"description": "Default 2, because an SSO outage takes every dependent login with it. An explicit 0 is allowed and is the per-workload way to stop the release. Above 1 the rolling-update restriction applies: a minor version jump needs the whole cluster down.",
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"db": {
|
||||
"description": "May be null / empty: the chart then falls back to its defaults. The host has no default and is enforced by required() in the Deployment.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"vendor": {
|
||||
"description": "A Keycloak BUILD option - it is passed to `kc.sh build` in the init container, never to the serving container. dev-file and dev-mem are rejected on purpose: they are Keycloak's development defaults, and an identity provider on an embedded database works until the pod moves. Only 'postgres' is exercised by this chart's CI.",
|
||||
"type": "string",
|
||||
"enum": ["postgres", "mariadb", "mysql", "mssql", "oracle"]
|
||||
},
|
||||
"host": {
|
||||
"description": "Host of the external PostgreSQL. Required - enforced by required() in the Deployment, because a null here would render an empty KC_DB_URL_HOST and a pod that waits on a connection it can never make.",
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"port": { "$ref": "#/definitions/port" },
|
||||
"database": {
|
||||
"type": ["string", "null"],
|
||||
"minLength": 1
|
||||
},
|
||||
"pool": {
|
||||
"description": "Connection pool sizes, PER REPLICA. Upstream's db-pool-max-size default is 100; this chart ships 20, because two replicas at 100 ask a default PostgreSQL (max_connections = 100) for twice what it has.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"initialSize": { "type": ["integer", "null"], "minimum": 0 },
|
||||
"minSize": { "type": ["integer", "null"], "minimum": 0 },
|
||||
"maxSize": { "type": ["integer", "null"], "minimum": 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"bootstrapAdmin": {
|
||||
"description": "The temporary bootstrap administrator. Switch this off once a permanent administrator exists - it also stops the per-start log line about the initial administrator not being created.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" }
|
||||
}
|
||||
},
|
||||
"metrics": {
|
||||
"description": "A Keycloak BUILD option (metrics-enabled). ON, and not for the metrics: upstream's table of health checks has a 'Requires Metrics' column, and the DATABASE check is the row that says yes. With this false, /health/ready still answers 200 while the database is gone - the urlaubsverwaltung shape (#218), with the dependency hidden behind an option whose name says observability. The price is that /metrics then answers on the management port, unauthenticated, carrying realm and client names - that port is ClusterIP-only and in no Ingress. Switching this off is a decision about whether readiness can fail.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" }
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"description": "Additional Keycloak feature flags (--features), a BUILD option. A list rather than a comma-separated string, so two entries cannot arrive as one unknown feature name - which Keycloak rejects at build time, inside the init container. The pattern keeps a value out that could not be a feature name.",
|
||||
"type": ["array", "null"],
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z0-9][A-Za-z0-9:._-]*$"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"clusterName": {
|
||||
"description": "--cache-embedded-cluster-name. null means Keycloak's default, 'ISPN'. Read the comment in values.yaml before setting it: two releases sharing one database MERGE into one cluster unless the names differ, but upstream also warns that cache invalidation does not work between differently-named clusters.",
|
||||
"type": ["string", "null"],
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"ports": {
|
||||
"description": "May be null / empty: the chart then falls back to its defaults. The Deployment renders these as containerPorts and the Services address them by name, so both follow one source.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"http": { "$ref": "#/definitions/port" },
|
||||
"management": {
|
||||
"description": "The management port. It carries the unauthenticated /health/* endpoints and optionally /metrics, and all three probes address it by name. It is deliberately in no Ingress.",
|
||||
"$ref": "#/definitions/port"
|
||||
},
|
||||
"jgroups": {
|
||||
"description": "JGroups transport port. Declared as a containerPort for the record; jdbc-ping opens direct pod-to-pod connections on it.",
|
||||
"$ref": "#/definitions/port"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"description": "Resources of the SERVING container. The memory request is not only about scheduling here: the image sizes the JVM heap relative to the container limit (-XX:MaxRAMPercentage=70), so request x limitFactor decides how much memory the application believes it has.",
|
||||
"$ref": "#/definitions/resources"
|
||||
},
|
||||
"build": {
|
||||
"description": "The build init container, which runs the one-shot Quarkus augmentation. Its resources are transient - it is gone before the serving container starts - but they count for scheduling, because a pod's effective request is the maximum of its init containers' and the sum of its containers'.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"resources": { "$ref": "#/definitions/resources" }
|
||||
}
|
||||
},
|
||||
"livenessProbe": { "$ref": "#/definitions/probe" },
|
||||
"readinessProbe": { "$ref": "#/definitions/probe" },
|
||||
"startupProbe": { "$ref": "#/definitions/probe" },
|
||||
"securityContext": { "$ref": "#/definitions/securityContext" },
|
||||
"podDisruptionBudget": {
|
||||
"description": "The first PodDisruptionBudget in this repository. It is rendered only while the effective replica count is >= 2: minAvailable 1 with a single replica forbids every voluntary eviction, so a node drain would block forever on a pod nothing may move.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"minAvailable": { "type": "integer", "minimum": 1 }
|
||||
}
|
||||
},
|
||||
"env": { "$ref": "#/definitions/env" }
|
||||
}
|
||||
},
|
||||
"ingress": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"description": "Gates the public Ingress object only (issue #93). Checked in both directions for this chart: ingress.domain is ALSO read as KC_HOSTNAME on the Deployment, so switching the Ingress off frees clusterIssuer but NOT domain - Keycloak refuses to start in production mode without a hostname.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"clusterIssuer": { "type": ["string", "null"] },
|
||||
"domain": {
|
||||
"description": "The public hostname, a plain host - the chart renders it as the full URL https://<domain> for KC_HOSTNAME, which upstream requires as soon as hostname-admin is in play. Required even with enabled: false.",
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"className": { "type": ["string", "null"] },
|
||||
"annotations": {
|
||||
"description": "Extra annotations for the public Ingress, merged with the cert-manager annotation the chart always sets. Free-form keys by nature, so this object stays open.",
|
||||
"type": "object"
|
||||
},
|
||||
"admin": {
|
||||
"description": "The administration console on a hostname of its own. NOT AN ACCESS CONTROL: hostname-admin decides which URLs the server generates; upstream states that it does not prevent reaching the Administration REST API through the public hostname, and that restricting that API is reverse-proxy work. The chart renders no deny rule for /admin.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"domain": {
|
||||
"description": "The admin hostname, a plain host - rendered as https://<domain> for KC_HOSTNAME_ADMIN. Required while enabled.",
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"className": {
|
||||
"description": "null falls back to ingress.className. An internal ingress class here keeps the console off the public controller, which is the useful half of the admin hostname.",
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"annotations": {
|
||||
"description": "Extra annotations for the admin Ingress, merged with the cert-manager annotation. Free-form keys by nature, so this object stays open.",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"port": {
|
||||
"type": ["integer", "null"],
|
||||
"minimum": 1,
|
||||
"maximum": 65535
|
||||
},
|
||||
"resources": {
|
||||
"description": "May be null / empty: the chart then falls back to its default requests and computed limits (issue #99) instead of rendering a container without resource accounting.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"limitFactor": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0
|
||||
},
|
||||
"requests": { "$ref": "#/definitions/resourceList" },
|
||||
"limits": { "$ref": "#/definitions/resourceList" }
|
||||
}
|
||||
},
|
||||
"resourceList": {
|
||||
"description": "Both spellings of the ephemeral-storage key are accepted because the resources helper normalizes camelCase to the Kubernetes key.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"cpu": { "type": ["string", "number", "null"] },
|
||||
"memory": { "type": ["string", "number", "null"] },
|
||||
"ephemeral-storage": { "type": ["string", "number", "null"] },
|
||||
"ephemeralStorage": { "type": ["string", "number", "null"] }
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"description": "Additive container env entries. An entry repeating a name the chart already sets does not replace it - on an existing release the pair collapses to the chart's own value while helm upgrade reports success. A BUILD option's variable name (KC_DB, KC_HEALTH_ENABLED, KC_METRICS_ENABLED, KC_FEATURES, ...) is refused by the template with a fail: on the serving container it would disagree with what the init container built, and Keycloak answers that by re-augmenting onto a read-only path.",
|
||||
"type": ["array", "null"],
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name"],
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"value": { "type": "string" },
|
||||
"valueFrom": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"secretKeyRef": { "$ref": "#/definitions/keyRef" },
|
||||
"configMapKeyRef": { "$ref": "#/definitions/keyRef" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"keyRef": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "key"],
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"key": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"securityContext": {
|
||||
"description": "May be null / empty at any level: the chart then falls back to its hardening defaults (issue #99). This chart's fallback reproduces ITS defaults, which deviate from the repo's usual 20000/1000 pair: the published image declares keycloak:x:1000:0 in /etc/passwd and copies the whole install tree with --chown=1000:0, so uid 1000 is the OWNER of everything the process reads. The GROUP is 1000 and not the image's own 0 - counted across the image layer, 521 of 522 entries are owned by 1000:0 with every mode granting the owner full access, so group 0 (upstream's OpenShift arbitrary-uid pattern) buys nothing once the owning uid is pinned, and leaving it out keeps a root GID out of the pod. Every container in the pod, both init containers included, uses the same container context: readOnlyRootFilesystem: true holds throughout, which is the reason the chart has two init containers rather than one writable one.",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"pod": {
|
||||
"description": "Merged onto the chart's pod hardening defaults and rendered as the pod's securityContext; kept open because the chart does not interpret the individual fields.",
|
||||
"type": ["object", "null"]
|
||||
},
|
||||
"container": {
|
||||
"description": "Merged onto the chart's container hardening defaults and rendered as the securityContext of every container in the pod; kept open because the chart does not interpret the individual fields.",
|
||||
"type": ["object", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"probe": {
|
||||
"description": "Merged onto the chart's probe defaults and rendered as a container probe; kept open because the chart does not interpret the individual fields. May be null / empty, which means the defaults apply (issue #99) rather than the probe disappearing. The defaults carry the probe's CONTEXT: the port NAME 'management' for all three, and three DIFFERENT paths - /health/live for liveness (no database check), /health/ready for readiness (with one), /health/started for startup. Pointing liveness at /health or /health/ready would restart every pod of the cluster for as long as the database is away.",
|
||||
"type": ["object", "null"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,582 +0,0 @@
|
||||
# Keycloak - an OpenID Connect and SAML identity provider, deployed as a
|
||||
# TWO-NODE cluster against an external PostgreSQL.
|
||||
#
|
||||
# READ THIS FIRST, because three properties shape every value below and none of
|
||||
# them is obvious from the outside (Gitea #83).
|
||||
#
|
||||
# 1. THERE IS NO ConfigMap IN THIS CHART, AND THAT IS NOT AN OMISSION.
|
||||
# Keycloak is configured entirely through options - CLI arguments and KC_*
|
||||
# environment variables - so no file is rendered, mounted or hashed. The
|
||||
# repo's checksum/config rule (#82) therefore has nothing to attach to:
|
||||
# there is no ConfigMap whose change could fail to restart the workload.
|
||||
# What IS covered is the op:// PATH of the two vault items, through
|
||||
# checksum/secret-ref on the pod template. A change INSIDE an item still
|
||||
# restarts nothing - Helm never sees it - and NOTES.txt names the manual
|
||||
# rollout restart for that case.
|
||||
#
|
||||
# 2. THE CHART RUNS A BUILD STEP IN AN INIT CONTAINER, AND IT HAS TO.
|
||||
# Keycloak distinguishes BUILD options from runtime options. Three of the
|
||||
# options this chart needs are build options - verified against the 26.8.0
|
||||
# "All configuration" page, where each carries the tool icon:
|
||||
# db (default dev-file)
|
||||
# health-enabled (default false)
|
||||
# metrics-enabled (default false)
|
||||
# The published image is augmented with those DEFAULTS. Setting any of them
|
||||
# at runtime makes Keycloak re-augment itself at start, and the augmentation
|
||||
# rewrites four files INSIDE the install tree:
|
||||
# /opt/keycloak/lib/quarkus/{generated-bytecode.jar,
|
||||
# transformed-bytecode.jar,quarkus-application.dat,
|
||||
# build-system.properties}
|
||||
# (read out of the image layer itself, not inferred). Upstream says what
|
||||
# that start does, in the container guide: it "detects and applies the build
|
||||
# options first", which "significantly increases startup time and CREATES AN
|
||||
# IMAGE THAT IS MUTABLE". A mutable image is exactly what
|
||||
# readOnlyRootFilesystem: true forbids - so the stock image, probes on
|
||||
# /health and a read-only root filesystem are not a combination that can
|
||||
# start. And "then do not set db" is no way out: THE PROBES ALONE force the
|
||||
# augmentation, because health-enabled is a build option too.
|
||||
#
|
||||
# So an init container from the SAME image runs `kc.sh build` into an
|
||||
# emptyDir that the main container mounts at /opt/keycloak/lib/quarkus, and
|
||||
# the main container runs `start --optimized`. No second image to pin (this
|
||||
# repository publishes charts, not images), hardening intact, and the
|
||||
# augmentation happens once in the init container instead of inside the
|
||||
# serving container on every start.
|
||||
#
|
||||
# THE CONDITION THAT MAKES IT SAFE, and the line a later round will want to
|
||||
# "simplify": the two containers must never disagree about the build
|
||||
# options, because a disagreement makes the MAIN container re-augment - on
|
||||
# a path that is now read-only, i.e. a crash loop. This chart enforces that
|
||||
# structurally rather than by care: the build options exist in exactly one
|
||||
# place (the keycloak.buildArgs helper, which only the init container uses),
|
||||
# the main container passes `start --optimized` and NO build option at all,
|
||||
# and the render FAILS if a keycloak.env entry carries a build option's
|
||||
# variable name. Do not add a KC_DB, KC_HEALTH_ENABLED, KC_METRICS_ENABLED
|
||||
# or KC_FEATURES to the main container "for clarity".
|
||||
#
|
||||
# 3. NO PVC, AND NO StatefulSet. Keycloak 26 keeps everything in the database,
|
||||
# user sessions included (persistent user sessions are the default since 26;
|
||||
# volatile sessions are deprecated as of 26.8). There is no state on disk to
|
||||
# keep and no stable identity to preserve, so this is a Deployment - the
|
||||
# repo's StatefulSet rule is about STATEFUL applications, and here it would
|
||||
# be decoration.
|
||||
|
||||
# Scale the whole chart down without uninstalling it: scaleDown: true renders
|
||||
# the Deployment with replicas: 0 and takes precedence over keycloak.replicas.
|
||||
# Secrets, Services and Ingresses stay in place; scaleDown: false brings the
|
||||
# workload back up.
|
||||
#
|
||||
# THIS IS ALSO HALF OF THE MINOR-UPGRADE PROCEDURE. Keycloak supports a rolling
|
||||
# update only inside a minor line, so a 26.8 -> 26.9 jump is: scaleDown: true,
|
||||
# back up configuration and database, change the version, scaleDown: false.
|
||||
# NOTES.txt has the sequence and the compatibility check that decides it.
|
||||
scaleDown: false
|
||||
|
||||
secrets:
|
||||
# op:// path of the 1Password item carrying the database credentials.
|
||||
# REQUIRED - the chart has no default database and Keycloak does not start
|
||||
# without one.
|
||||
#
|
||||
# Must carry the fields:
|
||||
# username the database role -> KC_DB_USERNAME
|
||||
# password that role's password -> KC_DB_PASSWORD
|
||||
#
|
||||
# Host, port and database name are NOT in the vault: they are plain values
|
||||
# under keycloak.db below, because they are not secret and because putting
|
||||
# them in the item would mean editing a vault entry to move a database.
|
||||
#
|
||||
# THEY ARRIVE AS ENVIRONMENT VARIABLES, NOT AS FILES, and that is forced
|
||||
# rather than chosen. Keycloak 26 has no _FILE indirection for an option's
|
||||
# value: there is no KC_DB_PASSWORD_FILE and no generic KC_<OPTION>_FILE.
|
||||
# Checked against the 26.8.0 "All configuration" page - every KC_*_FILE
|
||||
# variable there is a PATH option (certificate, key store, trust store, log
|
||||
# file, export file), i.e. "point at this file", never "read this option's
|
||||
# value from a file". The file form valkey argues for is simply unavailable
|
||||
# here, as it was for garage, for a different reason.
|
||||
#
|
||||
# The residual price, stated rather than hidden: the password is in the
|
||||
# container's process environment, so anyone who can exec into the pod or
|
||||
# read its /proc can see it. (Keycloak's own Vault SPI, KC_VAULT=file, is a
|
||||
# different and larger mechanism - a directory of secret files with its own
|
||||
# naming scheme - and deliberately not used here.)
|
||||
databaseSecretRef: null
|
||||
|
||||
# op:// path of the 1Password item carrying the TEMPORARY bootstrap
|
||||
# administrator. Required while keycloak.bootstrapAdmin.enabled is true
|
||||
# (the default), which is what a first install needs.
|
||||
#
|
||||
# Must carry the fields:
|
||||
# username -> KC_BOOTSTRAP_ADMIN_USERNAME (upstream default: temp-admin)
|
||||
# password -> KC_BOOTSTRAP_ADMIN_PASSWORD
|
||||
#
|
||||
# TWO THINGS ABOUT THIS ACCOUNT THAT CATCH PEOPLE OUT, both in NOTES.txt as
|
||||
# well because they are first-install work:
|
||||
# - the variables are named this way only since Keycloak 26. Before that
|
||||
# they were KEYCLOAK_ADMIN / KEYCLOAK_ADMIN_PASSWORD, and a values file
|
||||
# copied from an older deployment silently creates no administrator at
|
||||
# all.
|
||||
# - the account is TEMPORARY. Upstream: "A user or service admin account
|
||||
# created using one of the methods described below is temporary ... the
|
||||
# account should exist only for the duration necessary to perform
|
||||
# operations needed to gain permanent and more secure admin access.
|
||||
# After that, the account needs to be removed manually." Creating a
|
||||
# permanent administrator is therefore a MANDATORY step after the first
|
||||
# install, not a recommendation.
|
||||
bootstrapAdminSecretRef: null
|
||||
|
||||
keycloak:
|
||||
# TWO, and the reason is the kind of service this is. A mail server may be
|
||||
# away briefly because SMTP retries; a blob store has one consumer. An SSO
|
||||
# server takes EVERY dependent login down with it, so the second replica is
|
||||
# the point of this chart rather than a tuning option.
|
||||
#
|
||||
# What makes two replicas cheap here: node discovery runs over the database
|
||||
# that is there anyway. cache=ispn plus Keycloak's default jdbc-ping stack
|
||||
# means no headless Service, no JGroups DNS wiring and no extra option - the
|
||||
# nodes register themselves in a table and then talk directly to each other
|
||||
# on port 7800. (cache-stack=kubernetes, the DNS_PING variant, is marked
|
||||
# deprecated upstream.)
|
||||
#
|
||||
# What it costs: twice the database connections, which is why the pool sizes
|
||||
# below are not upstream's defaults, and the rolling-update restriction -
|
||||
# with one replica a minor jump is a restart, with two it is a procedure.
|
||||
#
|
||||
# An explicit 0 is allowed and is the per-workload way to stop the release;
|
||||
# the chart-global scaleDown flag does the same for everything at once.
|
||||
replicas: 2
|
||||
|
||||
db:
|
||||
# THE DATABASE VENDOR, AND THE ONE VALUE THAT IS A BUILD OPTION (point 2
|
||||
# at the top of this file). It is passed to `kc.sh build` in the init
|
||||
# container and never to the serving container. Changing it re-runs the
|
||||
# build on the next pod start, which is correct and costs one init-container
|
||||
# run - it does NOT migrate any data.
|
||||
#
|
||||
# "postgres" is the only value this chart is tested against. The others
|
||||
# upstream accepts (mariadb, mysql, mssql, oracle) would render, but
|
||||
# nothing here or in CI has ever started them. dev-file and dev-mem are
|
||||
# rejected by the schema: they are Keycloak's development defaults, and an
|
||||
# identity provider on an embedded H2 file is the kind of install that
|
||||
# works until the pod moves.
|
||||
vendor: postgres
|
||||
|
||||
# Host of the external PostgreSQL. REQUIRED - there is no sensible default
|
||||
# and a wrong one is a pod that waits on a connection it will never get.
|
||||
host: null
|
||||
port: 5432
|
||||
database: keycloak
|
||||
|
||||
# CONNECTION POOL, AND A DELIBERATE DEPARTURE FROM UPSTREAM'S DEFAULT.
|
||||
# db-pool-max-size ships as 100 upstream (verified in the 26.8.0 option
|
||||
# table). With two replicas that is 200 connections against a PostgreSQL
|
||||
# whose own default max_connections is 100 - so the upstream default does
|
||||
# not merely waste connections, it hands out more than a default server
|
||||
# has, and the failure arrives under load as "too many clients already"
|
||||
# rather than at install time.
|
||||
#
|
||||
# 20 per replica = 40 in total leaves room for everything else on that
|
||||
# server. Raise it together with the server's max_connections, never alone,
|
||||
# and remember to multiply by keycloak.replicas when you do.
|
||||
#
|
||||
# initialSize and minSize have no upstream default; keeping a few warm
|
||||
# connections removes the first-request latency after an idle period.
|
||||
pool:
|
||||
initialSize: 2
|
||||
minSize: 2
|
||||
maxSize: 20
|
||||
|
||||
bootstrapAdmin:
|
||||
# Render KC_BOOTSTRAP_ADMIN_USERNAME / KC_BOOTSTRAP_ADMIN_PASSWORD from the
|
||||
# vault item above. ON, because a fresh install has no way in otherwise:
|
||||
# Keycloak "only allows to create the initial admin user from a local
|
||||
# network connection", which a container never has.
|
||||
#
|
||||
# SET THIS TO false ONCE A PERMANENT ADMINISTRATOR EXISTS. While the
|
||||
# variables stay set, every start logs a line about the initial
|
||||
# administrator not being created, because the master realm already exists
|
||||
# and the bootstrap path is skipped. That line is harmless and it is NOT a
|
||||
# defect - but it is the kind of message somebody eventually investigates,
|
||||
# so switching this off after the first install removes both the noise and
|
||||
# a credential the pod no longer needs.
|
||||
enabled: true
|
||||
|
||||
# metrics-enabled, a BUILD option - it goes to `kc.sh build`, so changing it
|
||||
# re-runs the init containers.
|
||||
#
|
||||
# ON, AND NOT FOR THE METRICS. This value is misnamed from our point of view:
|
||||
# it also decides whether /health/ready CONTAINS A DATABASE CHECK AT ALL.
|
||||
# Upstream's table of available checks has a "Requires Metrics" column, and
|
||||
# the Database check - "Returns the status of the database connection pool" -
|
||||
# is the one row that says YES. The Cluster, Graceful shutdown and Keycloak
|
||||
# Initialized checks say no.
|
||||
#
|
||||
# So with metrics-enabled=false, a pod whose database is gone still reports
|
||||
# Ready and keeps serving errors to every login. That is precisely the shape
|
||||
# urlaubsverwaltung cost this repo two years of false confidence (#218, and
|
||||
# README guideline 12.3): the probe path was right and the thing behind it
|
||||
# contained nothing external. Here the dependency hides behind an option
|
||||
# whose name says "observability".
|
||||
#
|
||||
# The price, which is real and is why the value still exists: /metrics then
|
||||
# answers on the management port, UNAUTHENTICATED, carrying realm names,
|
||||
# client ids and login counters. That port is ClusterIP-only and in no
|
||||
# Ingress (see keycloak.ports.management), so the exposure is in-cluster.
|
||||
#
|
||||
# SWITCHING THIS OFF IS A DECISION ABOUT WHETHER READINESS CAN FAIL, not
|
||||
# about scraping. If you do it, the readiness probe stops being able to
|
||||
# detect a database outage, and nothing else in this chart replaces it.
|
||||
metrics:
|
||||
enabled: true
|
||||
|
||||
# Additional Keycloak feature flags (--features), e.g. ["organization",
|
||||
# "declarative-ui"]. A BUILD option: entries here change what the init
|
||||
# container builds, so adding one re-runs the build on the next pod start.
|
||||
#
|
||||
# Deliberately a list rather than a free-form string: the option is
|
||||
# comma-separated, and a string would let two entries arrive as one unknown
|
||||
# feature name, which Keycloak rejects at BUILD time - in the init container,
|
||||
# where the error is one `kubectl logs -c` away from being invisible.
|
||||
features: []
|
||||
|
||||
cache:
|
||||
# --cache-embedded-cluster-name. NULL, i.e. Keycloak's default "ISPN", and
|
||||
# the value exists because of a trap rather than a need.
|
||||
#
|
||||
# THE TRAP: node discovery runs through the database, so - upstream's own
|
||||
# words - "clusters that share the same database but should remain separate
|
||||
# will merge unless they have distinct cluster names". Two releases of this
|
||||
# chart pointed at the SAME database therefore form ONE Infinispan cluster,
|
||||
# silently, with no error on either side. If that is your situation, the
|
||||
# first thing to check is whether they should be sharing a database at all;
|
||||
# separate databases need no cluster name at all.
|
||||
#
|
||||
# WHY IT IS NOT SET PER RELEASE BY DEFAULT, although that is the obvious
|
||||
# fix: the same upstream page warns about the other direction - "As
|
||||
# invalidation of caches between clusters with distinct names will not
|
||||
# work, this will lead to stale caches and incorrect behavior. Use only
|
||||
# together with the stateless feature." That warning is not unambiguous
|
||||
# about which configuration it applies to, and a chart default that trades
|
||||
# a documented merge for a possibly undocumented invalidation gap is not an
|
||||
# improvement. So: the value is here, the trap is written down, and the
|
||||
# default is upstream's.
|
||||
clusterName: null
|
||||
|
||||
# THE THREE PORTS, IN ONE PLACE, because two templates have to agree on them:
|
||||
# the Deployment renders them as containerPorts and the Services address them
|
||||
# BY NAME. The probes also address the management port by name, so renaming
|
||||
# one here breaks them.
|
||||
ports:
|
||||
# The serving port. Behind the Ingress, plain HTTP - see http-enabled in
|
||||
# the Deployment.
|
||||
http: 8080
|
||||
|
||||
# THE MANAGEMENT PORT, AND THE ONE THING TO KNOW BEFORE TOUCHING ANY OF
|
||||
# THIS: it carries /health, /health/live, /health/ready, /health/started
|
||||
# and - with keycloak.metrics.enabled - /metrics, and it carries them
|
||||
# WITHOUT AUTHENTICATION. All three probes use it, which is why it exists
|
||||
# as a containerPort and as a ClusterIP Service.
|
||||
#
|
||||
# IT MUST NEVER GET AN INGRESS. There is no value in this chart that would
|
||||
# publish it and none should be added: an unauthenticated readiness
|
||||
# endpoint on the internet tells an attacker when the database is down, and
|
||||
# /metrics tells them the realm and client names. Upstream's alternative
|
||||
# (legacy-observability-interface=true, which moves health back onto 8080
|
||||
# where the Ingress is) is explicitly "not recommended for security
|
||||
# reasons" and this chart does not offer it.
|
||||
management: 9000
|
||||
|
||||
# JGroups. Not served to anything and not in any Service - it is declared
|
||||
# so that the manifest says where cluster traffic goes, and so that a
|
||||
# NetworkPolicy (none yet in this chart) has a port to name. jdbc-ping
|
||||
# discovers peers through the database and then opens DIRECT pod-to-pod
|
||||
# connections on this port, so the two replicas need plain L3 reachability
|
||||
# between their pod IPs - not a Service, and not a headless one either.
|
||||
jgroups: 7800
|
||||
|
||||
resources:
|
||||
# Limits are optional: memory and ephemeral-storage limits default to
|
||||
# limitFactor x the request, an explicitly set limit always wins, and a CPU
|
||||
# limit is only set when configured explicitly - CPU is compressible, so
|
||||
# leaving it unlimited throttles instead of OOM-killing.
|
||||
#
|
||||
# 700Mi x 3 = 2100Mi, AND THAT NUMBER IS LOAD-BEARING RATHER THAN TIDY.
|
||||
# The image sizes the JVM heap RELATIVE to the container limit
|
||||
# (-XX:MaxRAMPercentage=70, -XX:InitialRAMPercentage=50 in kc.sh), so the
|
||||
# limit is the value the heap is computed from, not a ceiling that is
|
||||
# hopefully never reached. Upstream's container guide: "you should always
|
||||
# set the memory limit for the container", "at least 750 MB", and "For
|
||||
# smaller production-ready deployments, the recommended memory limit is
|
||||
# 2 GB". 2100Mi is that recommendation; 70% of it is a ~1470Mi heap.
|
||||
#
|
||||
# Lowering the request without raising limitFactor shrinks the heap
|
||||
# proportionally. That is the connection to keep in mind here - in most
|
||||
# charts of this repo the request is about scheduling, in this one it also
|
||||
# decides how much memory the application believes it has.
|
||||
#
|
||||
# The ephemeral-storage request covers the three emptyDirs (the Quarkus
|
||||
# build output, /opt/keycloak/data and /tmp) plus container logs. The build
|
||||
# output is a few MB; the head room is for the augmentation's temporary
|
||||
# files.
|
||||
limitFactor: 3
|
||||
requests:
|
||||
cpu: 500m
|
||||
memory: 700Mi
|
||||
ephemeral-storage: 256Mi
|
||||
|
||||
# The two INIT CONTAINERS - the seed step and the Quarkus build. Both use
|
||||
# this resources block, because the seed step is a single `cp` whose cost
|
||||
# disappears next to the build's and a second block would only be a second
|
||||
# thing to keep in step.
|
||||
build:
|
||||
# Resources for the build init containers. Their own block, because they do
|
||||
# something quite different from the serving container: a single-shot
|
||||
# Quarkus augmentation, CPU-bound, measured by upstream's own issue reports
|
||||
# at roughly 13-23 seconds of "Quarkus augmentation completed in ...".
|
||||
#
|
||||
# More CPU here shortens every pod start, and the memory is transient - the
|
||||
# init containers are gone before the serving container begins, so these
|
||||
# requests do not add to the pod's running footprint. They DO count for
|
||||
# scheduling, though: a pod's effective request is the maximum of the init
|
||||
# containers' and the sum of the containers', so raising cpu above the
|
||||
# serving container's request raises what the scheduler needs to find.
|
||||
resources:
|
||||
limitFactor: 3
|
||||
requests:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
ephemeral-storage: 256Mi
|
||||
|
||||
# LIVENESS: /health/live on the management port - the endpoint that reports
|
||||
# whether this process is alive, and deliberately NOT /health or
|
||||
# /health/ready.
|
||||
#
|
||||
# This is guideline 12.3 in the one shape where upstream has already done the
|
||||
# work for us: Keycloak splits the three checks, and /health/live contains no
|
||||
# database check, while /health/ready and the aggregate /health do. A
|
||||
# liveness probe on either of those restarts every pod of the cluster for as
|
||||
# long as the database is away - which cannot fix a database, and which turns
|
||||
# a degraded login service into none at all. Same lesson as zipline (README,
|
||||
# guideline 12.3); here it is one path away.
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health/live
|
||||
port: management
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
|
||||
# READINESS: /health/ready - which includes the database, and must. A node
|
||||
# that cannot reach its database serves errors to every login, so no Service
|
||||
# should route to it.
|
||||
#
|
||||
# AND THAT "INCLUDES THE DATABASE" IS CONDITIONAL, which is the trap worth
|
||||
# knowing before anybody tidies up keycloak.metrics.enabled: the database
|
||||
# check is only part of the report while metrics-enabled is true (upstream's
|
||||
# check table, "Requires Metrics: Yes"). The path alone guarantees nothing.
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health/ready
|
||||
port: management
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
|
||||
# STARTUP: /health/started, Keycloak's own "the server has finished booting"
|
||||
# endpoint. It gates liveness and readiness, which is why neither of those
|
||||
# carries an initialDelaySeconds.
|
||||
#
|
||||
# BUDGET = periodSeconds x failureThreshold = 10 x 60 = 600s, and the gap
|
||||
# between that and the MEASURED time is deliberate rather than lazy.
|
||||
#
|
||||
# MEASURED, in the install-test of Gitea PR #87 on the Gitea docker runner
|
||||
# (k3s v1.35.5, two replicas, empty database, image pulled during the run):
|
||||
# from `helm install` to BOTH pods Ready in 71.8 seconds - the first at
|
||||
# ~70.8s, the second one second later, 0 restarts. That figure already
|
||||
# contains the image pull, both init containers and the schema creation.
|
||||
#
|
||||
# Why the budget is still eight times that: everything in the measured run
|
||||
# was the cheap case. A first install against a real database creates the
|
||||
# whole schema through Liquibase, and with two replicas the second waits on
|
||||
# the first's migration lock instead of arriving a second later; a loaded
|
||||
# node, a slower disk or a bigger existing schema all push in the same
|
||||
# direction. A budget that is too short there produces a CrashLoopBackOff
|
||||
# that looks like an application fault and is really a stopwatch - and the
|
||||
# cost of the headroom is nothing, because a pod that IS healthy becomes
|
||||
# Ready on the first probe either way.
|
||||
#
|
||||
# The Quarkus augmentation is NOT in this budget - it runs in the init
|
||||
# container, which has no probe and no deadline of its own. That is the
|
||||
# second thing the init container buys.
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /health/started
|
||||
port: management
|
||||
periodSeconds: 10
|
||||
failureThreshold: 60
|
||||
timeoutSeconds: 5
|
||||
|
||||
securityContext:
|
||||
pod:
|
||||
runAsNonRoot: true
|
||||
# uid 1000, NOT this repo's 20000, and that is the image's requirement
|
||||
# rather than a free choice: it declares "keycloak:x:1000:0" in
|
||||
# /etc/passwd and copies the whole install tree with --chown=1000:0, so
|
||||
# 1000 is the OWNER of everything the process reads and executes. Read
|
||||
# off the image rather than guessed - `USER 1000` and the chown are in
|
||||
# the config blob's history of quay.io/keycloak/keycloak:26.8.0.
|
||||
#
|
||||
# GROUP 1000 AND NOT 0, WHICH IS WHERE THIS CHART DIVERGES FROM THE
|
||||
# IMAGE'S OWN CONVENTION - deliberately, and measured before deciding.
|
||||
# Upstream puts the keycloak user in the ROOT GROUP and makes the install
|
||||
# tree group-writable (mode 775/664, owner 1000, group 0). That is the
|
||||
# OpenShift arbitrary-uid pattern: it lets ANY uid that happens to be in
|
||||
# group 0 write there. We do not need it, because we pin the uid that
|
||||
# OWNS the files - counted across the image layer, 521 of 522 entries are
|
||||
# owned by 1000:0 and every mode grants the owner full access, so uid
|
||||
# 1000 gets what it needs through the owner bits whatever its group is.
|
||||
# The one remaining entry is /opt itself, which needs only traverse.
|
||||
#
|
||||
# What that buys: no root primary or supplementary GID in this pod, i.e.
|
||||
# no AVD-KSV-0116 finding and no suppression for it. A suppression would
|
||||
# have had to cover the whole chart, serving container included, to
|
||||
# silence a check that is making a fair point.
|
||||
#
|
||||
# If this is ever changed back to 0, the reason has to be a path that
|
||||
# uid 1000 does NOT own - and there is none in this image today.
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
fsGroup: 1000
|
||||
fsGroupChangePolicy: Always
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
container:
|
||||
allowPrivilegeEscalation: false
|
||||
# TRUE, and keeping it true is the whole reason this chart has an init
|
||||
# container (point 2 at the top). Setting it to false would make the
|
||||
# init container unnecessary - and would also mean an identity provider
|
||||
# that rewrites its own installation directory at every start. Do not
|
||||
# trade one for the other without reading that section.
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
# EMPTY, and it can stay empty: 8080, 9000 and 7800 are all above
|
||||
# 1024, and the entrypoint is a shell script calling java - no file
|
||||
# capabilities, so no execve-time capability check like stalwart's.
|
||||
add: []
|
||||
|
||||
podDisruptionBudget:
|
||||
# A PodDisruptionBudget, the FIRST in this repository - and with two
|
||||
# replicas it is the piece that makes the second one worth having. Without
|
||||
# it a node drain is free to take both pods at once, and a drain is the
|
||||
# most common reason a pod moves: a node upgrade, not a crash.
|
||||
enabled: true
|
||||
|
||||
# minAvailable, not maxUnavailable, because the guarantee we want is about
|
||||
# the service and not about the disruption: at least one node answers
|
||||
# logins throughout a drain.
|
||||
#
|
||||
# THE TRAP THIS CHART AVOIDS BY NOT RENDERING THE OBJECT BELOW TWO
|
||||
# REPLICAS: minAvailable: 1 with exactly one replica forbids every
|
||||
# voluntary eviction, so `kubectl drain` hangs forever on a pod nothing can
|
||||
# move - and the message points at the PDB, not at the replica count. The
|
||||
# template therefore skips the PDB entirely while the effective replica
|
||||
# count is below 2 (keycloak.replicas: 1, or scaleDown: true), and
|
||||
# NOTES.txt prints whether it was rendered.
|
||||
minAvailable: 1
|
||||
|
||||
# Additive environment entries for the Keycloak container, rendered through
|
||||
# tpl and supporting the value, secretKeyRef and configMapKeyRef forms.
|
||||
#
|
||||
# ADDITIVE MEANS ADDITIVE: an entry repeating a name the chart already sets
|
||||
# does not replace it. At install time both reach the pod and the later one
|
||||
# wins; on an EXISTING release the pair collapses to the chart's own value,
|
||||
# because Kubernetes merges env by name - and helm upgrade still reports
|
||||
# success. So do not use this to override KC_HOSTNAME or the database
|
||||
# variables; change the values that produce them.
|
||||
#
|
||||
# AND ONE CLASS OF ENTRY IS REFUSED AT RENDER TIME: a BUILD option's variable
|
||||
# name (KC_DB, KC_HEALTH_ENABLED, KC_METRICS_ENABLED, KC_FEATURES and the
|
||||
# rest of the verified list in _helpers.tpl). A build option in the serving
|
||||
# container's environment disagrees with what the init container built, and
|
||||
# Keycloak answers a disagreement by re-augmenting - onto a read-only path,
|
||||
# i.e. a crash loop whose cause is three files away from the error message.
|
||||
# The render fails instead, naming the variable. Build options belong in the
|
||||
# values that feed the init container.
|
||||
#
|
||||
# Genuinely additive and useful here: JAVA_OPTS_APPEND, KC_LOG_LEVEL,
|
||||
# KC_SPI_* provider settings.
|
||||
env: []
|
||||
|
||||
ingress:
|
||||
# The PUBLIC ingress - the login endpoint, the token endpoints, the account
|
||||
# console. On by default, because an identity provider nothing can reach is
|
||||
# not an identity provider.
|
||||
enabled: true
|
||||
|
||||
# The public hostname. REQUIRED, AND REQUIRED EVEN WITH ingress.enabled:
|
||||
# false - this is guideline 12.1, checked in both directions: the value is
|
||||
# read a second time OUTSIDE the Ingress template, as KC_HOSTNAME on the
|
||||
# Deployment. Keycloak refuses to start in production mode without a
|
||||
# hostname (hostname-strict defaults to true), so gating it away would
|
||||
# produce a release that renders cleanly and never starts.
|
||||
#
|
||||
# IT IS RENDERED AS A FULL URL, https://<domain>, not as a bare host, and
|
||||
# that is upstream's requirement rather than a preference: "If hostname-admin
|
||||
# is configured, hostname must be set to a URL (not just hostname). Otherwise
|
||||
# Keycloak would not know what is the correct frontend URL". The chart
|
||||
# derives the URL, so this value stays a plain hostname.
|
||||
domain: null
|
||||
|
||||
# cert-manager cluster issuer. Required while either Ingress is enabled;
|
||||
# nothing outside the Ingress templates reads it, so switching both off frees
|
||||
# it.
|
||||
clusterIssuer: null
|
||||
className: nginx
|
||||
|
||||
# Extra annotations for the public Ingress; merged with the cert-manager
|
||||
# annotation the chart always sets.
|
||||
annotations: {}
|
||||
|
||||
admin:
|
||||
# THE ADMIN CONSOLE ON A HOSTNAME OF ITS OWN - and read the next paragraph
|
||||
# before you count on it for anything.
|
||||
#
|
||||
# THIS IS NOT AN ACCESS CONTROL. It sets Keycloak's hostname-admin option,
|
||||
# which decides WHICH URLs THE SERVER GENERATES - redirects, the CSS and JS
|
||||
# the console loads, and the API address the console itself calls. It does
|
||||
# not close anything. Upstream, verbatim: "Using the hostname-admin option
|
||||
# does not prevent accessing the Administration REST API endpoints via the
|
||||
# frontend URL specified by the hostname option. If you want to restrict
|
||||
# access to the Administration REST API, you need to do it on the reverse
|
||||
# proxy level."
|
||||
#
|
||||
# So /admin/realms/... stays reachable through ingress.domain, with or
|
||||
# without this block, and an IP restriction on the admin host buys exactly
|
||||
# nothing on its own. Whoever wants the administration API restricted does
|
||||
# it on the reverse proxy in front of the PUBLIC host. This chart renders
|
||||
# no such rule: a path rule on /admin that nobody has tested against a
|
||||
# running console breaks redirects and asset loading, and a security
|
||||
# measure that only looks like one is worse than an absent one.
|
||||
#
|
||||
# What the second hostname IS good for: the console generates its own URLs
|
||||
# for it, so it can live on an internal ingress class or a different DNS
|
||||
# zone, and operators reach it by a name that is not the public login name.
|
||||
enabled: true
|
||||
|
||||
# The admin hostname. Required while ingress.admin.enabled is true. Also
|
||||
# rendered as a full URL (https://<domain>) for the same upstream reason as
|
||||
# ingress.domain.
|
||||
domain: null
|
||||
|
||||
# null falls back to ingress.className. Set it to an internal ingress class
|
||||
# to keep the console off the public controller - which is a useful thing
|
||||
# to do, and still not the access control the paragraph above is about.
|
||||
className: null
|
||||
|
||||
# Extra annotations for the admin Ingress only; merged with the cert-manager
|
||||
# annotation. This is where an IP allow-list would go
|
||||
# (nginx.ingress.kubernetes.io/whitelist-source-range) - with the caveat
|
||||
# above firmly in mind.
|
||||
annotations: {}
|
||||
@@ -393,51 +393,6 @@
|
||||
datasourceTemplate: "docker",
|
||||
depTypeTemplate: "app-version",
|
||||
},
|
||||
{
|
||||
description: [
|
||||
"keycloak: the counter-case to garage and stalwart, and the one block in this file whose",
|
||||
"DESIGN DECISION IS AN ABSENCE. quay.io/keycloak/keycloak carries four tag shapes side by",
|
||||
"side (measured 2026-10-04 against the tag API): release tags 26.8.0; REBUILD tags with a",
|
||||
"numeric suffix, 26.8.0-0 / 26.7.3-1 / 26.7.2-2, where the same app version is rebuilt for",
|
||||
"a base-image or CVE fix (26.8.0-0 resolves to the SAME manifest digest as 26.8.0);",
|
||||
"floating minor tags 26.8 / 26.7; and the floating names latest and nightly, the latter",
|
||||
"rebuilt daily.",
|
||||
"",
|
||||
"That is the same genus of trap as garage's commit-SHA tags - and here plain `docker`",
|
||||
"versioning already handles all of it, so this block carries NO versioningTemplate.",
|
||||
"Docker versioning splits a tag at the first dash and only ever offers candidates with the",
|
||||
"SAME suffix as the current value; ours has none, so the -N rebuilds are invisible to it.",
|
||||
"The floating minors fail on the missing third part, and latest/nightly parse as no",
|
||||
"version at all.",
|
||||
"",
|
||||
"MEASURED, NOT ASSUMED, and written down because a later round will otherwise add a",
|
||||
"constraint 'to be safe' - which would be a line that does nothing, and one more thing to",
|
||||
"keep correct. Renovate 44.132.5, --platform=local --dry-run=lookup, real registry:",
|
||||
" appVersion 26.8.0 -> updates [], warnings []",
|
||||
" appVersion 26.7.0 -> exactly one: 26.8.0 (updateType minor)",
|
||||
" appVersion 26.7.4 -> exactly two: 26.7.5 (patch), 26.8.0 (minor)",
|
||||
"No run offered a rebuild tag, a floating minor, latest or nightly. The 26.7.4 fixture is",
|
||||
"the guard the nginx and qbittorrent rules taught us to run: it proves real updates still",
|
||||
"come through rather than the filter quietly emptying the list.",
|
||||
"",
|
||||
"AND WHAT THIS BLOCK DELIBERATELY DOES NOT CARRY: an automerge exception. Keycloak",
|
||||
"supports a rolling update only WITHIN a minor line - a minor jump needs every node shut",
|
||||
"down first - so a packageRules entry switching automerge off for minor and major was",
|
||||
"proposed and DECIDED AGAINST (Gitea #83): the global automerge at the top of this file",
|
||||
"applies here too. That is an accepted risk rather than an oversight, and the consequence",
|
||||
"is documented instead of prevented: keycloak/templates/NOTES.txt and the wiki page carry",
|
||||
"the minor-upgrade procedure (scaleDown, back up, bump, scale up) together with the",
|
||||
"statement that the automerge does not know about it, plus the `kc.sh",
|
||||
"update-compatibility metadata|check` calls whose exit code is meant to drive exactly this",
|
||||
"decision. If a minor PR has merged itself, that procedure is the repair.",
|
||||
],
|
||||
customType: "regex",
|
||||
managerFilePatterns: ["keycloak/Chart.yaml"],
|
||||
matchStrings: ["(?m)^appVersion:\\s*\"?(?<currentValue>[^\"\\s]+)\"?"],
|
||||
depNameTemplate: "quay.io/keycloak/keycloak",
|
||||
datasourceTemplate: "docker",
|
||||
depTypeTemplate: "app-version",
|
||||
},
|
||||
{
|
||||
customType: "regex",
|
||||
managerFilePatterns: ["matrix-synapse/Chart.yaml"],
|
||||
|
||||
Reference in new issue
Block a user