Two replicas with cache=ispn, clustered through Keycloak's default jdbc-ping stack: discovery runs over the PostgreSQL that is there anyway, so no headless Service and no JGroups wiring. Deployment rather than StatefulSet - Keycloak 26 keeps everything in the database, user sessions included. The chart's defining property is that it has TWO INIT CONTAINERS and needs both. db, health-enabled and metrics-enabled are Keycloak BUILD options (verified in the 26.8.0 option table), the published image is augmented with their defaults, and supplying different ones at runtime makes Keycloak rewrite /opt/keycloak/lib/quarkus at start - which readOnlyRootFilesystem: true forbids and which upstream describes as a start that "creates an image that is mutable". Not setting db is no way out: the probes alone force it, because health-enabled is a build option too. An empty emptyDir over that path fails as well, because QuarkusEntryPoint loads quarkus/quarkus-application.dat before it dispatches any command, `build` included. So one init container seeds the directory into a volume, the next runs `kc.sh build` into it, and the serving container runs `start --optimized` - every container keeping the full hardening baseline, with no documented deviation to carry. Build options therefore exist in exactly one place, and the asymmetry is enforced rather than trusted: the serving container passes no build option at all, and a build option's variable name in keycloak.env fails the render with a message naming it. Hostname handling is the security-relevant part. KC_HOSTNAME is derived from ingress.domain as a full URL (which upstream requires alongside hostname-admin), hostname-strict stays at its default true, and proxy-headers is set rather than optional. The admin hostname is NOT presented as an access control: upstream states that hostname-admin "does not prevent accessing the Administration REST API endpoints via the frontend URL specified by the hostname option", so the chart renders no /admin deny rule and values.yaml, NOTES.txt and the wiki all say in the same words that restricting that API is reverse-proxy work. The management port 9000 is a containerPort and a ClusterIP Service for in-cluster scraping, and is in no Ingress: it serves /health/* and optionally /metrics without authentication. Liveness uses /health/live and deliberately not /health or /health/ready - those include the database, and a liveness probe on them would restart the whole cluster for as long as the database is away. Also in this change: - First PodDisruptionBudget in this repository, which makes it a convention (README section 11): minAvailable as a value, and rendered only while the computed replica count is >= 2, because minAvailable: 1 with one replica blocks every node drain forever with a message that names the PDB rather than the replica count. - First quay.io image, so quay.io joins the trusted registries in ci/trivy/data/ksv0125.yaml - configured rather than suppressed, so AVD-KSV-0125 keeps being able to say something. - Renovate block with NO version constraint, which is the counter-case to garage and stalwart and is measured rather than assumed: docker versioning already excludes the -N rebuild tags, the floating minors and latest/nightly. Three dry-run fixtures are recorded at the block, and against the committed config 26.7.4 offers exactly 26.7.5 (patch) and 26.8.0 (minor). - No automerge exception, by decision (Gitea #83): the global automerge applies, and the minor-upgrade procedure plus `kc.sh update-compatibility` is documented in NOTES.txt instead. - Two upstream defaults deviated from, each with its reason at the value: db-pool-max-size 20 instead of 100 (two replicas at 100 ask a default PostgreSQL for twice its max_connections), and uid 1000 / gid 0 instead of this repo's 20000/1000, because the image declares keycloak:x:1000:0 and chowns its install tree to that pair. Part of #83
532 lines
23 KiB
Smarty
532 lines
23 KiB
Smarty
{{/*
|
|
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" false)
|
|
"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" 0
|
|
"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 DOES include 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.
|
|
*/}}
|
|
{{- 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 -}}
|