diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index ca47c29..b5d96df 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -285,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/Makefile b/Makefile index 2b7199f..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. @@ -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) @@ -205,7 +205,7 @@ verify-registerrecord: ## 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/infra/docker-compose.local.yml b/infra/docker-compose.local.yml index a148115..db3a89b 100644 --- a/infra/docker-compose.local.yml +++ b/infra/docker-compose.local.yml @@ -624,6 +624,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 c7a1fde..fc1cee8 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -640,6 +640,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 @@ -782,6 +801,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/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