Land RPF-WP-0039 third apps-pg consumer source.
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

Admit vergabe_demo_company as the third declared consumer, track
the bounded provisioner and scoped controls, and refresh admission
occupancy to 3/3. Overflow remains apps-pg-2 before any fourth.

Assistant: grok
Assistant-Session: 01a0a23b-3bf0-7341-b4e5-9dc05f72573a
This commit is contained in:
codex 2026-09-15 01:29:38 +02:00
parent 0178ec3c6d
commit f05ef49c69
18 changed files with 446 additions and 19 deletions

2
.gitignore vendored
View file

@ -11,6 +11,8 @@ helm/*.yaml
!helm/*-databases.yaml !helm/*-databases.yaml
!helm/*-backup.yaml !helm/*-backup.yaml
!helm/*-probe.yaml !helm/*-probe.yaml
!helm/vergabe-demo-company-role.yaml
!helm/vergabe-demo-company-database.yaml
# ArgoCD repository credentials — encrypt locally, never commit # ArgoCD repository credentials — encrypt locally, never commit
argocd/repositories/*.repository.sops.yaml argocd/repositories/*.repository.sops.yaml

View file

@ -5,7 +5,8 @@
"apps-pg": { "apps-pg": {
"consumers": [ "consumers": [
"coulomb_social", "coulomb_social",
"vergabe" "vergabe",
"vergabe_demo_company"
], ],
"ceiling": 3, "ceiling": 3,
"retention_days": 30, "retention_days": 30,
@ -14,15 +15,18 @@
"max_connections": 100, "max_connections": 100,
"service_classes": { "service_classes": {
"coulomb_social": "interactive", "coulomb_social": "interactive",
"vergabe": "interactive" "vergabe": "interactive",
"vergabe_demo_company": "interactive"
}, },
"connection_limits": { "connection_limits": {
"vergabe": 20, "vergabe": 20,
"coulomb_social": 20 "coulomb_social": 20,
"vergabe_demo_company": 20
}, },
"owners": { "owners": {
"coulomb_social": "coulomb-social", "coulomb_social": "coulomb-social",
"vergabe": "vergabe-teilnahme" "vergabe": "vergabe-teilnahme",
"vergabe_demo_company": "vergabe-demo-company"
} }
}, },
"apps-pg-2": { "apps-pg-2": {
@ -63,6 +67,7 @@
}, },
"platform-pg-2": { "platform-pg-2": {
"consumers": [ "consumers": [
"canned-prompts",
"sbom-nexus" "sbom-nexus"
], ],
"ceiling": 4, "ceiling": 4,
@ -71,23 +76,26 @@
"memory_limit": "1Gi", "memory_limit": "1Gi",
"max_connections": 100, "max_connections": 100,
"service_classes": { "service_classes": {
"canned-prompts": "interactive",
"sbom-nexus": "interactive" "sbom-nexus": "interactive"
}, },
"owners": { "owners": {
"canned-prompts": "canned-prompts",
"sbom-nexus": "sbom-nexus" "sbom-nexus": "sbom-nexus"
} }
} }
}, },
"sources": { "sources": {
"railiance-platform/helm/apps-pg-cluster.yaml": "9885c81338e1c3bdb0558839552315720068c5d355819e4a1320fbd677d0ffcf", "railiance-platform/helm/apps-pg-cluster.yaml": "81e594601238f9c39bead395ffe5fde41e72377a5cdc704b63e40ed201d2b6f5",
"railiance-platform/helm/apps-pg-2-cluster.yaml": "e9e615a0b5de8597a5501b2d8740a763ba3622cbebb765ce651236c2175de942", "railiance-platform/helm/apps-pg-2-cluster.yaml": "e9e615a0b5de8597a5501b2d8740a763ba3622cbebb765ce651236c2175de942",
"rapp-postgres/consumers/audit-core.yaml": "a2db53f901df056546b2c028f287f8fc50486e6e6cd81026b9e926b6b64272b3", "rapp-postgres/consumers/audit-core.yaml": "a2db53f901df056546b2c028f287f8fc50486e6e6cd81026b9e926b6b64272b3",
"rapp-postgres/consumers/canned-prompts.yaml": "97d4b439cec7e788b7c1fa2c7181c2606ed098dcf37a38c1f73f7393e6046429",
"rapp-postgres/consumers/core-hub.yaml": "8fe3e8b4488548b2f5e01f1d46edd9fd5fe4f0e4f65a3821f172adbfb5ae1d84", "rapp-postgres/consumers/core-hub.yaml": "8fe3e8b4488548b2f5e01f1d46edd9fd5fe4f0e4f65a3821f172adbfb5ae1d84",
"rapp-postgres/consumers/isolation-probe.yaml": "8ea24d3bd04ab683b47ceff2586932ea4fdb0d45c49896167bb5b352a67a5cfb", "rapp-postgres/consumers/isolation-probe.yaml": "8ea24d3bd04ab683b47ceff2586932ea4fdb0d45c49896167bb5b352a67a5cfb",
"rapp-postgres/consumers/sbom-nexus.yaml": "0b21d05fe7b9d21cbc9eb761f455329739ceb71f8dd3d1200971426962c11236", "rapp-postgres/consumers/sbom-nexus.yaml": "0b21d05fe7b9d21cbc9eb761f455329739ceb71f8dd3d1200971426962c11236",
"rapp-postgres/consumers/tenant-engine.yaml": "92f36d587f001c86ff401aedd25cb49fa171d49e000b0a6ab551e182e5c11448", "rapp-postgres/consumers/tenant-engine.yaml": "92f36d587f001c86ff401aedd25cb49fa171d49e000b0a6ab551e182e5c11448",
"railiance-platform/tools/verify_apps_pg_capacity.py": "b571ec63b0a7501dabba37a84af9d2cfcb7737ed891a7ea2ab05c54010b1893e", "railiance-platform/tools/verify_apps_pg_capacity.py": "b571ec63b0a7501dabba37a84af9d2cfcb7737ed891a7ea2ab05c54010b1893e",
"railiance-platform/assurance/placement-owners.json": "b9a52f755552dc4bb83536dca8aadca691a4f8bc47089e9e1962902abcec7aa2", "railiance-platform/assurance/placement-owners.json": "e71037f868e3264d43ff5c42511558272a89926dc8dc2f7ee952e018020b4891",
"rapp-postgres/scripts/render_consumers.py": "2dcfd68f94a3d6c7e1072c4a55a497b1697eab30acaecae53865c9b1a596773a", "rapp-postgres/scripts/render_consumers.py": "2dcfd68f94a3d6c7e1072c4a55a497b1697eab30acaecae53865c9b1a596773a",
"rapp-postgres/helm/platform-pg-cluster.yaml": "461da0d43e61a4ddca29d6e98080b8b1642bd3303f3395da1b0beb28775c2c5c", "rapp-postgres/helm/platform-pg-cluster.yaml": "461da0d43e61a4ddca29d6e98080b8b1642bd3303f3395da1b0beb28775c2c5c",
"rapp-postgres/helm/platform-pg-2-cluster.yaml": "06b92a76189b1a33d1eee3f5c835a3e94269ad7b040383c04e8028c2c3fe766a" "rapp-postgres/helm/platform-pg-2-cluster.yaml": "06b92a76189b1a33d1eee3f5c835a3e94269ad7b040383c04e8028c2c3fe766a"

View file

@ -11,6 +11,12 @@
"service_class": "interactive", "service_class": "interactive",
"evidence": "docs/evidence/RPF-WP-0019-isolation-2026-08-20.md" "evidence": "docs/evidence/RPF-WP-0019-isolation-2026-08-20.md"
}, },
"vergabe_demo_company": {
"owner": "vergabe-demo-company",
"cell": "apps-pg",
"service_class": "interactive",
"evidence": "workplans/RPF-WP-0039-vergabe-demo-company-database.md"
},
"audit-core": { "audit-core": {
"owner": "audit-core", "owner": "audit-core",
"cell": "platform-pg", "cell": "platform-pg",

View file

@ -11,7 +11,8 @@
"operator_role": "attended platform operator; application proof by consumer owner", "operator_role": "attended platform operator; application proof by consumer owner",
"consumers": [ "consumers": [
"vergabe", "vergabe",
"coulomb_social" "coulomb_social",
"vergabe_demo_company"
], ],
"failure_domain": "single-node railiance01", "failure_domain": "single-node railiance01",
"availability": { "availability": {
@ -76,7 +77,8 @@
"package_or_consumer_owner": "rapp-postgres", "package_or_consumer_owner": "rapp-postgres",
"operator_role": "attended platform operator; application proof by consumer owner", "operator_role": "attended platform operator; application proof by consumer owner",
"consumers": [ "consumers": [
"sbom-nexus" "sbom-nexus",
"canned-prompts"
], ],
"failure_domain": "single-node railiance01", "failure_domain": "single-node railiance01",
"availability": { "availability": {

View file

@ -23,7 +23,8 @@ it as a consumer credential.
**Two reachable clusters each carry a CNPG `Cluster` named `apps-pg` in a **Two reachable clusters each carry a CNPG `Cluster` named `apps-pg` in a
namespace named `databases`.** The one this document describes is namespace named `databases`.** The one this document describes is
**railiance01** (k3s v1.35.1) — it also carries `platform-pg` and `forgejo-db`, **railiance01** (k3s v1.35.1) — it also carries `platform-pg` and `forgejo-db`,
and holds both apps-pg consumers (`vergabe_db`, `coulomb_social_db`). The other and holds the three apps-pg consumers (`vergabe_db`, `coulomb_social_db`,
`vergabe_demo_company`). The other
cluster carries `gitea-db` and only one apps-pg consumer. cluster carries `gitea-db` and only one apps-pg consumer.
Selecting the right one by kubeconfig filename is not safe: `KUBECONFIG` is an Selecting the right one by kubeconfig filename is not safe: `KUBECONFIG` is an
@ -115,6 +116,7 @@ the standard PostgreSQL, instance manager, and metrics ports.
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| vergabe-teilnahme | `vergabe` | `vergabe_db` | `vergabe-app-credentials` | `vergabe-teilnahme` | | vergabe-teilnahme | `vergabe` | `vergabe_db` | `vergabe-app-credentials` | `vergabe-teilnahme` |
| coulomb-social | `coulomb_social` | `coulomb_social_db` | `coulomb-social-app-credentials` | `coulomb-social` | | coulomb-social | `coulomb_social` | `coulomb_social_db` | `coulomb-social-app-credentials` | `coulomb-social` |
| vergabe-demo-company | `vergabe_demo_company` | `vergabe_demo_company` | `vergabe-demo-company-credentials` | `vergabe-demo-company` |
Bootstrapped 2026-08-09 on railiance01: cluster healthy; both Database CRs Bootstrapped 2026-08-09 on railiance01: cluster healthy; both Database CRs
applied; coulomb-social connectivity smoke from labeled consumer ns OK. applied; coulomb-social connectivity smoke from labeled consumer ns OK.
@ -152,3 +154,23 @@ labor, allocation drivers) is published under
isolation evidence captured on 2026-08-20 is in isolation evidence captured on 2026-08-20 is in
`docs/evidence/RPF-WP-0019-backup-restore-2026-08-20.md` and `docs/evidence/RPF-WP-0019-backup-restore-2026-08-20.md` and
`docs/evidence/RPF-WP-0019-isolation-2026-08-20.md`. `docs/evidence/RPF-WP-0019-isolation-2026-08-20.md`.
## Fresh Vergabe demo consumer — 2026-09-12
RPF-WP-0039 admits the third consumer, vergabe_demo_company, in namespace
vergabe-demo-company. Database and role both use that name; the provider Secret
is databases/vergabe-demo-company-credentials and the exact consumer runtime
bundle is vergabe-demo-company/vergabe-demo-env. The existing vergabe_db remains
separate. No bootstrap or other consumer credentials are reused.
`tools/provision-vergabe-demo.py inspect` returns metadata only. The dry-run/apply
modes require the observed cluster resourceVersion, append only the fixed third
role, and refuse conflicting existing state. Credentials are generated in memory
and delivered through captured child stdin/stdout, never arguments or files.
This is the documented interim platform-administered Kubernetes/CNPG custody
lane; it does not claim OpenBao lifecycle automation. Source declarations are
helm/vergabe-demo-company-{role,database}.yaml; scoped role/database controls are
helm/vergabe-demo-company-controls.sql. Do not apply historical consumer SQL to
accomplish this onboarding. See RAPPS-WP-0014 for live deployment and remaining
coherent recovery acceptance. The declared cell is now full at three consumers.

View file

@ -83,6 +83,7 @@ triggers. It is not the same role as the operator of the cluster.
| `forgejo` | `forgejo-db` | P2 | this repo | — | single consumer | | `forgejo` | `forgejo-db` | P2 | this repo | — | single consumer |
| `vergabe` | `apps-pg` | P1 | `vergabe-teilnahme` | this repo | 14 live boundary/control probes | | `vergabe` | `apps-pg` | P1 | `vergabe-teilnahme` | this repo | 14 live boundary/control probes |
| `coulomb_social` | `apps-pg` | P1 | `coulomb-social` | this repo | 14 live boundary/control probes | | `coulomb_social` | `apps-pg` | P1 | `coulomb-social` | this repo | 14 live boundary/control probes |
| `vergabe_demo_company` | `apps-pg` | P1 | `vergabe-demo-company` | this repo | RPF-WP-0039 provisioning and SQL isolation readback |
### 3.1 The two apps-pg P1 levels are now evidenced ### 3.1 The two apps-pg P1 levels are now evidenced
@ -110,7 +111,7 @@ artifact. Saturating `coulomb_social` bound at 20 and rejected an excess
connection while five `vergabe` queries remained available. Five-query connection while five `vergabe` queries remained available. Five-query
operator-path wall time rose from 5,285ms to 8,146ms (54.1%). operator-path wall time rose from 5,285ms to 8,146ms (54.1%).
Both consumers are `interactive`, but neither publishes a numeric database The three consumers are `interactive`, but none publishes a numeric database
latency objective, and the measurement includes five separate `kubectl exec` latency objective, and the measurement includes five separate `kubectl exec`
and `psql` startups. The artifact therefore evidences P1 boundary and continued and `psql` startups. The artifact therefore evidences P1 boundary and continued
availability, not a latency SLO or CPU/memory fairness. That limit is part of availability, not a latency SLO or CPU/memory fairness. That limit is part of
@ -148,11 +149,11 @@ one". "Provision a second cluster" is an answer; "revisit at the time" is not.
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `platform-pg` | 4 declarations (package ADR-0004) | `platform-pg-2`, deployed | 4/4 including probe and Core Hub; fifth refused | | `platform-pg` | 4 declarations (package ADR-0004) | `platform-pg-2`, deployed | 4/4 including probe and Core Hub; fifth refused |
| `platform-pg-2` | 4 declarations (same package validator) | No third cell approved | 1/4: sbom-nexus; owner admission and restore completed 2026-08-22 | | `platform-pg-2` | 4 declarations (same package validator) | No third cell approved | 1/4: sbom-nexus; owner admission and restore completed 2026-08-22 |
| `apps-pg` | 3 consumers; 20 connections each, 1Gi memory binds first | `apps-pg-2` | 2/3; overflow must exist before a 4th | | `apps-pg` | 3 consumers; 20 connections each, 1Gi memory binds first | `apps-pg-2` | 3/3; overflow must exist before a 4th |
| `forgejo-db` | n/a — single consumer | n/a | — | | `forgejo-db` | n/a — single consumer | n/a | — |
Source and live metadata reviewed 2026-09-05: platform-pg is full; apps-pg Source and live metadata reviewed 2026-09-15: platform-pg is full; apps-pg
remains 2/3 and apps-pg-2 is still unapplied. The generated is 3/3 after RPF-WP-0039 and apps-pg-2 is still unapplied. The generated
`assurance/admission-baseline.json` binds these disclosures to the actual owner `assurance/admission-baseline.json` binds these disclosures to the actual owner
validators/declarations. `make assurance-admission` refuses source drift. validators/declarations. `make assurance-admission` refuses source drift.
Package acceptance, rather than this table, authorizes consumer provisioning. Package acceptance, rather than this table, authorizes consumer provisioning.

View file

@ -62,7 +62,7 @@ before the corresponding live rollout:
| Property | Value | Note | | Property | Value | Note |
| --- | --- | --- | | --- | --- | --- |
| `max_connections` | 100 | Explicit aggregate cap. Forty connections remain outside the declared three-consumer ceiling for operator/CNPG overhead. | | `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. | | Per-consumer connections | 20 | CNPG `managed.roles[].connectionLimit`, for all three current roles. |
| `statement_timeout` | 15s | Per role; idempotent operator SQL because CNPG 1.28 has no managed-role settings field. | | `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. | | `idle_in_transaction_session_timeout` | 15s | Per role, same enforcement path. |
| Database boundary | own database only | `CONNECT` and `TEMPORARY` are revoked from `PUBLIC`; each consumer role receives them only on its own database. | | Database boundary | own database only | `CONNECT` and `TEMPORARY` are revoked from `PUBLIC`; each consumer role receives them only on its own database. |
@ -125,7 +125,7 @@ freshness budgets are not SLAs or approval to deploy schedules. Consumers that
require stronger guarantees need a placement/recovery decision. require stronger guarantees need a placement/recovery decision.
Current source occupancy: platform-pg 4/4 including the isolation probe, Current source occupancy: platform-pg 4/4 including the isolation probe,
platform-pg-2 1/4 for sbom-nexus, apps-pg 2/3, apps-pg-2 0/3 and unapplied. platform-pg-2 2/4 for sbom-nexus and canned-prompts, apps-pg 3/3, apps-pg-2 0/3 and unapplied.
All three deployed cells declare one instance, 1Gi memory limit, 100 aggregate All three deployed cells declare one instance, 1Gi memory limit, 100 aggregate
connections and 30-day retention; live metadata matched on 2026-09-05. The connections and 30-day retention; live metadata matched on 2026-09-05. The
second platform cell's approved restore took 51 seconds on 2026-08-22; this second platform cell's approved restore took 51 seconds on 2026-08-22; this

View file

@ -92,8 +92,8 @@ between effective package retention and the actual CR. Hashes pin every
consumed source artifact; changing package code or declarations requires review consumed source artifact; changing package code or declarations requires review
before refreshing the baseline. Do not blindly regenerate it in CI. before refreshing the baseline. Do not blindly regenerate it in CI.
Source occupancy: apps-pg 2/3, apps-pg-2 0/3 (not deployed), platform-pg 4/4 Source occupancy: apps-pg 3/3, apps-pg-2 0/3 (not deployed), platform-pg 4/4
including the probe, platform-pg-2 1/4 for sbom-nexus. Tenant-engine's PostgreSQL including the probe, platform-pg-2 2/4 for sbom-nexus and canned-prompts. Tenant-engine's PostgreSQL
cutover completed 2026-08-21; its old SQLite-current claim was stale. Platform-pg-2 cutover completed 2026-08-21; its old SQLite-current claim was stale. Platform-pg-2
was accepted and restored on 2026-08-22; it is no longer merely named overflow. was accepted and restored on 2026-08-22; it is no longer merely named overflow.
Core Hub is latency-critical alongside audit-core batch work; no service-class Core Hub is latency-critical alongside audit-core batch work; no service-class

View file

@ -59,6 +59,16 @@ spec:
connectionLimit: 20 connectionLimit: 20
passwordSecret: passwordSecret:
name: coulomb-social-app-credentials name: coulomb-social-app-credentials
- name: vergabe_demo_company
ensure: present
login: true
inherit: false
superuser: false
createdb: false
createrole: false
connectionLimit: 20
passwordSecret:
name: vergabe-demo-company-credentials
postgresql: postgresql:
parameters: parameters:
max_connections: "100" max_connections: "100"

View file

@ -1,6 +1,8 @@
-- Idempotent controls not expressible in the installed CNPG managed.roles CRD. -- Idempotent controls not expressible in the installed CNPG managed.roles CRD.
-- Apply as the controlled apps-pg cluster administrator only after notifying -- Apply as the controlled apps-pg cluster administrator only after notifying
-- both consumers of the published limits in docs/s3-consumer-interfaces.md. -- bound consumers of the published limits in docs/s3-consumer-interfaces.md.
-- The third consumer is scoped separately in helm/vergabe-demo-company-controls.sql;
-- do not reuse this historical file to onboard it.
ALTER ROLE vergabe SET statement_timeout = '15s'; ALTER ROLE vergabe SET statement_timeout = '15s';
ALTER ROLE vergabe SET idle_in_transaction_session_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 statement_timeout = '15s';

View file

@ -36,3 +36,20 @@ spec:
name: coulomb_social_db name: coulomb_social_db
owner: coulomb_social owner: coulomb_social
ensure: present ensure: present
---
apiVersion: postgresql.cnpg.io/v1
kind: Database
metadata:
name: vergabe-demo-company-db # RPF-WP-0039
namespace: databases
labels:
app.kubernetes.io/name: apps-pg
app.kubernetes.io/component: database-instance
railiance.io/layer: s3-platform
railiance.io/consumer: vergabe-demo-company
spec:
cluster:
name: apps-pg
name: vergabe_demo_company
owner: vergabe_demo_company
ensure: present

View file

@ -0,0 +1,5 @@
-- Only the fresh invited demo consumer. Never touches historical vergabe_db.
ALTER ROLE vergabe_demo_company SET statement_timeout = '15s';
ALTER ROLE vergabe_demo_company SET idle_in_transaction_session_timeout = '15s';
REVOKE CONNECT, TEMPORARY ON DATABASE vergabe_demo_company FROM PUBLIC;
GRANT CONNECT, TEMPORARY ON DATABASE vergabe_demo_company TO vergabe_demo_company;

View file

@ -0,0 +1,10 @@
apiVersion: postgresql.cnpg.io/v1
kind: Database
metadata:
name: vergabe-demo-company-db
namespace: databases
spec:
cluster:
name: apps-pg
name: vergabe_demo_company
owner: vergabe_demo_company

View file

@ -0,0 +1,11 @@
# Exact managed.roles entry; append through the bounded owner helper.
name: vergabe_demo_company
ensure: present
login: true
inherit: false
superuser: false
createdb: false
createrole: false
connectionLimit: 20
passwordSecret:
name: vergabe-demo-company-credentials

View file

@ -22,7 +22,7 @@ MANIFESTS = [ROOT / "helm/apps-pg-cluster.yaml", ROOT / "helm/apps-pg-2-cluster.
def test_reviewed_apps_pg_cells_fit_capacity_and_use_distinct_backups() -> None: def test_reviewed_apps_pg_cells_fit_capacity_and_use_distinct_backups() -> None:
assert verify(MANIFESTS) == {"apps-pg": 2, "apps-pg-2": 0} assert verify(MANIFESTS) == {"apps-pg": 3, "apps-pg-2": 0}
def test_fourth_role_is_rejected(tmp_path: Path) -> None: def test_fourth_role_is_rejected(tmp_path: Path) -> None:

View file

@ -0,0 +1,87 @@
import importlib.util
from pathlib import Path
from types import SimpleNamespace
import unittest
from unittest.mock import patch
spec = importlib.util.spec_from_file_location('provision', Path(__file__).resolve().parents[1] / 'tools/provision-vergabe-demo.py')
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
class DemoProvisioningTests(unittest.TestCase):
def cluster(self):
return {'metadata': {'uid': 'synthetic', 'resourceVersion': '7'},
'spec': {'managed': {'roles': [{'name': 'vergabe', 'connectionLimit': 20},
{'name': 'coulomb_social', 'connectionLimit': 20}]}}}
def test_patch_only_appends_reviewed_third_role_with_revision_tests(self):
role, _ = m.declarations()
before = self.cluster()
result = m.role_patch(before, role)
self.assertEqual([x['op'] for x in result], ['test', 'test', 'test', 'add'])
self.assertEqual(result[-1], {'op': 'add', 'path': '/spec/managed/roles/-', 'value': role})
self.assertEqual(len(before['spec']['managed']['roles']), 2)
def test_refuses_fourth_consumer_duplicates_and_relaxed_limits(self):
role, _ = m.declarations()
for change in ['fourth', 'duplicate', 'limit']:
before = self.cluster()
roles = before['spec']['managed']['roles']
if change == 'fourth':
roles.append({'name': 'another', 'connectionLimit': 20})
elif change == 'duplicate':
roles.append(roles[0])
else:
roles[0]['connectionLimit'] = -1
with self.subTest(change=change), self.assertRaises(m.Refused):
m.role_patch(before, role)
def test_exact_owned_role_is_noop_but_conflict_refused(self):
role, _ = m.declarations()
before = self.cluster()
before['spec']['managed']['roles'].append(dict(role))
self.assertIsNone(m.role_patch(before, role))
before['spec']['managed']['roles'][-1]['superuser'] = True
with self.assertRaises(m.Refused):
m.role_patch(before, role)
def test_existing_secret_requires_owner_and_exact_keys(self):
obj = {'metadata': {'labels': dict(m.OWNER)}, 'data': {'username': 'x', 'password': 'x'}}
m.valid_owned_secret(obj, ['username', 'password'])
for changed in [dict(obj, data=dict(obj['data'], extra='x')),
dict(obj, metadata={'labels': {}}),
dict(obj, metadata={'labels': m.OWNER, 'ownerReferences': [{'uid': 'other'}]})]:
with self.assertRaises(m.Refused):
m.valid_owned_secret(changed, ['username', 'password'])
def test_sensitive_child_error_is_not_forwarded(self):
result = SimpleNamespace(returncode=1, stdout='synthetic-secret', stderr='synthetic-secret')
with patch.object(m.subprocess, 'run', return_value=result), self.assertRaises(m.Refused) as caught:
m.kube(['create', '-f', '-'], {'stringData': {'password': 'synthetic-secret'}})
self.assertEqual(str(caught.exception), 'kubernetes_operation_failed')
def test_credentials_travel_on_stdin_not_arguments(self):
result = SimpleNamespace(returncode=0, stdout='{}', stderr='')
with patch.object(m.subprocess, 'run', return_value=result) as run:
m.kube(['create', '-f', '-'], {'stringData': {'password': 'synthetic-secret'}})
args, kwargs = run.call_args
self.assertNotIn('synthetic-secret', str(args))
self.assertIn('synthetic-secret', kwargs['input'])
self.assertTrue(kwargs['capture_output'])
def test_source_declarations_match_tracked_cluster_and_database_manifests(self):
import yaml
role, database = m.declarations()
cluster = yaml.safe_load((m.ROOT / 'helm/apps-pg-cluster.yaml').read_text())
tracked = [item for item in cluster['spec']['managed']['roles'] if item['name'] == m.NAME]
self.assertEqual(tracked, [role])
documents = list(yaml.safe_load_all((m.ROOT / 'helm/apps-pg-databases.yaml').read_text()))
named = next(item for item in documents if item['metadata']['name'] == 'vergabe-demo-company-db')
self.assertEqual(named['spec']['name'], database['spec']['name'])
self.assertEqual(named['spec']['owner'], database['spec']['owner'])
self.assertEqual(named['spec']['cluster'], database['spec']['cluster'])
if __name__ == '__main__':
unittest.main()

View file

@ -0,0 +1,173 @@
#!/usr/bin/env python3
"""Bounded apps-pg demo consumer provisioning; never emits credential values."""
import argparse
import base64
import json
from pathlib import Path
import secrets
import subprocess
from urllib.parse import quote
import yaml
ROOT = Path(__file__).resolve().parents[1]
CLUSTER_UID = 'a553c742-0115-43d4-99a4-a5ca56fe0786'
NAME = 'vergabe_demo_company'
NS = 'vergabe-demo-company'
SECRET = 'vergabe-demo-company-credentials'
RUNTIME = 'vergabe-demo-env'
OWNER = {'railiance.io/provisioning-workplan': 'RPF-WP-0039'}
KUBE = ['kubectl', '--kubeconfig', '/home/worsch/.kube/config-hosteurope',
'--server', 'https://127.0.0.1:16444', '--request-timeout=20s']
class Refused(Exception):
pass
def require(condition, reason):
if not condition:
raise Refused(reason)
def kube(args, value=None, *, raw=False):
# kubectl error bodies may contain Secret material; neither stream escapes.
result = subprocess.run(KUBE + args, input=None if value is None else json.dumps(value),
capture_output=True, text=True, timeout=30)
require(result.returncode == 0, 'kubernetes_operation_failed')
if raw:
return result.stdout
return json.loads(result.stdout) if result.stdout.strip() else None
def get(kind, name, namespace):
return kube(['-n', namespace, 'get', kind, name, '--ignore-not-found', '-o', 'json'])
def metadata(obj):
return {k: obj['metadata'][k] for k in ['name', 'namespace', 'uid', 'resourceVersion']}
def declarations():
role = yaml.safe_load((ROOT / 'helm/vergabe-demo-company-role.yaml').read_text())
database = yaml.safe_load((ROOT / 'helm/vergabe-demo-company-database.yaml').read_text())
require(role == {'name': NAME, 'ensure': 'present', 'login': True, 'inherit': False,
'superuser': False, 'createdb': False, 'createrole': False,
'connectionLimit': 20, 'passwordSecret': {'name': SECRET}},
'role_declaration_changed')
require(database['metadata'] == {'name': 'vergabe-demo-company-db', 'namespace': 'databases'}
and database['spec'] == {'cluster': {'name': 'apps-pg'}, 'name': NAME, 'owner': NAME},
'database_declaration_changed')
return role, database
def valid_owned_secret(obj, keys):
require(obj['metadata'].get('labels', {}).get(next(iter(OWNER))) == next(iter(OWNER.values()))
and not obj['metadata'].get('ownerReferences') and set(obj['data']) == set(keys),
'existing_secret_is_not_exact_owned_bundle')
def secret_object(name, namespace, fields):
return {'apiVersion': 'v1', 'kind': 'Secret',
'metadata': {'name': name, 'namespace': namespace,
'labels': dict(OWNER, **{'cnpg.io/reload': 'true'})},
'type': 'kubernetes.io/basic-auth' if name == SECRET else 'Opaque', 'stringData': fields}
def role_patch(cluster, role):
roles = cluster['spec']['managed']['roles']
names = [x['name'] for x in roles]
require(len(names) == len(set(names)) and len(roles) <= 3, 'consumer_ceiling_or_duplicate')
require(all(x.get('connectionLimit') == 20 for x in roles), 'consumer_limits_changed')
if NAME in names:
require(roles[names.index(NAME)] == role, 'existing_role_differs')
return None
require(set(names) == {'vergabe', 'coulomb_social'} and len(roles) == 2,
'unexpected_consumers')
return [{'op': 'test', 'path': '/metadata/uid', 'value': cluster['metadata']['uid']},
{'op': 'test', 'path': '/metadata/resourceVersion', 'value': cluster['metadata']['resourceVersion']},
{'op': 'test', 'path': '/spec/managed/roles', 'value': roles},
{'op': 'add', 'path': '/spec/managed/roles/-', 'value': role}]
def run(mode, expected_revision):
require(kube(['get', 'namespace', 'kube-system', '-o', 'json'])['metadata']['uid'] == CLUSTER_UID,
'wrong_cluster')
namespace = get('namespace', NS, NS)
require(namespace and namespace['metadata']['labels'].get('railiance.io/postgres-client') == 'apps-pg',
'namespace_not_bound')
role, database = declarations()
cluster = get('cluster', 'apps-pg', 'databases')
patch = role_patch(cluster, role)
db = get('database', 'vergabe-demo-company-db', 'databases')
require(not db or all(db['spec'].get(k) == v for k, v in database['spec'].items()),
'database_resource_conflict')
primary = cluster['status']['currentPrimary']
require(primary.startswith('apps-pg-'), 'unexpected_primary')
sql = ("SELECT 'role' FROM pg_roles WHERE rolname='vergabe_demo_company'; "
"SELECT 'database' FROM pg_database WHERE datname='vergabe_demo_company';")
existing = kube(['-n', 'databases', 'exec', primary, '-c', 'postgres', '--',
'psql', '-U', 'postgres', '-d', 'postgres', '-At', '-c', sql], raw=True).splitlines()
require('role' not in existing or patch is None, 'unmanaged_existing_role')
require('database' not in existing or db is not None, 'unmanaged_existing_database')
# These are the two newly named bundles for this lane, never bootstrap or historical secrets.
provider = get('secret', SECRET, 'databases')
consumer = get('secret', RUNTIME, NS)
if provider:
valid_owned_secret(provider, ['username', 'password'])
require(base64.b64decode(provider['data']['username']).decode() == NAME, 'username_conflict')
if consumer:
valid_owned_secret(consumer, ['SECRET_KEY', 'DATABASE_URL'])
require(provider is not None, 'runtime_without_provider')
receipt = {'mode': mode, 'cluster': metadata(cluster), 'managed_consumers': len(cluster['spec']['managed']['roles']),
'role_change_needed': patch is not None, 'database_exists': db is not None,
'provider_exists': provider is not None, 'runtime_exists': consumer is not None,
'values_emitted': False}
if mode == 'inspect':
return receipt
require(expected_revision == cluster['metadata']['resourceVersion'], 'cluster_revision_changed')
password = base64.b64decode(provider['data']['password']).decode() if provider else secrets.token_urlsafe(48)
uri = f'postgresql://{NAME}:{quote(password, safe="")}@apps-pg-rw.databases.svc.cluster.local:5432/{NAME}'
if consumer:
require(base64.b64decode(consumer['data']['DATABASE_URL']).decode() == uri,
'runtime_database_binding_conflict')
objects = []
if not provider:
objects.append(secret_object(SECRET, 'databases', {'username': NAME, 'password': password}))
if not consumer:
objects.append(secret_object(RUNTIME, NS, {'SECRET_KEY': secrets.token_urlsafe(64), 'DATABASE_URL': uri}))
dry = ['--dry-run=server'] if mode == 'dry-run' else []
for obj in objects:
result = kube(['create', '-f', '-', '-o', 'json', *dry], obj)
expected = {k: base64.b64encode(v.encode()).decode() for k, v in obj['stringData'].items()}
require(result['data'] == expected, 'secret_delivery_mismatch')
if patch:
result = kube(['-n', 'databases', 'patch', 'cluster', 'apps-pg', '--type=json',
'--patch-file=/dev/stdin', '-o', 'json', *dry], patch)
require(result['spec'] == dict(cluster['spec'], managed=dict(cluster['spec']['managed'],
roles=cluster['spec']['managed']['roles'] + [role])), 'unrelated_cluster_spec_changed')
if not db:
kube(['create', '-f', '-', '-o', 'json', *dry], database)
receipt.update(result='dry-run-passed' if mode == 'dry-run' else 'provisioned',
provider_secret='databases/' + SECRET, runtime_secret=NS + '/' + RUNTIME,
next='Wait for Database applied, then apply only the demo controls SQL and verify consumer login.')
return receipt
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('mode', choices=['inspect', 'dry-run', 'apply'])
parser.add_argument('--expected-cluster-resource-version')
args = parser.parse_args()
try:
print(json.dumps(run(args.mode, args.expected_cluster_resource_version), sort_keys=True))
return 0
except Refused as error:
print(json.dumps({'result': 'refused', 'reason': str(error)}))
except Exception:
print(json.dumps({'result': 'failed', 'reason': 'contained_operation_failed'}))
return 1
if __name__ == '__main__':
raise SystemExit(main())

View file

@ -0,0 +1,71 @@
---
id: RPF-WP-0039
type: workplan
title: "Provision the isolated Vergabe demo-company database consumer"
domain: financials
repo: railiance-platform
status: finished
owner: the-custodian
topic_slug: railiance
created: "2026-09-12"
updated: "2026-09-15"
related: [RAPPS-WP-0014, VERGABE-WP-0019, CUST-WP-0071]
state_hub_workstream_id: "fc52be19-470c-572e-92a6-661e054db50a"
---
The operator selected a fresh demo tenant, accepted the 60m application request,
and supplied the product DNS record. The platform-owned apps-pg lane permits
controlled provisioning until end-to-end OpenBao onboarding exists. This consumer
is the third of the declared maximum three; no bootstrap or historical consumer
credential is reused. No additional OpenBao control-plane login is needed for
this existing Kubernetes/CNPG lane.
## Provision the fresh database and contained runtime custody
```task
id: RPF-WP-0039-T01
status: done
priority: high
state_hub_task_id: "de793b19-b509-5de5-b816-5ca54246e901"
```
Bind only Railiance01 UID a553c742-0115-43d4-99a4-a5ca56fe0786, databases/apps-pg,
role/database vergabe_demo_company, databases/vergabe-demo-company-credentials,
and vergabe-demo-company/vergabe-demo-env. Generate new credentials in memory,
transport only through captured child stdin/stdout, and print metadata only.
Refuse existing conflicting objects, unmanaged matching database/role, changed
cluster revision, more than three consumers and unrelated configuration changes.
Apply the managed role and Database declaration, 20-connection ceiling and
15-second role timeouts; revoke PUBLIC database access. Preserve vergabe_db and
coulomb_social_db. Record the delivery and actual consumer connection evidence.
## Hand application recovery and measured sizing to their existing owners
```task
id: RPF-WP-0039-T02
status: done
priority: high
state_hub_task_id: "152a50e1-da02-5a18-a796-0919edeb2b46"
```
Verify CNPG application and role isolation and current apps-pg backup metadata.
RAPPS-WP-0014-T03 owns the consistent database/media/issue-state recovery point
and isolated restore, VERGABE-WP-0019-T06 owns identity handoff, and CUST-WP-0071
owns demand measurement and weekly resource review. Keep these records live;
provisioning success alone does not admit customer data or complete recovery.
Completed 2026-09-12. Six synthetic custody/guard tests, server dry-run and the
capacity checker pass (apps-pg 3/3). Initial Cluster UID
834d8a5e-ea2c-4b12-ad97-5561af348953/resourceVersion 59692221 guarded the append.
The Database CR reports applied, all fresh migrations completed, and actual
application connection reports database/role vergabe_demo_company. SQL readback
confirms non-privileged role, limits/timeouts, own-database CONNECT true and
historical/bootstrap database CONNECT false. No bootstrap credential was read.
The latest base backup is 2026-09-11T02:15:11Z and predates this new database;
continuous archival and that receipt are not a coherent demo recovery proof.
Residuals remain live before this plan is finished: RAPPS-WP-0014-T03 for matched
backup/restart/isolated restore, VERGABE-WP-0019-T06 for welcome/identity mapping,
and CUST-WP-0071 for measured demand and final weekly review. The prototype keeps
its approved 60m request; no fourth apps-pg consumer is admitted.