feat(infra): Objecten publishes register events to NRC (closes #152) #154
@@ -47,21 +47,36 @@ The kanaal name is **not ours to choose**: the Objects API sends
|
||||
slice reported first. Its filter set (`object_type`) matches the kenmerken the Objects API
|
||||
sends, so an abonnement can narrow to one objecttype instead of receiving every write.
|
||||
|
||||
### `SITE_DOMAIN: objecten.local:8000` — NRC rejects single-label hosts
|
||||
### Writers address Objecten as `objecten.local` — NRC rejects single-label hosts
|
||||
|
||||
NRC validates a notification's `hoofdObject`/`resourceUrl` with Django's `URLValidator`,
|
||||
which refuses a **single-label** host. Objecten builds those URLs from `objects.utils.get_domain`
|
||||
(i.e. `SITE_DOMAIN`), so with the compose service name they read `http://objecten:8000/...`
|
||||
and every publish is refused with `"Voer een geldige URL in."`
|
||||
NRC types a notification's `hoofdObject` and `resourceUrl` as DRF `URLField`s, so Django's
|
||||
`URLValidator` runs on them — and it refuses a **single-label** host. Objecten fills both
|
||||
from the object `url` that DRF built with `request.build_absolute_uri`, i.e. **the Host the
|
||||
caller used**. Write to `http://objecten:8000` and NRC answers every publish with
|
||||
|
||||
`SITE_DOMAIN` is therefore set to a dotted host, and the `objecten` service carries an
|
||||
`objecten.local` network alias so that host still **resolves in-network** — a subscriber
|
||||
that follows `resourceUrl` reaches the record it points at (which S-19b-2 will do). A
|
||||
dotted name that didn't resolve would trade one broken link for a quieter one.
|
||||
```
|
||||
{"hoofdObject":["Voer een geldige URL in."],"resourceUrl":["Voer een geldige URL in."]}
|
||||
```
|
||||
|
||||
which `objecten-celery` then retries with exponential backoff, forever, in the background —
|
||||
the write itself having returned 201.
|
||||
|
||||
`SITE_DOMAIN` does **not** fix this; it is not what builds those URLs. The fix is on the
|
||||
caller side: the `objecten` service carries an `objecten.local` network alias, and every
|
||||
component whose writes must be notified — the ACL (`Acl__Objecten__BaseUrl`), the gateway
|
||||
integration tests, this slice's verify check — addresses it there. An alias rather than a
|
||||
plain dotted `SITE_DOMAIN` so the host still **resolves in-network**: a subscriber that
|
||||
follows `resourceUrl` reaches the record it points at, which S-19b-2 will do. Readers are
|
||||
unaffected and keep using the plain service name.
|
||||
|
||||
This is the same class of constraint as ADR-0028's "the ACL's Objecttypen base URL must
|
||||
match Objecten's configured `api_root`": these modules put request-derived hosts into data
|
||||
that another module then validates or dereferences.
|
||||
another module then validates or dereferences.
|
||||
|
||||
- ponytail ceiling: nothing *enforces* that a new writer uses the alias — it would get a 201
|
||||
and silently no notification.
|
||||
- Upgrade path: if a second writer ever appears, rename the compose service to `objecten.local`
|
||||
so the plain name stops working, rather than adding a lint.
|
||||
|
||||
### A worker, not a synchronous send
|
||||
|
||||
@@ -93,8 +108,8 @@ config.
|
||||
- One more long-running container (`objecten-celery`) on an already memory-tight CI runner.
|
||||
- A second publisher on the shared `big-reference-seed` credential — a credential rotation
|
||||
now touches two modules.
|
||||
- One more hand-kept host constant: `SITE_DOMAIN` and the `objecten.local` alias must stay
|
||||
in step, in both compose files.
|
||||
- Objecten now has two in-network names, and which one a caller uses silently decides
|
||||
whether its writes are notified (ceiling above).
|
||||
- ponytail ceiling: notification delivery has no dead-letter or alerting — a failed publish
|
||||
is visible only in the worker log.
|
||||
- Upgrade path: if undelivered register events start mattering, subscribe an audit sink or
|
||||
|
||||
@@ -341,7 +341,8 @@ services:
|
||||
# Objecten holds the register, OpenZaak holds the process (S-19a, ADR-0028). Both APIs take a
|
||||
# static token, not a ZGW JWT. The objecttype URL is assigned at seed time, so the ACL resolves
|
||||
# it by name — lazily, on the first approval, so no depends_on is needed here.
|
||||
Acl__Objecten__BaseUrl: http://objecten:8000/
|
||||
# Dotted host on purpose — see the `objecten.local` alias below (ADR-0029).
|
||||
Acl__Objecten__BaseUrl: http://objecten.local:8000/
|
||||
Acl__Objecten__Token: ${OBJECTEN_TOKEN:-1234567890abcdef1234567890abcdef12345678}
|
||||
Acl__Objecten__ObjecttypenBaseUrl: http://objecttypen:8000/
|
||||
Acl__Objecten__ObjecttypenToken: ${OBJECTTYPEN_TOKEN:-0123456789abcdef0123456789abcdef01234567}
|
||||
@@ -691,13 +692,6 @@ services:
|
||||
CACHE_AXES: objecten-redis:6379/0
|
||||
DISABLE_2FA: "true"
|
||||
OTEL_SDK_DISABLED: "true"
|
||||
# NRC validates hoofdObject/resourceUrl with Django's URLValidator, which rejects a
|
||||
# single-label host — so notifications built from `objecten:8000` are refused with
|
||||
# "Voer een geldige URL in." SITE_DOMAIN fixes the host Objecten puts in its notifications
|
||||
# (objects.utils.get_domain), and the `objecten.local` network alias below keeps that host
|
||||
# resolvable in-network so a subscriber can actually fetch the record it points at (S-19b-2).
|
||||
SITE_DOMAIN: objecten.local:8000
|
||||
IS_HTTPS: "no"
|
||||
CELERY_BROKER_URL: redis://objecten-redis:6379/1
|
||||
CELERY_RESULT_BACKEND: redis://objecten-redis:6379/1
|
||||
# Publish register-record events to NRC on the `objecten` kanaal (S-19b-1, ADR-0029). The NRC
|
||||
@@ -735,6 +729,12 @@ services:
|
||||
condition: service_completed_successfully
|
||||
networks:
|
||||
cg:
|
||||
# Objecten reflects the *request* Host into the `url` it returns, and
|
||||
# notifications_api_common publishes that url as the notification's hoofdObject /
|
||||
# resourceUrl — which NRC types as a URLField, and Django's URLValidator rejects a
|
||||
# single-label host ("Voer een geldige URL in."). So every caller whose writes must be
|
||||
# notified addresses Objecten by this dotted alias instead of `objecten` (ADR-0029).
|
||||
# Reads are unaffected and still use the plain service name.
|
||||
aliases:
|
||||
- objecten.local
|
||||
|
||||
|
||||
@@ -326,7 +326,8 @@ services:
|
||||
# Objecten holds the register, OpenZaak holds the process (S-19a, ADR-0028). Both APIs take a
|
||||
# static token, not a ZGW JWT. The objecttype URL is assigned at seed time, so the ACL resolves
|
||||
# it by name — lazily, on the first approval, so no depends_on is needed here.
|
||||
Acl__Objecten__BaseUrl: http://objecten:8000/
|
||||
# Dotted host on purpose — see the `objecten.local` alias below (ADR-0029).
|
||||
Acl__Objecten__BaseUrl: http://objecten.local:8000/
|
||||
Acl__Objecten__Token: ${OBJECTEN_TOKEN:-1234567890abcdef1234567890abcdef12345678}
|
||||
Acl__Objecten__ObjecttypenBaseUrl: http://objecttypen:8000/
|
||||
Acl__Objecten__ObjecttypenToken: ${OBJECTTYPEN_TOKEN:-0123456789abcdef0123456789abcdef01234567}
|
||||
@@ -717,13 +718,6 @@ services:
|
||||
CACHE_AXES: objecten-redis:6379/0
|
||||
DISABLE_2FA: "true"
|
||||
OTEL_SDK_DISABLED: "true"
|
||||
# NRC validates hoofdObject/resourceUrl with Django's URLValidator, which rejects a
|
||||
# single-label host — so notifications built from `objecten:8000` are refused with
|
||||
# "Voer een geldige URL in." SITE_DOMAIN fixes the host Objecten puts in its notifications
|
||||
# (objects.utils.get_domain), and the `objecten.local` network alias below keeps that host
|
||||
# resolvable in-network so a subscriber can actually fetch the record it points at (S-19b-2).
|
||||
SITE_DOMAIN: objecten.local:8000
|
||||
IS_HTTPS: "no"
|
||||
CELERY_BROKER_URL: redis://objecten-redis:6379/1
|
||||
CELERY_RESULT_BACKEND: redis://objecten-redis:6379/1
|
||||
# Publish register-record events to NRC on the `objecten` kanaal (S-19b-1, ADR-0029). The NRC
|
||||
@@ -764,6 +758,12 @@ services:
|
||||
condition: service_completed_successfully
|
||||
networks:
|
||||
cg:
|
||||
# Objecten reflects the *request* Host into the `url` it returns, and
|
||||
# notifications_api_common publishes that url as the notification's hoofdObject /
|
||||
# resourceUrl — which NRC types as a URLField, and Django's URLValidator rejects a
|
||||
# single-label host ("Voer een geldige URL in."). So every caller whose writes must be
|
||||
# notified addresses Objecten by this dotted alias instead of `objecten` (ADR-0029).
|
||||
# Reads are unaffected and still use the plain service name.
|
||||
aliases:
|
||||
- objecten.local
|
||||
|
||||
|
||||
@@ -11,9 +11,10 @@
|
||||
#
|
||||
# All in-network, reaching services by container IP (a single-label host isn't URL-valid for NRC's
|
||||
# callbackUrl validator; the runner can't reach published ports — gitea-actions-gotchas.md §5/§6).
|
||||
# EXCEPT Objecten/Objecttypen, which must be reached by SERVICE NAME: Objecttypen echoes the request
|
||||
# Host into the objecttype `url` and Objecten only accepts the one matching its configured api_root
|
||||
# (ADR-0028).
|
||||
# EXCEPT Objecttypen, which must be reached by SERVICE NAME: it echoes the request Host into the
|
||||
# objecttype `url` and Objecten only accepts the one matching its configured api_root (ADR-0028);
|
||||
# and Objecten, reached by its `objecten.local` alias because it reflects the request Host into the
|
||||
# notification's hoofdObject/resourceUrl, which NRC validates as a URL (ADR-0029).
|
||||
#
|
||||
# Does NOT manage the stack lifecycle, but cleans up the sink/driver it creates.
|
||||
set -euo pipefail
|
||||
@@ -48,7 +49,7 @@ echo ">> sink at $sink_ip:9000"
|
||||
echo ">> registering the abonnement + writing a RegisterRecord"
|
||||
docker rm -f rr-overify >/dev/null 2>&1 || true
|
||||
drv="$(docker create --network "$net" --name rr-overify \
|
||||
-e "OBJECTEN=http://objecten:8000" \
|
||||
-e "OBJECTEN=http://objecten.local:8000" \
|
||||
-e "OBJECTEN_TOKEN=${OBJECTEN_TOKEN:-1234567890abcdef1234567890abcdef12345678}" \
|
||||
-e "OBJECTTYPEN=http://objecttypen:8000" \
|
||||
-e "OBJECTTYPEN_TOKEN=${OBJECTTYPEN_TOKEN:-0123456789abcdef0123456789abcdef01234567}" \
|
||||
|
||||
@@ -20,7 +20,7 @@ public sealed class ObjectenGatewayIntegrationTests
|
||||
new HttpClient(),
|
||||
new ObjectenOptions
|
||||
{
|
||||
BaseUrl = new(Env("OBJECTEN_BASE", "http://objecten:8000")),
|
||||
BaseUrl = new(Env("OBJECTEN_BASE", "http://objecten.local:8000")),
|
||||
Token = Env("OBJECTEN_TOKEN", "1234567890abcdef1234567890abcdef12345678"),
|
||||
ObjecttypenBaseUrl = new(Env("OBJECTTYPEN_BASE", "http://objecttypen:8000")),
|
||||
ObjecttypenToken = Env("OBJECTTYPEN_TOKEN", "0123456789abcdef0123456789abcdef01234567"),
|
||||
@@ -65,7 +65,7 @@ public sealed class ObjectenGatewayIntegrationTests
|
||||
{
|
||||
using var http = new HttpClient();
|
||||
var objecttype = await ResolveObjecttypeUrlAsync(http);
|
||||
var query = new Uri(new Uri(Env("OBJECTEN_BASE", "http://objecten:8000")),
|
||||
var query = new Uri(new Uri(Env("OBJECTEN_BASE", "http://objecten.local:8000")),
|
||||
"/api/v2/objects?type=" + Uri.EscapeDataString(objecttype) +
|
||||
"&data_attrs=id__exact__" + Uri.EscapeDataString(id));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user