From d61594974ff41af8517b955ee67e591c5c59412b Mon Sep 17 00:00:00 2001 From: Edwin van den Houdt Date: Thu, 4 Jun 2026 09:16:35 +0200 Subject: [PATCH] feat(workflow): Flowable up with registratie.bpmn external task (closes #4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add infra/flowable/docker-compose.yml (flowable-rest on Postgres, host :8090) and workflows/registratie.bpmn — a minimal "Registratie ontvangen" process: start -> external-worker task OpenZaakAanmaken -> end. A flowable-init container deploys the model via the REST API on boot (idempotent: skips if already deployed). Add `make flowable-up/flowable-smoke/flowable-down`; flowable-smoke runs infra/flowable/verify.py, which starts an instance and asserts it parks on the OpenZaakAanmaken external task, then cleans up. Runbook included. Verified clean-slate: down --volumes -> `make flowable-smoke` deploys on boot, starts an instance, and confirms it waits at OpenZaakAanmaken. Co-Authored-By: Claude Opus 4.8 --- Makefile | 19 ++++++++- docs/runbooks/flowable.md | 40 +++++++++++++++++++ infra/flowable/docker-compose.yml | 65 +++++++++++++++++++++++++++++++ infra/flowable/verify.py | 54 +++++++++++++++++++++++++ workflows/registratie.bpmn | 48 +++++++++++++++++++++++ 5 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 docs/runbooks/flowable.md create mode 100644 infra/flowable/docker-compose.yml create mode 100644 infra/flowable/verify.py create mode 100644 workflows/registratie.bpmn diff --git a/Makefile b/Makefile index 6bbe262..60a648e 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,8 @@ NRC_COMPOSE := infra/opennotificaties/docker-compose.yml NRC_BASE := http://localhost:8001 KC_COMPOSE := infra/keycloak/docker-compose.yml KC_BASE := http://localhost:8180 +FL_COMPOSE := infra/flowable/docker-compose.yml +FL_BASE := http://localhost:8090/flowable-rest/service STACK_FILES := -f $(OZ_COMPOSE) -f $(NRC_COMPOSE) # On a rootless Podman dev box, point Docker CLI/Compose at the Podman socket — @@ -26,7 +28,7 @@ export DOCKER_HOST := unix://$(PODMAN_SOCK) endif endif -.PHONY: ci lint build unit smoke down changelog openzaak-up openzaak-smoke openzaak-seed openzaak-down stack-up stack-smoke stack-down keycloak-up keycloak-smoke keycloak-down help +.PHONY: ci lint build unit smoke 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, smoke (mirrors Gitea Actions) ci: lint build unit smoke @@ -124,6 +126,21 @@ keycloak-smoke: keycloak-up keycloak-down: docker compose -f $(KC_COMPOSE) down --volumes +## flowable-up: start Flowable (deploys registratie.bpmn on boot) +flowable-up: + docker compose -f $(FL_COMPOSE) up -d + +## flowable-smoke: start Flowable, then verify a started instance waits on the external task +flowable-smoke: flowable-up + @bash -c 'for i in $$(seq 1 80); do \ + c=$$(curl -s -o /dev/null -w "%{http_code}" -u rest-admin:test $(FL_BASE)/repository/process-definitions?key=registratie || true); \ + [ "$$c" = "200" ] && break; sleep 3; done; echo "Flowable ready ($$c)"' + python3 infra/flowable/verify.py + +## flowable-down: stop and remove Flowable +flowable-down: + docker compose -f $(FL_COMPOSE) down --volumes + ## help: list available targets help: @grep -E '^## ' $(MAKEFILE_LIST) | sed 's/^## //' diff --git a/docs/runbooks/flowable.md b/docs/runbooks/flowable.md new file mode 100644 index 0000000..29ca573 --- /dev/null +++ b/docs/runbooks/flowable.md @@ -0,0 +1,40 @@ +# Flowable runbook + +Flowable (`infra/flowable/docker-compose.yml`) runs the **flowable-rest** engine on +Postgres. The `workflows/registratie.bpmn` model is deployed via the REST API at boot by +the `flowable-init` container. Host port **:8090**; REST API under +`http://localhost:8090/flowable-rest/service/` (basic auth **rest-admin / test**, dev only). + +## The model — `registratie` + +A minimal "Registratie ontvangen" process: **start → external-worker task +`OpenZaakAanmaken` → end**. The external task is where the Workflow Client / ACL will +later create the zaak in OpenZaak (S-04/S-05); for now a started instance parks there. + +## Quick test (`make`) + +```bash +make flowable-up # start engine + deploy registratie.bpmn on boot +make flowable-smoke # start + verify a new instance waits on the external task +make flowable-down # stop + wipe +``` + +`make flowable-smoke` runs `infra/flowable/verify.py`, which: +1. waits for the `registratie` process definition to be deployed, +2. starts an instance and asserts it did **not** end immediately, +3. asserts an execution is parked at activity **`OpenZaakAanmaken`**, +4. deletes the test instance. + +## Notes + +- **Deploy on boot** is idempotent: `flowable-init` skips if a deployment named + `registratie` already exists (so restarts on the same volume don't pile up versions). +- **Dev creds:** `rest-admin` / `test`. Override via the flowable-rest app config for + anything beyond local dev. +- **Image** `flowable/flowable-rest:latest` — pin a tag when stabilising. +- Start an instance by hand: + ```bash + curl -s -u rest-admin:test -H 'Content-Type: application/json' \ + -d '{"processDefinitionKey":"registratie"}' \ + http://localhost:8090/flowable-rest/service/runtime/process-instances + ``` diff --git a/infra/flowable/docker-compose.yml b/infra/flowable/docker-compose.yml new file mode 100644 index 0000000..0aa879a --- /dev/null +++ b/infra/flowable/docker-compose.yml @@ -0,0 +1,65 @@ +# Flowable (S-03): the flowable-rest engine on Postgres. +# The registratie.bpmn model is deployed via the REST API on startup by flowable-init. +# +# docker compose -f infra/flowable/docker-compose.yml up -d +# # REST API (basic auth) under http://localhost:8090/flowable-rest/service/ +# +# Host port 8090 (8000/8001/8080/8180 are taken by OpenZaak/NRC/BFF/Keycloak). +services: + flowable-db: + image: docker.io/library/postgres:16 + environment: + POSTGRES_USER: flowable + POSTGRES_PASSWORD: flowable + POSTGRES_DB: flowable + volumes: + - flowable-db:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U flowable -d flowable"] + interval: 5s + timeout: 3s + retries: 10 + networks: [cg] + + flowable-rest: + image: docker.io/flowable/flowable-rest:latest + environment: + SPRING_DATASOURCE_DRIVER-CLASS-NAME: org.postgresql.Driver + SPRING_DATASOURCE_URL: jdbc:postgresql://flowable-db:5432/flowable + SPRING_DATASOURCE_USERNAME: flowable + SPRING_DATASOURCE_PASSWORD: flowable + ports: + - "8090:8080" + depends_on: + flowable-db: + condition: service_healthy + networks: [cg] + + # Deploys workflows/registratie.bpmn via the REST API once flowable-rest is up. + # Idempotent: skips if a deployment named "registratie" already exists. + flowable-init: + image: docker.io/curlimages/curl:latest + restart: "no" + volumes: + - ../../workflows/registratie.bpmn:/work/registratie.bpmn: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" + else + curl -sf -u rest-admin:test -F 'file=@/work/registratie.bpmn;filename=registratie.bpmn' "$$base" >/dev/null && echo "deployed registratie" + fi + depends_on: + flowable-rest: + condition: service_started + networks: [cg] + +volumes: + flowable-db: + +networks: + cg: diff --git a/infra/flowable/verify.py b/infra/flowable/verify.py new file mode 100644 index 0000000..7379051 --- /dev/null +++ b/infra/flowable/verify.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Smoke-check Flowable: the registratie process is deployed, and starting an +instance parks it on the OpenZaakAanmaken external task. Stdlib only. +""" +import base64, json, sys, time, urllib.error, urllib.request + +BASE = "http://localhost:8090/flowable-rest/service" +AUTH = "Basic " + base64.b64encode(b"rest-admin:test").decode() + + +def call(method, path, payload=None): + data = json.dumps(payload).encode() if payload is not None else None + req = urllib.request.Request(BASE + path, data=data, method=method, headers={ + "Authorization": AUTH, "Content-Type": "application/json", "Accept": "application/json"}) + with urllib.request.urlopen(req, timeout=30) as r: + return r.status, json.loads(r.read() or "null") + + +def main(): + # 1. process definition deployed? (wait for the async init-container deploy) + defs = {"total": 0} + for _ in range(40): + _, defs = call("GET", "/repository/process-definitions?key=registratie") + if defs["total"] >= 1: + break + time.sleep(3) + assert defs["total"] >= 1, "registratie process definition not deployed" + print(f"process definition 'registratie' deployed (total={defs['total']})") + + # 2. start an instance + st, pi = call("POST", "/runtime/process-instances", {"processDefinitionKey": "registratie"}) + assert st == 201, f"start failed: {st} {pi}" + pid = pi["id"] + assert pi.get("ended") is False, "instance ended immediately — external task not reached" + print(f"started instance {pid} (ended={pi.get('ended')})") + + # 3. waiting on the external task? + _, ex = call("GET", f"/runtime/executions?processInstanceId={pid}") + activities = [e.get("activityId") for e in ex["data"]] + assert "OpenZaakAanmaken" in activities, f"not waiting at OpenZaakAanmaken: {activities}" + print(f"instance is waiting at the external task: {activities}") + + # 4. cleanup + try: + call("DELETE", f"/runtime/process-instances/{pid}") + print("cleaned up instance") + except urllib.error.HTTPError: + pass + + print("flowable smoke OK") + + +if __name__ == "__main__": + main() diff --git a/workflows/registratie.bpmn b/workflows/registratie.bpmn new file mode 100644 index 0000000..e49e22a --- /dev/null +++ b/workflows/registratie.bpmn @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +