Finish RPF-WP-0018; RPF-WP-0019 repository-complete
RPF-WP-0018 closed: all seven tasks done. The provider-declaration finding was adopted upstream and its canonical form is the provider: block in tenancy.yaml; adaptive-pricing declined the standing co-signature and supplied typed tier minima instead, recorded in ADR-0002. Three corrections against our own output are recorded in the documents rather than edited away. RPF-WP-0019 T03 done (ceiling of three, memory binding, apps-pg-2 named as overflow, enforced by make apps-pg-verify-capacity). T01/T02 are repository-complete: backup target, retention, per-consumer connection limits, role timeouts and Burstable resources are declared in source and published in s3-consumer-interfaces 1.1.0 before rollout. They stay in progress because no live application, backup success or restore proof exists, and declared configuration is not a section 13 artifact. T04 waits on that window. apps-pg R reason corrected to say the target is declared-not-applied rather than absent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
e52ae26001
commit
dc4245361d
20 changed files with 650 additions and 73 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -9,6 +9,7 @@ helm/*.yaml
|
|||
!helm/*-cluster.yaml
|
||||
!helm/*-networkpolicies.yaml
|
||||
!helm/*-databases.yaml
|
||||
!helm/*-backup.yaml
|
||||
|
||||
# ArgoCD repository credentials — encrypt locally, never commit
|
||||
argocd/repositories/*.repository.sops.yaml
|
||||
|
|
|
|||
20
Makefile
20
Makefile
|
|
@ -82,7 +82,10 @@ consumption-preflight: ## Refuse a new order that exceeds a restricted entity's
|
|||
|
||||
##@ Shared apps-pg (S5 application databases)
|
||||
|
||||
apps-pg-deploy: ## Apply shared apps-pg cnpg Cluster + NetworkPolicies
|
||||
apps-pg-verify-capacity: ## Verify cell ceiling, role limits and distinct backup prefixes
|
||||
python3 tools/verify_apps_pg_capacity.py helm/apps-pg-cluster.yaml helm/apps-pg-2-cluster.yaml
|
||||
|
||||
apps-pg-deploy: apps-pg-verify-capacity ## Apply shared apps-pg cnpg Cluster + NetworkPolicies
|
||||
@if [ -n "$(CONSUMING_ENTITY)" ]; then \
|
||||
$(MAKE) consumption-preflight CONSUMING_ENTITY='$(CONSUMING_ENTITY)' \
|
||||
ESTIMATE_EUR='$(ESTIMATE_EUR)' CONSUMPTION_CLASS=new-order; \
|
||||
|
|
@ -90,6 +93,21 @@ apps-pg-deploy: ## Apply shared apps-pg cnpg Cluster + NetworkPolicies
|
|||
$(KUBECTL) apply -f helm/apps-pg-cluster.yaml
|
||||
$(KUBECTL) apply -f helm/apps-pg-networkpolicies.yaml
|
||||
|
||||
apps-pg-backup-deploy: ## Apply the daily backup after the governed S3 Secret exists
|
||||
@$(KUBECTL) get secret platform-pg-backup-s3 -n databases >/dev/null || { echo 'missing governed platform-pg-backup-s3 secret' >&2; exit 2; }
|
||||
$(KUBECTL) apply -f helm/apps-pg-backup.yaml
|
||||
|
||||
apps-pg-overflow-dry-run: apps-pg-verify-capacity ## Server-validate the unapplied overflow cell
|
||||
@$(KUBECTL) get secret apps-pg-2-credentials -n databases >/dev/null || { echo 'missing distinct apps-pg-2-credentials secret' >&2; exit 2; }
|
||||
$(KUBECTL) apply --dry-run=server -f helm/apps-pg-2-cluster.yaml
|
||||
$(KUBECTL) apply --dry-run=server -f helm/apps-pg-2-networkpolicies.yaml
|
||||
$(KUBECTL) apply --dry-run=server -f helm/apps-pg-2-backup.yaml
|
||||
|
||||
apps-pg-overflow-deploy: apps-pg-overflow-dry-run ## Provision only for an approved fourth consumer
|
||||
$(KUBECTL) apply -f helm/apps-pg-2-cluster.yaml
|
||||
$(KUBECTL) apply -f helm/apps-pg-2-networkpolicies.yaml
|
||||
$(KUBECTL) apply -f helm/apps-pg-2-backup.yaml
|
||||
|
||||
apps-pg-status: ## Show apps-pg cnpg cluster health
|
||||
$(KUBECTL) cnpg status apps-pg -n databases 2>/dev/null || \
|
||||
$(KUBECTL) get cluster apps-pg -n databases -o wide
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
| workplan | RAILIANCE-WP-0016 | finished | — | workplans/RAILIANCE-WP-0016-architecture-cleanup-backlog.md |
|
||||
| workplan | RAILIANCE-WP-0017 | finished | — | workplans/RAILIANCE-WP-0017-consumption-mode-enforcement.md |
|
||||
| workplan | RPF-WP-0018 | active | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
|
||||
| workplan | RPF-WP-0019 | active | — | workplans/RPF-WP-0019-apps-pg-recoverability-and-controls.md |
|
||||
| task | RAILIANCE-WP-0005-T01 | done | — | workplans/RAILIANCE-WP-0005-credential-request-and-lease-broker.md |
|
||||
| task | RAILIANCE-WP-0005-T02 | done | — | workplans/RAILIANCE-WP-0005-credential-request-and-lease-broker.md |
|
||||
| task | RAILIANCE-WP-0005-T03 | done | — | workplans/RAILIANCE-WP-0005-credential-request-and-lease-broker.md |
|
||||
|
|
@ -88,3 +89,7 @@
|
|||
| task | RPF-WP-0018-T05 | done | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
|
||||
| task | RPF-WP-0018-T06 | done | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
|
||||
| task | RPF-WP-0018-T07 | done | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
|
||||
| task | RPF-WP-0019-T01 | progress | — | workplans/RPF-WP-0019-apps-pg-recoverability-and-controls.md |
|
||||
| task | RPF-WP-0019-T02 | progress | — | workplans/RPF-WP-0019-apps-pg-recoverability-and-controls.md |
|
||||
| task | RPF-WP-0019-T03 | done | — | workplans/RPF-WP-0019-apps-pg-recoverability-and-controls.md |
|
||||
| task | RPF-WP-0019-T04 | wait | — | workplans/RPF-WP-0019-apps-pg-recoverability-and-controls.md |
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ review_interval: 6m
|
|||
|
||||
## Context
|
||||
|
||||
`net-kingdom/canon/standards/tenancy-posture_v0.1.md` §8.2 proposes that
|
||||
An earlier draft of
|
||||
`net-kingdom/canon/standards/tenancy-posture_v0.1.md` §8.2 proposed that
|
||||
database placement policy — dedicated versus shared, and when that changes —
|
||||
be owned by `railiance-platform`, co-signed by `adaptive-pricing`.
|
||||
`tenant-engine` raised the same gap independently on 2026-08-16: both patterns
|
||||
|
|
@ -49,15 +50,18 @@ compliance position; S3 knows the substrate. A placement change needs both.
|
|||
Putting the decision solely here would recreate the problem §8.2 exists to
|
||||
avoid — an operations-shaped repo holding a commercial decision alone.
|
||||
|
||||
`adaptive-pricing` co-signature is requested, not assumed.
|
||||
`adaptive-pricing` declined the standing co-signature on 2026-08-17. The
|
||||
replacement is binding and machine-reconcilable: it publishes typed tier
|
||||
minimums; this policy validates placement against them. Ownership stays here.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `docs/placement-policy.md` is the operative document; this ADR is the
|
||||
decision behind it.
|
||||
- §19.8 is answered by obligation rather than by number: Rule P-4.1 requires
|
||||
`platform-pg` and `apps-pg` to declare a ceiling before their third consumer.
|
||||
Both are at two consumers.
|
||||
- §19.8 is answered for `platform-pg` by `rapp-postgres` ADR-0004: four
|
||||
consumers, with `platform-pg-2` named as the still-absent overflow target.
|
||||
Rule P-4.1 still requires `apps-pg` to declare its own ceiling before its
|
||||
third consumer.
|
||||
- This repo owes `apps-pg`'s ceiling under its own rule. A policy whose author
|
||||
is exempt is not a policy.
|
||||
- §8.1 requires triggers to be *monitored*. This repo has no monitoring plane,
|
||||
|
|
@ -65,8 +69,8 @@ avoid — an operations-shaped repo holding a commercial decision alone.
|
|||
pending `railiance-telemetry`. Declaring a ceiling is a partial substitute —
|
||||
a written threshold is checkable by a human at provisioning, which beats a
|
||||
limit discovered by an OOM kill.
|
||||
- If `adaptive-pricing` declines, this ADR is revised to record single
|
||||
ownership explicitly rather than leaving an unsigned co-signature line.
|
||||
- `adaptive-pricing`'s decline is recorded as single policy ownership plus a
|
||||
mandatory typed constraint join, not as an absent signature.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
|
|
|||
|
|
@ -97,18 +97,30 @@ applied; coulomb-social connectivity smoke from labeled consumer ns OK.
|
|||
|
||||
## Backup And Roadmap
|
||||
|
||||
`apps-pg` starts as a conservative single-instance, 10Gi cluster to match
|
||||
the current node capacity and existing CNPG footprint. Adding a replica,
|
||||
PgBouncer/CNPG `Pooler`, resize policy, and CNPG-native backup coverage
|
||||
are follow-up platform work items.
|
||||
`apps-pg` remains a conservative single-instance, 10Gi cluster. The desired
|
||||
state now includes continuous WAL archival, a daily 02:15 UTC base backup and
|
||||
30-day retention under the distinct `apps-pg/` object-store prefix. The
|
||||
credential lane is shared with `platform-pg`; the backup data path is not.
|
||||
|
||||
Until backup coverage is explicitly added, consumer onboarding should
|
||||
record whether app data is disposable, externally reproducible, or
|
||||
requires an immediate backup follow-up before production use.
|
||||
The declared ceiling is three consumers. Each gets at most 20 connections;
|
||||
40 of the explicit 100-connection aggregate remains for CNPG and operator
|
||||
headroom. Memory (1Gi limit), not the clean connection refusal, is treated as
|
||||
the binding safety constraint. A fourth consumer goes to the named
|
||||
`apps-pg-2` overflow substrate, which must be provisioned before onboarding.
|
||||
Reviewed, unapplied source for that cell is
|
||||
`helm/apps-pg-2-{cluster,backup,networkpolicies}.yaml`; it uses a distinct
|
||||
bootstrap Secret and backup prefix. `make apps-pg-verify-capacity` rejects a
|
||||
fourth role on either cell, missing connection limits, duplicate roles, missing
|
||||
resource envelopes, or a reused backup path.
|
||||
|
||||
`statement_timeout` and `idle_in_transaction_session_timeout` are 15 seconds
|
||||
per consumer role. CNPG 1.28 cannot express those role settings, so
|
||||
`helm/apps-pg-consumer-controls.sql` is the idempotent controlled-operator
|
||||
step; `connectionLimit` remains declaratively reconciled by CNPG.
|
||||
|
||||
Resource evidence for `resource:railiance:apps-pg` (capacity, recovery,
|
||||
labor, allocation drivers) is published under
|
||||
`docs/evidence/RAILIANCE-WP-0016-apps-pg-resource-evidence.md`. The
|
||||
2026-08-14 observation: no ScheduledBackup, railiance01 `apps-pg` is
|
||||
not an Option A target, RPO is unbounded, RTO is undefined. That is
|
||||
unresolved local-storage risk, not verified recovery.
|
||||
2026-08-14 observation remains historically correct. The 2026-08-18 desired
|
||||
state is not called verified recovery until the first backup succeeds and a
|
||||
scratch restore artifact is recorded.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ Effective: 2026-08-17
|
|||
Framework: `net-kingdom/canon/standards/tenancy-posture_v0.1.md` §4.4, §8
|
||||
Contract version of this document: `1.0.0`
|
||||
Status: **provisional** — binding on this repo now; formally conditional on
|
||||
the framework reaching `accepted` and on `adaptive-pricing` co-signature.
|
||||
the framework reaching `accepted`.
|
||||
|
||||
Answers `tenant-engine`'s 2026-08-16 request ("a default and the conditions
|
||||
that override it, so migrations stop being decided by whichever neighbour was
|
||||
|
|
@ -13,8 +13,9 @@ consulted") and Tenancy Posture §19.2.
|
|||
|
||||
## 1. Ownership, and its limit
|
||||
|
||||
§8.2 proposes placement ownership to `railiance-platform` co-signed by
|
||||
`adaptive-pricing`. **Accepted, with the scope stated:**
|
||||
Draft-8 §8.2 assigns the placement rule to `railiance-platform`, substrate
|
||||
numbers to the package repo, workload requirements to the consumer and tier
|
||||
minimums to `adaptive-pricing`. **Accepted, with the scope stated:**
|
||||
|
||||
> **This repo owns the rule. The package repo owns the number.**
|
||||
|
||||
|
|
@ -32,10 +33,11 @@ an observation only the operator of the package can make. Our obligation under
|
|||
this policy is §4 below — that a ceiling and an overflow target **exist and
|
||||
are published**. Theirs is what they are.
|
||||
|
||||
`adaptive-pricing` co-signature is **requested, not assumed**. The reasoning in
|
||||
§8.2 is sound: tenancy model selection is commercial as much as technical. If
|
||||
they decline, this document records single ownership and says so plainly rather
|
||||
than leaving a co-signature line nobody signed.
|
||||
`adaptive-pricing` declined a standing co-signature on 2026-08-17 and supplied
|
||||
the stronger replacement adopted by draft-8: typed tier minima are joined to
|
||||
consumer and provider declarations at tier definition and on change. This repo
|
||||
therefore owns this policy singly; it must validate, not copy, the commercial
|
||||
constraint artifact.
|
||||
|
||||
## 2. The default, and what overrides it
|
||||
|
||||
|
|
@ -73,7 +75,7 @@ triggers. It is not the same role as the operator of the cluster.
|
|||
| Workload | Substrate | Provided level | Placement owner | Co-signer | §13 evidence |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `audit-core` | `platform-pg` | P1 | `audit-core` | this repo | probes exist |
|
||||
| `tenant-engine` | `platform-pg` | P1 | `tenant-engine` | this repo | probes exist |
|
||||
| `tenant-engine` | SQLite PVC current; `platform-pg` desired | off-ladder current; P1 desired | `tenant-engine` | this repo | source conformance; live cutover absent |
|
||||
| `user-engine` | `user-engine-pg` | P2 | `net-kingdom` | this repo | not assessed here |
|
||||
| `target-revenue` | `target-revenue-pg` | P2 | `target-revenue` | this repo | not assessed here |
|
||||
| `forgejo` | `forgejo-db` | P2 | this repo | — | single consumer |
|
||||
|
|
@ -85,7 +87,7 @@ triggers. It is not the same role as the operator of the cluster.
|
|||
§13.1 claims a level only with its artifact present. The P1–P4 artifact is
|
||||
*"provisioning declaration plus the platform's isolation probes"*.
|
||||
|
||||
`platform-pg` has them — `rapp-postgres` runs 15 adversarial probes against the
|
||||
`platform-pg` has them — `rapp-postgres` runs 19 adversarial probes against the
|
||||
consumer boundary. **`apps-pg` has none.** `scripts/capture-apps-pg-evidence.py`
|
||||
captures *capacity* evidence for `resource-control`; it makes no isolation
|
||||
assertion. So the P1 levels recorded above for `vergabe` and `coulomb_social`
|
||||
|
|
@ -145,8 +147,8 @@ one". "Provision a second cluster" is an answer; "revisit at the time" is not.
|
|||
|
||||
| Cluster | Ceiling declared | Overflow target | Standing |
|
||||
| --- | --- | --- | --- |
|
||||
| `platform-pg` | not yet — `rapp-postgres` owes it | not yet | **2 consumers of ~6 practical; owed before the 3rd** |
|
||||
| `apps-pg` | not yet — this repo owes it | not yet | 2 consumers; owed before the 3rd |
|
||||
| `platform-pg` | 4 declarations (estimated; ADR-0004) | `platform-pg-2`, named but absent | 3/4 including the isolation probe; measure before the next workload |
|
||||
| `apps-pg` | 3 consumers; 20 connections each, 1Gi memory binds first | `apps-pg-2` | 2/3; overflow must exist before a 4th |
|
||||
| `forgejo-db` | n/a — single consumer | n/a | — |
|
||||
|
||||
Both live shared clusters are one consumer away from the trigger. This repo
|
||||
|
|
|
|||
|
|
@ -55,29 +55,28 @@ provisions the role and CNPG `Database`, labels the namespace
|
|||
RW endpoint: `apps-pg-rw.databases.svc.cluster.local:5432`. Bootstrap
|
||||
role `apps_admin` is not a consumer credential.
|
||||
|
||||
**Quotas and limits** — read from `helm/apps-pg-cluster.yaml`, 2026-08-17:
|
||||
**Quotas and limits** — declared in `helm/apps-pg-cluster.yaml` and
|
||||
`helm/apps-pg-consumer-controls.sql`, 2026-08-18. These values are published
|
||||
before the corresponding live rollout:
|
||||
|
||||
| Property | Value | Note |
|
||||
| --- | --- | --- |
|
||||
| `max_connections` | **PostgreSQL default (100)** | Not tuned in the CR. Not partitioned per consumer — there is no per-consumer connection allowance, so one consumer can exhaust the cluster. |
|
||||
| `statement_timeout` | **none** | Not set. A runaway query runs until it finishes. |
|
||||
| `idle_in_transaction_session_timeout` | **none** | Not set. |
|
||||
| CPU / memory limits | **none declared** | The pod is BestEffort QoS and is the first thing evicted under node pressure. |
|
||||
| `max_connections` | 100 | Explicit aggregate cap. Forty connections remain outside the declared three-consumer ceiling for operator/CNPG overhead. |
|
||||
| Per-consumer connections | 20 | CNPG `managed.roles[].connectionLimit`, for both current roles. |
|
||||
| `statement_timeout` | 15s | Per role; idempotent operator SQL because CNPG 1.28 has no managed-role settings field. |
|
||||
| `idle_in_transaction_session_timeout` | 15s | Per role, same enforcement path. |
|
||||
| CPU / memory | 100m / 256Mi requests; 1 CPU / 1Gi limits | Burstable QoS; memory is expected to bind before connections if workload size grows. |
|
||||
| Instances | 1 | No HA. Restart recovery only, per the single-node rail. |
|
||||
| Storage | 10Gi | |
|
||||
| Backup | **none configured** | No `barmanObjectStore`, no `retentionPolicy`. |
|
||||
| Effective erasure horizon | **undefined** | Follows from the line above. |
|
||||
| Backup | 30d; daily 02:15 UTC + continuous WAL | S3 prefix `s3://railiance-platform-pg-backup/apps-pg/`; rollout is not evidence of recoverability until first backup and restore proof exist. |
|
||||
| Effective erasure horizon | 30d, or the max across co-residents | Shared-cluster rule from placement policy §7. |
|
||||
| Declared ceiling / overflow | 3 consumers / `apps-pg-2` | Memory is the binding safety constraint; provision the named overflow substrate before admitting a fourth. |
|
||||
|
||||
**Read the backup row before provisioning against this cluster.** `apps-pg`
|
||||
has no backup at all — not a short retention, none. Recovery from data loss is
|
||||
not available on this interface today. This is disclosed rather than fixed
|
||||
because §10.2 requires disclosure now and the fix needs a backup target; it is
|
||||
recorded as the R-axis gap in `docs/tenancy-posture.md` and is the highest
|
||||
value item that declaration surfaced.
|
||||
|
||||
The unpartitioned connection pool is the same shape of risk `rapp-postgres`
|
||||
reported for `platform-pg`: the bound is aggregate, so every consumer can be
|
||||
politely inside its own expectations while the cluster still fails.
|
||||
The manifests close the configuration gap, not the evidence gap. Until the
|
||||
cluster change is applied and `LastBackupSucceeded=True` plus a scratch restore
|
||||
are captured, recovery remains operationally unproven. Likewise the P1 noisy-
|
||||
neighbour claim waits for the controlled probe window; source configuration is
|
||||
not substituted for either artifact.
|
||||
|
||||
## `rapp-postgres.v1`
|
||||
|
||||
|
|
@ -99,7 +98,7 @@ Consumer is a `consumers[].declaration`. Boundary unit is
|
|||
| Backup retention | 30d | Instance-wide, per `rapp-postgres` ADR-0002. |
|
||||
| Effective erasure horizon | **30d, or the max across co-residents** | A consumer needing shorter cannot have it at P1. See `docs/placement-policy.md` §7. |
|
||||
| Statement timeout | consumer-set | `tenant-engine` sets 5s for its own path; not a platform default. |
|
||||
| Declared ceiling / overflow target | **not yet declared** | Owed by `rapp-postgres` before the third consumer, per `docs/placement-policy.md` Rule P-4.1. |
|
||||
| Declared ceiling / overflow target | **4 declarations / `platform-pg-2`** | The isolation probe counts. `rapp-postgres` rejects a fifth declaration on one cell; ADR-0004 owns the number. |
|
||||
|
||||
Service classes currently co-resident on `platform-pg`: `latency-critical`
|
||||
(`tenant-engine`) and `batch` (`audit-core`). Nothing prioritises between them
|
||||
|
|
|
|||
|
|
@ -2,9 +2,13 @@
|
|||
|
||||
Work record: `RPF-WP-0018` T01
|
||||
Declared: 2026-08-17
|
||||
Framework: `net-kingdom/canon/standards/tenancy-posture_v0.1.md` (draft-5, proposed)
|
||||
Framework: `net-kingdom/canon/standards/tenancy-posture_v0.1.md` (draft-8, proposed)
|
||||
Contract version of this document: `1.0.0`
|
||||
|
||||
Machine-readable source: `tenancy.yaml`. This document preserves the review
|
||||
reasoning; where a snippet and the root declaration differ, `tenancy.yaml` is
|
||||
authoritative.
|
||||
|
||||
This is `railiance-platform`'s response to the Tenancy Posture ratification
|
||||
test (§20.2): *"Each publishes its own posture vector as part of review. The
|
||||
framework is validated by whether it can describe them accurately."*
|
||||
|
|
@ -55,8 +59,8 @@ surface to the `rapp-*` repos while custody and policy stayed here.
|
|||
tenancy:
|
||||
service: apps-pg
|
||||
role: storage-provider
|
||||
current: { I: 0, A: 0, E: 0, P: null, R: 0 }
|
||||
target: { I: 0, A: 0, E: 0, P: null, R: 2 }
|
||||
current: { I: 0, A: 0, E: 0, P: "n/a", R: 0, V: 0 }
|
||||
target: { I: 0, A: 0, E: 0, P: "n/a", R: 2, V: 1 }
|
||||
service_class: interactive
|
||||
reviewed: "2026-08-17"
|
||||
gap:
|
||||
|
|
@ -70,7 +74,7 @@ tenancy:
|
|||
concept, carries no tenant claim, and applies no tenant predicate. The
|
||||
tenant boundary for data on this cluster sits entirely in the consuming
|
||||
S5 application. See the provider note below."
|
||||
- "P is null rather than a number. See §P-axis note."
|
||||
- "P is n/a rather than a number. See §P-axis note."
|
||||
```
|
||||
|
||||
### `forgejo-db` — dedicated cluster for the forge
|
||||
|
|
@ -79,8 +83,8 @@ tenancy:
|
|||
tenancy:
|
||||
service: forgejo-db
|
||||
role: storage-provider
|
||||
current: { I: 0, A: 0, E: 0, P: null, R: 0 }
|
||||
target: { I: 0, A: 0, E: 0, P: null, R: 2 }
|
||||
current: { I: 0, A: 0, E: 0, P: "n/a", R: 0, V: 0 }
|
||||
target: { I: 0, A: 0, E: 0, P: "n/a", R: 2, V: 1 }
|
||||
service_class: interactive
|
||||
reviewed: "2026-08-17"
|
||||
gap:
|
||||
|
|
@ -97,8 +101,8 @@ tenancy:
|
|||
tenancy:
|
||||
service: openbao
|
||||
role: credential-provider
|
||||
current: { I: 0, A: 0, E: 0, P: 0, R: 0 }
|
||||
target: { I: 0, A: 0, E: 0, P: 0, R: 1 }
|
||||
current: { I: 0, A: 0, E: 0, P: 0, R: 0, V: 0 }
|
||||
target: { I: 0, A: 0, E: 0, P: 0, R: 1, V: 1 }
|
||||
permanent: [I, A, E]
|
||||
service_class: latency-critical
|
||||
reviewed: "2026-08-17"
|
||||
|
|
@ -145,7 +149,7 @@ great deal, it is central, and it is mechanically evidenced —
|
|||
it *can* sign with `ssh/sign/agt-role`, and asserts it *cannot* read policy
|
||||
metadata, then revokes by accessor. That is exactly the shape §13 asks for at
|
||||
`A2`: choke point identified, unbound request refused. It is pointed at the
|
||||
**consumer** boundary, and none of the five axes has anywhere to put it.
|
||||
**consumer** boundary, and none of the tenancy axes has anywhere to put it.
|
||||
|
||||
That evidence is therefore cited under the provider statement rather than
|
||||
against a consumer axis. Consumer separation in OpenBao is structural — a
|
||||
|
|
@ -164,7 +168,7 @@ owner per consumer, and the triggers. The numbers — `instances: 1`,
|
|||
`max_connections: 100`, `1Gi`, `retentionPolicy: 30d` — are `rapp-postgres`'s
|
||||
cluster CR and its declaration to make.
|
||||
|
||||
## The P-axis note — why two entries are `null`
|
||||
## The P-axis note — why two entries are `n/a`
|
||||
|
||||
The P ladder describes **which substrate holds a tenant's data**, from the
|
||||
point of view of the workload whose data it is. `apps-pg` and `forgejo-db` are
|
||||
|
|
@ -173,14 +177,18 @@ well-formed answer: `apps-pg` *provides* P1 to each of its consumers, and
|
|||
`forgejo-db` *provides* P2 to forgejo, but neither *is* at a level.
|
||||
|
||||
Declaring `P: 1` for `apps-pg` would read as though the cluster had a
|
||||
placement, and a later reader would use it to argue about isolation. `null`
|
||||
plus the provider statement is the accurate form. This is routed as a finding.
|
||||
placement, and a later reader would use it to argue about isolation. `n/a`
|
||||
plus the provider statement is the accurate canonical form.
|
||||
|
||||
## Finding routed to `net-kingdom`
|
||||
## Finding adopted by `net-kingdom`
|
||||
|
||||
**The five ladders describe a consumer of storage. They do not describe a
|
||||
**The six ladders describe a consumer of storage. They do not describe a
|
||||
provider of it.**
|
||||
|
||||
**Resolved in draft-7 and formalised in draft-8.** Decision 5.5 adopted the
|
||||
provider declaration; the draft-8 schema standardises `available`, `maximum`,
|
||||
conditions and evidence per axis. The canonical form is now in `tenancy.yaml`.
|
||||
|
||||
**Narrowed on re-reading, 2026-08-17.** An earlier version of this finding
|
||||
claimed the framework had no way to say "this zero is structural, not weak".
|
||||
That was wrong: §6 says exactly that, and §13.1a supplies the mechanism —
|
||||
|
|
@ -208,15 +216,16 @@ rather than a tenant one. A reader scanning a table of E values would rank
|
|||
OpenBao below a service doing per-query filtering in application code, which
|
||||
inverts the actual security position.
|
||||
|
||||
**Proposed amendment, offered as the first correction from review rather than
|
||||
**Proposed amendment at draft-5, offered as the first correction from review rather than
|
||||
from research (§Status observes that every correction so far came from research
|
||||
or relocation):** add a **provider declaration** beside the posture vector — a
|
||||
platform service states, per axis, the maximum level it makes reachable and
|
||||
what the consumer must do to reach it. For `apps-pg` that is: E4 unreachable
|
||||
(shared credential per consumer, no per-tenant credential), E3 reachable once
|
||||
the GUC contract exists, R2 blocked on a backup target. That is the sentence a
|
||||
consumer actually needs, and no arrangement of the five consumer ladders
|
||||
produces it.
|
||||
consumer actually needs, and no arrangement of the consumer ladders produces
|
||||
it. Draft-8 adopted and standardised that amendment; the root declaration is
|
||||
now authoritative.
|
||||
|
||||
This is offered as an addition, not a replacement. The consumer vector is
|
||||
right for consumers, and this repo is not asking for the ladders to be
|
||||
|
|
|
|||
14
helm/apps-pg-2-backup.yaml
Normal file
14
helm/apps-pg-2-backup.yaml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
apiVersion: postgresql.cnpg.io/v1
|
||||
kind: ScheduledBackup
|
||||
metadata:
|
||||
name: apps-pg-2-daily
|
||||
namespace: databases
|
||||
labels:
|
||||
cnpg.io/cluster: apps-pg-2
|
||||
spec:
|
||||
schedule: "0 15 2 * * *"
|
||||
backupOwnerReference: self
|
||||
cluster:
|
||||
name: apps-pg-2
|
||||
immediate: true
|
||||
method: barmanObjectStore
|
||||
45
helm/apps-pg-2-cluster.yaml
Normal file
45
helm/apps-pg-2-cluster.yaml
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# Provisionable overflow cell for the apps-pg three-consumer ceiling.
|
||||
# Do not apply while empty; see docs/apps-pg.md.
|
||||
apiVersion: postgresql.cnpg.io/v1
|
||||
kind: Cluster
|
||||
metadata:
|
||||
name: apps-pg-2
|
||||
namespace: databases
|
||||
labels:
|
||||
app.kubernetes.io/name: apps-pg-2
|
||||
app.kubernetes.io/component: database
|
||||
app.kubernetes.io/managed-by: manual
|
||||
railiance.io/layer: s3-platform
|
||||
railiance.io/role: shared-apps-database
|
||||
spec:
|
||||
instances: 1
|
||||
imageName: ghcr.io/cloudnative-pg/postgresql:16
|
||||
resources:
|
||||
requests: {cpu: 100m, memory: 256Mi}
|
||||
limits: {cpu: "1", memory: 1Gi}
|
||||
storage:
|
||||
size: 10Gi
|
||||
bootstrap:
|
||||
initdb:
|
||||
database: apps_meta
|
||||
owner: apps_admin
|
||||
secret:
|
||||
name: apps-pg-2-credentials
|
||||
postgresql:
|
||||
parameters:
|
||||
max_connections: "100"
|
||||
log_connections: "on"
|
||||
log_disconnections: "on"
|
||||
log_lock_waits: "on"
|
||||
log_min_duration_statement: "1000"
|
||||
track_io_timing: "on"
|
||||
backup:
|
||||
retentionPolicy: "30d"
|
||||
barmanObjectStore:
|
||||
destinationPath: s3://railiance-platform-pg-backup/apps-pg-2/
|
||||
endpointURL: https://s3.nl-ams.scw.cloud
|
||||
s3Credentials:
|
||||
accessKeyId: {name: platform-pg-backup-s3, key: ACCESS_KEY_ID}
|
||||
secretAccessKey: {name: platform-pg-backup-s3, key: ACCESS_SECRET_KEY}
|
||||
wal: {compression: gzip, maxParallel: 2}
|
||||
data: {compression: gzip, jobs: 2}
|
||||
54
helm/apps-pg-2-networkpolicies.yaml
Normal file
54
helm/apps-pg-2-networkpolicies.yaml
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-egress-kube-api-apps-pg-2
|
||||
namespace: databases
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels: {cnpg.io/cluster: apps-pg-2}
|
||||
policyTypes: [Egress]
|
||||
egress:
|
||||
- ports:
|
||||
- {port: 443, protocol: TCP}
|
||||
- {port: 6443, protocol: TCP}
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels: {kubernetes.io/metadata.name: kube-system}
|
||||
ports:
|
||||
- {port: 53, protocol: UDP}
|
||||
- {port: 53, protocol: TCP}
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-ingress-from-cnpg-operator-apps-pg-2
|
||||
namespace: databases
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels: {cnpg.io/cluster: apps-pg-2}
|
||||
policyTypes: [Ingress]
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels: {kubernetes.io/metadata.name: cnpg-system}
|
||||
ports:
|
||||
- {port: 5432, protocol: TCP}
|
||||
- {port: 8000, protocol: TCP}
|
||||
- {port: 9187, protocol: TCP}
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-ingress-from-app-namespaces-apps-pg-2
|
||||
namespace: databases
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels: {cnpg.io/cluster: apps-pg-2}
|
||||
policyTypes: [Ingress]
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels: {railiance.io/postgres-client: apps-pg-2}
|
||||
podSelector: {}
|
||||
ports:
|
||||
- {port: 5432, protocol: TCP}
|
||||
14
helm/apps-pg-backup.yaml
Normal file
14
helm/apps-pg-backup.yaml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
apiVersion: postgresql.cnpg.io/v1
|
||||
kind: ScheduledBackup
|
||||
metadata:
|
||||
name: apps-pg-daily
|
||||
namespace: databases
|
||||
labels:
|
||||
cnpg.io/cluster: apps-pg
|
||||
spec:
|
||||
schedule: "0 15 2 * * *"
|
||||
backupOwnerReference: self
|
||||
cluster:
|
||||
name: apps-pg
|
||||
immediate: true
|
||||
method: barmanObjectStore
|
||||
|
|
@ -26,6 +26,13 @@ metadata:
|
|||
spec:
|
||||
instances: 1 # bump to 3 when node RAM > 8GB
|
||||
imageName: ghcr.io/cloudnative-pg/postgresql:16
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: "1"
|
||||
memory: 1Gi
|
||||
storage:
|
||||
size: 10Gi
|
||||
bootstrap:
|
||||
|
|
@ -43,14 +50,41 @@ spec:
|
|||
- name: vergabe # RAILIANCE-WP-0002 T04 (vergabe-teilnahme)
|
||||
ensure: present
|
||||
login: true
|
||||
connectionLimit: 20
|
||||
passwordSecret:
|
||||
name: vergabe-app-credentials
|
||||
- name: coulomb_social # CSOC-WP-0002 (coulomb-social)
|
||||
|
||||
ensure: present
|
||||
login: true
|
||||
connectionLimit: 20
|
||||
passwordSecret:
|
||||
name: coulomb-social-app-credentials
|
||||
postgresql:
|
||||
parameters:
|
||||
max_connections: "100"
|
||||
log_connections: "on"
|
||||
log_disconnections: "on"
|
||||
log_lock_waits: "on"
|
||||
log_min_duration_statement: "1000"
|
||||
track_io_timing: "on"
|
||||
backup:
|
||||
retentionPolicy: "30d"
|
||||
barmanObjectStore:
|
||||
destinationPath: s3://railiance-platform-pg-backup/apps-pg/
|
||||
endpointURL: https://s3.nl-ams.scw.cloud
|
||||
s3Credentials:
|
||||
accessKeyId:
|
||||
name: platform-pg-backup-s3
|
||||
key: ACCESS_KEY_ID
|
||||
secretAccessKey:
|
||||
name: platform-pg-backup-s3
|
||||
key: ACCESS_SECRET_KEY
|
||||
wal:
|
||||
compression: gzip
|
||||
maxParallel: 2
|
||||
data:
|
||||
compression: gzip
|
||||
jobs: 2
|
||||
# HA replica + connection pooler are deferred (RAILIANCE-WP-0003 Notes):
|
||||
# services:
|
||||
# additional:
|
||||
|
|
|
|||
7
helm/apps-pg-consumer-controls.sql
Normal file
7
helm/apps-pg-consumer-controls.sql
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
-- Idempotent controls not expressible in the installed CNPG managed.roles CRD.
|
||||
-- Apply as the controlled apps-pg cluster administrator only after notifying
|
||||
-- both consumers of the published limits in docs/s3-consumer-interfaces.md.
|
||||
ALTER ROLE vergabe SET statement_timeout = '15s';
|
||||
ALTER ROLE vergabe SET idle_in_transaction_session_timeout = '15s';
|
||||
ALTER ROLE coulomb_social SET statement_timeout = '15s';
|
||||
ALTER ROLE coulomb_social SET idle_in_transaction_session_timeout = '15s';
|
||||
|
|
@ -22,8 +22,19 @@ spec:
|
|||
- Egress
|
||||
egress:
|
||||
- ports:
|
||||
- port: 443
|
||||
protocol: TCP
|
||||
- port: 6443
|
||||
protocol: TCP
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
ports:
|
||||
- port: 53
|
||||
protocol: UDP
|
||||
- port: 53
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
|
|
|
|||
126
tenancy.yaml
Normal file
126
tenancy.yaml
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
# Canonical vector set for services owned or held in custody here.
|
||||
# Human reasoning remains in docs/tenancy-posture.md.
|
||||
schema_version: "0.1"
|
||||
framework: netkingdom-tenancy-posture
|
||||
services:
|
||||
- service: apps-pg
|
||||
role: relational-storage-provider
|
||||
tenancy:
|
||||
current: { I: 0, A: 0, E: 0, P: "n/a", R: 0, V: 0 }
|
||||
target: { I: 0, A: 0, E: 0, P: "n/a", R: 2, V: 1 }
|
||||
reviewed: "2026-08-17"
|
||||
review_due: "2027-02-17"
|
||||
service_class: interactive
|
||||
permanent: [I, A, E, P]
|
||||
reason:
|
||||
I: "The storage provider receives credentials, not tenant claims."
|
||||
A: "No tenant context is bound at the database provider."
|
||||
E: "Tenant enforcement belongs to each consuming application."
|
||||
P: "apps-pg provides P1; a substrate is not itself at a P level."
|
||||
R: "A backup target and 30-day retention are declared in source and
|
||||
not yet applied; no successful backup or restore exists. R stays 0
|
||||
until evidenced, per §13.1 — declared configuration is not an
|
||||
artifact."
|
||||
V: "No recovery exercise or failover evidence exists."
|
||||
gap:
|
||||
R: "RPF-WP-0019 establishes a backup target and declared horizon."
|
||||
V: "Exercise recovery after the backup target is established."
|
||||
provider:
|
||||
capability: data.relational
|
||||
profile: s5-application-databases
|
||||
axes:
|
||||
E:
|
||||
available: 2
|
||||
maximum: 3
|
||||
conditions:
|
||||
- "E2 is entirely consumer-side."
|
||||
- "E3 requires adoption of the rapp-postgres GUC contract."
|
||||
- "E4 is unreachable on a shared cluster without per-tenant credentials."
|
||||
P:
|
||||
available: 1
|
||||
maximum: 2
|
||||
conditions:
|
||||
- "P2 requires a new dedicated cluster for the consumer."
|
||||
R:
|
||||
available: 0
|
||||
maximum: 2
|
||||
conditions:
|
||||
- "A governed backup target must exist before R2 is available."
|
||||
V:
|
||||
available: 0
|
||||
maximum: 1
|
||||
conditions:
|
||||
- "Single-node recovery must be documented and exercised."
|
||||
|
||||
- service: forgejo-db
|
||||
role: relational-storage-provider
|
||||
tenancy:
|
||||
current: { I: 0, A: 0, E: 0, P: "n/a", R: 0, V: 0 }
|
||||
target: { I: 0, A: 0, E: 0, P: "n/a", R: 2, V: 1 }
|
||||
reviewed: "2026-08-17"
|
||||
review_due: "2027-02-17"
|
||||
service_class: interactive
|
||||
permanent: [I, A, E, P]
|
||||
reason:
|
||||
I: "The database receives a Forgejo credential, not tenant claims."
|
||||
A: "No NetKingdom tenant context is bound at this provider."
|
||||
E: "Tenant enforcement, if any, belongs to Forgejo."
|
||||
P: "forgejo-db provides P2 to Forgejo; it is not itself at P2."
|
||||
R: "A backup procedure exists, but no declared retention or erasure horizon does."
|
||||
V: "No exercised recovery objective or failover evidence exists."
|
||||
gap:
|
||||
R: "Declare the backup window and publish the effective erasure horizon."
|
||||
V: "Exercise recovery and record the interruption."
|
||||
provider:
|
||||
capability: data.relational
|
||||
profile: dedicated-forgejo-database
|
||||
axes:
|
||||
P:
|
||||
available: 2
|
||||
maximum: 2
|
||||
R:
|
||||
available: 0
|
||||
maximum: 2
|
||||
conditions:
|
||||
- "Declare and evidence retention on the existing backup path."
|
||||
V:
|
||||
available: 0
|
||||
maximum: 1
|
||||
conditions:
|
||||
- "Single-node recovery must be exercised."
|
||||
|
||||
- service: openbao
|
||||
role: credential-provider
|
||||
tenancy:
|
||||
current: { I: 0, A: 0, E: 0, P: 0, R: 0, V: 0 }
|
||||
target: { I: 0, A: 0, E: 0, P: 0, R: 1, V: 1 }
|
||||
reviewed: "2026-08-17"
|
||||
review_due: "2027-02-17"
|
||||
service_class: latency-critical
|
||||
permanent: [I, A, E, P]
|
||||
reason:
|
||||
I: "OpenBao authenticates workloads and does not resolve NetKingdom tenants."
|
||||
A: "No tenant context is carried or bound."
|
||||
E: "OpenBao holds workload secrets, not tenant-keyed records."
|
||||
P: "One instance is shared by every credential consumer."
|
||||
R: "No retention position covers audit output or KV version history."
|
||||
V: "Node recovery requires operator unseal; no recovery objective is evidenced."
|
||||
gap:
|
||||
R: "Declare the platform default for audit output and KV history."
|
||||
V: "Exercise the complete restart and unseal path against a recovery objective."
|
||||
provider:
|
||||
capability: credential.vending
|
||||
axes:
|
||||
E:
|
||||
available: 0
|
||||
maximum: 4
|
||||
conditions:
|
||||
- "Per-tenant paths, roles and credentials are required for tenant-level E4."
|
||||
- "Current evidence proves consumer-boundary scoping, not tenant-boundary scoping."
|
||||
evidence:
|
||||
- "scripts/openbao-verify-token-grants.py"
|
||||
V:
|
||||
available: 0
|
||||
maximum: 1
|
||||
conditions:
|
||||
- "Single-node restart and unseal recovery must be exercised."
|
||||
47
tests/test_apps_pg_capacity.py
Normal file
47
tests/test_apps_pg_capacity.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).parents[1]
|
||||
SPEC = importlib.util.spec_from_file_location(
|
||||
"verify_apps_pg_capacity", ROOT / "tools/verify_apps_pg_capacity.py"
|
||||
)
|
||||
capacity = importlib.util.module_from_spec(SPEC)
|
||||
assert SPEC.loader is not None
|
||||
sys.modules[SPEC.name] = capacity
|
||||
SPEC.loader.exec_module(capacity)
|
||||
MAX_CONSUMERS = capacity.MAX_CONSUMERS
|
||||
verify = capacity.verify
|
||||
MANIFESTS = [ROOT / "helm/apps-pg-cluster.yaml", ROOT / "helm/apps-pg-2-cluster.yaml"]
|
||||
|
||||
|
||||
def test_reviewed_apps_pg_cells_fit_capacity_and_use_distinct_backups() -> None:
|
||||
assert verify(MANIFESTS) == {"apps-pg": 2, "apps-pg-2": 0}
|
||||
|
||||
|
||||
def test_fourth_role_is_rejected(tmp_path: Path) -> None:
|
||||
primary = yaml.safe_load(MANIFESTS[0].read_text(encoding="utf-8"))
|
||||
template = deepcopy(primary["spec"]["managed"]["roles"][0])
|
||||
while len(primary["spec"]["managed"]["roles"]) <= MAX_CONSUMERS:
|
||||
role = deepcopy(template)
|
||||
role["name"] = f"consumer_{len(primary['spec']['managed']['roles'])}"
|
||||
primary["spec"]["managed"]["roles"].append(role)
|
||||
path = tmp_path / "apps-pg.yaml"
|
||||
path.write_text(yaml.safe_dump(primary), encoding="utf-8")
|
||||
with pytest.raises(ValueError, match="3-consumer ceiling"):
|
||||
verify([path, MANIFESTS[1]])
|
||||
|
||||
|
||||
def test_unbounded_role_is_rejected(tmp_path: Path) -> None:
|
||||
primary = yaml.safe_load(MANIFESTS[0].read_text(encoding="utf-8"))
|
||||
del primary["spec"]["managed"]["roles"][0]["connectionLimit"]
|
||||
path = tmp_path / "apps-pg.yaml"
|
||||
path.write_text(yaml.safe_dump(primary), encoding="utf-8")
|
||||
with pytest.raises(ValueError, match="connectionLimit"):
|
||||
verify([path, MANIFESTS[1]])
|
||||
84
tools/verify_apps_pg_capacity.py
Normal file
84
tools/verify_apps_pg_capacity.py
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
MAX_CONSUMERS = 3
|
||||
MIN_RESERVED_CONNECTIONS = 40
|
||||
REVIEWED_CELLS = ("apps-pg", "apps-pg-2")
|
||||
|
||||
|
||||
def verify(paths: list[Path]) -> dict[str, int]:
|
||||
cells: dict[str, dict] = {}
|
||||
role_owner: dict[str, str] = {}
|
||||
prefixes: dict[str, str] = {}
|
||||
for path in paths:
|
||||
document = yaml.safe_load(path.read_text(encoding="utf-8"))
|
||||
if document.get("kind") != "Cluster":
|
||||
raise ValueError(f"{path}: expected a CNPG Cluster")
|
||||
name = document["metadata"]["name"]
|
||||
if name not in REVIEWED_CELLS:
|
||||
raise ValueError(f"{path}: {name!r} is not a reviewed apps-pg cell")
|
||||
cells[name] = document
|
||||
missing = set(REVIEWED_CELLS) - cells.keys()
|
||||
if missing:
|
||||
raise ValueError(f"missing reviewed overflow desired state: {sorted(missing)}")
|
||||
|
||||
counts: dict[str, int] = {}
|
||||
for name, document in cells.items():
|
||||
spec = document["spec"]
|
||||
roles = spec.get("managed", {}).get("roles", [])
|
||||
if len(roles) > MAX_CONSUMERS:
|
||||
raise ValueError(
|
||||
f"{name}: {len(roles)} roles exceeds the {MAX_CONSUMERS}-consumer ceiling"
|
||||
)
|
||||
max_connections = int(spec["postgresql"]["parameters"]["max_connections"])
|
||||
allocated = 0
|
||||
for role in roles:
|
||||
role_name = role["name"]
|
||||
if role_name in role_owner:
|
||||
raise ValueError(
|
||||
f"role {role_name!r} appears in both {role_owner[role_name]} and {name}"
|
||||
)
|
||||
role_owner[role_name] = name
|
||||
limit = role.get("connectionLimit")
|
||||
if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1:
|
||||
raise ValueError(f"{name}/{role_name}: positive connectionLimit is required")
|
||||
allocated += limit
|
||||
if allocated + MIN_RESERVED_CONNECTIONS > max_connections:
|
||||
raise ValueError(
|
||||
f"{name}: {allocated} consumer connections leave fewer than "
|
||||
f"{MIN_RESERVED_CONNECTIONS} reserved connections"
|
||||
)
|
||||
resources = spec.get("resources", {})
|
||||
if not resources.get("requests") or not resources.get("limits"):
|
||||
raise ValueError(f"{name}: pod requests and limits are required")
|
||||
prefix = spec["backup"]["barmanObjectStore"]["destinationPath"]
|
||||
if prefix in prefixes:
|
||||
raise ValueError(f"{name}: backup prefix is shared with {prefixes[prefix]}")
|
||||
prefixes[prefix] = name
|
||||
counts[name] = len(roles)
|
||||
return counts
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Verify apps-pg cell admission controls")
|
||||
parser.add_argument("manifests", nargs="+", type=Path)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
counts = verify(args.manifests)
|
||||
except (KeyError, TypeError, ValueError, yaml.YAMLError) as error:
|
||||
parser.error(str(error))
|
||||
print(
|
||||
"apps-pg capacity verified: "
|
||||
+ ", ".join(f"{name}={counts[name]}/{MAX_CONSUMERS}" for name in REVIEWED_CELLS)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -4,11 +4,11 @@ type: workplan
|
|||
title: "Align S3 with the estate policy surface (Tenancy Posture + policy-nexus)"
|
||||
domain: financials
|
||||
repo: railiance-platform
|
||||
status: active
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: railiance
|
||||
created: "2026-08-17"
|
||||
updated: "2026-08-17"
|
||||
updated: "2026-08-18"
|
||||
related:
|
||||
- POLICY-NEXUS-WP-0001
|
||||
- TEN-WP-0009
|
||||
|
|
@ -297,3 +297,37 @@ this workplan routes the finding and does not build the ingress.
|
|||
**A declaration that goes stale the day it is written.** Mitigation is the
|
||||
`reviewed:` date on the vector set and the review interval in the ADR
|
||||
template — the same currency mechanism `policy-nexus` T05 will read.
|
||||
|
||||
## Closed 2026-08-18
|
||||
|
||||
All seven tasks done. What the workplan produced, and what it changed
|
||||
elsewhere:
|
||||
|
||||
- `docs/tenancy-posture.md` + `tenancy.yaml` — the vector set, machine-readable
|
||||
with the human reasoning kept beside it.
|
||||
- `docs/placement-policy.md` — placement rule, owner per workload, triggers,
|
||||
retention floor and ceiling.
|
||||
- `docs/s3-consumer-interfaces.md` `1.1.0` — quota disclosure.
|
||||
- `docs/adr/` — created from nothing; `ADR-0001`, `ADR-0002`, `ADR-0003`.
|
||||
|
||||
**Two findings were adopted upstream.** The provider-declaration proposal (F4,
|
||||
narrowed) is in the framework and its canonical form is the `provider:` block
|
||||
in `tenancy.yaml`. `adaptive-pricing` declined the standing co-signature and
|
||||
supplied a stronger replacement — typed tier minima joined at tier definition —
|
||||
which draft-8 adopted; `ADR-0002` records the outcome as single ownership plus
|
||||
a mandatory typed constraint join, not as an absent signature.
|
||||
|
||||
**Three corrections were issued against our own output**, all in the same
|
||||
direction — claiming levels we could not evidence. `openbao A: 2` retracted to
|
||||
`A: 0`; the provider finding narrowed once §6's `flex-auth` precedent was read;
|
||||
the §11 summary to `adaptive-pricing` corrected, since §11.2 keeps marketing
|
||||
vocabulary free and only the *cannot-reach* claim is restricted, to E4. All
|
||||
three are recorded in the documents rather than edited away.
|
||||
|
||||
**What this workplan deliberately did not do:** fix anything. It found that
|
||||
`apps-pg` had no backup, no per-consumer controls and no isolation probes, and
|
||||
published those as visible defects. `RPF-WP-0019` closes them.
|
||||
|
||||
**Left open, not owned here:** `bao.coulomb.social` still needs confirmation
|
||||
against live reef state (`railiance-master` `ba477968`), and F1's substrate
|
||||
split is with `policy-nexus` to act on.
|
||||
|
|
|
|||
|
|
@ -4,15 +4,16 @@ type: workplan
|
|||
title: "apps-pg: backup, per-consumer controls, and the isolation probes they make possible"
|
||||
domain: financials
|
||||
repo: railiance-platform
|
||||
status: proposed
|
||||
status: active
|
||||
owner: codex
|
||||
topic_slug: railiance
|
||||
created: "2026-08-17"
|
||||
updated: "2026-08-17"
|
||||
updated: "2026-08-18"
|
||||
related:
|
||||
- RPF-WP-0018
|
||||
origin: residual
|
||||
origin_ref: RPF-WP-0018
|
||||
state_hub_workstream_id: "160e226d-27b0-4c94-9e99-331f4354dd12"
|
||||
---
|
||||
|
||||
# RPF-WP-0019 — apps-pg recoverability and per-consumer controls
|
||||
|
|
@ -65,12 +66,43 @@ the `platform-pg-backup-s3` credential lane — the same handoff
|
|||
`make postgres-backup-deploy` waits on. Check whether that is now live before
|
||||
assuming this is blocked.
|
||||
|
||||
## Status 2026-08-18 — repository-complete, live-blocked
|
||||
|
||||
Everything this repo can do without touching the cluster is done and
|
||||
committed. What remains on T01, T02 and T04 is a single operator window
|
||||
against a live shared rail, in this order:
|
||||
|
||||
1. `make apps-pg-deploy` — Cluster reconcile: role connection limits,
|
||||
Burstable requests/limits, explicit aggregate parameters.
|
||||
2. Apply `helm/apps-pg-consumer-controls.sql` — the two 15s role timeouts.
|
||||
Idempotent; CNPG 1.28 has no managed-role settings field, so this is
|
||||
operator SQL by necessity, not by preference.
|
||||
3. `make apps-pg-backup-deploy` — the ScheduledBackup, once the governed
|
||||
Secret is confirmed live.
|
||||
4. Capture `LastBackupSucceeded=True` and a scratch restore. **Until both
|
||||
exist, `apps-pg` R stays 0** — declared configuration is not a §13
|
||||
artifact, and this workplan exists because that distinction was missed once
|
||||
already.
|
||||
5. T04's probes, in an announced window, after 1–3 have settled.
|
||||
|
||||
**Blocker as of this session:** the cluster is unreachable from the
|
||||
workstation — `kubectl` returns `Unauthorized` against
|
||||
`config-hosteurope`. Credentials or the tunnel need attention before any of
|
||||
the above runs. This is a session-local access problem, not a defect in the
|
||||
manifests: `make apps-pg-verify-capacity` passes and the capacity tests are
|
||||
green.
|
||||
|
||||
**Do not treat the rollout as evidence.** T04's P1 claim and the R-axis both
|
||||
need artifacts produced *after* application, and `docs/placement-policy.md`
|
||||
§3.1 and `tenancy.yaml` should be updated only then.
|
||||
|
||||
## Tasks
|
||||
|
||||
```task
|
||||
id: RPF-WP-0019-T01
|
||||
status: todo
|
||||
status: progress
|
||||
priority: high
|
||||
state_hub_task_id: "0f5175c7-bde3-46a0-87a8-b50b5eeac32c"
|
||||
```
|
||||
**Establish a backup target for `apps-pg`.** Confirm the state of the
|
||||
`resource-control` bucket and the `platform-pg-backup-s3` OpenBao Secret; if
|
||||
|
|
@ -78,10 +110,17 @@ live, configure `barmanObjectStore` and a `retentionPolicy` on the cluster. If
|
|||
not live, record the dependency and say so — do not leave the absence
|
||||
undocumented a second time.
|
||||
|
||||
2026-08-18 repository readiness: the governed Secret exists live and reviewed
|
||||
desired state now carries a distinct `apps-pg/` prefix, 30-day retention,
|
||||
continuous WAL and a daily 02:15 backup. NetworkPolicy admits the DNS/HTTPS
|
||||
path archival needs. The ScheduledBackup is not applied and no successful
|
||||
backup/restore evidence exists, so T01 remains progress.
|
||||
|
||||
```task
|
||||
id: RPF-WP-0019-T02
|
||||
status: todo
|
||||
status: progress
|
||||
priority: high
|
||||
state_hub_task_id: "c5b0b2ac-b9f5-42fb-8e75-2fac2ab1e852"
|
||||
```
|
||||
**Declare and enforce per-consumer controls.** Per-consumer connection
|
||||
allowance, `statement_timeout`, `idle_in_transaction_session_timeout`, and
|
||||
|
|
@ -90,20 +129,34 @@ every value in `docs/s3-consumer-interfaces.md` before it takes effect —
|
|||
§10.2 is a disclosure rule, and applying a timeout consumers learn about by
|
||||
hitting it would breach the rule while implementing it.
|
||||
|
||||
2026-08-18 repository readiness: both roles declare a 20-connection limit,
|
||||
the pod has Burstable requests/limits, aggregate/logging parameters are
|
||||
explicit, and controlled operator SQL sets both 15s role timeouts. Every value
|
||||
was published in `docs/s3-consumer-interfaces.md` before application. Live SQL
|
||||
and Cluster reconciliation remain an operator window, so T02 remains progress.
|
||||
|
||||
```task
|
||||
id: RPF-WP-0019-T03
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "736cbc11-1992-4f1b-9ff0-cb4622ff39a5"
|
||||
```
|
||||
**Declare the ceiling and overflow target.** Owed under this repo's own
|
||||
Rule P-4.1 before `apps-pg`'s third consumer; it is at two. Name the binding
|
||||
resource per Rule P-4.2 — memory or connections — and a named overflow
|
||||
substrate per P-4.3.
|
||||
|
||||
Completed 2026-08-18. The declared ceiling is three, memory is the binding
|
||||
constraint, and `apps-pg-2` is a named, source-provisionable overflow cell with
|
||||
a distinct credential and backup prefix. `make apps-pg-verify-capacity`
|
||||
rejects a fourth consumer per cell and unbounded/duplicate roles. The cell
|
||||
intentionally remains absent until a fourth consumer is approved.
|
||||
|
||||
```task
|
||||
id: RPF-WP-0019-T04
|
||||
status: todo
|
||||
status: wait
|
||||
priority: medium
|
||||
state_hub_task_id: "2fccdd26-c9c6-43cb-b069-27ed5668afac"
|
||||
```
|
||||
**Isolation probes, after T02.** Consumer-boundary probes on the
|
||||
`rapp-postgres` model, then the §13 noisy-neighbour artifact: per-consumer
|
||||
|
|
@ -112,6 +165,10 @@ judged against each consumer's declared service class. Update
|
|||
`docs/placement-policy.md` §3.1 and `docs/tenancy-posture.md` when the P1
|
||||
claims become evidenced.
|
||||
|
||||
Waiting on T02 live application and an announced probe window. No saturation
|
||||
or destructive recovery experiment is run against the shared production rail
|
||||
as part of repository preparation.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- `apps-pg` only. `platform-pg`'s equivalents are `rapp-postgres`'s.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue