From d37d4c96c6dd63474313b9d66e39133008f0e81b Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Mon, 27 Jul 2026 15:14:16 +0000 Subject: [PATCH] =?UTF-8?q?S-18c=20=C2=B7=20RegisterRecord=20objecttype=20?= =?UTF-8?q?defined=20+=20registered=20(closes=20#141)=20(#146)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What & why S-18c, the **final** slice of the S-18 (#19) split (after S-18a #142, S-18b #143). Defines the **RegisterRecord** objecttype — the schema S-19 (#20) will write canonical register records against on approval — and registers it in the Objecttypen API at startup. Closes #141 ### What - **Schema** (`infra/objecttypen-registerrecord/registerrecord.schema.json`): public-safe by construction — `id`, `status` (enum `INGEDIEND`/`INGESCHREVEN`), `reference` only, `additionalProperties: false`, `dataClassification: open`. Mirrors the BFF's `OpenbaarEntry` — **no `bsn`/`naam`** (ADR-0027). - **Registration**: a `registerrecord-init` compose one-shot (stdlib Python on the stack network) POSTs the objecttype + a **published** version over the API once Objecttypen is healthy. The Objecttypen `setup_configuration` (3.4.2) only provisions tokens — no declarative objecttype step — so this follows the ADR-0020 self-seed pattern. **Idempotent**: if a `RegisterRecord` with a version already exists it is a no-op. - **Wiring**: schema + `register.py` streamed into the external `rr-registerrecord-config` volume by `seed-config.sh registerrecord` (main) / bind-mounted (local); added to `SEED`, `CFG_VOLS`, and the CI log-dump. `registerrecord-init` is a one-shot (not in `WAIT_SVCS`). - **Smoke**: `verify-registerrecord` (`run-registerrecord-check.sh` + `registerrecord-check.py`) asserts the objecttype exists, has a **published** version, and that version's schema carries `id`/`status`/`reference`; added as a verify-stack step + a row in the #136 summary. - **ADR-0027**: records the public-safe schema decision (mirror the BFF public view, not the internal projection; API-seeded one-shot). The slice issue #141 flagged the schema as ADR-worthy, so no separate adr-proposal issue was opened. ## Verified locally (end to end, real compose) Seeded `rr-registerrecord-config`, brought Objecttypen up, ran `registerrecord-init` → `registered RegisterRecord v1 (published)`. `make verify-registerrecord` → **OK — RegisterRecord v1 published, fields=['id', 'reference', 'status']**. Re-running the one-shot → **no-op** (idempotent). `docker compose config` clean on both files; schema + script + ci.yaml validated. ## Definition of Done - [x] Failing smoke committed first (`test(infra): …`, "no objecttype named RegisterRecord"); implementation makes it pass. - [x] Conventional Commits referencing #141. - [x] CI green (verify-stack registerrecord step — validated locally; runner already unstarved by #145). - [x] `docker compose up` reaches health (one-shot registers after Objecttypen healthy). - [x] Docs: ADR-0027 + demo note. - [x] Closed by the merging PR (`closes #141`). This closes out the S-18 (#19) split — Objecttypen (S-18a) + Objecten (S-18b) + RegisterRecord (S-18c) are all up. Next: **S-19 (#20)** — ACL writes the register record to Objecten on approval, against this schema. 🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: https://git.labs.respellion.tech/eho/register-referentie/pulls/146 --- .gitea/workflows/ci.yaml | 7 +- BACKLOG.md | 2 +- Makefile | 17 ++-- ...r-0027-registerrecord-objecttype-schema.md | 81 +++++++++++++++++++ docs/demo-script.md | 29 +++++++ infra/docker-compose.local.yml | 15 ++++ infra/docker-compose.yml | 22 +++++ infra/objecttypen-registerrecord/register.py | 71 ++++++++++++++++ .../registerrecord.schema.json | 23 ++++++ infra/registerrecord-check.py | 66 +++++++++++++++ infra/run-registerrecord-check.sh | 28 +++++++ infra/seed-config.sh | 5 +- 12 files changed, 356 insertions(+), 10 deletions(-) create mode 100644 docs/architecture/adr-0027-registerrecord-objecttype-schema.md create mode 100644 infra/objecttypen-registerrecord/register.py create mode 100644 infra/objecttypen-registerrecord/registerrecord.schema.json create mode 100644 infra/registerrecord-check.py create mode 100755 infra/run-registerrecord-check.sh diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 7eabeb2..b5d96df 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -207,6 +207,9 @@ jobs: - name: Objecten API up + token authenticates + trusts Objecttypen id: objecten run: OBJECTEN_TIMEOUT=120 make verify-objecten + - name: RegisterRecord objecttype registered + published + id: registerrecord + run: REGISTERRECORD_TIMEOUT=120 make verify-registerrecord - name: ACL ↔ OpenZaak integration tests id: acl run: make verify-acl @@ -241,6 +244,7 @@ jobs: OBS: ${{ steps.obs.outcome }} OBJECTTYPEN: ${{ steps.objecttypen.outcome }} OBJECTEN: ${{ steps.objecten.outcome }} + REGISTERRECORD: ${{ steps.registerrecord.outcome }} ACL: ${{ steps.acl.outcome }} NRC: ${{ steps.nrc.outcome }} PROJECTION: ${{ steps.projection.outcome }} @@ -261,6 +265,7 @@ jobs: echo "| Observability backplane | $(icon "$OBS") |" echo "| Objecttypen API + token | $(icon "$OBJECTTYPEN") |" echo "| Objecten API + token | $(icon "$OBJECTEN") |" + echo "| RegisterRecord objecttype | $(icon "$REGISTERRECORD") |" echo "| ACL ↔ OpenZaak | $(icon "$ACL") |" echo "| OpenZaak → NRC | $(icon "$NRC") |" echo "| NRC → Event Subscriber → projection | $(icon "$PROJECTION") |" @@ -280,7 +285,7 @@ jobs: # Log dump must precede teardown (which removes the containers). - name: Dump container logs on failure if: failure() - run: docker compose -f infra/docker-compose.yml logs --no-color --tail=100 oz-init openzaak nrc-init nrc-web nrc-celery nrc-beat flowable-db flowable-rest flowable-init keycloak acl bff domain projection-db event-subscriber projection-api self-service openbaar behandel beheer objecttypen-db objecttypen-redis objecttypen-init objecttypen objecten-db objecten-redis objecten-init objecten tempo prometheus grafana 2>&1 || true + run: docker compose -f infra/docker-compose.yml logs --no-color --tail=100 oz-init openzaak nrc-init nrc-web nrc-celery nrc-beat flowable-db flowable-rest flowable-init keycloak acl bff domain projection-db event-subscriber projection-api self-service openbaar behandel beheer objecttypen-db objecttypen-redis objecttypen-init objecttypen objecten-db objecten-redis objecten-init objecten registerrecord-init tempo prometheus grafana 2>&1 || true - name: Tear down if: always() run: make down diff --git a/BACKLOG.md b/BACKLOG.md index d00afac..476d485 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -285,7 +285,7 @@ Split into independently deployable sub-slices (CLAUDE.md §13): - **S-18a** (#139, ✅) · Objecttypen API up in compose (own DB + seeded config + health + static token). - **S-18b** (#140, ✅) · Objecten API up in compose, wired to Objecttypen. Depends on S-18a. -- **S-18c** (#141) · RegisterRecord objecttype defined + registered (public-safe JSON schema). Depends on S-18a/b. +- **S-18c** (#141, ✅) · RegisterRecord objecttype defined + registered (public-safe JSON schema). Depends on S-18a/b. ### S-19 · ACL extension: write register-record to Objecten on approval diff --git a/Makefile b/Makefile index a399cbf..5b817e2 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ WAIT_SVCS := openzaak nrc-web acl bff domain event-subscriber projection-api se # volumes are `external`, so compose won't remove them — CFG_VOLS lists them for # explicit teardown. See docs/runbooks/gitea-actions-gotchas.md. SEED := bash infra/seed-config.sh -CFG_VOLS := rr-oz-config rr-nrc-config rr-kc-realms rr-fl-bpmn rr-objecttypen-config rr-objecten-config +CFG_VOLS := rr-oz-config rr-nrc-config rr-kc-realms rr-fl-bpmn rr-objecttypen-config rr-objecten-config rr-registerrecord-config # Local-only stack: same services but config is bind-mounted (no seed step), so a # plain `docker compose -f infra/docker-compose.local.yml up` works on any local # engine. This is the no-make / Windows-friendly path. See that file's header. @@ -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-observability verify-tracing verify-metrics verify-objecttypen verify-objecten 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 +.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-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). @@ -94,14 +94,14 @@ mutation: # podman-compose, and needing no `--wait` flag or host port access. The one-shots # (oz-init, flowable-init) aren't polled; they just need to have run. smoke: - $(SEED) oz nrc kc fl objecttypen objecten + $(SEED) oz nrc kc fl objecttypen objecten registerrecord docker compose -f $(COMPOSE) up -d --build bash -c 'WAIT_TIMEOUT=420 bash infra/wait-healthy.sh $(WAIT_SVCS); rc=$$?; docker compose -f $(COMPOSE) down --volumes; docker volume rm -f $(CFG_VOLS) >/dev/null 2>&1; exit $$rc' ## up: seed config volumes and start the full stack (use instead of bare ## `docker compose up`, which can't self-seed the external config volumes) up: - $(SEED) oz nrc kc fl objecttypen objecten + $(SEED) oz nrc kc fl objecttypen objecten registerrecord docker compose -f $(COMPOSE) up -d --build ## down: stop and remove the local stack (incl. the external config volumes) @@ -139,7 +139,7 @@ changelog: ## verify-up: bring the FULL stack up and wait for health (CI verify-stack step 1; ## subsumes the old compose-smoke health gate — the DoD "up reaches green" check). verify-up: - $(SEED) oz nrc kc fl objecttypen objecten + $(SEED) oz nrc kc fl objecttypen objecten registerrecord docker compose -f $(COMPOSE) up -d --build WAIT_TIMEOUT=420 bash infra/wait-healthy.sh $(WAIT_SVCS) @@ -196,11 +196,16 @@ verify-objecttypen: verify-objecten: bash infra/run-objecten-check.sh +## verify-registerrecord: assert the RegisterRecord objecttype is registered + published in the +## Objecttypen API (S-18c), against the already-running stack. +verify-registerrecord: + bash infra/run-registerrecord-check.sh + ## verify: local mirror of the CI verify-stack job — full stack up once, all checks, ## tear down (always). For fast single-concern local iteration use `integration` ## (oz-only) or `verify-notifications` (oz+nrc) instead. verify: - $(SEED) oz nrc kc fl objecttypen objecten + $(SEED) oz nrc kc fl objecttypen objecten registerrecord docker compose -f $(COMPOSE) up -d --build @bash -c 'set -e; rc=0; \ WAIT_TIMEOUT=420 bash infra/wait-healthy.sh $(WAIT_SVCS) \ diff --git a/docs/architecture/adr-0027-registerrecord-objecttype-schema.md b/docs/architecture/adr-0027-registerrecord-objecttype-schema.md new file mode 100644 index 0000000..a64dfa5 --- /dev/null +++ b/docs/architecture/adr-0027-registerrecord-objecttype-schema.md @@ -0,0 +1,81 @@ +# ADR-0027: The RegisterRecord objecttype is public-safe by construction + +- **Status:** Accepted +- **Date:** 2026-07-27 +- **Deciders:** Respellion engineering +- **Slice:** S-18c (#141), third of the S-18 (#19) split + +## Context + +S-18 stands up Objecttypen (S-18a) and Objecten (S-18b) as the authoritative +register-record store (PRD §"Objecten as the authoritative register record store"). +S-19 (#20) will, on approval, write the canonical register record to the Objecten API +instead of OpenZaak zaak-eigenschappen, and the openbaar (public) register will read it. + +Objecten validates every object against a **objecttype version's JSON schema**. So the +schema is a contract: it fixes which fields a register record may carry. The register is +read **anonymously** by the openbaar portal (ADR-0010), so the schema is also a +disclosure boundary — anything the schema allows can end up public. + +Two questions: **which fields** the schema defines, and **how** the objecttype gets into +the Objecttypen API (which has no declarative objecttype step). + +## Decision + +**Define a `RegisterRecord` objecttype whose published schema carries exactly the +public-safe fields — `id`, `status`, `reference` — and register it over the API at +startup with a one-shot, idempotently.** + +### The schema mirrors the BFF's public projection, not the internal one + +The public-safe field set already exists: the BFF's `OpenbaarEntry` +(`services/bff/Bff.Api/DownstreamClients.cs`) — `id`, `status`, `reference` — is what +`OpenbaarProjection.PublicView` narrows every row down to, dropping `bsn` and +`naamPlaceholder` at the boundary (S-09). The RegisterRecord schema mirrors that record, +**not** the internal `RegisterEntry` / `RegisterEntryRow` (which carry bsn/naam): + +| field | type | notes | +|-------|------|-------| +| `id` | string (required) | zaak id — the entry's stable key | +| `status` | string (required) | enum `INGEDIEND` \| `INGESCHREVEN` (`RegistrationStatus`) | +| `reference` | string \| null | citizen-facing zaak identificatie (ADR-0012) | + +`additionalProperties: false` so a record can't smuggle a field the schema didn't +sanction, and `dataClassification: "open"` records the intent that this objecttype is +public. **`bsn` and `naamPlaceholder` are deliberately absent** — public-safe by +construction, so S-19 cannot write a personal-data field into the public register even by +mistake. + +### Registered over the API by a one-shot, not setup_configuration + +The Objecttypen API's `setup_configuration` (3.4.2) provisions only tokens — it has no +declarative step to create an objecttype with a schema. So a `registerrecord-init` +compose one-shot (stdlib Python, on the stack network) creates the objecttype + a +**published** version over the API once Objecttypen is healthy, following the ADR-0020 +self-seed pattern. It is **idempotent**: if a `RegisterRecord` with a version already +exists it is a no-op, so it is safe on every `up`. + +- ponytail ceiling: no schema-migration/versioning story — a schema change means editing + `registerrecord.schema.json` and bumping the version by hand; the one-shot only ever + adds v1 if none exists. +- Upgrade path: if the schema evolves, have the one-shot diff the published schema and + POST a new version, or move to a declarative step once the upstream supports one. + +## Consequences + +**Positive** + +- The public register's disclosure surface is fixed in one reviewed artifact + (`registerrecord.schema.json`) and enforced by Objecten's own validation. +- Self-seeds on a fresh `make up` / bare local compose; no manual step, no built image. + +**Negative / costs** + +- The public-safe field set now lives in two places — the BFF's `OpenbaarEntry` and this + schema — that must be kept in sync by hand (a drift check is a candidate for later). +- Hand-managed schema version (ceiling above). + +## Coupling rules touched (CLAUDE.md §8) + +None new. Registration talks to the Objecttypen API over its documented API. S-19 will +write records via the ACL (§8.1) — this ADR only fixes the schema they conform to. diff --git a/docs/demo-script.md b/docs/demo-script.md index 363a905..e3f133a 100644 --- a/docs/demo-script.md +++ b/docs/demo-script.md @@ -5,6 +5,35 @@ copy-pasteable walkthrough against a local `make up` stack. --- +## S-18c — RegisterRecord objecttype defined + registered (#141, ADR-0027) + +**Outcome:** a **RegisterRecord** objecttype with a **published** JSON schema is registered in the +Objecttypen API at startup. The schema is public-safe by construction — `id`, `status`, `reference` +only, mirroring the BFF's `OpenbaarEntry` (no `bsn`/`naam`), `dataClassification: open`. This is the +schema S-19 writes register records against on approval. A `registerrecord-init` one-shot creates it +over the API once Objecttypen is healthy (the Objecttypen `setup_configuration` has no objecttype +step), idempotently. + +```bash +make up +# The RegisterRecord objecttype exists with a published version: +curl -s -H "Authorization: Token 0123456789abcdef0123456789abcdef01234567" \ + "http://localhost:8020/api/v2/objecttypes" | python3 -c \ + 'import sys,json; o=[x for x in json.load(sys.stdin)["results"] if x["name"]=="RegisterRecord"][0]; print(o["name"], o["dataClassification"], o["versions"])' +# → RegisterRecord open ['http://.../objecttypes//versions/1'] +# +# Automated (a CI verify-stack step): asserts the objecttype exists, has a published version, and +# that version's schema carries id/status/reference. +make verify-registerrecord # → OK — RegisterRecord v1 published, fields=['id', 'reference', 'status'] +``` + +**The path:** `infra/objecttypen-registerrecord/registerrecord.schema.json` (the reviewed public-safe +contract) + `register.py` are streamed into an external config volume by `infra/seed-config.sh +registerrecord` (bind-mounted locally); the `registerrecord-init` one-shot POSTs the objecttype + a +published version. Re-running is a no-op. S-19 (#20) writes records against this schema in Objecten. + +--- + ## S-18b — Objecten API up in compose, wired to Objecttypen (#140) **Outcome:** the upstream Maykin **Objecten API** runs in the stack — own **PostGIS** DB + redis, diff --git a/infra/docker-compose.local.yml b/infra/docker-compose.local.yml index cef59b0..dbbf4e5 100644 --- a/infra/docker-compose.local.yml +++ b/infra/docker-compose.local.yml @@ -631,6 +631,21 @@ services: condition: service_completed_successfully networks: [cg] + # ── RegisterRecord objecttype (S-18c) — API-seeded one-shot (local variant) ─ + registerrecord-init: + image: docker.io/library/python:3-slim + environment: + OBJECTTYPEN: http://objecttypen:8000 + OBJECTTYPEN_TOKEN: ${OBJECTTYPEN_TOKEN:-0123456789abcdef0123456789abcdef01234567} + SCHEMA: /config/registerrecord.schema.json + command: python /config/register.py + volumes: + - ./objecttypen-registerrecord:/config:ro,z + depends_on: + objecttypen: + condition: service_healthy + networks: [cg] + # ── Objecten API (S-18b) — bind-mounted config (local variant) ───────────── objecten-db: image: docker.io/postgis/postgis:17-3.5 diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index ee5d0aa..c836501 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -649,6 +649,25 @@ services: condition: service_completed_successfully networks: [cg] + # ── RegisterRecord objecttype (S-18c) — API-seeded one-shot ──────────────── + # The Objecttypen setup_configuration (3.4.2) can only provision tokens — no declarative objecttype + # step — so this one-shot creates the RegisterRecord objecttype + a published version over the API + # once Objecttypen is healthy (idempotent; ADR-0020 self-seed, ADR-0027 schema). The schema + script + # are streamed into the external config volume by infra/seed-config.sh, like the *-init volumes. + registerrecord-init: + image: docker.io/library/python:3-slim + environment: + OBJECTTYPEN: http://objecttypen:8000 + OBJECTTYPEN_TOKEN: ${OBJECTTYPEN_TOKEN:-0123456789abcdef0123456789abcdef01234567} + SCHEMA: /config/registerrecord.schema.json + command: python /config/register.py + volumes: + - registerrecord-config:/config:ro + depends_on: + objecttypen: + condition: service_healthy + networks: [cg] + # ── Objecten API (S-18b) — upstream Maykin image, verbatim ───────────────── # The authoritative object store. Same shape as Objecttypen (own DB + redis, an `-init` that runs # setup_configuration from the external config volume, a health-checked web). Two differences: the @@ -791,6 +810,9 @@ volumes: objecttypen-config: external: true name: rr-objecttypen-config + registerrecord-config: + external: true + name: rr-registerrecord-config objecten-config: external: true name: rr-objecten-config diff --git a/infra/objecttypen-registerrecord/register.py b/infra/objecttypen-registerrecord/register.py new file mode 100644 index 0000000..df07aad --- /dev/null +++ b/infra/objecttypen-registerrecord/register.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""S-18c (#141): register the RegisterRecord objecttype + a published version in the Objecttypen API. + +Run by the `registerrecord-init` compose one-shot once Objecttypen is healthy. The Objecttypen API's +setup_configuration (3.4.2) can only provision tokens — it has no declarative objecttype step — so +the objecttype is created over the API here (the ADR-0020 self-seed pattern), idempotently: if a +"RegisterRecord" objecttype with a published version already exists, it's a no-op. Stdlib only. +""" +import json +import os +import sys +import time +import urllib.error +import urllib.request + +BASE = os.environ.get("OBJECTTYPEN", "http://objecttypen:8000").rstrip("/") +TOKEN = os.environ["OBJECTTYPEN_TOKEN"] +SCHEMA_PATH = os.environ.get("SCHEMA", "/config/registerrecord.schema.json") +NAME = "RegisterRecord" + + +def api(method, path, body=None): + data = json.dumps(body).encode() if body is not None else None + req = urllib.request.Request( + f"{BASE}{path}", data=data, method=method, + headers={"Authorization": f"Token {TOKEN}", "Content-Type": "application/json"}, + ) + with urllib.request.urlopen(req, timeout=15) as r: + return json.load(r) if r.length != 0 else {} + + +def wait_ready(): + """Objecttypen depends_on health already, but tolerate a slow first request.""" + for _ in range(20): + try: + api("GET", "/api/v2/objecttypes") + return + except (urllib.error.URLError, ConnectionError, TimeoutError): + time.sleep(3) + api("GET", "/api/v2/objecttypes") # last try, let it raise + + +def main(): + schema = json.load(open(SCHEMA_PATH)) + wait_ready() + + existing = next( + (o for o in api("GET", "/api/v2/objecttypes").get("results", []) if o.get("name") == NAME), + None, + ) + if existing and existing.get("versions"): + print(f"RegisterRecord already registered ({len(existing['versions'])} version(s)) — no-op") + return 0 + + ot = existing or api("POST", "/api/v2/objecttypes", { + "name": NAME, + "namePlural": "RegisterRecords", + "description": schema.get("description", ""), + "dataClassification": "open", # public-safe: the openbaar register may show it + }) + uuid = ot["uuid"] + ver = api("POST", f"/api/v2/objecttypes/{uuid}/versions", { + "status": "published", + "jsonSchema": schema, + }) + print(f"registered RegisterRecord {uuid} v{ver.get('version')} ({ver.get('status')})") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/infra/objecttypen-registerrecord/registerrecord.schema.json b/infra/objecttypen-registerrecord/registerrecord.schema.json new file mode 100644 index 0000000..998b3b5 --- /dev/null +++ b/infra/objecttypen-registerrecord/registerrecord.schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "RegisterRecord", + "description": "Public-safe register entry shown in the openbaar (public) register. Mirrors the BFF's OpenbaarEntry (services/bff/Bff.Api/DownstreamClients.cs) — deliberately NO bsn or naam. S-19 (#20) writes records against this schema in the Objecten API on approval. See ADR-0027.", + "type": "object", + "additionalProperties": false, + "required": ["id", "status"], + "properties": { + "id": { + "type": "string", + "description": "Zaak id — the register entry's stable primary key (the projection key)." + }, + "status": { + "type": "string", + "enum": ["INGEDIEND", "INGESCHREVEN"], + "description": "Registration lifecycle status (RegistrationStatus)." + }, + "reference": { + "type": ["string", "null"], + "description": "Citizen-facing zaak identificatie shown publicly (ADR-0012)." + } + } +} diff --git a/infra/registerrecord-check.py b/infra/registerrecord-check.py new file mode 100644 index 0000000..6e02dca --- /dev/null +++ b/infra/registerrecord-check.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""S-18c (#141): prove the RegisterRecord objecttype is registered + published in the Objecttypen API. + +Assert the objecttype named "RegisterRecord" exists, has a **published** version, and that version's +jsonSchema carries the public-safe fields (id, status, reference) — i.e. registerrecord-init ran and +seeded the schema S-19 will write records against. Stdlib only so it runs in a bare python:3-slim +container on the compose network. +""" +import json +import os +import sys +import time +import urllib.error +import urllib.request + +BASE = os.environ["OBJECTTYPEN"] # http://:8000 +TOKEN = os.environ["OBJECTTYPEN_TOKEN"] +TIMEOUT = int(os.environ.get("REGISTERRECORD_TIMEOUT", "60")) +NAME = "RegisterRecord" +EXPECTED_FIELDS = {"id", "status", "reference"} + + +def get(path): + req = urllib.request.Request(f"{BASE}{path}", headers={"Authorization": f"Token {TOKEN}"}) + with urllib.request.urlopen(req, timeout=10) as r: + return json.load(r) + + +def check(): + """Return (ok, detail). Raises on transport errors so the caller can retry.""" + ots = get("/api/v2/objecttypes").get("results", []) + match = next((o for o in ots if o.get("name") == NAME), None) + if not match: + return False, f"no objecttype named {NAME!r} (have: {[o.get('name') for o in ots]})" + if not match.get("versions"): + return False, f"{NAME} exists but has no versions" + # The versions list holds URLs; fetch each to find a published one. + for ver_url in match["versions"]: + ver = get(ver_url[len(BASE):] if ver_url.startswith(BASE) else ver_url) + if ver.get("status") != "published": + continue + props = set((ver.get("jsonSchema") or {}).get("properties", {})) + if not EXPECTED_FIELDS <= props: + return False, f"published version missing fields: {EXPECTED_FIELDS - props}" + return True, f"{NAME} v{ver.get('version')} published, fields={sorted(props)}" + return False, f"{NAME} has versions but none are published" + + +def main(): + deadline = time.time() + TIMEOUT + detail = "no attempt" + while time.time() < deadline: + try: + ok, detail = check() + if ok: + print(f"OK — {detail}") + return 0 + except (urllib.error.URLError, ConnectionError, TimeoutError) as e: + detail = f"transport: {e}" + time.sleep(3) + print(f"FAIL — {detail}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/infra/run-registerrecord-check.sh b/infra/run-registerrecord-check.sh new file mode 100755 index 0000000..3f4d546 --- /dev/null +++ b/infra/run-registerrecord-check.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# +# S-18c (#141): assert the RegisterRecord objecttype is registered + published in the Objecttypen +# API, against an ALREADY-RUNNING stack. Runs the check in a python:3-slim container on the stack +# network (the service is reached by container IP; the runner can't reach published ports — +# gitea-actions-gotchas.md §5/§6). Does NOT manage the stack lifecycle. +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# The dev token provisioned by infra/objecttypen/setup_configuration/data.yaml. +TOKEN="${OBJECTTYPEN_TOKEN:-0123456789abcdef0123456789abcdef01234567}" + +ot="$(docker ps -q --filter 'name=objecttypen' --filter 'health=healthy' | head -1)" +[ -n "$ot" ] || ot="$(docker ps -q --filter 'name=[-_]objecttypen[-_]' | head -1)" +[ -n "$ot" ] || { echo "ERROR: no running objecttypen container — bring the stack up first" >&2; exit 1; } +net="$(docker inspect -f '{{range $k,$_ := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' "$ot" | head -1)" +ip="$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$ot")" +echo ">> network=$net objecttypen=$ip" + +cid="$(docker create --network "$net" \ + -e "OBJECTTYPEN=http://$ip:8000" -e "OBJECTTYPEN_TOKEN=$TOKEN" \ + -e "REGISTERRECORD_TIMEOUT=${REGISTERRECORD_TIMEOUT:-60}" \ + python:3-slim python /registerrecord-check.py)" +docker cp "$here/registerrecord-check.py" "$cid:/registerrecord-check.py" >/dev/null +rc=0; docker start -a "$cid" || rc=$? +docker rm -f "$cid" >/dev/null +exit $rc diff --git a/infra/seed-config.sh b/infra/seed-config.sh index 6895d6c..dc80e74 100755 --- a/infra/seed-config.sh +++ b/infra/seed-config.sh @@ -13,7 +13,7 @@ # subcommand. Fixed-name `external` volumes keep the names deterministic across # both runtimes. See docs/runbooks/gitea-actions-gotchas.md. # -# Usage: seed-config.sh [ ...] where key ∈ { oz, nrc, kc, fl, objecttypen, objecten } +# Usage: seed-config.sh [ ...] where key ∈ { oz, nrc, kc, fl, objecttypen, objecten, registerrecord } set -euo pipefail here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -33,7 +33,7 @@ populate() { # volume source(file or dir/.) echo " seeded $vol" } -[ "$#" -gt 0 ] || { echo "usage: seed-config.sh ..." >&2; exit 2; } +[ "$#" -gt 0 ] || { echo "usage: seed-config.sh ..." >&2; exit 2; } # The registratie process (BPMN) and its diploma-eligibility DMN are deployed as SEPARATE Flowable # deployments — the process engine and the DMN engine each own theirs (S-13, ADR-0016). flowable-rest @@ -51,6 +51,7 @@ for key in "$@"; do kc) populate rr-kc-realms "$here/keycloak/realms/." ;; objecttypen) populate rr-objecttypen-config "$here/objecttypen/setup_configuration/." ;; objecten) populate rr-objecten-config "$here/objecten/setup_configuration/." ;; + registerrecord) populate rr-registerrecord-config "$here/objecttypen-registerrecord/." ;; fl) d="$(mktemp -d)"; stage_flowable_workflows "$d"; populate rr-fl-bpmn "$d/." ;; *) echo "unknown seed key: $key" >&2; exit 2 ;; esac