Records the decision #177 asked for, the other way round: the hypervisor has no
inbound path and the labs Caddy already holds 80/443 and the wildcard cert, so
the portals go through it over a reverse SSH tunnel instead of an in-cluster
edge. Covers keycloakUrl, KC_PROXY_HEADERS and the optional demo OTP autofill.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
## What & why
`verify-stack` now runs only on a push to `main` (a merge), not on pull requests. Every job before it (lint, k8s, build, unit, frontend, mutation) is unchanged and still runs on PRs.
**Why:** the Gitea runner shares the 15 GB lab node with the deployed stack. `verify-stack` boots the whole stack a second time inside `dind`, which is what got the runner OOM-killed (#182). Running it once per merge instead of on every PR push roughly halves how often that happens.
- `.gitea/workflows/ci.yaml`: `if: github.event_name == 'push' && github.ref == 'refs/heads/main'` on `verify-stack`, with a comment.
- `docs/runbooks/ci.md`: the job table notes "push to main only".
Refs #182
## Definition of Done
- [x] Linked Gitea issue (above).
- [ ] Failing test first. *(CI config.)*
- [x] Conventional Commits referencing the issue.
- [ ] CI green. **This PR's own run should show `verify-stack` as skipped**, which is the check for the PR half.
- [x] Docs updated (`docs/runbooks/ci.md`).
## Notes for reviewers
- **gotchas §7:** on Gitea 1.27 + act_runner 2.0.0, a `needs` job gated by a *status-function* `if` (`always()`/`cancelled()`) never leaves `waiting`. This `if` is a plain event check, so it keeps the implicit `success()` and should not hit that path. It's only proven once the first merge to `main` runs `verify-stack`. If that run sits in `waiting` with no logs, force-cancel it and revert this.
- **Policy change:** CLAUDE.md §3/§15 say the compose-up smoke test "runs in CI and gates merges". After this it runs *after* the merge, so a live-stack break shows up as a red `main` (P0 per §15) instead of a blocked PR. CLAUDE.md changes need their own issue and PR, so I left it untouched. It should be updated if this approach is kept.
- If `verify-stack` is a required status check in branch protection, remove it there too. Otherwise PRs will wait for a check that never runs.
🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #184
## What & why
Follow-up to #179. After login through `https://big-mijn.labs.respellion.tech`, the browser blocked the token request as mixed content. `KC_HOSTNAME_BACKCHANNEL_DYNAMIC=true` makes Keycloak build its token, userinfo and certs URLs from the incoming request. Behind the labs Caddy that request arrives as plain `http`, so the discovery document listed `http://big-auth…/token`.
`KC_PROXY_HEADERS=xforwarded` makes Keycloak trust the `X-Forwarded-Proto: https` that Caddy sends. In-cluster calls (the BFF → `keycloak:8080`) carry no such header, so they are unchanged, and so is the localhost/NodePort setup.
Refs #177
## Definition of Done
- [x] Linked Gitea issue (above).
- [ ] Failing test committed before the implementation. *(One env var, verified live instead.)*
- [x] Conventional Commits referencing the issue (`refs #NN`).
- [ ] CI green
- [x] Docs updated if behaviour, contracts, or operations changed. *(Comment in values.yaml.)*
## Notes for reviewers
- I already applied this to the running cluster with `kubectl set env` and checked it. Both realms' discovery documents now have 0 `http://` URLs, and a `jan-burger` token from the public Keycloak still gets a 204 from the BFF. Merging keeps the next deploy from reverting it.
- Keycloak now trusts `X-Forwarded-*` from anything that can reach it. Its only entry points are in-cluster callers and the NodePort, which the reverse tunnel exposes only to Caddy. `KC_PROXY_TRUSTED_ADDRESSES` could narrow that if the NodePort is ever exposed more widely.
🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #180
## 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
## What & why
`verify-stack` is being killed by the OOM controller on the shared Talos node, on main (run 827) and on #180 (run 830). The cause is Celery: with `CELERY_WORKER_CONCURRENCY` unset, `oz-celery` and `nrc-celery` each fork one worker per CPU. That's 22 each on the lab node, 49 Celery processes at about 225 MB apiece. Details and the kernel log evidence are in #182.
This sets `CELERY_WORKER_CONCURRENCY: "2"` in the oz and nrc env groups:
- **compose** (`&oz-env`, `&nrc-env`): what `verify-stack` starts inside `dind`.
- **chart** (`envGroups.oz` / `.nrc`): the deployed demo on the same node.
Both images' `/celery_worker.sh` honour the variable; I checked in the running pods. Web, init and beat containers share the anchors and ignore it. `objecten-celery` already defaults to 1.
Closes#182
## Definition of Done
- [x] Linked Gitea issue (above).
- [ ] Failing test committed before the implementation. *(Resource setting; the evidence is the OOM log in #182.)*
- [x] Conventional Commits referencing the issue (`refs #NN`).
- [ ] CI green. This PR's `verify-stack` run is the check.
- [x] `docker compose config` renders the variable into all 7 services on the two anchors.
- [x] Docs: comments next to the setting, following the existing uWSGI notes.
## Notes for reviewers
- `make k8s-lint` and `make k8s-drift` pass.
- **Not applied live.** I couldn't patch the running cluster from my session. After merge, the deploy updates the `oz-env` / `nrc-env` ConfigMaps. The celery pods only pick that up on restart, and the deploy step restarts only this repo's nine services. So run once:
`kubectl -n big rollout restart deploy/oz-celery deploy/nrc-celery`
- **Why 2 and not 1:** this matches `UWSGI_THREADS: "2"`, and it keeps one notification delivery from blocking behind a slow task. It cuts roughly 40 processes, about 9 GB RSS (less in practice, because forked workers share pages).
- **Longer term:** CI and the demo share one 15 GB VM. Resource requests on the runner, or moving the runner off the node, would stop one from starving the other.
🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #183
## What & why
Makes the portals reachable on real hostnames through the Caddy that already fronts `*.labs.respellion.tech`, instead of five SSH port-forwards:
| URL | Service |
|---|---|
| `https://big-register.labs.respellion.tech` | openbaar |
| `https://big-mijn.labs.respellion.tech` | self-service |
| `https://big-behandel.labs.respellion.tech` | behandel |
| `https://big-beheer.labs.respellion.tech` | beheer |
| `https://big-auth.labs.respellion.tech` | Keycloak (`/admin` blocked) |
Chain: browser → labs Caddy (TLS) → `openssh-server` container → reverse SSH tunnel → Fedora host → Talos NodePorts. The Caddy routes and the tunnel unit are already on `main` in the Infra repo (`infra/development/`).
This repo's part:
- **Chart:** a `keycloakUrl` value. When set it replaces `host` + Keycloak's NodePort as the pinned issuer (`KC_HOSTNAME`) and the portals' OIDC authority. Both now come from one helper, `big.keycloakUrl`, so they can't drift apart (ADR-0010). Empty = rendered output identical to today.
- **Deploy workflow:** passes the `KEYCLOAK_URL` repo variable as `--set keycloakUrl=…`.
- **Runbook:** new section "Publishing through the labs Caddy".
Refs #177
## Definition of Done
- [x] Linked Gitea issue (above).
- [ ] Failing test committed before the implementation. *(Infra/config change, no test added.)*
- [x] Implementation makes the test pass; refactor commit if structure improved.
- [x] Conventional Commits referencing the issue (`refs #NN`).
- [ ] CI green — all Gitea Actions jobs (or `make ci` green while no runner exists).
- [x] `docker compose up` from a fresh clone reaches green health checks within 3 minutes. *(Compose untouched.)*
- [x] Docs updated if behaviour, contracts, or operations changed.
- [ ] ADR added in `docs/architecture/` if a non-obvious decision was made.
- [ ] Demo note in `docs/demo-script.md` if user-visible.
## Notes for reviewers
- **This takes the option #177 rejects.** #177 proposes an in-cluster Caddy edge (branch `feat/177-public-tls-edge`). This PR uses the existing labs Caddy instead, because it already holds 80/443 and the wildcard certificate. So it only *refs* #177. If we go this way, #177's ADR should record the host-Caddy option instead.
- `make k8s-lint` and `infra/check-docs-nav.py` pass. I rendered the chart with and without `keycloakUrl`: empty gives the same output as before; set, it gives `https://big-auth.labs.respellion.tech` for both the issuer and the authority.
- Once `KEYCLOAK_URL` is set, the `localhost` port-forward workflow (runbook §5) no longer logs in, because the issuer is a single string.
- The portals are public, with no Azure `authorize` in front of them the way `marketing` has one. The test users use `test123`.
- Rollout after merge: install `big-portals-tunnel.service` on the Fedora host, run `docker compose up -d caddy` on the labs server, then set the `KEYCLOAK_URL` variable.
🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #179
## What & why
The chart has been deployable by hand since #25 and linted in CI since #168. This makes a
merged PR actually ship it to the Talos VM on the lab server.
`.gitea/workflows/deploy.yaml` runs on a push to `main` (a squash-merged PR) and on manual
dispatch:
1. **Tunnel** — neither the Kubernetes API nor the in-cluster registry is publicly reachable,
so 6443, 30500 and 30141 are forwarded over the same SSH hop into the Fedora host that the
Gitea-runner pipeline uses (`ssh -p 6667 user@labs.respellion.tech`).
2. **Images** — `make k8s-images K8S_REGISTRY=localhost:30500`, pushed *through* the tunnel.
3. **Deploy** — `make k8s-reseed TALOS_HOST=… K8S_REGISTRY=<vm-ip>:30500`, pulled by the node
from its own NodePort.
4. **Roll** — `rollout restart` + `rollout status` on the nine repo deployments.
5. **Smoke** — `GET /openbaar/register` through the openbaar portal.
Three decisions worth the review:
- **One registry, two names.** The push target (`localhost:30500`, the tunnel) and the pull
target (`<vm-ip>:30500`, the node's own NodePort) address the same store. The pull name has
to be the one in the node's registry-mirror patch, which is what makes plain HTTP acceptable.
- **`k8s-reseed`, not `k8s-up`.** A Job's pod template is immutable, so a chart change to any
bootstrap Job would otherwise fail the upgrade with `cannot patch … with kind Job`. The Jobs
are idempotent by design, so re-running them every deploy is safe and removes that whole
class of failure. Cost: a few minutes per deploy, and `seed-zaaktype` needs egress from the VM.
- **No re-run of the checks.** PR CI is the merge gate, so `main` is green by construction.
Deploys **queue** (`cancel-in-progress: false`) — a `helm upgrade` killed half-way leaves the
release in `pending-upgrade` and has to be unwedged by hand.
Settings on the repo (already added): secrets `TALOS_SSH_KEY` and `TALOS_KUBECONFIG`
(base64, and its `server:` must be `https://127.0.0.1:6443` — Talos puts `127.0.0.1` in the
apiserver cert SANs, so TLS still verifies through the tunnel); variables `TALOS_VM_IP`
(default `192.168.122.173`) and `TALOS_HOST` (default `localhost`).
Closes#175
## Definition of Done
- [x] Linked Gitea issue (above).
- [ ] Failing test committed before the implementation — **n/a**: this is a deployment
workflow with no unit under test. Its check is the run itself: `rollout status` and the
public-register smoke both have to pass or the job fails. `make k8s-lint` / `make k8s-drift`
(#168) already gate the chart it deploys.
- [x] Implementation — one workflow file, no production code touched.
- [x] Conventional Commits referencing the issue (`refs #175`).
- [ ] CI green — awaiting the run on this PR.
- [x] `docker compose up` unaffected — no service, image or compose file is touched.
- [x] Docs updated — `docs/runbooks/kubernetes-talos.md` §9 (the tunnel, the two registry
names, the secrets table, the smoke) and a pointer from `docs/runbooks/ci.md`.
- [x] No ADR needed: no new dependency (kubectl/helm/crane are already prerequisites of the
`k8s-*` targets), no service boundary moved, no CLAUDE.md §8 rule bent.
- [ ] Demo note — not user-visible.
## Notes for reviewers
- **The first deploy is the real test.** It cannot be dry-run: the tunnel, the secrets and the
registry only exist on the lab server. Merging is how we find out; `Pods on failure` dumps
`get pods,jobs` if it doesn't.
- **Known gap — the portals still aren't browsable.** PKCE needs a secure context, so a
NodePort on an IP can't serve them (runbook §5); they need `make k8s-portals` or an SSH
forward. Giving the server a hostname + TLS is the follow-up, and is where `TALOS_HOST`
stops defaulting to `localhost`.
- **Databases are `emptyDir`.** Any change to a database pod's template wipes it; the
`k8s-reseed` in the deploy re-runs the bootstrap, so the stack recovers, but submitted
registrations do not. Persistence is runbook §6.
🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #176
## What & why
The Helm chart landed in #167 with two gaps written into ADR-0033: `make k8s-lint` existed
but no CI job ran it, and *"a second deployment description to keep in step with compose —
nothing enforces that today; a drift check belongs in CI (follow-up)"*. Both are closed here.
**`make k8s-drift`** (`infra/helm/check-drift.py`, stdlib only) compares what each stack
actually deploys rather than diffing two files that differ by design: workload names and
resolved container images, taken from `docker compose config --format json` and a rendered
chart. The six differences that exist today are declared in `DEVIATIONS` with the reason
each was forced — the four `*-init` Django services folded into their web pods, and the two
bootstrap Jobs compose runs from the host — so only a *new* difference fails.
**A `k8s` CI job** runs `k8s-lint` then `k8s-drift` on every push and PR. No cluster, no
marketplace action: helm is fetched as the pinned static binary the Talos runbook already
gives developers.
Closes#168
## Definition of Done
- [x] Linked Gitea issue (above).
- [x] Failing test committed before the implementation — the red commit reports all six
real differences; the green commit declares them.
- [x] Implementation makes the test pass.
- [x] Conventional Commits referencing the issue (`refs #168`).
- [x] CI green — awaiting the run on this PR (`make k8s-lint` and `make k8s-drift` pass locally).
- [x] `docker compose up` unaffected — no service, image or compose file is touched.
- [x] Docs updated — `docs/runbooks/ci.md` (job table + the one place local and CI now
differ), `docs/runbooks/kubernetes-talos.md` §7/§"not ported", and ADR-0033's cost note.
- [x] No ADR needed: no new dependency (python stdlib, and helm/docker were already
prerequisites of the `k8s-*` targets), no boundary moved, no §8 rule bent.
- [x] Not user-visible, so no demo note.
## Notes for reviewers
Verified by hand that both drift classes fail the check, not just that it passes today:
- bumping `OPENZAAK_TAG` in compose alone → reports `openzaak` and `oz-celery` with both
image strings;
- adding a workload to `values.yaml` alone → reports it by name.
Deliberate limits (there is a `ponytail:` note in the script):
- **Names and images only**, as sets — no per-workload env, ports or volumes. Those differ
by design in four documented places, so comparing them would mean re-encoding every
deviation field by field for very little more signal.
- **The three observability workloads are rendered with `enabled=true`** by the check, even
though both stacks default them off, so their images can't drift unwatched.
- **`k8s-lint`/`k8s-drift` are not in `make ci`**, to avoid making `helm` a hard
prerequisite for everyone. That is now the only local/CI difference; it's called out in
`docs/runbooks/ci.md`.
Follow-ups filed while reviewing the chart, not addressed here: #169 (the published docs
omit every ADR after 0010 and all runbooks but `ci.md`) and #170 (the production-posture
ADR #25 asked for — secrets are still plain text in `values.yaml`).Reviewed-on: #171
## What & why
`docs/` is the source of truth (CLAUDE.md §12), but only pages listed in `mkdocs.yml`'s nav
are published — and mkdocs' own `validation.nav.omitted_files: warn` keeps the build green
while dropping the rest. So the site had quietly stopped at **ADR-0010** and
**`runbooks/ci.md`**: 31 pages, including every ADR from 0011 to 0034, six of the seven
runbooks, and `synthetic-data.md`, existed in the repo and nowhere else.
- `infra/check-docs-nav.py` fails when a page under `docs/` is not in the nav. It runs in
`make lint`, so the existing CI job gates it — python3 only, no new tooling, and no
mkdocs install needed to check it.
- The nav now lists all 34 ADRs, all 7 runbooks and `synthetic-data.md`.
- ADR-0033's `Slice:` header said "none yet"; #25 closed it.
- The landing page gained a pointer to the Talos runbook.
Closes#169
## Definition of Done
- [x] Linked Gitea issue (above).
- [x] Failing test committed before the fix — the red commit lists all 31 missing pages.
- [x] Implementation makes the test pass.
- [x] Conventional Commits referencing the issue (`refs #169`).
- [ ] CI green — awaiting the run on this PR (`python3 infra/check-docs-nav.py` passes locally;
`make lint` also needs the .NET SDK, which CI has).
- [x] `docker compose up` unaffected — docs and `mkdocs.yml` only, plus one `make lint` line.
- [x] Docs updated — that is the change.
- [x] No ADR needed: no dependency, no boundary, no §8 rule touched.
- [x] Not user-visible, so no demo note.
## Notes for reviewers
- The check is a **substring test**, not a YAML parse (there's a `ponytail:` note in the
script): a page's path either appears in `mkdocs.yml` or it doesn't. That keeps it
dependency-free — `mkdocs.yml` can't be read by `yaml.safe_load` anyway, it carries a
`!!python/name:` tag for the mermaid fence. It does not check that an entry *points at a
file that exists*; mkdocs' `not_found: warn` covers that direction.
- ADR labels in the nav are shortened by hand (`"ADR-0013: Behandel-portal wiring"`), since
several H1s are a full sentence.
**Known gap, not fixed here:** CLAUDE.md §12 says the site is "published via a Gitea Actions
workflow to Gitea Pages", and no such workflow exists — `mkdocs build` is never run, by CI or
by any make target. Gitea has no built-in Pages, so publishing needs a decision (a
`gitea-pages` server, an artifact, or a static host) rather than a patch. Worth its own issue
if the published site is actually wanted; until then this PR makes the nav correct for whoever
runs `mkdocs serve`.Reviewed-on: #172
## What & why
Two changes, made and verified together on a real cluster.
**S-24 / #25 — a Helm chart for the platform.** One chart, `infra/helm/big-reference`,
whose `values.yaml` is a near-literal transcription of `infra/docker-compose.yml`, rendered
by three generic templates (Deployment, Job, Service) over a `workloads` map. Adding a
service is a values edit. `make k8s-lint` renders and schema-checks the whole stack without
a cluster. The issue asked for a *sketch*; this is deployed and verified end to end (see
below), which is more than it asked for — the part it asked for that is **not** here is the
production-posture write-up (HA, secrets, backup), see Known gaps.
**#166 — Caddy replaces nginx in the portals.** nginx resolves a variable `proxy_pass`
upstream itself, using only the `resolver` directive and never `/etc/resolv.conf`'s search
domains. That had cost two workarounds in one script: rewriting the resolver address for
rootless podman, and injecting a full FQDN so the bare `bff` name could resolve on
Kubernetes. Caddy dials per request through the system resolver, so `reverse_proxy
bff:8080` works on every engine unchanged; `apps/portal-nginx-resolver.sh` and the chart's
`BFF_HOST` env are deleted.
Closes#25Closes#166
## Definition of Done
- [x] Linked Gitea issue (above).
- [x] Failing test committed before the implementation — twice: the Caddyfile contract test
before the Caddyfiles, `make k8s-lint` before the chart.
- [x] Implementation makes the test pass.
- [x] Conventional Commits referencing the issues (`refs #25` / `refs #166`).
- [ ] CI green — awaiting the run on this PR (`make k8s-lint`, `dotnet format` and the new
unit self-check pass locally; the compose e2e and mutation lanes are CI's).
- [ ] `docker compose up` from a fresh clone reaches green health checks within 3 minutes —
the portal images were rebuilt and verified standalone, but a full `make up` run has
not been done on this branch. Please confirm in review or let CI's smoke test speak.
- [x] Docs updated — `docs/runbooks/kubernetes-talos.md` (new), `frontend-decisions.md`,
`demo-script.md`, and the docs that named nginx.
- [x] ADR added — ADR-0033 (chart) and ADR-0034 (Caddy).
- [ ] Demo note in `docs/demo-script.md` — not added: the deployment target is not a
user-visible slice, and the Caddy swap is invisible to the demo script beyond the
wording fix included here.
## How it was verified
Brought up from scratch on a single-node Talos v1.14.0 VM (6 vCPU / 10 GB, virtio disk)
under virt-manager: **29 pods ready and four bootstrap Jobs complete in under three
minutes, zero restarts**, using ~4.4 GB of the VM's 10 GB.
- Full Common Ground path: portal Caddy → BFF → domain → Flowable → ACL → OpenZaak +
Objecten → NRC → event-subscriber → projection → public register (`INGEDIEND`, reference
matching the submitted registration).
- Werkbak read with an MFA'd medewerker token → 200.
- The browser flow driven with Playwright against `http://localhost:30140`: secure context,
`crypto.subtle` present, Keycloak form reached, login completed, **no console errors**.
- Routing checked against a stub BFF: SPA fallback serves deep links, each portal proxies
its own groups, and a portal does *not* proxy a neighbour's group.
## Notes for reviewers
Three bugs this shook out, each fixed at the cause rather than the symptom:
1. **`command` vs `args`.** Compose's `command:` replaces the image CMD; Kubernetes'
replaces the ENTRYPOINT. Transcribing one to the other broke every upstream image that
relies on its entrypoint — postgres refused to run as root, Keycloak tried to exec
`start-dev`. The chart now `fail`s at render time on `command`.
2. **Concurrent migrations.** Both `/setup_configuration.sh` and `/start.sh` run
`manage.py migrate`; compose serialises them with `depends_on`, Kubernetes has no such
edge, so the init Job and its web pod raced (`relation "zgw_consumers_service" already
exists`). The four Django services now do both steps in order in the web pod — which
also deletes four workloads.
3. **`emptyDir` databases are wiped by any pod-template change.** `make k8s-reseed` now
also restarts `event-subscriber` and `projection-api`, which create the projection
schema on start and otherwise keep writing to a schema-less database.
Known gaps / follow-ups:
- **Secrets.** `values.yaml` carries the dev credentials in plain text (`admin/admin`, the
ZGW client secret, the two Objecten tokens) and the chart has no `Secret` objects. Fine
for a laptop demo, and exactly what #25's "production posture" ADR should address — I
suggest a follow-up issue rather than stretching this PR.
- **No CI gate for the chart yet.** `make k8s-lint` exists but is not wired into
`.gitea/workflows/ci.yaml`, and nothing enforces that the chart and the compose file stay
in step. Worth a small follow-up.
- **This is two slices in one PR.** They were built and verified together and the diff is
entangled (the chart was written against Caddy from the start), so splitting now would
mean re-creating an nginx-shaped chart to throw away. Happy to split if you'd rather.
- **Rebased onto #161** (merged as #165) rather than merged, to keep the history linear.
One conflict, in the `unit:` target where both branches add a self-check line — resolved
by keeping both. #161's `infra/host-browser.yml` arrived with
`/usr/share/nginx/html/config.json` and is fixed to `/usr/share/caddy/` inside the
`feat(portals)` commit, so no commit on this branch leaves that overlay pointing at a
path the images no longer have.Reviewed-on: #167
2026-09-10 08:53:58 +00:00
22 changed files with 672 additions and 15 deletions
- Images are **pushed** to `localhost:30500` (the tunnel) and **pulled** by the node from
`<TALOS_VM_IP>:30500` (its own NodePort, the address in the Talos registry-mirror patch).
Same registry, two names — hence the two `K8S_REGISTRY` values in the workflow.
- It calls `make k8s-reseed`, not `make k8s-up`: the bootstrap Jobs are idempotent, and
deleting them first is what stops a changed Job template from wedging `helm upgrade` (§7).
-`dev` is a mutable tag, so a `rollout restart` of the nine repo deployments is what
actually puts the new images in the pods.
- Deploys **queue** (`cancel-in-progress: false`): a helm upgrade killed half-way leaves the
release in `pending-upgrade`, which has to be unwedged by hand.
Settings, all on the repository in Gitea:
| Kind | Name | What |
|---|---|---|
| Secret | `TALOS_SSH_KEY` | private key for `user@labs.respellion.tech` (the Fedora host) |
| Secret | `TALOS_KUBECONFIG` | base64 of the kubeconfig, **`server: https://127.0.0.1:6443`** — Talos puts `127.0.0.1` in the apiserver cert SANs, so TLS still verifies through the tunnel |
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.