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
CloudNativePG mints the connection secret itself: a cluster named `X` produces
secret `X-app` with key `uri`. The Deployment references it via
`secretKeyRef`, exactly as `user-engine` does with
`USER_ENGINE_DATABASE_URL``user-engine-pg-app/uri`.
**Corrected 2026-08-17.** This section originally said CloudNativePG mints an
`X-app` secret we reference with `secretKeyRef`, as `user-engine` does. That is
true for a cluster's *own* app database — and wrong for the path we are
actually taking.
So **no secret enters this repo, this workplan, or any evidence dump**, and no
`warden route` lookup is needed — this is not a credential we fetch, custody,
or rotate. Runtime secret custody remains OpenBao's per `SCOPE.md`; an
operator-minted in-cluster secret is outside that lane.
`railiance-platform/docs/rapp-postgres-boundary.md` is explicit: `rapp-postgres`
owns the role and database provisioning surface for the shared cluster, and
*"the workload receives a short-lived lease through the platform broker"* — the
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
@ -110,16 +128,62 @@ priority: high
state_hub_task_id: "b4701256-f235-4474-9dcf-7cb09f62b873"
```
Provision a dedicated **database** on an existing **shared** cluster, per the
principles above. Candidates are `net-kingdom-pg` (already exists, and
`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.
**Target: `platform-pg`, via a `PostgresConsumer` declaration to
`rapp-postgres`.** Not `net-kingdom-pg` or `apps-pg`, and not provisioned by us.
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
and ours is reversible in an afternoon; picking dedicated now would spend
capacity on an isolation guarantee nothing has asked for yet.
and ours is reversible; picking dedicated now would spend capacity on an
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
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
choice, which is worth checking rather than assuming.
Write whatever manifest the choice needs into `deploy/` — a CNPG `Database`
resource against the shared cluster, not a new `Cluster`, if that is the
correct shape.
**Do not write a CNPG `Database` or `Cluster` manifest into this repo's
`deploy/`.** That surface belongs to `rapp-postgres`. Our `deploy/` gains only
the consumption side — how the workload reaches the leased credential.
Done when the database exists on shared capacity, the connection reaches the
app only through an injected URL, and the move-to-dedicated trigger list and
runbook are recorded.
Done when `rapp-postgres` has provisioned the consumer, the connection reaches
the app only through an injected, refreshable credential, and the
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`