TEN-WP-0009-T01: target platform-pg via rapp-postgres consumer declaration

Corrects the credential assumption: shared-cluster access is a broker lease,
not a CNPG-minted secretKeyRef.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-17 10:44:59 +02:00
parent 358dc506fa
commit 05d12fe39f

View file

@ -48,15 +48,33 @@ than a formality wrapped around one implementation.
## Credential handling ## Credential handling
CloudNativePG mints the connection secret itself: a cluster named `X` produces **Corrected 2026-08-17.** This section originally said CloudNativePG mints an
secret `X-app` with key `uri`. The Deployment references it via `X-app` secret we reference with `secretKeyRef`, as `user-engine` does. That is
`secretKeyRef`, exactly as `user-engine` does with true for a cluster's *own* app database — and wrong for the path we are
`USER_ENGINE_DATABASE_URL``user-engine-pg-app/uri`. actually taking.
So **no secret enters this repo, this workplan, or any evidence dump**, and no `railiance-platform/docs/rapp-postgres-boundary.md` is explicit: `rapp-postgres`
`warden route` lookup is needed — this is not a credential we fetch, custody, owns the role and database provisioning surface for the shared cluster, and
or rotate. Runtime secret custody remains OpenBao's per `SCOPE.md`; an *"the workload receives a short-lived lease through the platform broker"* — the
operator-minted in-cluster secret is outside that lane. OpenBao database secrets engine — rather than a static secret. `railiance-platform`
retains cluster-wide governance and the credential-broker grant catalog.
Consequences, and the second one is real work:
- **We do not provision our own database.** We *declare a consumer request* and
`rapp-postgres` provisions it. Writing a CNPG `Database` manifest into this
repo's `deploy/`, or adding a managed role to a shared cluster's spec, would
both be reaching across that boundary — the latter also mutating shared
infrastructure other consumers depend on.
- **Credentials are leased, not mounted.** A short-lived lease means the
connection string can change under a running pod, so the store cannot read a
DSN once at startup and hold it forever. T03 and T05 must account for
credential refresh; a long-lived pool built on an expired lease fails at the
worst possible moment.
Either way **no secret enters this repo, this workplan, or any evidence dump**.
Runtime secret custody stays OpenBao's per `SCOPE.md`, which is exactly what
the broker lease implements.
## Placement: shared by default, movable by design ## Placement: shared by default, movable by design
@ -110,16 +128,62 @@ priority: high
state_hub_task_id: "b4701256-f235-4474-9dcf-7cb09f62b873" state_hub_task_id: "b4701256-f235-4474-9dcf-7cb09f62b873"
``` ```
Provision a dedicated **database** on an existing **shared** cluster, per the **Target: `platform-pg`, via a `PostgresConsumer` declaration to
principles above. Candidates are `net-kingdom-pg` (already exists, and `rapp-postgres`.** Not `net-kingdom-pg` or `apps-pg`, and not provisioned by us.
`tenant-engine` is a NetKingdom service) and `apps-pg`. Prefer the one whose
grouping matches the product family, since that is the axis a future split
would most likely follow — moving is cheap, but moving less often is cheaper.
Note the deviation from precedent and why: `user-engine` runs a dedicated Reasoning, revised 2026-08-17 after finding the actual contract. The candidate
list originally weighed product-family fit and would have picked
`net-kingdom-pg`. But `platform-pg` is the shared cluster that has a
*governed provisioning path* — `rapp-postgres` owns its consumer declarations,
role provisioning, isolation tests, and recovery procedure. The other clusters
carry their roles as hand-added entries in the shared cluster spec
(`net-kingdom-pg` has `privacyidea`, `apps-pg` has `vergabe` and
`coulomb_social`). Choosing product-family fit would mean improvising outside
the one contract that exists, to gain an affinity that portability makes cheap
to change later anyway. `audit-core` is the precedent: a platform service, on
`platform-pg`, declared as a consumer.
Note the deviation from `user-engine` and why: they run a dedicated
`user-engine-pg`. We are deliberately *not* copying that. Their choice is fine `user-engine-pg`. We are deliberately *not* copying that. Their choice is fine
and ours is reversible in an afternoon; picking dedicated now would spend and ours is reversible; picking dedicated now would spend capacity on an
capacity on an isolation guarantee nothing has asked for yet. isolation guarantee nothing has asked for yet.
The declaration to request, modelled on `consumers/audit-core.yaml`:
```yaml
apiVersion: rapp-postgres.railiance.io/v1alpha1
kind: PostgresConsumer
metadata:
name: tenant-engine
spec:
database: tenant_engine
schema: tenant_engine
costAttributionKey: platform:tenant-engine
clientNamespaces: [tenant-engine]
roles:
owner: tenant_engine_owner
migration: tenant_engine_migrate
runtime: tenant_engine_app
limits:
migrationConnections: 2
runtimeConnections: 12
statementTimeout: 30s
idleInTransactionSessionTimeout: 15s
tenantKeyingRequired: true
tenantIsolation: consumer-service-boundary
```
Two things to raise with them rather than assume:
- **`tenantKeyingRequired: true`** is right for `audit-core`, whose rows are
per-tenant. Ours are *about* tenants — `tenant_id` is the primary key of the
`tenants` table, not a partition key on someone else's data. Confirm that
their isolation tests read our shape correctly rather than flagging a false
positive.
- **Statement timeout.** 30s is generous for this workload; every query here is
a single-row lookup or a small transaction. A tighter timeout is a better
failure mode for a service `flex-auth` calls synchronously on the
authorization path — a slow query should fail closed fast, not hold the PDP.
Record what would trigger a move to dedicated, so the reversal is a judgement Record what would trigger a move to dedicated, so the reversal is a judgement
already made rather than one improvised under pressure. Candidate triggers: already made rather than one improvised under pressure. Candidate triggers:
@ -133,13 +197,17 @@ backup story at all, so this is a gain to claim explicitly rather than inherit
by accident — and a shared cluster means inheriting *someone else's* retention by accident — and a shared cluster means inheriting *someone else's* retention
choice, which is worth checking rather than assuming. choice, which is worth checking rather than assuming.
Write whatever manifest the choice needs into `deploy/` — a CNPG `Database` **Do not write a CNPG `Database` or `Cluster` manifest into this repo's
resource against the shared cluster, not a new `Cluster`, if that is the `deploy/`.** That surface belongs to `rapp-postgres`. Our `deploy/` gains only
correct shape. the consumption side — how the workload reaches the leased credential.
Done when the database exists on shared capacity, the connection reaches the Done when `rapp-postgres` has provisioned the consumer, the connection reaches
app only through an injected URL, and the move-to-dedicated trigger list and the app only through an injected, refreshable credential, and the
runbook are recorded. move-to-dedicated trigger list and runbook are recorded.
Status 2026-08-17: declaration requested from `rapp-postgres`; awaiting their
provisioning and an answer on how a workload consumes the broker lease in
practice. Nothing to apply on our side until that returns.
## T02 - Implement `PostgresTenantStore` ## T02 - Implement `PostgresTenantStore`