Land RPF-WP-0039 third apps-pg consumer source.
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:
parent
0178ec3c6d
commit
f05ef49c69
18 changed files with 446 additions and 19 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
5
helm/vergabe-demo-company-controls.sql
Normal file
5
helm/vergabe-demo-company-controls.sql
Normal 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;
|
||||
10
helm/vergabe-demo-company-database.yaml
Normal file
10
helm/vergabe-demo-company-database.yaml
Normal 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
|
||||
11
helm/vergabe-demo-company-role.yaml
Normal file
11
helm/vergabe-demo-company-role.yaml
Normal 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
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
87
tests/test_vergabe_demo_provisioning.py
Normal file
87
tests/test_vergabe_demo_provisioning.py
Normal 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()
|
||||
173
tools/provision-vergabe-demo.py
Normal file
173
tools/provision-vergabe-demo.py
Normal 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())
|
||||
71
workplans/RPF-WP-0039-vergabe-demo-company-database.md
Normal file
71
workplans/RPF-WP-0039-vergabe-demo-company-database.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue