fix(infra): address Objecten by a dotted host so NRC accepts its notifications (refs #152)
CI / build (pull_request) Successful in 1m9s
CI / lint (pull_request) Successful in 1m24s
CI / unit (pull_request) Successful in 1m32s
CI / frontend (pull_request) Successful in 3m13s
CI / mutation (pull_request) Successful in 6m25s
CI / verify-stack (pull_request) Failing after 8m11s
CI / build (pull_request) Successful in 1m9s
CI / lint (pull_request) Successful in 1m24s
CI / unit (pull_request) Successful in 1m32s
CI / frontend (pull_request) Successful in 3m13s
CI / mutation (pull_request) Successful in 6m25s
CI / verify-stack (pull_request) Failing after 8m11s
The worker published and NRC answered 400 on every message:
{"hoofdObject":["Voer een geldige URL in."],"resourceUrl":["Voer een geldige URL in."]}
NRC types both as DRF `URLField`, and Django's URLValidator refuses a single-label host.
Objecten fills them from the object `url` DRF built with `request.build_absolute_uri` —
the Host the *caller* used — so `SITE_DOMAIN` never entered into it. Dropped that env pair;
it was a wrong guess at the mechanism.
The fix is on the caller side: keep the `objecten.local` network alias and point every
writer whose writes must be notified at it — the ACL, the gateway integration tests, and
this slice's verify driver. Readers keep the plain service name.
ADR-0029 updated with the real mechanism and the ceiling it leaves: a new writer using
`objecten:8000` gets a 201 and silently no notification.
This commit is contained in:
@@ -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
|
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.
|
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`,
|
NRC types a notification's `hoofdObject` and `resourceUrl` as DRF `URLField`s, so Django's
|
||||||
which refuses a **single-label** host. Objecten builds those URLs from `objects.utils.get_domain`
|
`URLValidator` runs on them — and it refuses a **single-label** host. Objecten fills both
|
||||||
(i.e. `SITE_DOMAIN`), so with the compose service name they read `http://objecten:8000/...`
|
from the object `url` that DRF built with `request.build_absolute_uri`, i.e. **the Host the
|
||||||
and every publish is refused with `"Voer een geldige URL in."`
|
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
|
{"hoofdObject":["Voer een geldige URL in."],"resourceUrl":["Voer een geldige URL in."]}
|
||||||
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.
|
|
||||||
|
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
|
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
|
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
|
### 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.
|
- 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
|
- A second publisher on the shared `big-reference-seed` credential — a credential rotation
|
||||||
now touches two modules.
|
now touches two modules.
|
||||||
- One more hand-kept host constant: `SITE_DOMAIN` and the `objecten.local` alias must stay
|
- Objecten now has two in-network names, and which one a caller uses silently decides
|
||||||
in step, in both compose files.
|
whether its writes are notified (ceiling above).
|
||||||
- ponytail ceiling: notification delivery has no dead-letter or alerting — a failed publish
|
- ponytail ceiling: notification delivery has no dead-letter or alerting — a failed publish
|
||||||
is visible only in the worker log.
|
is visible only in the worker log.
|
||||||
- Upgrade path: if undelivered register events start mattering, subscribe an audit sink or
|
- 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
|
# 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
|
# 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.
|
# 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__Token: ${OBJECTEN_TOKEN:-1234567890abcdef1234567890abcdef12345678}
|
||||||
Acl__Objecten__ObjecttypenBaseUrl: http://objecttypen:8000/
|
Acl__Objecten__ObjecttypenBaseUrl: http://objecttypen:8000/
|
||||||
Acl__Objecten__ObjecttypenToken: ${OBJECTTYPEN_TOKEN:-0123456789abcdef0123456789abcdef01234567}
|
Acl__Objecten__ObjecttypenToken: ${OBJECTTYPEN_TOKEN:-0123456789abcdef0123456789abcdef01234567}
|
||||||
@@ -691,13 +692,6 @@ services:
|
|||||||
CACHE_AXES: objecten-redis:6379/0
|
CACHE_AXES: objecten-redis:6379/0
|
||||||
DISABLE_2FA: "true"
|
DISABLE_2FA: "true"
|
||||||
OTEL_SDK_DISABLED: "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_BROKER_URL: redis://objecten-redis:6379/1
|
||||||
CELERY_RESULT_BACKEND: 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
|
# 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
|
condition: service_completed_successfully
|
||||||
networks:
|
networks:
|
||||||
cg:
|
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:
|
aliases:
|
||||||
- objecten.local
|
- objecten.local
|
||||||
|
|
||||||
|
|||||||
@@ -326,7 +326,8 @@ services:
|
|||||||
# Objecten holds the register, OpenZaak holds the process (S-19a, ADR-0028). Both APIs take a
|
# 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
|
# 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.
|
# 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__Token: ${OBJECTEN_TOKEN:-1234567890abcdef1234567890abcdef12345678}
|
||||||
Acl__Objecten__ObjecttypenBaseUrl: http://objecttypen:8000/
|
Acl__Objecten__ObjecttypenBaseUrl: http://objecttypen:8000/
|
||||||
Acl__Objecten__ObjecttypenToken: ${OBJECTTYPEN_TOKEN:-0123456789abcdef0123456789abcdef01234567}
|
Acl__Objecten__ObjecttypenToken: ${OBJECTTYPEN_TOKEN:-0123456789abcdef0123456789abcdef01234567}
|
||||||
@@ -717,13 +718,6 @@ services:
|
|||||||
CACHE_AXES: objecten-redis:6379/0
|
CACHE_AXES: objecten-redis:6379/0
|
||||||
DISABLE_2FA: "true"
|
DISABLE_2FA: "true"
|
||||||
OTEL_SDK_DISABLED: "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_BROKER_URL: redis://objecten-redis:6379/1
|
||||||
CELERY_RESULT_BACKEND: 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
|
# 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
|
condition: service_completed_successfully
|
||||||
networks:
|
networks:
|
||||||
cg:
|
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:
|
aliases:
|
||||||
- objecten.local
|
- 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
|
# 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).
|
# 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
|
# EXCEPT Objecttypen, which must be reached by SERVICE NAME: it echoes the request Host into the
|
||||||
# Host into the objecttype `url` and Objecten only accepts the one matching its configured api_root
|
# objecttype `url` and Objecten only accepts the one matching its configured api_root (ADR-0028);
|
||||||
# (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.
|
# Does NOT manage the stack lifecycle, but cleans up the sink/driver it creates.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -48,7 +49,7 @@ echo ">> sink at $sink_ip:9000"
|
|||||||
echo ">> registering the abonnement + writing a RegisterRecord"
|
echo ">> registering the abonnement + writing a RegisterRecord"
|
||||||
docker rm -f rr-overify >/dev/null 2>&1 || true
|
docker rm -f rr-overify >/dev/null 2>&1 || true
|
||||||
drv="$(docker create --network "$net" --name rr-overify \
|
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 "OBJECTEN_TOKEN=${OBJECTEN_TOKEN:-1234567890abcdef1234567890abcdef12345678}" \
|
||||||
-e "OBJECTTYPEN=http://objecttypen:8000" \
|
-e "OBJECTTYPEN=http://objecttypen:8000" \
|
||||||
-e "OBJECTTYPEN_TOKEN=${OBJECTTYPEN_TOKEN:-0123456789abcdef0123456789abcdef01234567}" \
|
-e "OBJECTTYPEN_TOKEN=${OBJECTTYPEN_TOKEN:-0123456789abcdef0123456789abcdef01234567}" \
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public sealed class ObjectenGatewayIntegrationTests
|
|||||||
new HttpClient(),
|
new HttpClient(),
|
||||||
new ObjectenOptions
|
new ObjectenOptions
|
||||||
{
|
{
|
||||||
BaseUrl = new(Env("OBJECTEN_BASE", "http://objecten:8000")),
|
BaseUrl = new(Env("OBJECTEN_BASE", "http://objecten.local:8000")),
|
||||||
Token = Env("OBJECTEN_TOKEN", "1234567890abcdef1234567890abcdef12345678"),
|
Token = Env("OBJECTEN_TOKEN", "1234567890abcdef1234567890abcdef12345678"),
|
||||||
ObjecttypenBaseUrl = new(Env("OBJECTTYPEN_BASE", "http://objecttypen:8000")),
|
ObjecttypenBaseUrl = new(Env("OBJECTTYPEN_BASE", "http://objecttypen:8000")),
|
||||||
ObjecttypenToken = Env("OBJECTTYPEN_TOKEN", "0123456789abcdef0123456789abcdef01234567"),
|
ObjecttypenToken = Env("OBJECTTYPEN_TOKEN", "0123456789abcdef0123456789abcdef01234567"),
|
||||||
@@ -65,7 +65,7 @@ public sealed class ObjectenGatewayIntegrationTests
|
|||||||
{
|
{
|
||||||
using var http = new HttpClient();
|
using var http = new HttpClient();
|
||||||
var objecttype = await ResolveObjecttypeUrlAsync(http);
|
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) +
|
"/api/v2/objects?type=" + Uri.EscapeDataString(objecttype) +
|
||||||
"&data_attrs=id__exact__" + Uri.EscapeDataString(id));
|
"&data_attrs=id__exact__" + Uri.EscapeDataString(id));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user