Compare commits

..
Author SHA1 Message Date
notandClaude Opus 5.5 4568b682a6 ci: run verify-stack only on push to main, not on PRs (refs #182)
CI / lint (pull_request) Successful in 1m44s
CI / k8s (pull_request) Successful in 9s
CI / build (pull_request) Successful in 1m21s
CI / unit (pull_request) Successful in 1m31s
CI / frontend (pull_request) Successful in 2m31s
CI / mutation (pull_request) Successful in 5m2s
CI / verify-stack (pull_request) Skipped
The runner shares the lab node with the deployed stack; a second full stack
per PR is what got it OOM-killed. PRs keep every other job as the merge gate.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 14:46:59 +02:00
10 changed files with 8 additions and 159 deletions
-11
View File
@@ -98,17 +98,6 @@ jobs:
[ -n "${GITHUB_STEP_SUMMARY:-}" ] || exit 0
python3 infra/trx-summary.py TestResults >> "$GITHUB_STEP_SUMMARY"
# The docs site must build with --strict (#173). setup-python so `make docs` can
# create its venv regardless of what the runner image ships.
docs:
runs-on: ubuntu-latest
steps:
- uses: https://github.com/actions/checkout@v4
- uses: https://github.com/actions/setup-python@v5
with:
python-version: '3.12'
- run: make docs
# Frontend (Nx/Angular) lane: install with pnpm, then Nx lint + test + build.
frontend:
runs-on: ubuntu-latest
+1 -4
View File
@@ -34,9 +34,6 @@ jobs:
# `true` fills in the medewerker OTP step for the public demo (chart value
# demo.otpAutofill). The fixture secret is committed: demo only.
OTP_AUTOFILL: ${{ vars.OTP_AUTOFILL }}
# Tempo for the services' traces, e.g. http://tempo.monitoring.svc:4317 (the
# cluster monitoring stack, Infra repo). Empty = the chart default.
OTEL_ENDPOINT: ${{ vars.OTEL_ENDPOINT }}
steps:
- uses: https://github.com/actions/checkout@v4
@@ -103,7 +100,7 @@ jobs:
make k8s-reseed \
TALOS_HOST=${TALOS_HOST:-localhost} \
K8S_REGISTRY=${TALOS_VM_IP:-192.168.122.173}:30500 \
K8S_SET="${KEYCLOAK_URL:+--set keycloakUrl=$KEYCLOAK_URL} --set demo.otpAutofill=${OTP_AUTOFILL:-false}${OTEL_ENDPOINT:+ --set otelEndpoint=$OTEL_ENDPOINT}"
K8S_SET="${KEYCLOAK_URL:+--set keycloakUrl=$KEYCLOAK_URL} --set demo.otpAutofill=${OTP_AUTOFILL:-false}"
# `dev` is a mutable tag and helm sees an unchanged pod template, so the
# new images only land on a restart (pullPolicy is already Always).
-4
View File
@@ -61,7 +61,3 @@ __pycache__/
TestResults/
test-output/
tests/e2e/playwright-report.json
# MkDocs build (`make docs`)
.venv-docs/
site/
+2 -11
View File
@@ -43,11 +43,11 @@ export DOCKER_HOST := unix://$(PODMAN_SOCK)
endif
endif
.PHONY: ci lint build unit mutation frontend docs integration verify verify-up verify-acl verify-nrc verify-projection verify-bff verify-domain verify-observability verify-tracing verify-metrics verify-objecttypen verify-objecten verify-registerrecord verify-objecten-notifications verify-notifications smoke up down local verify-local local-down changelog openzaak-up openzaak-smoke openzaak-seed openzaak-down stack-up stack-smoke stack-down keycloak-up keycloak-smoke keycloak-down flowable-up flowable-smoke flowable-down k8s-lint k8s-drift k8s-registry k8s-images k8s-seed k8s-up k8s-reseed k8s-portals k8s-down k8s-purge help
.PHONY: ci lint build unit mutation frontend integration verify verify-up verify-acl verify-nrc verify-projection verify-bff verify-domain verify-observability verify-tracing verify-metrics verify-objecttypen verify-objecten verify-registerrecord verify-objecten-notifications verify-notifications smoke up down local verify-local local-down changelog openzaak-up openzaak-smoke openzaak-seed openzaak-down stack-up stack-smoke stack-down keycloak-up keycloak-smoke keycloak-down flowable-up flowable-smoke flowable-down k8s-lint k8s-drift k8s-registry k8s-images k8s-seed k8s-up k8s-reseed k8s-portals k8s-down k8s-purge help
## ci: run the full pipeline — lint, build, unit, mutation, frontend, verify (mirrors Gitea Actions)
## `verify` is the live-stack stage (full stack up once → ACL + notification checks).
ci: lint build unit mutation frontend docs verify
ci: lint build unit mutation frontend verify
## frontend: install deps and run the Nx lint/test/build for the portals (pnpm + Node required)
# Tests run in their own phase, ahead of the build. The @angular/build:unit-test
@@ -81,15 +81,6 @@ unit:
python3 infra/test_playwright_summary.py
python3 infra/test_portal_caddyfiles.py
## docs: build the MkDocs site with --strict (a broken link or nav entry fails)
# Pinned in a throwaway venv: Material 9.7 is the last line on MkDocs 1.x, and MkDocs
# 2.0 drops the plugin/theme system this site relies on. Publishing is a separate
# decision (#173); this only proves the site builds.
docs:
python3 -m venv .venv-docs
.venv-docs/bin/pip install --quiet mkdocs==1.6.1 mkdocs-material==9.7.7
.venv-docs/bin/mkdocs build --strict
## mutation: run the Stryker.NET ratchet on each service with branching logic (fails below baseline)
# Stryker is pinned as a local dotnet tool (.config/dotnet-tools.json); `tool restore`
# makes `make mutation` work from a fresh clone. Each service owns its config + break
@@ -1,108 +0,0 @@
# ADR-0035: The deployed stack is published through the existing labs Caddy
- **Status:** Accepted
- **Date:** 2026-09-25
- **Deciders:** Respellion engineering
- **Slice:** [#177](https://git.labs.respellion.tech/eho/register-referentie/issues/177) —
that issue proposed the opposite (an in-cluster Caddy edge); this ADR records why the
host-side option won. Implemented in #179, #180 and #181.
## Context
The stack deploys to a single-node Talos VM (ADR-0033, #175). Until now it was only usable
through five SSH port-forwards: the portals' OIDC flow uses PKCE, PKCE needs
`crypto.subtle`, and browsers expose that only in a **secure context**, meaning HTTPS or a
`localhost` origin. A NodePort on the VM's address is neither. We want a URL a demo
audience can simply open.
Three facts about where things run shape the answer:
- The Talos VM is a libvirt guest on a **Fedora hypervisor in the office**, behind NAT
with no public address. The only way in from outside is an existing reverse SSH tunnel
(`autossh-reverse-tunnel.service`) into an `openssh-server` container on the labs
server.
- The **labs server** (public IP) already runs Caddy for `*.labs.respellion.tech`, with
the wildcard certificate (DNS-01 via Cloudflare) and ports 80/443. Every other labs
service is published there (repo `Infra`, `infra/development/`).
- #177 proposed a Caddy **inside the cluster**, fed by a layer-4 forward on the host, so
that routing and certificates would be cluster state. That assumes the public IP is on
the hypervisor. It isn't: the hypervisor has no inbound path, and 80/443 on the labs
server are already taken by the labs Caddy.
## Decision
**Publish the portals and Keycloak through the existing labs Caddy. Carry the traffic to
the cluster over a second reverse SSH tunnel from the hypervisor.**
```
browser ─https─▶ labs Caddy ─▶ openssh-server:3014x/30180
─reverse SSH tunnel─▶ Fedora hypervisor ─▶ Talos NodePorts
```
- **Hostnames** under the existing wildcard: `big-register` (openbaar), `big-mijn`
(self-service), `big-behandel`, `big-beheer`, and `big-auth` (Keycloak, with `/admin*`
answered 404).
- **Tunnel:** `big-portals-tunnel.service` on the hypervisor (repo `Infra`)
reverse-forwards the five browser-facing NodePorts into `openssh-server`. It is
separate from the access tunnel on `:6667`, so a failed forward can't cut SSH access.
Caddy joins the `openssh_default` network to reach the tunnel ends.
- **Keycloak's issuer** is the public origin. The chart value `keycloakUrl` replaces
`host` + NodePort in one helper, `big.keycloakUrl`, which feeds both `KC_HOSTNAME` and
the portals' `config.json` authority, so the two cannot drift (ADR-0010). The deploy
workflow sets it from the `KEYCLOAK_URL` repository variable.
- **`KC_PROXY_HEADERS=xforwarded`:** `KC_HOSTNAME_BACKCHANNEL_DYNAMIC` builds the token,
userinfo and certs URLs from the request. That request reaches Keycloak as plain HTTP,
so the URLs came out `http://` and browsers blocked them as mixed content. Trusting
Caddy's `X-Forwarded-Proto` keeps them HTTPS. In-cluster calls send no such header and
still use `keycloak:8080`.
- **Demo MFA (optional):** `demo.otpAutofill` (`OTP_AUTOFILL`) makes the `big-demo` theme
(`infra/keycloak/themes/big-demo`) Keycloak's default. Its script fills in and submits
the medewerker OTP from the fixture secret (ADR-0031), so the step is visibly enforced
without an authenticator. It is off by default.
### Alternatives considered
- **In-cluster Caddy edge (#177, PR #178).** It would keep routes and certificates in
cluster state. But it needs a public inbound path to the hypervisor that doesn't exist,
plus a second certificate authority beside the labs Caddy, which already holds the
wildcard. Closed unmerged.
- **Port-forward on the office router to the hypervisor.** This opens the office network
itself to the internet. Rejected.
- **Move the cluster to a host with a public IP.** It would remove the tunnel, but it's a
bigger change than publishing one demo. It remains the natural step if the stack
outgrows a lab VM.
- **Keep the SSH port-forwards.** Fine for one developer, but not something you can send
to someone.
## Consequences
**Positive**
- Real hostnames and HTTPS, so PKCE works in any browser with no client-side setup.
- No new certificate handling: the labs Caddy's wildcard covers the new hosts.
- The chart stays edge-agnostic. With `keycloakUrl` empty it renders exactly as before,
so compose, CI and the `localhost` workflow are untouched.
**Negative / costs**
- **Routing lives outside the cluster**, in the Infra repo's Caddyfile. That is exactly
what #177 wanted to avoid. Adding a portal means changing three places: a NodePort in
the chart, a forward in the tunnel unit, and a host in the Caddyfile.
- **Two SSH hops in the data path.** If the hypervisor or the tunnel is down, the
portals return 502 even though the cluster is healthy.
- **One issuer string.** With `keycloakUrl` set, the `localhost` port-forward workflow
(runbook §5) can no longer log in.
- **Keycloak trusts `X-Forwarded-*`** from anything that reaches it. Today that is only
in-cluster callers and the tunnel. `KC_PROXY_TRUSTED_ADDRESSES` can narrow it if the
NodePort is ever exposed more widely.
- **The portals are public.** Anyone with the link can log in with the committed test
credentials, and with `OTP_AUTOFILL` on, no second factor stands in the way. That is
acceptable for synthetic data. Put the labs Caddy's Azure `authorize` in front of the
`big-*` hosts if the audience must be restricted.
**Follow-up**
- Runbook: `docs/runbooks/kubernetes-talos.md`, "Publishing through the labs Caddy".
- Dev-mode Keycloak generates new signing keys on every restart, and the BFF re-fetches
them at most every 5 minutes, so expect a few minutes of 401s after a Keycloak restart.
Persisting Keycloak's database (runbook §6) would remove that.
+1 -5
View File
@@ -19,7 +19,6 @@ and CI cannot drift:
| `build` | `make build` → `dotnet build … -c Release` | .NET 10 SDK |
| `unit` | `make unit` → `dotnet test … -c Release --filter "Category!=Integration"` | .NET 10 SDK |
| `frontend` | `make frontend` → Nx lint/test/build for the four portals | pnpm + Node |
| `docs` | `make docs` → `mkdocs build --strict` in a pinned venv (fails on a broken link or nav entry; the site is not published yet, #173) | Python 3 |
| `k8s` | `make k8s-lint` (render + schema-check the Helm chart) → `make k8s-drift` (chart still describes the same stack as `infra/docker-compose.yml`) | pinned `helm` binary + `docker compose` |
| `mutation` | `make mutation` → `dotnet tool restore` → `dotnet stryker` (ACL); uploads the HTML report as an artifact | .NET 10 SDK |
| `verify-stack` | **push to `main` only, skipped on PRs** (#182) — the single live-stack stage — steps: `make verify-up` (full stack up + health, the DoD smoke) → `make verify-acl` (ACL ↔ OpenZaak) → `make verify-nrc` (OpenZaak → NRC delivery) → `make down` | container engine + egress (base images, nuget, `selectielijst.openzaak.nl`) |
@@ -58,12 +57,9 @@ dotnet tool (`.config/dotnet-tools.json`), so it runs identically locally and in
make mutation # dotnet tool restore + dotnet stryker on the ACL
```
Config lives in `services/acl/stryker-config.json`.
Config lives in [`services/acl/stryker-config.json`](../../services/acl/stryker-config.json).
It runs in **solution mode** against `Acl.slnx`, mutating the two projects under test
(`Acl.Application`, `Acl.Infrastructure`); `Acl.Api` has no tests and is skipped.
`Acl.slnx` leaves out `Acl.IntegrationTests`: it needs a live OpenZaak, and Stryker
runs every test project in the solution, so keeping it in makes 8 tests fail in the
initial run (#174).
**Baseline (the ratchet):** the ACL is the first service with branching logic, so it
sets the repo-wide baseline. Observed score **95%**; enforced `break` threshold **90%**
-2
View File
@@ -403,8 +403,6 @@ upgrade path.
## Publishing through the labs Caddy
Why this route and not an in-cluster edge: [ADR-0035](../architecture/adr-0035-public-access-through-the-labs-caddy.md).
The portals can be reached on real hostnames through the Caddy that already fronts
`*.labs.respellion.tech` (repo `Infra`, `infra/development/`). The chain:
+3 -10
View File
@@ -30,12 +30,6 @@ host: 192.168.122.100
# portals' authority (runbook, "Publishing through the labs Caddy").
keycloakUrl: ""
# Where the .NET services send traces (OTLP gRPC). The default is the chart's own
# `tempo` workload (off by default, like compose). Point it at a Tempo outside the
# release, e.g. the cluster monitoring stack's http://tempo.monitoring.svc:4317 —
# with no Tempo at all, every export fails and is counted as a .NET exception.
otelEndpoint: http://tempo:4317
demo:
# Fill in and submit the medewerker OTP step from the fixture secret, so a public
# demo shows MFA enforced without an authenticator: makes the big-demo theme
@@ -166,11 +160,10 @@ envGroups:
NOTIFICATIONS_DISABLED: "false"
RUN_SETUP_CONFIG: "true"
# Traces for the .NET services. Always set, like compose. With no Tempo behind
# `otelEndpoint` the exporter fails quietly but throws on every batch, which
# shows up as HttpRequestException/SocketException in dotnet_exceptions_total.
# Traces for the .NET services. Always set, like compose: the exporter fails
# harmlessly when Tempo is absent (services/*/Program.cs).
otel:
OTEL_EXPORTER_OTLP_ENDPOINT: '{{ .Values.otelEndpoint }}'
OTEL_EXPORTER_OTLP_ENDPOINT: http://tempo:4317
OTEL_EXPORTER_OTLP_PROTOCOL: grpc
# ── Workloads ──────────────────────────────────────────────────────────────────
-1
View File
@@ -56,7 +56,6 @@ nav:
- "ADR-0032: Werkbak live refresh": architecture/adr-0032-werkbak-live-refresh.md
- "ADR-0033: Kubernetes via one Helm chart": architecture/adr-0033-kubernetes-via-one-helm-chart.md
- "ADR-0034: Caddy serves the portals": architecture/adr-0034-caddy-serves-the-portals.md
- "ADR-0035: Public access through the labs Caddy": architecture/adr-0035-public-access-through-the-labs-caddy.md
- FDS-architectuur:
- Overzicht: architecture/fds/README.md
- Componentview (L3): architecture/fds/c4-component-view.md
+1 -3
View File
@@ -1,9 +1,7 @@
<Solution>
<!-- Stryker-only. Acl.IntegrationTests is left out on purpose: it needs a live
OpenZaak, so in the mutation job it fails its initial run (#174). The root
register-referentie.slnx still builds and lints it. -->
<Project Path="Acl.Api/Acl.Api.csproj" />
<Project Path="Acl.Application/Acl.Application.csproj" />
<Project Path="Acl.Infrastructure/Acl.Infrastructure.csproj" />
<Project Path="Acl.IntegrationTests/Acl.IntegrationTests.csproj" />
<Project Path="Acl.Tests/Acl.Tests.csproj" />
</Solution>