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/*-backup.yaml
!helm/*-probe.yaml
!helm/vergabe-demo-company-role.yaml
!helm/vergabe-demo-company-database.yaml
# ArgoCD repository credentials — encrypt locally, never commit
argocd/repositories/*.repository.sops.yaml

View file

@ -5,7 +5,8 @@
"apps-pg": {
"consumers": [
"coulomb_social",
"vergabe"
"vergabe",
"vergabe_demo_company"
],
"ceiling": 3,
"retention_days": 30,
@ -14,15 +15,18 @@
"max_connections": 100,
"service_classes": {
"coulomb_social": "interactive",
"vergabe": "interactive"
"vergabe": "interactive",
"vergabe_demo_company": "interactive"
},
"connection_limits": {
"vergabe": 20,
"coulomb_social": 20
"coulomb_social": 20,
"vergabe_demo_company": 20
},
"owners": {
"coulomb_social": "coulomb-social",
"vergabe": "vergabe-teilnahme"
"vergabe": "vergabe-teilnahme",
"vergabe_demo_company": "vergabe-demo-company"
}
},
"apps-pg-2": {
@ -63,6 +67,7 @@
},
"platform-pg-2": {
"consumers": [
"canned-prompts",
"sbom-nexus"
],
"ceiling": 4,
@ -71,23 +76,26 @@
"memory_limit": "1Gi",
"max_connections": 100,
"service_classes": {
"canned-prompts": "interactive",
"sbom-nexus": "interactive"
},
"owners": {
"canned-prompts": "canned-prompts",
"sbom-nexus": "sbom-nexus"
}
}
},
"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",
"rapp-postgres/consumers/audit-core.yaml": "a2db53f901df056546b2c028f287f8fc50486e6e6cd81026b9e926b6b64272b3",
"rapp-postgres/consumers/canned-prompts.yaml": "97d4b439cec7e788b7c1fa2c7181c2606ed098dcf37a38c1f73f7393e6046429",
"rapp-postgres/consumers/core-hub.yaml": "8fe3e8b4488548b2f5e01f1d46edd9fd5fe4f0e4f65a3821f172adbfb5ae1d84",
"rapp-postgres/consumers/isolation-probe.yaml": "8ea24d3bd04ab683b47ceff2586932ea4fdb0d45c49896167bb5b352a67a5cfb",
"rapp-postgres/consumers/sbom-nexus.yaml": "0b21d05fe7b9d21cbc9eb761f455329739ceb71f8dd3d1200971426962c11236",
"rapp-postgres/consumers/tenant-engine.yaml": "92f36d587f001c86ff401aedd25cb49fa171d49e000b0a6ab551e182e5c11448",
"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/helm/platform-pg-cluster.yaml": "461da0d43e61a4ddca29d6e98080b8b1642bd3303f3395da1b0beb28775c2c5c",
"rapp-postgres/helm/platform-pg-2-cluster.yaml": "06b92a76189b1a33d1eee3f5c835a3e94269ad7b040383c04e8028c2c3fe766a"

View file

@ -11,6 +11,12 @@
"service_class": "interactive",
"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": {
"owner": "audit-core",
"cell": "platform-pg",

View file

@ -11,7 +11,8 @@
"operator_role": "attended platform operator; application proof by consumer owner",
"consumers": [
"vergabe",
"coulomb_social"
"coulomb_social",
"vergabe_demo_company"
],
"failure_domain": "single-node railiance01",
"availability": {
@ -76,7 +77,8 @@
"package_or_consumer_owner": "rapp-postgres",
"operator_role": "attended platform operator; application proof by consumer owner",
"consumers": [
"sbom-nexus"
"sbom-nexus",
"canned-prompts"
],
"failure_domain": "single-node railiance01",
"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
namespace named `databases`.** The one this document describes is
**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.
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` |
| 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
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
`docs/evidence/RPF-WP-0019-backup-restore-2026-08-20.md` and
`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 |
| `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 |
| `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
@ -110,7 +111,7 @@ artifact. Saturating `coulomb_social` bound at 20 and rejected an excess
connection while five `vergabe` queries remained available. Five-query
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`
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
@ -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-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 | — |
Source and live metadata reviewed 2026-09-05: platform-pg is full; apps-pg
remains 2/3 and apps-pg-2 is still unapplied. The generated
Source and live metadata reviewed 2026-09-15: platform-pg is full; apps-pg
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
validators/declarations. `make assurance-admission` refuses source drift.
Package acceptance, rather than this table, authorizes consumer provisioning.

View file

@ -62,7 +62,7 @@ before the corresponding live rollout:
| Property | Value | Note |
| --- | --- | --- |
| `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. |
| `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. |
@ -125,7 +125,7 @@ freshness budgets are not SLAs or approval to deploy schedules. Consumers that
require stronger guarantees need a placement/recovery decision.
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
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

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
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
including the probe, platform-pg-2 1/4 for sbom-nexus. Tenant-engine's PostgreSQL
Source occupancy: apps-pg 3/3, apps-pg-2 0/3 (not deployed), platform-pg 4/4
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
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

View file

@ -59,6 +59,16 @@ spec:
connectionLimit: 20
passwordSecret:
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:
parameters:
max_connections: "100"

View file

@ -1,6 +1,8 @@
-- 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.
-- 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 idle_in_transaction_session_timeout = '15s';
ALTER ROLE coulomb_social SET statement_timeout = '15s';

View file

@ -36,3 +36,20 @@ spec:
name: coulomb_social_db
owner: coulomb_social
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:
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:

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.