CI / k8s (push) Successful in 8s
CI / build (push) Successful in 1m38s
CI / lint (push) Successful in 1m55s
CI / mutation (push) Canceled after 0s
CI / verify-stack (push) Canceled after 0s
CI / frontend (push) Canceled after 12s
CI / unit (push) Canceled after 18s
Deploy to Talos / deploy (push) Successful in 2m30s
## What & why For the public demo on `big-behandel` / `big-beheer`, visitors should see MFA being enforced without needing an authenticator app. This adds an opt-in Keycloak theme that fills in and submits the medewerker OTP code itself. - **Theme as real files in `infra/keycloak/themes/big-demo/`**, next to the realms: - `login/theme.properties`: `keycloak.v2` plus `scripts=js/otp-autofill.js`. I checked the 26.1 source: `keycloak.v2` loads theme `scripts` and sets none of its own. - `login/resources/js/otp-autofill.js`: on the OTP page, computes the code (RFC 6238, Keycloak's default policy) from the fixture secret `BIGMEDEWERKEROTPSEED` and submits it. - `account`, `admin`, `email`: plain children of Keycloak 26's defaults. Without them the account console returns 500 (see notes). - **Seeded like every other file input:** `infra/helm/seed-configmaps.sh` creates the `rr-kc-theme` ConfigMap, and the chart mounts it as a directory. The podspec gains `items` so flat ConfigMap keys map to theme paths. Keycloak runs `start-dev` (no theme cache), so edits show up about a minute after a reseed. - **Switch:** `demo.otpAutofill` only decides whether `KC_SPI_THEME_DEFAULT=big-demo` is set. `big.env` now skips env values that render empty, and no existing env var is empty. **Off, the render is identical to main except for that one missing variable,** so Keycloak keeps its stock theme. The realm JSONs are untouched, so compose and the e2e tests still require a code. - **Single-use codes:** a second login in the same 30 s window spends the next counter, as `nextUnusedCounter` does in the e2e. Past that it only fills in the field and doesn't submit, so a rejected code can't loop. - **Deploy workflow:** repo variable `OTP_AUTOFILL=true` → `--set demo.otpAutofill=true`. Flipping it changes the pod's env, so Keycloak restarts. Refs #177 ## Definition of Done - [x] Linked Gitea issue (above). - [ ] Failing test committed before the implementation. *(Not done; checks below.)* - [x] Conventional Commits referencing the issue (`refs #NN`). - [ ] CI green - [x] `docker compose up` unaffected (chart only). - [x] Docs updated (Talos runbook, "Publishing through the labs Caddy"). - [ ] ADR. The fixture-secret trade-off is ADR-0031's; this only automates typing it in. ## Notes for reviewers - **Tested on the live cluster.** I patched the running Keycloak with the rendered theme (autofill on) and ran real headless Chromium logins against the public hosts: - `merel-behandelaar` on big-behandel: only username and password typed. The OTP page loaded the script, submitted by itself, and the user landed in the Werkbak. - `jan-burger` on big-mijn still logs in (regression check). - `/realms/medewerker/account/` returns 200. - **Account console 500, found live and fixed in the second commit.** `KC_SPI_THEME_DEFAULT` applies to every theme type, and Keycloak does *not* fall back for a type the theme lacks (`NullPointerException ... "theme" is null`). `big-demo` now declares login, account, admin and email, each a plain child of Keycloak 26's default. It's one ConfigMap mounted as a directory; the podspec gains `items` for that. - **Keycloak restarts cause about 5 minutes of BFF 401s.** This is not caused by this PR, but you'll see it whenever Keycloak restarts. Dev-mode Keycloak makes new signing keys on each boot, and the BFF refreshes its cached keys at most every 5 minutes. Seen live: 401 right after the restart, 204 about 4½ minutes later. Flipping `OTP_AUTOFILL` restarts Keycloak, so expect this briefly. - `make k8s-lint` and `make k8s-drift` pass. The rendered script's code matches `infra/keycloak/check_realms.py otp`. - **Security:** with it on, the public behandel and beheer portals are protected only by the committed password `test123`. That's intentional for synthetic demo data. Never enable it anywhere real. 🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #181
159 lines
4.9 KiB
Smarty
159 lines
4.9 KiB
Smarty
{{/*
|
|
One pod spec for every workload, Deployment and Job alike. The chart is
|
|
values-driven on purpose: `.Values.workloads` is a near-literal transcription of
|
|
infra/docker-compose.yml, so the two stacks can be diffed by eye instead of by
|
|
archaeology. Adding a service is a values edit, not a template edit.
|
|
|
|
Called as: include "big.podspec" (dict "root" $ "name" $name "w" $w)
|
|
*/}}
|
|
{{- define "big.podspec" -}}
|
|
{{- $root := .root -}}
|
|
{{- $name := .name -}}
|
|
{{- $w := .w -}}
|
|
{{- with $root.Values.imagePullSecrets }}
|
|
imagePullSecrets:
|
|
{{- toYaml . | nindent 2 }}
|
|
{{- end }}
|
|
{{- with $w.waitFor }}
|
|
initContainers:
|
|
- name: wait-for-deps
|
|
image: {{ $root.Values.images.busybox }}
|
|
command:
|
|
- sh
|
|
- -c
|
|
- |
|
|
for t in {{ join " " . }}; do
|
|
echo "waiting for $t"
|
|
until nc -z "${t%:*}" "${t#*:}"; do sleep 2; done
|
|
done
|
|
{{- end }}
|
|
containers:
|
|
- name: {{ $name }}
|
|
image: {{ include "big.image" (dict "root" $root "name" $name "w" $w) }}
|
|
# Only this repo's images get the configured policy: their `dev` tag is mutable.
|
|
# Upstream tags are pinned, so IfNotPresent keeps them out of pod-template diffs —
|
|
# which matters because a changed template makes a Job unpatchable (immutable).
|
|
imagePullPolicy: {{ if $w.own }}{{ $root.Values.images.pullPolicy }}{{ else }}IfNotPresent{{ end }}
|
|
{{- if $w.command }}
|
|
{{- fail (printf "workload %s: use `args`, not `command` — compose's `command:` replaces CMD, but Kubernetes' `command:` replaces the image ENTRYPOINT (postgres would run as root, keycloak would exec `start-dev`)" $name) }}
|
|
{{- end }}
|
|
{{- with $w.args }}
|
|
args:
|
|
{{- toYaml . | nindent 6 }}
|
|
{{- end }}
|
|
{{- with $w.envFrom }}
|
|
envFrom:
|
|
{{- range . }}
|
|
- configMapRef:
|
|
# optional: an env group whose feature is disabled (e.g. otel) simply
|
|
# isn't rendered, and the pod must still start.
|
|
name: {{ printf "%s-env" . }}
|
|
optional: true
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- with $w.env }}
|
|
env:
|
|
{{- include "big.env" (list $root .) | nindent 6 }}
|
|
{{- end }}
|
|
{{- with $w.ports }}
|
|
ports:
|
|
{{- range . }}
|
|
- name: {{ .name }}
|
|
containerPort: {{ .targetPort | default .port }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- with $w.probe }}
|
|
readinessProbe:
|
|
{{- toYaml . | nindent 6 }}
|
|
{{- end }}
|
|
{{- with $w.resources }}
|
|
resources:
|
|
{{- toYaml . | nindent 6 }}
|
|
{{- end }}
|
|
{{- if or $w.files $w.data }}
|
|
volumeMounts:
|
|
{{- range $w.files }}
|
|
- name: {{ .configMap }}
|
|
mountPath: {{ .mountPath }}
|
|
{{- with .subPath }}
|
|
subPath: {{ . }}
|
|
{{- end }}
|
|
readOnly: true
|
|
{{- end }}
|
|
{{- with $w.data }}
|
|
- name: data
|
|
mountPath: {{ .mountPath }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- if or $w.files $w.data }}
|
|
volumes:
|
|
{{- range $w.files }}
|
|
- name: {{ .configMap }}
|
|
configMap:
|
|
name: {{ .configMap }}
|
|
{{- with .defaultMode }}
|
|
defaultMode: {{ . }}
|
|
{{- end }}
|
|
{{- with .items }}
|
|
items:
|
|
{{- toYaml . | nindent 8 }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- with $w.data }}
|
|
- name: data
|
|
{{- if $root.Values.persistence.storageClass }}
|
|
persistentVolumeClaim:
|
|
claimName: {{ $name }}-data
|
|
{{- else }}
|
|
# No StorageClass configured: the databases are emptyDir, so the stack needs
|
|
# no CSI driver to come up. Data then lives as long as the pod does — see
|
|
# docs/runbooks/kubernetes-talos.md for switching on local-path.
|
|
emptyDir: {}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- end -}}
|
|
|
|
{{/* Image ref: `own: true` workloads are built from this repo, everything else is upstream. */}}
|
|
{{- define "big.image" -}}
|
|
{{- $root := .root -}}
|
|
{{- $w := .w -}}
|
|
{{- if $w.own -}}
|
|
{{- $ref := printf "%s/%s:%s" $root.Values.images.repositoryPrefix .name $root.Values.images.tag -}}
|
|
{{- with $root.Values.images.registry }}{{ printf "%s/%s" . $ref }}{{ else }}{{ $ref }}{{ end }}
|
|
{{- else -}}
|
|
{{- $w.image -}}
|
|
{{- end -}}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
Env list from a map. Every value is run through `tpl`, so values.yaml can name
|
|
cluster-internal hosts ({{ .Release.Namespace }}) and the node address
|
|
({{ .Values.host }}) without the chart hard-coding either. A value that renders
|
|
empty is left out, which is how a setting is made conditional on a chart value.
|
|
*/}}
|
|
{{- define "big.env" -}}
|
|
{{- $root := index . 0 -}}
|
|
{{- range $k, $v := index . 1 }}
|
|
{{- $val := tpl (toString $v) $root }}
|
|
{{- if $val }}
|
|
- name: {{ $k }}
|
|
value: {{ $val | quote }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- end -}}
|
|
|
|
{{/*
|
|
The origin a browser reaches Keycloak on: the issuer Keycloak pins and the
|
|
authority the portals use, from one place so they cannot drift (ADR-0010).
|
|
*/}}
|
|
{{- define "big.keycloakUrl" -}}
|
|
{{- .Values.keycloakUrl | default (printf "http://%s:%v" .Values.host (index .Values.nodePorts "keycloak")) -}}
|
|
{{- end -}}
|
|
|
|
{{- define "big.labels" -}}
|
|
app.kubernetes.io/name: {{ .name }}
|
|
app.kubernetes.io/instance: {{ .root.Release.Name }}
|
|
app.kubernetes.io/managed-by: Helm
|
|
{{- end -}}
|