## What & why The host-browser stack (`make local`) had drifted behind three slices, so a fresh bring-up couldn't complete the flow: registrations stuck at `OpenZaakAanmaken`, the behandel werkbak stayed empty, and the openbaar register showed nothing. The `verify-*` scripts do this setup for CI at test time; `make local` had no equivalent. This makes the local stack **self-seed at bring-up** so it just works in a browser: - **DMN** — `flowable-init` now also deploys `diploma-eligibility.dmn` (was BPMN-only), so completing `WachtOpDocumenten` routes through the DMN to `Beoordelen` instead of 404ing. - **Zaaktype + ACL** — a `local-seed` one-shot publishes the BIG zaaktype (whose UUID is server-assigned, hence not static in the compose file) and writes the real URLs to `seed-env:/acl.env`; the ACL sources it on startup via an entrypoint override. - **NRC abonnement** — an `nrc-subscribe` one-shot registers the `zaken` subscription at the event-subscriber callback, so notifications reach the projection/openbaar register. Both one-shots reach OpenZaak/NRC by **container IP** (a single-label host fails their Django URLValidator), mirroring the CI verify scripts. Design + trade-offs in **ADR-0020**. Closes #110 ## Definition of Done - [x] Linked Gitea issue (#110). - [x] Failing test committed before the implementation — `test(infra): …` adds `infra/run-local-flow-check.sh` / `make verify-local`; the three gaps' failures were observed live on a fresh `make local` (red), and the fix turns it green. - [x] Implementation makes the test pass; docs commit follows. - [x] Conventional Commits referencing the issue (`refs #110`). - [ ] CI green — running on the restored runner. Infra-only change; the CI `verify-stack` job uses `docker-compose.yml` (untouched). Also validated locally: `make verify-local` passes against a fresh `make local` (see below). - [x] `docker compose up` from a fresh clone reaches green health checks — verified: `make local` healthy in ~2m20s, then `make verify-local` green. - [x] Docs updated — ADR-0020 + demo-script note. - [x] ADR added in `docs/architecture/` — ADR-0020. - [x] Demo note in `docs/demo-script.md`. ## Notes for reviewers - **Infra-only** — no service code changes; the ACL image and the CI stack (`docker-compose.yml`) are untouched. - **Verified end-to-end on a fresh stack** (`make local-down && make local && make verify-local`): ``` >> 2. zaak opened (zaaktype seeded + wired) >> 3. documents accepted 204 (DMN deployed) >> 4. in the werkbak (DMN routing → Beoordelen) >> 5. visible in the openbaar register (NRC abonnement) OK — a fresh local stack completed the flow with no manual seeding ``` - **Follow-up:** the cleaner design — ACL resolving its zaaktype by `identificatie` instead of a pinned server-assigned URL — is split out as **S-27 (#113)**; landing it would remove the `acl.env` injection here. ADR-0020 records this. - The `seed-env` volume carries the generated `acl.env` from `local-seed` to the ACL; a `down --volumes` (as `make local-down` does) resets it cleanly. Reviewed-on: #114
This commit was merged in pull request #114.
This commit is contained in:
@@ -43,7 +43,7 @@ export DOCKER_HOST := unix://$(PODMAN_SOCK)
|
||||
endif
|
||||
endif
|
||||
|
||||
.PHONY: ci lint build unit mutation frontend integration verify verify-up verify-acl verify-nrc verify-projection verify-bff verify-domain verify-notifications smoke up down 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 help
|
||||
.PHONY: ci lint build unit mutation frontend integration verify verify-up verify-acl verify-nrc verify-projection verify-bff verify-domain 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 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).
|
||||
@@ -114,6 +114,11 @@ local:
|
||||
docker compose -f $(LOCAL_COMPOSE) up -d --build
|
||||
WAIT_TIMEOUT=420 bash infra/wait-healthy.sh $(WAIT_SVCS)
|
||||
|
||||
## verify-local: acceptance check for the local stack (S-B04) — a fresh `make local` completes the
|
||||
## whole flow (zaaktype seeded + DMN deployed + NRC abonnement) with NO manual seeding.
|
||||
verify-local:
|
||||
bash infra/run-local-flow-check.sh
|
||||
|
||||
## local-down: stop and remove the bind-mount stack
|
||||
local-down:
|
||||
docker compose -f $(LOCAL_COMPOSE) down --volumes
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# ADR-0020: The local stack self-seeds the zaaktype, DMN, and NRC abonnement at bring-up
|
||||
|
||||
- **Status:** Accepted
|
||||
- **Date:** 2026-07-22
|
||||
- **Deciders:** Respellion engineering
|
||||
- **Relates to:** S-B04 (#110). Local-stack twin of the seeding the verify-* scripts do for CI
|
||||
(`infra/run-domain-check.sh`, `infra/verify-notification-driver.py`). Superseded in part by S-27
|
||||
(#113), which would let the ACL resolve its zaaktype by identificatie and remove the URL injection.
|
||||
|
||||
## Context
|
||||
|
||||
`infra/docker-compose.local.yml` is the host-browser-friendly stack (`make local`) — the one a
|
||||
developer clicks through the portals with. It had drifted behind three slices, so a fresh bring-up
|
||||
could not complete the flow:
|
||||
|
||||
1. The ACL pointed at a placeholder zaaktype (`…/00000000-…`), so zaak creation failed with OpenZaak
|
||||
`400` and the registratie process stuck at `OpenZaakAanmaken` (S-05).
|
||||
2. `flowable-init` deployed only `registratie.bpmn`, not `diploma-eligibility.dmn`, so completing
|
||||
`WachtOpDocumenten` 404'd on the missing decision and never reached `Beoordelen` (S-10a/S-13).
|
||||
3. No NRC abonnement was registered, so notifications reached NRC and went nowhere — the projection
|
||||
and the openbaar register stayed empty (S-06).
|
||||
|
||||
The CI stack (`infra/docker-compose.yml`) does not hit this because its `verify-*` scripts seed the
|
||||
zaaktype, deploy the DMN, and register the abonnement at *test* time. The local stack has no such
|
||||
harness — a developer just runs `make local` and browses. The non-obvious wrinkle is (1): the
|
||||
zaaktype **UUID is assigned by OpenZaak at creation**, so the ACL's zaaktype URL is not knowable when
|
||||
the compose file is written and cannot be a static value.
|
||||
|
||||
## Decision
|
||||
|
||||
**Make the local stack self-seed at bring-up via one-shot init containers, and hand the ACL its
|
||||
server-assigned zaaktype URL through a shared-volume env file it sources on startup.**
|
||||
|
||||
- **DMN (gap 2).** `flowable-init` now deploys `diploma-eligibility.dmn` to the DMN engine
|
||||
(`/flowable-rest/dmn-api/dmn-repository/deployments`) as a separate deployment alongside the BPMN —
|
||||
identical to the CI `flowable-init`. Idempotent.
|
||||
- **Zaaktype + ACL wiring (gap 1).** A `local-seed` one-shot runs the existing
|
||||
`infra/openzaak/seed_catalogus.py` (`OZ_PUBLISH=1`) against OpenZaak and writes the resulting
|
||||
`Acl__Defaults__ZaaktypeUrl` / `…InformatieobjecttypeUrl` / `Acl__OpenZaak__BaseUrl` into
|
||||
`seed-env:/out/acl.env`. The ACL mounts that volume read-only and overrides its entrypoint to
|
||||
`sh -c 'set -a; . /seed/acl.env; set +a; exec dotnet Acl.Api.dll'`, so the real values override the
|
||||
compose placeholders before the app reads config. The ACL `depends_on: local-seed
|
||||
(service_completed_successfully)`.
|
||||
- **Abonnement (gap 3).** A `nrc-subscribe` one-shot registers an abonnement on the `zaken` kanaal
|
||||
pointing at the event-subscriber's `/notifications` callback (`infra/local/register-abonnement.py`).
|
||||
It is a leaf — nothing depends on it — so it can wait for the event-subscriber without forming a
|
||||
cycle with the ACL bootstrap.
|
||||
- **Reach OpenZaak/NRC by container IP, not service name.** Both the seed's ZTC calls and the
|
||||
abonnement's `callbackUrl` are validated by Django's URLValidator, which rejects a single-label host
|
||||
like `openzaak` / `event-subscriber`. The scripts resolve the target's container IP at runtime (as
|
||||
`infra/run-domain-check.sh` does), keeping the seeded URLs valid **and** host-consistent — the ACL's
|
||||
base URL is set to the same OpenZaak IP that owns the zaaktype URL.
|
||||
- **Acceptance.** `make verify-local` (`infra/run-local-flow-check.sh`) submits against a fresh stack
|
||||
and asserts the zaak opens, the case reaches the werkbak after documents, and the reference appears
|
||||
in the openbaar register — the red-to-green test for all three gaps.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive**
|
||||
|
||||
- A fresh `make local` completes the full demo (submit → werkbak → openbaar) with no manual seeding —
|
||||
the slice's stated outcome.
|
||||
- Reuses the proven CI mechanisms (`seed_catalogus.py`, the DMN deploy, the abonnement driver) rather
|
||||
than inventing new ones; the only genuinely new piece is the entrypoint-sourced env file.
|
||||
- No service code changes — the fix is entirely in `infra/` (compose + two small scripts), so the ACL
|
||||
image and the CI stack are untouched.
|
||||
|
||||
**Negative / costs**
|
||||
|
||||
- The two compose files diverge further: the CI stack seeds at test time, the local stack at bring-up.
|
||||
Mitigated by reusing the same underlying scripts and cross-referencing them.
|
||||
- The ACL entrypoint override couples the local ACL to the seed-written file path (`/seed/acl.env`);
|
||||
if the seed fails, the ACL fails to start (loud, healthcheck-visible — preferred over silently
|
||||
running with a placeholder).
|
||||
- Container-IP-based URLs are re-derived on each bring-up; a keep-volumes restart with a changed
|
||||
OpenZaak IP relies on OpenZaak rebuilding hyperlinked URLs from the request host (it does) so the
|
||||
idempotent re-seed reports current-IP URLs.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **ACL resolves its zaaktype by identificatie (`BIG-REGISTRATIE`) at startup.** The cleaner,
|
||||
less-brittle design — no server-assigned URL to capture — and it would help the CI stack too. But it
|
||||
changes a service's runtime behaviour and its config contract, needs new ACL tests + mutation
|
||||
coverage, and still needs a seed step to *create* the zaaktype. Deliberately split out as its own
|
||||
slice with its own ADR (S-27 / #113) rather than folded into this infra-only fix.
|
||||
- **A documented `make local-seed` step run after `make local`.** Smallest change, but it fails the
|
||||
slice's "no manual seeding" outcome — the local stack is exactly the one meant to just work in a
|
||||
browser. Rejected.
|
||||
- **Fixed zaaktype UUID via OpenZaak `setup_configuration`/fixtures.** OpenZaak assigns UUIDs on POST;
|
||||
declaratively creating a fully *published* zaaktype (statustypen + resultaattypen validated against
|
||||
the Selectielijst + roltypen + iot relations) is not something `setup_configuration` supports
|
||||
cleanly in 1.28.2. Rejected as more fragile than reusing `seed_catalogus.py`.
|
||||
@@ -5,6 +5,33 @@ copy-pasteable walkthrough against a local `make up` stack.
|
||||
|
||||
---
|
||||
|
||||
## S-B04 — `make local` completes the whole flow with no manual seeding (#110, ADR-0020)
|
||||
|
||||
**Outcome:** the host-browser stack (`make local`) now self-seeds at bring-up — it publishes the BIG
|
||||
zaaktype and wires the ACL to it, deploys the `diploma-eligibility` DMN, and registers the NRC
|
||||
abonnement — so a fresh bring-up runs submit → werkbak → openbaar without the manual seeding the
|
||||
`verify-*` scripts do for CI. (Previously the process stuck at `OpenZaakAanmaken`, the werkbak stayed
|
||||
empty, and the openbaar register showed nothing.)
|
||||
|
||||
```bash
|
||||
# 1. Fresh bring-up (self-seeding init containers: local-seed, nrc-subscribe; DMN in flowable-init).
|
||||
make local
|
||||
|
||||
# 2. Assert the whole flow works with no manual seeding — submit opens a zaak, documents route it to
|
||||
# the werkbak, and the reference appears in the openbaar register:
|
||||
make verify-local # → "OK — a fresh local stack completed the flow with no manual seeding ..."
|
||||
|
||||
# 3. Or by hand in the browser: log in at http://localhost:8140 (jan-burger / test123), submit +
|
||||
# upload a PDF, then approve it in the werkbak at http://localhost:8142 (merel-behandelaar /
|
||||
# test123); it shows as INGESCHREVEN in the openbaar register at http://localhost:8141.
|
||||
```
|
||||
|
||||
> The zaaktype UUID is server-assigned, so `local-seed` writes the real URL into a shared volume as
|
||||
> `acl.env` and the ACL sources it on startup (ADR-0020). The cleaner long-term fix — the ACL
|
||||
> resolving its zaaktype by `identificatie` — is tracked separately as S-27 (#113).
|
||||
|
||||
---
|
||||
|
||||
## S-08d — Walking skeleton complete: browser → submit, end-to-end
|
||||
|
||||
**Outcome:** the self-service portal is served in the stack and the full front-of-house happy path
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
# LOCAL development stack — runs with a plain `docker compose up`, no make / no
|
||||
# seed step / no bash. Use this on a local engine (Docker Desktop on Windows or
|
||||
# external seed step / no bash. Use this on a local engine (Docker Desktop on Windows or
|
||||
# macOS, or rootless Podman on Linux).
|
||||
#
|
||||
# Self-seeding (S-B04, #110, ADR-0020): unlike the CI stack — where the verify-* scripts seed the
|
||||
# zaaktype and register the NRC abonnement at test time — this stack does that itself, via one-shot
|
||||
# init containers (local-seed, nrc-subscribe) + a DMN deploy in flowable-init, so a fresh bring-up
|
||||
# completes the whole flow with no manual steps. `make verify-local` asserts it.
|
||||
#
|
||||
# docker compose -f infra/docker-compose.local.yml up -d --build # podman
|
||||
# docker compose -f infra/docker-compose.local.yml up -d --build --wait # Docker Desktop
|
||||
# docker compose -f infra/docker-compose.local.yml down --volumes
|
||||
@@ -257,28 +262,65 @@ services:
|
||||
restart: "no"
|
||||
volumes:
|
||||
- ../workflows/registratie.bpmn:/work/registratie.bpmn:ro,z
|
||||
- ../workflows/diploma-eligibility.dmn:/work/diploma-eligibility.dmn:ro,z
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
base=http://flowable-rest:8080/flowable-rest/service/repository/deployments
|
||||
until curl -sf -u rest-admin:test "$$base" >/dev/null 2>&1; do echo "waiting for flowable-rest..."; sleep 3; done
|
||||
if curl -s -u rest-admin:test "$$base?name=registratie" | grep -q '"name":"registratie"'; then
|
||||
echo "registratie already deployed; skip"
|
||||
svc=http://flowable-rest:8080/flowable-rest/service/repository/deployments
|
||||
dmn=http://flowable-rest:8080/flowable-rest/dmn-api/dmn-repository/deployments
|
||||
until curl -sf -u rest-admin:test "$$svc" >/dev/null 2>&1; do echo "waiting for flowable-rest..."; sleep 3; done
|
||||
# Deploy the DMN to the DMN engine and the BPMN to the process engine as SEPARATE deployments:
|
||||
# flowable-rest does NOT cascade a .dmn bundled in a process .bar into the DMN engine, so the DMN
|
||||
# must go via dmn-api. The registratie process's DMN service task then resolves the decision across
|
||||
# deployments by key (S-13, ADR-0016). Without this the WachtOpDocumenten completion 404s on the
|
||||
# missing decision and the case never reaches Beoordelen (S-B04). Both steps are idempotent.
|
||||
if curl -s -u rest-admin:test "$$dmn" | grep -q '"name":"diploma-eligibility.dmn"'; then
|
||||
echo "diploma-eligibility DMN already deployed; skip"
|
||||
else
|
||||
curl -sf -u rest-admin:test -F 'file=@/work/registratie.bpmn;filename=registratie.bpmn' "$$base" >/dev/null && echo "deployed registratie"
|
||||
curl -sf -u rest-admin:test -F 'file=@/work/diploma-eligibility.dmn;filename=diploma-eligibility.dmn' "$$dmn" >/dev/null && echo "deployed diploma-eligibility DMN"
|
||||
fi
|
||||
if curl -s -u rest-admin:test "$$svc?name=registratie" | grep -q '"name":"registratie"'; then
|
||||
echo "registratie BPMN already deployed; skip"
|
||||
else
|
||||
curl -sf -u rest-admin:test -F 'file=@/work/registratie.bpmn;filename=registratie.bpmn' "$$svc" >/dev/null && echo "deployed registratie BPMN"
|
||||
fi
|
||||
depends_on:
|
||||
flowable-rest:
|
||||
condition: service_started
|
||||
networks: [cg]
|
||||
|
||||
# ── Local bootstrap: seed the zaaktype + wire the ACL (S-B04, #110, ADR-0020) ─────────────────
|
||||
# The zaaktype UUID is assigned by OpenZaak at creation, so it can't be a static value in this
|
||||
# file. This one-shot seeds + publishes the BIG zaaktype (and the Diploma informatieobjecttype)
|
||||
# and writes their server-assigned URLs into a shared volume as acl.env, which the ACL sources on
|
||||
# startup (below). It is the local-stack equivalent of what infra/run-domain-check.sh does for CI.
|
||||
# Reaches OpenZaak by its container IP because a single-label host fails OpenZaak's URLValidator.
|
||||
local-seed:
|
||||
image: docker.io/library/python:3-slim
|
||||
restart: "no"
|
||||
volumes:
|
||||
- ./openzaak/seed_catalogus.py:/work/seed_catalogus.py:ro,z
|
||||
- ./local/seed-zaaktype.sh:/work/seed-zaaktype.sh:ro,z
|
||||
- seed-env:/out
|
||||
command: ["sh", "/work/seed-zaaktype.sh"]
|
||||
depends_on:
|
||||
openzaak:
|
||||
condition: service_healthy
|
||||
networks: [cg]
|
||||
|
||||
# ── ACL ──────────────────────────────────────────────────────────────────
|
||||
acl:
|
||||
build:
|
||||
context: ../services/acl
|
||||
dockerfile: Dockerfile
|
||||
image: register-referentie/acl:dev
|
||||
# The base/zaaktype/informatieobjecttype below are PLACEHOLDERS. The real, server-assigned
|
||||
# values are written by the local-seed one-shot into seed-env:/seed/acl.env, which the entrypoint
|
||||
# sources (set -a) so they override these before the app starts (S-B04, #110, ADR-0020). Sourcing
|
||||
# a runtime-generated env file is why we override the entrypoint here rather than use `env_file:`
|
||||
# (which compose reads at parse time, before the seed has run).
|
||||
entrypoint: ["/bin/sh", "-c", "set -a; . /seed/acl.env; set +a; exec dotnet Acl.Api.dll"]
|
||||
environment:
|
||||
Acl__OpenZaak__BaseUrl: http://openzaak:8000/
|
||||
Acl__OpenZaak__ClientId: big-reference-seed
|
||||
@@ -286,9 +328,12 @@ services:
|
||||
Acl__Defaults__Bronorganisatie: "517439943"
|
||||
Acl__Defaults__VerantwoordelijkeOrganisatie: "517439943"
|
||||
Acl__Defaults__Vertrouwelijkheidaanduiding: openbaar
|
||||
Acl__Defaults__ZaaktypeUrl: ${ACL_ZAAKTYPE_URL:-http://openzaak:8000/catalogi/api/v1/zaaktypen/00000000-0000-0000-0000-000000000000}
|
||||
Acl__Defaults__ZaaktypeUrl: http://openzaak:8000/catalogi/api/v1/zaaktypen/00000000-0000-0000-0000-000000000000
|
||||
Acl__Defaults__InformatieobjecttypeUrl: http://openzaak:8000/catalogi/api/v1/informatieobjecttypen/00000000-0000-0000-0000-000000000000
|
||||
ports:
|
||||
- "8100:8080"
|
||||
volumes:
|
||||
- seed-env:/seed:ro
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
|
||||
interval: 5s
|
||||
@@ -298,6 +343,8 @@ services:
|
||||
depends_on:
|
||||
openzaak:
|
||||
condition: service_healthy
|
||||
local-seed:
|
||||
condition: service_completed_successfully
|
||||
networks: [cg]
|
||||
|
||||
# ── BFF ──────────────────────────────────────────────────────────────────
|
||||
@@ -400,6 +447,31 @@ services:
|
||||
condition: service_healthy
|
||||
networks: [cg]
|
||||
|
||||
# ── Local bootstrap: register the NRC abonnement (S-B04, #110, ADR-0020) ──────────────────────
|
||||
# Without a subscription, OpenZaak's notifications reach NRC and are delivered nowhere, so the
|
||||
# projection (and the openbaar register) stay empty. This one-shot registers an abonnement on the
|
||||
# `zaken` kanaal pointing at the event-subscriber's /notifications callback — the CI equivalent is
|
||||
# infra/verify-notification-driver.py. The callback uses the event-subscriber's container IP (a
|
||||
# single-label host fails NRC's URLValidator). It is a leaf (nothing depends on it), so it can wait
|
||||
# for the event-subscriber without creating a cycle with the ACL bootstrap.
|
||||
nrc-subscribe:
|
||||
image: docker.io/library/python:3-slim
|
||||
restart: "no"
|
||||
volumes:
|
||||
- ./local/register-abonnement.py:/work/register-abonnement.py:ro,z
|
||||
environment:
|
||||
NRC_BASE: http://nrc-web:8000
|
||||
SINK_HOST: event-subscriber
|
||||
SINK_PORT: "8080"
|
||||
SINK_AUTH: ${NOTIFICATION_WEBHOOK_TOKEN:-Bearer big-reference-notifications}
|
||||
command: ["python", "/work/register-abonnement.py"]
|
||||
depends_on:
|
||||
nrc-web:
|
||||
condition: service_healthy
|
||||
event-subscriber:
|
||||
condition: service_started
|
||||
networks: [cg]
|
||||
|
||||
projection-api:
|
||||
build:
|
||||
context: ..
|
||||
@@ -492,6 +564,8 @@ volumes:
|
||||
nrc-db:
|
||||
flowable-db:
|
||||
projection-db:
|
||||
# Carries the seed-generated acl.env (server-assigned zaaktype URLs) from local-seed to the ACL.
|
||||
seed-env:
|
||||
|
||||
networks:
|
||||
cg:
|
||||
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Local-stack bootstrap (S-B04, #110, ADR-0020) — register the NRC abonnement.
|
||||
|
||||
Runs as the `nrc-subscribe` init container of infra/docker-compose.local.yml. Registers an
|
||||
abonnement on the `zaken` kanaal pointing at the event-subscriber's /notifications callback, so
|
||||
OpenZaak's notifications (zaak create + status set) reach the projection — without this the openbaar
|
||||
(public) register stays empty. This is what infra/verify-notification-driver.py does for CI (minus
|
||||
the test zaak it also creates).
|
||||
|
||||
The callback host is the event-subscriber's resolved **container IP**, not `event-subscriber`, because
|
||||
NRC validates callbackUrl with Django's URLValidator (a single-label host is rejected — same reason the
|
||||
zaaktype seed uses OpenZaak's IP). Idempotent + restart-safe: it removes any stale /notifications
|
||||
abonnement first, then registers one for the current IP. Stdlib only.
|
||||
|
||||
Env: NRC_BASE, SINK_HOST, SINK_PORT, SINK_AUTH, OZ_CLIENT_ID, OZ_SECRET.
|
||||
"""
|
||||
import base64, hashlib, hmac, json, os, socket, sys, time, urllib.error, urllib.request
|
||||
|
||||
NRC = os.environ.get("NRC_BASE", "http://nrc-web:8000").rstrip("/")
|
||||
SINK_HOST = os.environ.get("SINK_HOST", "event-subscriber")
|
||||
SINK_PORT = os.environ.get("SINK_PORT", "8080")
|
||||
SINK_AUTH = os.environ.get("SINK_AUTH", "Bearer big-reference-notifications")
|
||||
CID = os.environ.get("OZ_CLIENT_ID", "big-reference-seed")
|
||||
SECRET = os.environ.get("OZ_SECRET", "insecure-dev-secret-change-me")
|
||||
|
||||
|
||||
def token():
|
||||
b64 = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=")
|
||||
seg = (
|
||||
b64(json.dumps({"alg": "HS256", "typ": "JWT"}, separators=(",", ":")).encode())
|
||||
+ b"."
|
||||
+ b64(json.dumps(
|
||||
{"iss": CID, "iat": int(time.time()), "client_id": CID,
|
||||
"user_id": "local-seed", "user_representation": "local-seed"},
|
||||
separators=(",", ":")).encode())
|
||||
)
|
||||
return (seg + b"." + b64(hmac.new(SECRET.encode(), seg, hashlib.sha256).digest())).decode()
|
||||
|
||||
|
||||
def call(method, url, body=None):
|
||||
data = json.dumps(body).encode() if body is not None else None
|
||||
req = urllib.request.Request(url, data=data, method=method, headers={
|
||||
"Authorization": "Bearer " + token(),
|
||||
"Content-Type": "application/json", "Accept": "application/json"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
raw = r.read()
|
||||
return r.status, (json.loads(raw) if raw else None)
|
||||
except urllib.error.HTTPError as e:
|
||||
raw = e.read()
|
||||
return e.code, (json.loads(raw) if raw else None)
|
||||
|
||||
|
||||
def main():
|
||||
ip = socket.gethostbyname(SINK_HOST)
|
||||
callback = f"http://{ip}:{SINK_PORT}/notifications"
|
||||
|
||||
# Restart-safe: drop any prior /notifications abonnement (its IP may be stale) before creating a
|
||||
# fresh one for the current event-subscriber IP.
|
||||
status, body = call("GET", f"{NRC}/api/v1/abonnement")
|
||||
for ab in (body or []) if status == 200 else []:
|
||||
if str(ab.get("callbackUrl", "")).endswith("/notifications"):
|
||||
if ab.get("callbackUrl") == callback:
|
||||
print(f"abonnement already current: {ab['url']}")
|
||||
return
|
||||
call("DELETE", ab["url"])
|
||||
print(f"removed stale abonnement {ab['url']}")
|
||||
|
||||
status, ab = call("POST", f"{NRC}/api/v1/abonnement", {
|
||||
"callbackUrl": callback, "auth": SINK_AUTH,
|
||||
"kanalen": [{"naam": "zaken", "filters": {}}]})
|
||||
if status != 201:
|
||||
sys.exit(f"create abonnement -> {status}: {json.dumps(ab)}")
|
||||
print(f"abonnement registered: {ab['url']} -> {callback}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/bin/sh
|
||||
# Local-stack bootstrap (S-B04, #110, ADR-0020) — the "seed zaaktype + wire the ACL" step.
|
||||
#
|
||||
# Runs as the `local-seed` init container of infra/docker-compose.local.yml. It seeds + publishes
|
||||
# the BIG zaaktype (and the Diploma informatieobjecttype) into OpenZaak, then writes the resulting
|
||||
# **server-assigned** URLs into /out/acl.env, which the ACL entrypoint sources before starting. This
|
||||
# is the local-stack equivalent of what infra/run-domain-check.sh does for CI: the zaaktype UUID is
|
||||
# assigned by OpenZaak at creation, so it can't be a static value in the compose file.
|
||||
#
|
||||
# Why the container IP and not the `openzaak` service name: OpenZaak validates URL query params
|
||||
# (e.g. ?catalogus=) with Django's URLValidator, which rejects a single-label host like `openzaak`.
|
||||
# Seeding against the resolved IP keeps the seeded URLs valid AND host-consistent with the ACL, which
|
||||
# we point at the same IP below. See docs/runbooks/gitea-actions-gotchas.md and ADR-0020.
|
||||
set -eu
|
||||
|
||||
oz_ip="$(python3 -c "import socket;print(socket.gethostbyname('openzaak'))")"
|
||||
OZ_BASE="http://${oz_ip}:8000"
|
||||
export OZ_BASE OZ_PUBLISH=1
|
||||
|
||||
echo ">> seeding + publishing the BIG zaaktype at ${OZ_BASE} (idempotent)"
|
||||
out="$(python3 /work/seed_catalogus.py)"
|
||||
echo "$out"
|
||||
|
||||
zt="$(printf '%s\n' "$out" | sed -n 's/^ZAAKTYPE_URL //p' | head -1)"
|
||||
iot="$(printf '%s\n' "$out" | sed -n 's/^INFORMATIEOBJECTTYPE_URL //p' | head -1)"
|
||||
[ -n "$zt" ] || { echo "ERROR: seed did not report a ZAAKTYPE_URL" >&2; exit 1; }
|
||||
[ -n "$iot" ] || { echo "ERROR: seed did not report an INFORMATIEOBJECTTYPE_URL" >&2; exit 1; }
|
||||
|
||||
# The ACL entrypoint sources this; these keys override the placeholder defaults in the compose file.
|
||||
cat > /out/acl.env <<EOF
|
||||
Acl__OpenZaak__BaseUrl=${OZ_BASE}/
|
||||
Acl__Defaults__ZaaktypeUrl=${zt}
|
||||
Acl__Defaults__InformatieobjecttypeUrl=${iot}
|
||||
EOF
|
||||
echo ">> wrote /out/acl.env (base=${OZ_BASE}/ zaaktype=${zt})"
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Acceptance check for the local stack (S-B04, #110): a fresh `make local` must complete the whole
|
||||
# flow with NO manual seeding. Run against an already-up local stack (infra/docker-compose.local.yml)
|
||||
# via the host-published ports. It exercises, and thereby covers, the three bring-up gaps the slice
|
||||
# fixes:
|
||||
#
|
||||
# 1. zaaktype seeded + ACL wired -> a submitted registration opens a zaak (zaakUrl gets filled).
|
||||
# 2. diploma-eligibility DMN deployed -> providing documents completes WachtOpDocumenten, routes
|
||||
# through the DMN, and the case lands on Beoordelen (visible in the behandel werkbak).
|
||||
# 3. NRC abonnement registered -> the zaak shows up in the openbaar (public) register.
|
||||
#
|
||||
# Before the fix this fails at step 1 (ACL points at a placeholder zaaktype -> OpenZaak 400).
|
||||
set -euo pipefail
|
||||
|
||||
DOM=${DOM:-http://localhost:8130} # domain
|
||||
BFF=${BFF:-http://localhost:8080} # bff (openbaar register)
|
||||
BSN=${BSN:-123456782}
|
||||
# A minimal, valid PDF, base64-encoded (the diploma upload).
|
||||
PDF_B64="$(printf '%%PDF-1.4\n1 0 obj<</Type/Catalog>>endobj\ntrailer<</Root 1 0 R>>\n%%%%EOF\n' | base64 | tr -d '\n')"
|
||||
|
||||
echo ">> 1. submit a registration (no manual seeding expected)"
|
||||
loc="$(curl -fsS -D - -o /dev/null -X POST "$DOM/registrations" \
|
||||
-H 'Content-Type: application/json' -d "{\"bsn\":\"$BSN\"}" \
|
||||
| sed -n 's/\r$//; s/^[Ll]ocation: //p' | head -1)"
|
||||
[ -n "$loc" ] || { echo "FAIL: POST /registrations returned no Location" >&2; exit 1; }
|
||||
id="${loc##*/}"
|
||||
echo " accepted: $id"
|
||||
|
||||
echo ">> 2. poll until the ACL opens the zaak (proves the zaaktype is seeded + wired)"
|
||||
zaak=""
|
||||
for _ in $(seq 1 30); do
|
||||
zaak="$(curl -fsS "$DOM$loc" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("zaakUrl") or "")' 2>/dev/null || true)"
|
||||
[ -n "$zaak" ] && break
|
||||
sleep 3
|
||||
done
|
||||
[ -n "$zaak" ] || { echo "FAIL: zaak never opened — ACL zaaktype not wired (gap 1)" >&2; exit 1; }
|
||||
echo " zaak opened: $zaak"
|
||||
|
||||
echo ">> 3. provide documents (proves the diploma-eligibility DMN is deployed)"
|
||||
code="$(curl -s -o /dev/null -w '%{http_code}' -X POST "$DOM/registrations/$id/documents" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"bsn\":\"$BSN\",\"contentBase64\":\"$PDF_B64\",\"fileName\":\"diploma.pdf\",\"contentType\":\"application/pdf\"}")"
|
||||
[ "$code" = "204" ] || { echo "FAIL: provide documents -> $code (DMN missing routes WachtOpDocumenten to a 404 — gap 2)" >&2; exit 1; }
|
||||
echo " documents accepted (204)"
|
||||
|
||||
echo ">> 4. poll the werkbak until the registration awaits beoordeling (reached Beoordelen)"
|
||||
in_werkbak=""
|
||||
for _ in $(seq 1 20); do
|
||||
in_werkbak="$(curl -fsS "$DOM/behandel/werkbak" | python3 -c "import sys,json;print(any(r.get('registrationId')=='$id' for r in json.load(sys.stdin)))" 2>/dev/null || true)"
|
||||
[ "$in_werkbak" = "True" ] && break
|
||||
sleep 3
|
||||
done
|
||||
[ "$in_werkbak" = "True" ] || { echo "FAIL: registration never reached the werkbak (gap 2)" >&2; exit 1; }
|
||||
echo " in the werkbak"
|
||||
|
||||
echo ">> 5. poll the openbaar register until the reference is publicly visible (proves NRC abonnement)"
|
||||
public=""
|
||||
for _ in $(seq 1 30); do
|
||||
public="$(curl -fsS "$BFF/openbaar/register" | python3 -c "import sys,json;print(any(r.get('reference')=='$id' for r in json.load(sys.stdin)))" 2>/dev/null || true)"
|
||||
[ "$public" = "True" ] && break
|
||||
sleep 3
|
||||
done
|
||||
[ "$public" = "True" ] || { echo "FAIL: reference never appeared in the openbaar register — NRC abonnement not registered (gap 3)" >&2; exit 1; }
|
||||
echo " visible in the openbaar register"
|
||||
|
||||
echo "OK — a fresh local stack completed the flow with no manual seeding (zaaktype + DMN + abonnement)"
|
||||
Reference in New Issue
Block a user