From 5197aa742558c34efd67bf3f69cd133182326d48 Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 18 Aug 2026 12:18:45 +0200 Subject: [PATCH 1/5] docs: add Railiance app wrapper best-practice guide Record how rapp-* repos wrap first-party apps for Railiance, using user-engine as the pilot. Opens RMGR-WP-0006 (T01 done) and includes fix-consistency ID writeback for WP-0004/0005/0006. --- README.md | 1 + WORK-RECORDS.md | 19 + docs/RailianceAppDeploymentGuide.md | 843 ++++++++++++++++++ ...P-0004-repository-standards-conformance.md | 2 + ...gistrar-consolidation-deterministic-ids.md | 11 + ...MGR-WP-0006-railiance-app-wrapper-setup.md | 126 +++ 6 files changed, 1002 insertions(+) create mode 100644 docs/RailianceAppDeploymentGuide.md create mode 100644 workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md diff --git a/README.md b/README.md index b812c9b..fc9aeee 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Architecture: - [Observation and command contracts v0.1](docs/observation-command-contracts_v0.1.md) - [State Hub extraction inventory v0.1](docs/state-hub-extraction-inventory_v0.1.md) - [ADR-001 Implementation foundation](docs/adr-001-implementation-foundation.md) +- [Railiance app deployment guide](docs/RailianceAppDeploymentGuide.md) (`RMGR-WP-0006`) ```bash make install # or: uv pip install -e ".[dev]" diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 5a4b673..3482498 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -12,6 +12,8 @@ | workplan | RMGR-WP-0002 | finished | — | workplans/RMGR-WP-0002-retirement-dual-run-strangler.md | | workplan | RMGR-WP-0003 | finished | — | workplans/RMGR-WP-0003-production-pilot-dual-run.md | | workplan | RMGR-WP-0004 | proposed | — | workplans/RMGR-WP-0004-repository-standards-conformance.md | +| workplan | RMGR-WP-0005 | proposed | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md | +| workplan | RMGR-WP-0006 | active | — | workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md | | task | RMGR-WP-0001-T01 | done | — | workplans/RMGR-WP-0001-foundation.md | | task | RMGR-WP-0001-T02 | done | — | workplans/RMGR-WP-0001-foundation.md | | task | RMGR-WP-0001-T03 | done | — | workplans/RMGR-WP-0001-foundation.md | @@ -34,3 +36,20 @@ | task | RMGR-WP-0004-T05 | wait | — | workplans/RMGR-WP-0004-repository-standards-conformance.md | | task | RMGR-WP-0004-T06 | wait | — | workplans/RMGR-WP-0004-repository-standards-conformance.md | | task | RMGR-WP-0004-T07 | wait | — | workplans/RMGR-WP-0004-repository-standards-conformance.md | +| task | RMGR-WP-0004-T08 | wait | — | workplans/RMGR-WP-0004-repository-standards-conformance.md | +| task | RMGR-WP-0004-T09 | wait | — | workplans/RMGR-WP-0004-repository-standards-conformance.md | +| task | RMGR-WP-0005-T01 | todo | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md | +| task | RMGR-WP-0005-T02 | wait | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md | +| task | RMGR-WP-0005-T03 | wait | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md | +| task | RMGR-WP-0005-T04 | wait | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md | +| task | RMGR-WP-0005-T05 | wait | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md | +| task | RMGR-WP-0005-T06 | wait | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md | +| task | RMGR-WP-0005-T07 | wait | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md | +| task | RMGR-WP-0005-T08 | wait | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md | +| task | RMGR-WP-0005-T09 | wait | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md | +| task | RMGR-WP-0005-T10 | wait | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md | +| task | RMGR-WP-0006-T01 | done | — | workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md | +| task | RMGR-WP-0006-T02 | todo | — | workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md | +| task | RMGR-WP-0006-T03 | todo | — | workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md | +| task | RMGR-WP-0006-T04 | todo | — | workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md | +| task | RMGR-WP-0006-T05 | todo | — | workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md | diff --git a/docs/RailianceAppDeploymentGuide.md b/docs/RailianceAppDeploymentGuide.md new file mode 100644 index 0000000..e54baad --- /dev/null +++ b/docs/RailianceAppDeploymentGuide.md @@ -0,0 +1,843 @@ +# Railiance App Deployment Guide + +Best-practice guide for wrapping an ordinary application as a Railiance +managed workload (`rapp-*`) so Repo Manager can later scaffold the same +shape for `rapp-some-app`. + +This document is a practice and work-structure guide. It does **not** +restate the family schemas. Where a field list and a schema disagree, the +schema wins: + +- [`railiance-master/schemas/rapp.schema.json`](../../railiance-master/schemas/rapp.schema.json) +- [`railiance-master/docs/adr/ADR-0007-rapp-declaration-contract.md`](../../railiance-master/docs/adr/ADR-0007-rapp-declaration-contract.md) +- [`railiance-master/docs/repo-family-bootstrap-contract.md`](../../railiance-master/docs/repo-family-bootstrap-contract.md) + +Worked example throughout: `user-engine` + the empty stub `rapp-user-engine`. + +--- + +## 1. Purpose + +Repo Manager should make it easy to set up a managed Railiance application +wrapper. The product of that setup is a `rapp-` repository: a +packaging and operations home that can deploy, verify, roll back, and bind +the app onto a rail and a reef. + +This guide answers: + +1. What a managed Railiance application **is**. +2. How rails, reefs, and existing rapps already do it. +3. Which files, declarations, and operator surfaces a new wrapper needs. +4. How `user-engine` should be wrapped as `rapp-user-engine`. +5. What Repo Manager must later automate, in order. + +It is the first deliverable of `RMGR-WP-0006`. Implementation of the +scaffolder comes after this shape is accepted. + +--- + +## 2. Vocabulary + +Railiance classifies repositories on four complementary axes +(`railiance-master/docs/repository-axes.md`, ADR-0001): + +| Prefix | Axis | Question it answers | +| --- | --- | --- | +| `railiance-*` | ownership | Who owns this capability or layer? | +| `rail-*` | execution contract | How does a workload run here? | +| `rapp-*` | managed workload package | What is packaged and operated? | +| `reef-*` | substrate | Where does it run, and what is bound there? | + +A `rapp` is **managed packaging, not ownership**. The application repo +keeps domain logic, tests, image build, and schema migrations. The rapp +owns Helm/manifests, rail compatibility, smoke and rollback, secret +*references*, and reef binding. + +Do not derive a rapp from a Forgejo organization or a State Hub domain. +Rapp context is its own dimension (ADR-0007): + +- repos ↔ rapps is **many-to-many** +- deployables ↔ rapps is **one-to-one** + +Grouping is legitimate only where members share rollout and rollback fate. + +--- + +## 3. Current inventory (2026-08-18) + +### 3.1 Rails + +| Rail | Kind | Architecture | Readiness | Use when | +| --- | --- | --- | --- | --- | +| `rail-kubernetes` | base | Kubernetes | verified | Default for platform services **and** managed applications | +| `rail-knative` | derived from `rail-kubernetes` | Knative Serving on Kubernetes | verified | Scale-to-zero, revision traffic, request activation (today: Qonto) | + +Default bias: use `rail-kubernetes` unless the workload truly needs +Knative lifecycle semantics. Do not invent a new rail for an adapter or +values profile. + +`rail-kubernetes` also owns the generic staged-promotion contract +`railiance/app.toml` (Stage 1 local, Stage 2 canary, Stage 3 promote) and +the compatibility overlay-repo pattern. That overlay is the migration-era +wrapper. Durable first-class packaging belongs in `rapp-*`. + +### 3.2 Reefs + +| Reef | Kind | State | What it is | +| --- | --- | --- | --- | +| `reef-railiance` | `server-group` | active | Home compute reef (`Railiance01`). Hosts `rail-kubernetes` and `rail-knative`. Public host ports 80/443 (and 2224 for the provider agent) are granted. | +| `reef-storage` | `object-storage` | planned | Provider-delegated S3 (Scaleway). Hosts no rail. Backup target, not a compute placement. | + +A rapp names its placement in `bound_reefs`. The reef's `bound_rapps` is a +**derived projection** of that field, not a hand list. + +Topology is not readiness (ADR-0006). Binding a rapp to a reef does not +admit it to production and does not make it public (ADR-0008). + +### 3.3 Live and stub rapps + +| Repo | Declaration | Package type | Rail | Ownership repo | Readiness | Notes | +| --- | --- | --- | --- | --- | --- | --- | +| `rapp-openbao` | yes | helm-managed-platform-service | kubernetes | `railiance-platform` | verified | Third-party platform service. Proven S3 pattern. | +| `rapp-postgres` | yes | helm-managed-platform-service | kubernetes | `railiance-platform` | verified | Shared CNPG + per-consumer surface. Apps consume this; they do not run their own Postgres. | +| `rapp-qonto` | yes, pre-schema drift | knative-managed-service (implied) | knative | `qonto-assistant` | verified | First-party Knative app. Missing `composition`, `bound_reefs`, `package_type`; `workload_identity.name` is still `rapp-qonto`. | +| `rapp-policy-nexus` | yes | helm-managed-platform-service | kubernetes | `policy-nexus` | declared | Closest first-party Helm app wrapper. Public grant at `policy.coulomb.social`. | +| `rapp-user-engine` | **no** | — | — | — | stub | Empty git repo. Target of this guide. | +| `rapp-secrets-engine` | **no** | — | — | — | stub | README only. Same family as user-engine. | +| `rapp-tenant-engine` | **no** | — | — | — | stub | README only. Same family. | +| `rapp-vergabe-teilnahme` | never built | — | — | — | — | Planned user-facing proof; workload still lives in `railiance-apps`. | +| `rapp-forgejo` | not built | — | — | — | — | Deferred; forge still mixed into `railiance-forge` / `railiance-apps`. | + +The platform-service wrapper is proven twice. The first-party application +wrapper is only half-proven (`rapp-policy-nexus` declared; `rapp-qonto` +drifted; `rapp-vergabe-teilnahme` never extracted). `rapp-user-engine` is +the next application-shaped proof, not a substitute for the missing +Vergabe extraction. + +### 3.4 Ownership repos that still hold wrappers + +`railiance-apps` is still the S5 release surface for workloads that have +not been extracted (`vergabe-teilnahme`, `coulomb-social`, `core-hub`, +`reuse-surface`, `inter-hub`, Forgejo values). Its +`docs/s5-app-onboarding-checklist.md` remains the operator recipe for +Helm, probes, landing pages, and image pins. New first-class wrappers +should **copy those recipes into a `rapp-*` repo**, not add another chart +to `railiance-apps`. + +`railiance-platform` retains cross-workload policy, credential custody, +lane approval, and the postgres credential broker. + +--- + +## 4. Decide before creating a rapp + +Ask these in order. Stop at the first "no". + +1. **Does the workload have a stable identity?** A name that is not the + repo prefix, a package surface already visible in Git, and a reason to + version and roll back on its own. +2. **Is wrapping distinct from ownership?** If the only thing you would + put in the rapp already belongs in `railiance-platform` or + `railiance-apps` as shared policy, do not create a rapp. +3. **Do members share fate?** One rapp per bounded context that deploys + and rolls back together. Independent rollback → separate rapp. +4. **Which rail?** Default `rail-kubernetes`. Choose `rail-knative` only + for request-activation / scale-to-zero / revision-traffic semantics. +5. **Which reef?** New compute workloads bind `reef-railiance`. Object + storage is `reef-storage` and is consumed as a capability, not as a + placement. +6. **What package type?** One of the schema enums (see §6). +7. **What exposure?** Omit the field (private) until a grant exists. + +A service that fails (1) or (2) can still be operated from its ownership +repo. It is not blocked; it is just not ready to be a rapp. + +--- + +## 5. Ownership split + +Use the S3 ownership test from +`railiance-platform/docs/rapp-platform-service-pattern.md`, generalized +to applications: + +1. Would this asset survive replacing the product? → ownership / platform + repo. +2. Does any other workload depend on it? → platform or rail. +3. Does it encode *who may approve*, not *how to apply*? → platform / + reef admission. +4. Otherwise it is the package's. + +### Applied to a first-party app + +| Asset | Home | +| --- | --- | +| Domain code, tests, migrations, Containerfile, OpenAPI | app repo (`user-engine`) | +| Helm chart, values, NetworkPolicy, Ingress, ServiceAccount | rapp | +| `declarations/rapp.yaml`, `railiance/app.toml` | rapp | +| Deploy / dry-run / status / verify / rollback Makefile | rapp | +| Workload-specific smoke outcomes | rapp | +| Image **build** and digest publication | app repo + forge | +| Image **pin** in the release | rapp (digest, never `latest`) | +| Postgres cluster, backup, credential broker | `rapp-postgres` + `railiance-platform` | +| Consumer declaration (`database-per-consumer`) | `rapp-postgres/consumers/.yaml` requested by the rapp | +| Schema migrations | app repo, invoked by the rapp at rollout | +| OIDC / flex-auth / tenant policy | those ownership repos | +| Secret **values** | OpenBao via the platform broker | +| Secret **references** | rapp `secret_references` + `railiance/app.toml` | +| Public hostname grant | rapp `exposure.grant` + reef public surface | +| DNS / TLS / Traefik | reef / `railiance-cluster` | +| Shared S5 recipes (probe Host header, landing pages) | copied into the rapp; source of the lesson stays in `railiance-apps` | + +The rapp repo must never become a shadow ownership repo. If a paragraph +in `INTENT.md` starts explaining *why the product exists*, it belongs in +the app repo. + +--- + +## 6. Required repo baseline + +Every `rapp-*` starts with the first-wave bootstrap contract: + +```text +rapp-/ + .repo-classification.yaml + README.md + INTENT.md + SCOPE.md + AGENTS.md + CLAUDE.md + LICENSE + Makefile + declarations/rapp.yaml # family contract (normative schema) + workplans/ + workplans/archived/ +``` + +Then add the package surface. For a `rail-kubernetes` first-party app +the proven layout is the union of `rapp-policy-nexus` (Helm + digest +pins + `make render|deploy|verify-live|rollback`) and the +`rail-kubernetes` overlay contract: + +```text + railiance/app.toml # staged promotion (rail contract) + helm// # chart owned by the rapp + Chart.yaml + values.yaml + templates/ + deployment.yaml + service.yaml + serviceaccount.yaml + networkpolicy.yaml + ingress.yaml # only if exposure will become public/operator + consumers/ # only if this rapp provisions units + docs/ + runtime-contract.md + retained-handoff.md # if extracting from railiance-apps / platform + tests/ # render / contract tests, no cluster required + tools/ # verify_live.sh, smoke helpers + bindings/ # production binding records when needed +``` + +Implementation files come after the baseline. An empty `rapp-*` git +directory is not a wrapper; `railiance-master` already treats +`rapp-user-engine` as undeclared. + +### Classification + +Copy the shape used by live rapps. Domain follows the **workload**, not +the wrapper: + +```yaml +repo_classification: + standard: Repo Classification Standard + version: "1.0" + classified_at: "YYYY-MM-DD" + classified_by: agent + category: project + domain: + secondary_domains: + - infotech + capability_tags: + - platform + - operations + - configuration + business_stake: + - technology + - operations + business_mechanics: + - coordination + - operation +``` + +--- + +## 7. `declarations/rapp.yaml` + +Normative shape: `railiance-master/schemas/rapp.schema.json`. +Validate with: + +```bash +# from railiance-master, with sibling family repos checked out +tools/validate-family-declarations.py --root .. +``` + +### Required meaning, not a second schema + +| Field | Practice | +| --- | --- | +| `kind` | always `managed-workload-package` | +| `repo_family` | always `rapp` | +| `rapp_id` / `repo` | `rapp-` for a single-repo rapp | +| `ownership_repo` | the accountable app or layer repo, **never** the rapp itself. Application: `user-engine`. Platform service: `railiance-platform`. | +| `contract_version` | start at `1.0.0` | +| `readiness_state` | `draft` while scaffolding → `declared` once valid → `deployed` / `verified` only with evidence. There is no `production-approved` on a rapp. | +| `workload_identity.name` | the workload (`user-engine`), **not** `rapp-user-engine`. The schema forbids the `rapp-` prefix. | +| `workload_identity.package_type` | one of `helm-managed-platform-service`, `manifest-managed-platform-service`, `knative-managed-service`, `grouped-composition` | +| `data_classification` | highest class the workload handles | +| `criticality` | `low` \| `medium` \| `high` \| `critical` | +| `primary_rail` | must also appear in `supported_rails` | +| `bound_reefs` | `[]` until placed; then `[reef-railiance]` | +| `runtime_dependencies` | capability names, not the resource that satisfies them | +| `composition.purpose` | one sentence; if it needs "and also", split the rapp | +| `composition.member_repos` | one member for a single-repo rapp; each lists `deployables` | +| `composition.upstream_components` | pinned third-party charts/images. Exact version or digest. Never `latest`. | +| `rollout_contract.default_mode` | `helm-upgrade-install` or `kubectl-server-side-apply` (or `revision-canary` on Knative) | +| `smoke_contract.required` | **outcomes** that must be true, not a command diary | +| `rollback_contract.order` | preferred strategies first | +| `source_documents` | repo + path of the docs this declaration was derived from | + +Optional, use when true: + +- `secret_references` — OpenBao **paths**, never values +- `consumer_contract` / `consumers` — only if this rapp provisions isolated units to others (`rapp-postgres` is the model) +- `exposure` — omit for private. `public` requires a grant, `binding_admission: production-approved`, and a reef that has granted a public surface. + +### Package-type choice + +The schema has no `helm-managed-application` enum. Until that is added in +`railiance-master`, first-party Helm apps use +`helm-managed-platform-service` (as `rapp-policy-nexus` does). Do not +invent a local type. Record the naming mismatch in `source_documents` / +workplan notes if you want the schema extended later. + +### Capability names in use today + +There is not yet a controlled vocabulary shared with +`rail.required_substrate_capabilities`. Prefer names already live in +declarations rather than minting near-duplicates: + +- `kubernetes-api` +- `helm-apply-path` / `helm-repo-access` +- `traefik-ingress` +- `cert-manager` +- `forgejo-oci-registry` +- `pvc-storage` +- `operator-kubeconfig-access` +- `knative-serving` +- `keycape-oidc` / `key-cape-oidc` (spellings have already drifted — pick one and stay consistent inside a rapp) +- `flex-auth` +- `openbao` +- `openbao-database-secrets-engine` +- `s3-backup-target` + +Who provisions the capability is **outside** the rapp: +`resource-control` procures, the reef publishes non-secret attributes, +`railiance-platform` vends the credential. + +--- + +## 8. Service wrapper patterns + +Three live patterns. Copy the one that matches the rail and package type. + +### 8.1 Helm platform / app service (`rail-kubernetes`) + +References: `rapp-openbao`, `rapp-policy-nexus`, +`railiance-apps/docs/s5-app-onboarding-checklist.md`. + +Operator surface (minimum Makefile targets): + +| Target | Meaning | +| --- | --- | +| `make check` / `make test` / `make lint` | contract and `helm lint` with dummy digests | +| `make render` | write rendered manifests locally | +| `make server-dry-run` | `kubectl apply --dry-run=server` against a representative API | +| `make deploy` | `helm upgrade --install --atomic --wait` | +| `make status` | helm/kubectl status | +| `make verify-live` | named smoke outcomes from `smoke_contract.required` | +| `make rollback REVISION=…` | previous Helm revision | + +Pins: + +- image by **digest**, not tag (`rapp-policy-nexus` is the model) +- chart version and app version in `workload_identity` +- forge-owned registry path (`forgejo.coulomb.social/...`) + +Runtime hardening that should be the default in new charts: + +- non-root numeric user +- read-only root filesystem where the app allows it +- dropped capabilities +- NetworkPolicy default-deny plus explicit ingress/egress +- unauthenticated `/healthz` and `/readyz` +- `/metrics` cluster-internal and authorized +- probe `Host` header set when the app validates Host (Django lesson) + +### 8.2 Manifest-managed platform service + +Reference: `rapp-postgres` (`kubectl-server-side-apply` of CNPG Cluster +and NetworkPolicies; Helm only for the *operator*, which is cluster-owned). + +Use this when the workload is a set of reviewed manifests rather than a +chart you upgrade as a release. + +### 8.3 Knative managed service + +Reference: `rapp-qonto` (`runtime/knative/`, revision-canary, +previous-revision rollback). Only when `rail-knative` is the primary rail. + +### 8.4 Overlay compatibility path + +`rail-kubernetes/docs/overlay-repo-pattern.md` still describes +`-railiance-overlay/` with `railiance/app.toml`. New work should +not create overlay repos. If you find one, graduate it to `rapp-` +and keep `railiance/app.toml` inside the rapp. + +--- + +## 9. Platform handoffs every app wrapper needs + +### 9.1 Database + +Do not run a sidecar Postgres. Declare a consumer against `rapp-postgres`: + +- boundary unit: `database-per-consumer` +- credential lane: `railiance-platform-credential-broker` +- roles split: owner / migration / runtime +- tenant-keying required even for a single current tenant + +The consumer file lives in `rapp-postgres/consumers/.yaml`. The rapp +records the capability (`openbao-database-secrets-engine` or equivalent) +and the OpenBao path in `secret_references`. The app repo keeps +migrations and applies them with the migration role. + +If the app consumes a single `DATABASE_URL`, URL-encode the password. +Prefer discrete `POSTGRES_*` env vars when the framework allows it +(`railiance-apps/docs/django-on-railiance.md`). + +### 9.2 Secrets + +The rapp never stores values. Standing secrets go through a CCR and +`secret_references`. Short-lived leases go through the grant catalog and +`rapp_id`. See `railiance-platform/docs/rapp-credential-lane-binding.md`. + +In `railiance/app.toml`, declare `secrets.references` with route ids +(`openbao-api-key`, `key-cape-oidc-login`, …) and Kubernetes Secret / +ExternalSecret **names**. + +### 9.3 Identity and authorization + +Apps that speak OIDC / flex-auth declare those as runtime capabilities +and receive issuer, audience, client id, and token-file paths through +the broker. The rapp does not mint tenants or tokens. +`workload_identity.tenant` is an opaque NetKingdom id when one is +needed; the rapp never parses or authorizes on it. + +### 9.4 Images + +Build stays in the app repo. The rapp consumes a forge-published digest. +Verify the cluster can pull the image before the first non-smoke deploy +(`railiance-apps/tools/check-oci-image.sh` is the existing helper). + +### 9.5 Ingress, TLS, landing pages + +Private by default. When a public hostname is later granted: + +- name host, namespace, release, ingress, TLS secret in the runbook +- cert-manager owns certificates +- every public `/` has an intentional browser response (S5 landing-page + rule) +- do not break `/healthz`, `/readyz`, API prefixes, or OIDC callbacks + +### 9.6 Exposure (ADR-0008) + +| Posture | Who reaches the listener | +| --- | --- | +| omitted / `private` | in-cluster DNS, same-reef services | +| `operator` | same, plus a named ops-bridge / SSH tunnel | +| `public` | internet, only with grant + production-approved binding + reef public surface | + +`data_classification: public` is **not** exposure. Port `6443` is never +a grantable public surface. + +--- + +## 10. Worked example: `user-engine` → `rapp-user-engine` + +### 10.1 Current facts + +`user-engine` is a headless user-domain / identity-domain service with a +production WSGI portal. It already has: + +- `Containerfile` — Python 3.12 slim, non-root uid `10001`, port `8080` +- `python -m user_engine.runtime` +- `/healthz`, `/readyz`, `/metrics` (metrics require the trusted proxy marker) +- Postgres store + in-tree migrations +- fail-closed production adapters: OIDC, flex-auth, outbox, provisioning, + optional tenant-management and registration verification +- explicit non-ownership of IdP, authorization PDP, and secret custody + +`rapp-user-engine` is an empty git repository (2026-08-11). ADR-0007 +already lists it as undeclared. + +Sister stubs `rapp-secrets-engine` and `rapp-tenant-engine` should follow +the same guide after user-engine proves the path. Do not batch-scaffold +all three before the first declaration validates. + +### 10.2 Recommended decisions + +| Decision | Value | Why | +| --- | --- | --- | +| Create a rapp? | yes | stable identity, clear app/wrapper split, own rollout fate | +| `ownership_repo` | `user-engine` | application accountability, same pattern as `policy-nexus` / `qonto-assistant` | +| `workload_identity.name` | `user-engine` | schema rule | +| `package_type` | `helm-managed-platform-service` | only Helm-shaped enum available today | +| `primary_rail` | `rail-kubernetes` | long-running portal + durable store; no Knative need | +| `bound_reefs` | `[]` at `declared`; `[reef-railiance]` when placed | empty is legal and means "not yet placed" | +| `data_classification` | `confidential` | identity and account data | +| `criticality` | `high` | shared identity-domain service; not the secrets/postgres critical path | +| `exposure` | omit (private) | operator tunnel later if needed; public grant only with a hostname and residual-risk owner | +| Granularity | single-repo rapp | one deployable; do not group with tenant-engine or secrets-engine | + +### 10.3 Draft declaration (index, not authority) + +The live file will be `rapp-user-engine/declarations/rapp.yaml`. This is +the intended fill of the schema, to be validated before it is treated as +declared. + +```yaml +kind: managed-workload-package +repo_family: rapp +rapp_id: rapp-user-engine +repo: rapp-user-engine +ownership_repo: user-engine +contract_version: 1.0.0 +readiness_state: draft +workload_identity: + name: user-engine + package_type: helm-managed-platform-service + chart: rapp-user-engine/user-engine + chart_version: 0.1.0 + app_version: 0.1.0 +data_classification: confidential +criticality: high +primary_rail: rail-kubernetes +supported_rails: + - rail-kubernetes +bound_reefs: [] +runtime_dependencies: + - kubernetes-api + - helm-apply-path + - forgejo-oci-registry + - openbao-database-secrets-engine + - keycape-oidc + - flex-auth +composition: + purpose: Package and operate the user-engine identity-domain service on Railiance. + member_repos: + - repo: rapp-user-engine + role: Helm package, smoke, and rollback for the user-engine runtime + deployables: + - user-engine + - repo: user-engine + role: application source, image, and schema migrations + deployables: + - user-engine-migrate + upstream_components: + - name: user-engine + source: forgejo.coulomb.social//user-engine + version: sha256: +rollout_contract: + default_mode: helm-upgrade-install + commands: + - make render + - make server-dry-run + - make deploy + production_gate: reef-production-approved +smoke_contract: + required: + - healthz-ok + - readyz-ok-when-store-ready + - metrics-requires-proxy-marker + - local-issuer-rejected-in-production + - postgres-consumer-database-present + commands: + - make verify-live +rollback_contract: + order: + - previous-helm-revision + - apply-reviewed-git-revision + commands: + - make rollback REVISION= +source_documents: + - repo: user-engine + path: INTENT.md + - repo: user-engine + path: docs/configuration.md + - repo: user-engine + path: docs/operability.md + - repo: user-engine + path: docs/postgres-durable-store-consumer-requirements.md + - repo: railiance-master + path: schemas/rapp.schema.json + - repo: repo-manager + path: docs/RailianceAppDeploymentGuide.md +``` + +Cardinality note: `user-engine` the *Deployment* belongs to this rapp. +`user-engine-migrate` is a Job owned by the same rapp. Do not also list +the Deployment under another rapp. + +If the migrate Job is considered the same deployable as the app, collapse +to a single `user-engine` deployable and treat migration as a rollout +step, not a second live unit. Prefer the single-deployable form unless +the Job must be inventoried separately for the coverage check. + +### 10.4 Runtime inputs the chart must accept + +From `user-engine` `runtime.py` and `docs/configuration.md`. All values +arrive as secret-backed env or files. The chart names the keys; OpenBao +fills them. + +Required for production assembly: + +- `USER_ENGINE_DATABASE_URL` (or discrete Postgres vars + a rendered DSN) +- `USER_ENGINE_OIDC_ISSUER`, `USER_ENGINE_OIDC_AUDIENCE`, + `USER_ENGINE_OIDC_CLIENT_ID`, `USER_ENGINE_OIDC_REDIRECT_URI` +- `USER_ENGINE_FLEX_AUTH_URL`, `USER_ENGINE_FLEX_AUTH_TOKEN_FILE` +- `USER_ENGINE_PROXY_SECRET` +- `USER_ENGINE_LOGIN_URL` +- `USER_ENGINE_EVENT_URL`, `USER_ENGINE_EVENT_TOKEN` +- `USER_ENGINE_PROVISIONING_URL`, `USER_ENGINE_PROVISIONING_TOKEN` + +Optional / fail-closed when absent: + +- `USER_ENGINE_TENANT_MANAGEMENT_URL` + `USER_ENGINE_TENANT_MANAGEMENT_TOKEN` +- `USER_ENGINE_MAIL_URL` + `USER_ENGINE_MAIL_TOKEN` +- public-registration adapter settings + +Port: `USER_ENGINE_HTTP_PORT` (default `8080`). Do not use +`USER_ENGINE_PORT` — Kubernetes service-link injection occupies that name. + +Logical secret names the app already documents: `database.url`, +`event.signing_key`, `webhook.shared_secret`. Map those to broker routes +in `railiance/app.toml`; do not invent a second naming scheme in the +chart. + +### 10.5 Postgres consumer (to be added in `rapp-postgres`) + +Sketch only — the owning package is `rapp-postgres`: + +```yaml +apiVersion: rapp-postgres.railiance.io/v1alpha1 +kind: PostgresConsumer +metadata: + name: user-engine +spec: + database: user_engine + schema: user_engine + costAttributionKey: platform:user-engine + clientNamespaces: [user-engine] + roles: + owner: user_engine_owner + migration: user_engine_migrate + runtime: user_engine_app + tenantKeyingRequired: true +``` + +### 10.6 What stays in `user-engine` + +- domain model, adapters, tests, conformance harness +- `Containerfile` and image publish instructions +- SQL migrations +- portal/OIDC behavior +- `docs/operability.md` diagnostics + +The app repo should grow a short "consumed by `rapp-user-engine`" note +pointing here. It should not grow Helm charts. + +### 10.7 First-wave `railiance/app.toml` for this rapp + +Minimum: `schema_version = "railiance.app.v1"`, `app.id = "user-engine"`, +source digest policy `required`, postgres + oidc + flex-auth + openbao +dependencies, secret references by route, `/healthz` and `/readyz` +checks, Stage 1 (`make test` in the app repo + `helm template`), Stage 2 +canary requiring approval, Stage 3 promote with a recorded rollback +target. Copy structure from +`rail-kubernetes/examples/railiance/app.toml`. + +--- + +## 11. Operator checklist (new rapp) + +Use this when filling a stub or extracting a wrapper from +`railiance-apps`. + +### Shape + +- [ ] Confirm the workload passes §4. +- [ ] Create or fill `rapp-` with the §6 baseline. +- [ ] Write `INTENT.md` / `SCOPE.md` as a **package** boundary, not a + product manifesto. +- [ ] Author `declarations/rapp.yaml` to the schema. Start + `readiness_state: draft`. +- [ ] Add `railiance/app.toml` if the primary rail is `rail-kubernetes`. +- [ ] Add the Helm chart or manifest set. Pin images by digest. +- [ ] Add Makefile targets from §8.1. +- [ ] Name smoke **outcomes** and rollback **order**. +- [ ] Leave `exposure` unset. + +### Handoffs + +- [ ] Request a `rapp-postgres` consumer when a durable store is needed. +- [ ] Request credential lanes / CCRs in `railiance-platform`. Commit + paths only. +- [ ] Confirm forge image publish path in the **app** repo. +- [ ] Confirm cluster pull before the first deploy. +- [ ] Record `source_documents`. + +### Evidence before `declared` → `deployed` + +- [ ] `tools/validate-family-declarations.py --root ..` passes. +- [ ] `make render` and `make server-dry-run` pass. +- [ ] `make deploy` on `reef-railiance` only after the reef binding is + intended. +- [ ] `make verify-live` proves every `smoke_contract.required` item. +- [ ] Rollback is rehearsed at least once on a non-destructive revision. +- [ ] Deployable names match the live inventory + (`railiance-platform/docs/evidence/reef-railiance-deployables.json`) + when `--inventory` is run. + +### Do not + +- [ ] Commit credentials, kubeconfigs, or tokenized registry URLs. +- [ ] Add the workload to `reef.yaml` `bound_rapps` by hand. +- [ ] Put Helm charts in the app repo "just for now". +- [ ] Create a new rail. +- [ ] Create a reef because a hostname exists. +- [ ] Group unrelated engines into one rapp. +- [ ] Set `exposure.posture: public` to make a smoke test easier. + +--- + +## 12. What Repo Manager should automate + +This is the work structure for `RMGR-WP-0006`. Each step is a later +command or command flag on the existing governed-mutation path +(`RMGR-WP-0004` scaffolding, specialized for the rapp family). + +| Phase | Repo Manager does | Still human | +| --- | --- | --- | +| **P0 — this guide** | Record the shape. | Accept or amend the shape. | +| **P1 — bootstrap stub** | Create or complete `rapp-` with §6 baseline files, classification, and a `draft` `rapp.yaml` from a questionnaire (ownership repo, rail, classification, criticality, purpose). | Confirm ownership split and purpose sentence. | +| **P2 — package skeleton** | Generate Helm chart + Makefile targets from app facts: port, health paths, Containerfile user, image repository. Generate `railiance/app.toml` skeleton. | Review NetworkPolicy and resource requests. | +| **P3 — platform bindings** | Draft `rapp-postgres` consumer and `secret_references` from the app's documented logical secret names. | Approve CCR / consumer apply in the owning packages. | +| **P4 — validate** | Run family-declaration validation and `helm lint` / render. Refuse `readiness_state` promotions that skip evidence. | Promote `draft` → `declared`. | +| **P5 — place** | Set `bound_reefs: [reef-railiance]` only on an explicit place command. | Production gate and any exposure grant. | + +Command sketch (not implemented): + +```text +rmgr rapp init --app user-engine --ownership-repo user-engine \ + --rail rail-kubernetes --classification confidential --criticality high + +rmgr rapp skeleton --path ../rapp-user-engine --from-app ../user-engine + +rmgr rapp validate --path ../rapp-user-engine --family-root .. +``` + +Constraints on the scaffolder: + +- Files stay authoritative. The hub is not written to except via + `fix-consistency` after the files exist. +- No secret values, ever. +- Do not invent package types, rails, or reefs. +- Idempotent: re-running `init` on an already-declared rapp must refuse + or update in a reviewed diff, not overwrite a live contract. +- The three engine stubs (`user-engine`, `secrets-engine`, + `tenant-engine`) are the first cohort; `user-engine` is the pilot. + +--- + +## 13. Open questions and known drift + +Recorded so the scaffolder does not silently pick a side. + +1. **`ownership_repo` tightness.** Schema currently allows a layer repo + (`railiance-platform`) or an application repo. For application rapps, + prefer the application repo. (`railiance-master/schemas/README.md`) +2. **No application package_type.** First-party Helm apps reuse + `helm-managed-platform-service`. A future `railiance-master` schema + change may add `helm-managed-application`. +3. **`rapp-qonto` is not a copy target.** It is verified in production + but does not yet conform to ADR-0007 (`composition`, `bound_reefs`, + `package_type`, workload name). Copy `rapp-policy-nexus` + + `rapp-openbao` instead. +4. **Capability vocabulary is free-form.** Spellings already drift + (`keycape-oidc` vs `key-cape-oidc`). The scaffolder should emit a + small allow-list and refuse new names without a note. +5. **`rapp-vergabe-teilnahme` was the planned user-facing proof and was + never built.** Extracting it remains a `railiance-apps` residual. Do + not pretend `rapp-user-engine` closes that ADR-0003 item. +6. **Reef `bound_rapps` on `reef-railiance` has been stale.** Never + teach the scaffolder to edit it. +7. **Metrics and Host-validated apps** need extra chart care. The + scaffolder should read probe paths from the app if present + (`/healthz`, `/readyz` for user-engine) and not assume `/health`. + +--- + +## 14. References + +Architecture and contracts: + +- `railiance-master/docs/repository-axes.md` +- `railiance-master/docs/adr/ADR-0001-repository-prefix-architecture.md` +- `railiance-master/docs/adr/ADR-0003-rapp-first-wave-selection.md` +- `railiance-master/docs/adr/ADR-0005-derived-rail-composition.md` +- `railiance-master/docs/adr/ADR-0006-reef-production-admission.md` +- `railiance-master/docs/adr/ADR-0007-rapp-declaration-contract.md` +- `railiance-master/docs/adr/ADR-0008-private-by-default-exposure.md` +- `railiance-master/docs/repo-family-bootstrap-contract.md` +- `railiance-master/docs/rapp-first-wave-candidates.md` +- `railiance-master/schemas/README.md` + +Rails and reefs: + +- `rail-kubernetes/declarations/rail.yaml` +- `rail-kubernetes/docs/deployment-lifecycle.md` +- `rail-kubernetes/docs/app-toml-contract.md` +- `rail-kubernetes/docs/overlay-repo-pattern.md` +- `rail-knative/declarations/rail.yaml` +- `reef-railiance/declarations/reef.yaml` +- `reef-storage/declarations/reef.yaml` + +Proven wrappers and operator recipes: + +- `railiance-platform/docs/rapp-platform-service-pattern.md` +- `rapp-openbao/declarations/rapp.yaml` +- `rapp-postgres/declarations/rapp.yaml` +- `rapp-policy-nexus/declarations/rapp.yaml` +- `rapp-policy-nexus/docs/runtime-contract.md` +- `railiance-apps/docs/s5-app-onboarding-checklist.md` +- `railiance-apps/docs/django-on-railiance.md` +- `railiance-apps/docs/operator-recipes.md` + +Worked example: + +- `user-engine/INTENT.md` +- `user-engine/docs/configuration.md` +- `user-engine/docs/operability.md` +- `user-engine/docs/postgres-durable-store-consumer-requirements.md` +- `user-engine/Containerfile` +- `user-engine/src/user_engine/runtime.py` diff --git a/workplans/RMGR-WP-0004-repository-standards-conformance.md b/workplans/RMGR-WP-0004-repository-standards-conformance.md index a1ae7b5..7611d24 100644 --- a/workplans/RMGR-WP-0004-repository-standards-conformance.md +++ b/workplans/RMGR-WP-0004-repository-standards-conformance.md @@ -198,6 +198,7 @@ with compatibility tests. Cover: id: RMGR-WP-0004-T08 status: wait priority: high +state_hub_task_id: "fc7395b0-e86d-4231-baac-ea7fa5dc2174" ``` Implement `ADR-007` decision 1 as a repository standard: `PREFIX-WP-NNNN` names @@ -239,6 +240,7 @@ non-unique identifier would manufacture UUID collisions rather than remove them. id: RMGR-WP-0004-T09 status: wait priority: high +state_hub_task_id: "3a6ba954-4e35-4b2d-8dff-d3db84ed832c" ``` Three prefixes are shared across seven repositories, each a single number line diff --git a/workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md b/workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md index 68ecf1a..6d65df2 100644 --- a/workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md +++ b/workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md @@ -16,6 +16,7 @@ related: - STATE-WP-0080 - STATE-WP-0068 - CFED-WP-0001 +state_hub_workstream_id: "7ddb5421-d960-4a3c-94b1-40b6c96abfab" --- # Registrar consolidation and deterministic hub identifiers @@ -51,6 +52,7 @@ model; a file carrying a hub's private key is the file holding hub state. id: RMGR-WP-0005-T01 status: todo priority: high +state_hub_task_id: "b57a6882-280d-4f0a-9c73-899843dfc3d3" ``` Until derivation ships, exactly one instance may write hub identifiers into @@ -73,6 +75,7 @@ removes the need for the trade. id: RMGR-WP-0005-T02 status: wait priority: medium +state_hub_task_id: "8e679ddb-9845-457f-8672-1fd4b7455e7b" ``` Records minted by non-registrar instances before T01 need reconciliation. Known @@ -94,6 +97,7 @@ before then. id: RMGR-WP-0005-T03 status: wait priority: high +state_hub_task_id: "28067729-498d-4f47-89bd-5b9718e999c7" ``` Replace minted UUIDs with UUIDv5 derived from the globally unique @@ -116,6 +120,7 @@ same UUID for different workplans. Uniqueness must be enforced first. id: RMGR-WP-0005-T04 status: wait priority: high +state_hub_task_id: "503a23a9-ede1-4cf1-bd32-e9669b84ce58" ``` One-time pass over the 758 files carrying hub identifiers: compute the derived @@ -135,6 +140,7 @@ value, update the database to match, and write the file. id: RMGR-WP-0005-T05 status: wait priority: low +state_hub_task_id: "3946d1fc-2137-4d7b-a400-29b447ca83de" ``` Once derivation is live fleet-wide, remove the single-writer restriction from @@ -150,6 +156,7 @@ byte-identical writeback, and neither creates a duplicate record. id: RMGR-WP-0005-T07 status: wait priority: high +state_hub_task_id: "70f83359-0b61-4dc0-83b0-33f289b64e83" ``` Implement `ADR-010` decisions 1–3: the central hub on railiance is authoritative @@ -171,6 +178,7 @@ portion of the divergence is redundant and needs no merge — only a rebuild. id: RMGR-WP-0005-T08 status: wait priority: high +state_hub_task_id: "241cf058-2f3e-4d49-8cc9-5c714be4a1cf" ``` Implement `ADR-010` decision 4. The two kinds need opposite handling: @@ -197,6 +205,7 @@ reduces its scope. id: RMGR-WP-0005-T09 status: wait priority: high +state_hub_task_id: "d40cc4a8-4280-4940-ac1d-dc1049f1b678" ``` 28 records exist in the local instance with no backing file. They are the only @@ -227,6 +236,7 @@ register collision)` in `repo-manager`, independent corroboration of the id: RMGR-WP-0005-T10 status: wait priority: medium +state_hub_task_id: "15f0f167-a8d0-4d5c-8576-3e93b1e8792f" ``` Implement `ADR-010` decision 7. The retirement splits one hub into several, which @@ -245,6 +255,7 @@ Coordinate with the hub-extension architecture in id: RMGR-WP-0005-T06 status: wait priority: medium +state_hub_task_id: "d440d59c-f78e-4752-84c7-f3d5fdf7d3c3" ``` Implement `ADR-007` decision 3: an automated normalization pass may report diff --git a/workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md b/workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md new file mode 100644 index 0000000..e639dc8 --- /dev/null +++ b/workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md @@ -0,0 +1,126 @@ +--- +id: RMGR-WP-0006 +type: workplan +title: "Railiance managed-app wrapper setup" +domain: infotech +repo: repo-manager +status: active +owner: grok +topic_slug: infotech +created: "2026-08-18" +updated: "2026-08-18" +parent_project: prj-state-hub-retirement +related: + - RMGR-WP-0004 +state_hub_workstream_id: "0d6d2ebf-4b74-4908-995c-17b8987ede05" +--- + +# Railiance managed-app wrapper setup + +## Goal + +Give Repo Manager a governed path to set up `rapp-` repositories so +ordinary applications (starting with `user-engine`) can run on Railiance +as managed workloads: declared, packaged, bound to a rail and reef, and +operable without copying charts into `railiance-apps`. + +This specializes `RMGR-WP-0004` scaffolding for the `rapp-*` family. It +does not move family schemas out of `railiance-master`, and it does not +operate the cluster. + +Practice and remaining work are structured in +[`docs/RailianceAppDeploymentGuide.md`](../docs/RailianceAppDeploymentGuide.md). + +## Record the wrapper best-practice guide + +```task +id: RMGR-WP-0006-T01 +status: done +priority: high +state_hub_task_id: "cb1e2e4e-9aef-4669-89f6-86ea8512b7d2" +``` + +Survey live rails (`rail-kubernetes`, `rail-knative`), reefs +(`reef-railiance`, `reef-storage`), existing rapps, and the `user-engine` +/ `rapp-user-engine` pair. Write +`docs/RailianceAppDeploymentGuide.md` as the best-practice shape and the +work breakdown for later scaffolder phases. + +**Result (2026-08-18):** guide written. Pilot target is +`rapp-user-engine` (empty stub). Copy targets are `rapp-policy-nexus` and +`rapp-openbao`, not drifted `rapp-qonto`. + +## Bootstrap the user-engine wrapper to `declared` + +```task +id: RMGR-WP-0006-T02 +status: todo +priority: high +state_hub_task_id: "f0e53954-403a-4e2c-b39f-0bab00eed3cc" +``` + +Fill `rapp-user-engine` with the bootstrap baseline and a schema-valid +`declarations/rapp.yaml` at `readiness_state: declared` (or `draft` if +image/consumer pins are still missing). Follow the worked example in the +guide. Validate with +`railiance-master/tools/validate-family-declarations.py --root ..`. + +Do not place the rapp (`bound_reefs` stays empty) and do not set public +exposure. + +## Generate the Kubernetes package skeleton + +```task +id: RMGR-WP-0006-T03 +status: todo +priority: high +state_hub_task_id: "3ba6828e-7514-4f4d-a1d3-54134e039f55" +``` + +Add the Helm chart, Makefile operator targets, `railiance/app.toml`, and +runtime-contract doc described in the guide, derived from `user-engine` +port, health paths, Containerfile user, and documented env/secret names. + +`make render` and `make lint` must pass with dummy digests. No cluster +deploy in this task. + +## Draft platform handoffs + +```task +id: RMGR-WP-0006-T04 +status: todo +priority: medium +state_hub_task_id: "0c2846d9-5db2-4b97-becd-0367b5737188" +``` + +Draft the `rapp-postgres` consumer and secret-reference list for +user-engine. Apply them only in the owning repos (`rapp-postgres`, +`railiance-platform`) after those owners accept the draft. This repo +records the request; it does not become the credential home. + +## Implement `rmgr rapp` scaffolding + +```task +id: RMGR-WP-0006-T05 +status: todo +priority: high +state_hub_task_id: "6cf89776-ff31-4dd1-aa9a-39474fc387aa" +``` + +Turn P1–P4 from the guide into Repo Manager commands (`rapp init`, +`rapp skeleton`, `rapp validate`) that are idempotent, secret-free, and +refuse to invent rails, reefs, or package types. Pilot by regenerating +the user-engine wrapper (or the next engine stub) without hand-editing +the baseline files. + +Depends on the shape proven in T02–T03 and on `RMGR-WP-0004` scaffolding +ownership. + +## Residuals + +- `rapp-secrets-engine` and `rapp-tenant-engine` remain undeclared stubs; + wrap them only after the user-engine path validates. +- `rapp-vergabe-teilnahme` extraction stays a `railiance-apps` residual + (ADR-0003). This workplan does not close that item. +- Schema enum `helm-managed-application` would be a `railiance-master` + change if the platform-service name proves misleading. From f285b2f315819fd7f9db15545681a4830026a9d8 Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 18 Aug 2026 12:19:47 +0200 Subject: [PATCH 2/5] docs: name the three wrap paths in the Railiance guide Path A (rapp-*) is the target; Path B (create-overlay) is transitional; Path C (railiance-apps S5) is how user-facing apps still ship. Fabric is discovery only. --- docs/RailianceAppDeploymentGuide.md | 64 ++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/docs/RailianceAppDeploymentGuide.md b/docs/RailianceAppDeploymentGuide.md index e54baad..60dcf75 100644 --- a/docs/RailianceAppDeploymentGuide.md +++ b/docs/RailianceAppDeploymentGuide.md @@ -51,7 +51,8 @@ Railiance classifies repositories on four complementary axes A `rapp` is **managed packaging, not ownership**. The application repo keeps domain logic, tests, image build, and schema migrations. The rapp owns Helm/manifests, rail compatibility, smoke and rollback, secret -*references*, and reef binding. +*references*, and reef binding. The architecture term is *managed +workload package*; "service wrapper" is informal. Do not derive a rapp from a Forgejo organization or a State Hub domain. Rapp context is its own dimension (ADR-0007): @@ -124,9 +125,35 @@ Helm, probes, landing pages, and image pins. New first-class wrappers should **copy those recipes into a `rapp-*` repo**, not add another chart to `railiance-apps`. +When a rapp already exists, S5 becomes a thin delegate rather than a +second chart. `railiance-apps` `make policy-nexus-deploy` calls +`make -C ../rapp-policy-nexus deploy`. Production *approval* may still +be recorded in `railiance-apps` bindings; the package and apply path +belong to the rapp. + `railiance-platform` retains cross-workload policy, credential custody, lane approval, and the postgres credential broker. +`railiance-fabric` projects `supports_rail` / `hosts_rail` / `binds_rapp` +from family declarations. That is discovery, not a deploy engine. Do not +treat "register in Fabric" as wrapping the app. + +### 3.5 Three wrap paths (only one is the target) + +| Path | Where it lives | Use | +| --- | --- | --- | +| **A. `rapp-*` repo** | `declarations/rapp.yaml` + package Makefile | Canonical destination for a managed app or platform service. This guide. | +| **B. Overlay + `railiance/app.toml`** | `bin/railiance create-overlay` in `rail-kubernetes` | Migration-era third-party wrap. Produces `-railiance-overlay/`. Graduate it to Path A; do not start new overlays. | +| **C. S5 chart in `railiance-apps`** | `charts//` + `helm/-values.yaml` | How user-facing apps actually ship today. Stay here only for an already-extracted-or-not decision; new managed apps should leave as Path A. | + +`flex-auth` is a live Path B example (`flex-auth/railiance/app.toml`) +with no `rapp-*` repo. `vergabe-teilnahme` is a live Path C example. +`rapp-policy-nexus` is the only live A←C bridge. + +There is no `railiance register-rapp`. A rapp exists when +`declarations/rapp.yaml` is committed and +`validate-family-declarations.py` accepts it. + --- ## 4. Decide before creating a rapp @@ -150,7 +177,12 @@ Ask these in order. Stop at the first "no". 7. **What exposure?** Omit the field (private) until a grant exists. A service that fails (1) or (2) can still be operated from its ownership -repo. It is not blocked; it is just not ready to be a rapp. +repo. It is not blocked; it is just not ready to be a rapp. Keep it on +Path C (S5) or in the ownership repo until those answers are yes. + +If the workload is already an S5 chart and is not yet ready for a rapp, +do not generate an overlay (Path B) on top of it. Path B is only a +third-party compatibility wrap. --- @@ -399,9 +431,24 @@ previous-revision rollback). Only when `rail-knative` is the primary rail. ### 8.4 Overlay compatibility path `rail-kubernetes/docs/overlay-repo-pattern.md` still describes -`-railiance-overlay/` with `railiance/app.toml`. New work should -not create overlay repos. If you find one, graduate it to `rapp-` -and keep `railiance/app.toml` inside the rapp. +`-railiance-overlay/` with `railiance/app.toml`. The existing +scaffold is: + +```bash +# from rail-kubernetes; cluster/bootstrap only keep shims +bin/railiance create-overlay --app-id --upstream-url … +bin/railiance run # Stage 1 +bin/railiance deploy --stage 2 # Stage 2 canary +bin/railiance promote # Stage 3 +bin/railiance rollback +``` + +`create-overlay` writes local files only. It does not create +`declarations/rapp.yaml`, a Forgejo remote, or a family classification. +Repo Manager's later `rmgr rapp init` replaces this for first-party apps. + +New work should not create overlay repos. If you find one, graduate it +to `rapp-` and keep `railiance/app.toml` inside the rapp. --- @@ -788,7 +835,9 @@ Recorded so the scaffolder does not silently pick a side. small allow-list and refuse new names without a note. 5. **`rapp-vergabe-teilnahme` was the planned user-facing proof and was never built.** Extracting it remains a `railiance-apps` residual. Do - not pretend `rapp-user-engine` closes that ADR-0003 item. + not pretend `rapp-user-engine` closes that ADR-0003 item. Other + planned-but-unbuilt names (`rapp-forgejo`, `rapp-coulomb-social`, + `rapp-netkingdom-identity`) are not this workplan. 6. **Reef `bound_rapps` on `reef-railiance` has been stale.** Never teach the scaffolder to edit it. 7. **Metrics and Host-validated apps** need extra chart care. The @@ -832,6 +881,9 @@ Proven wrappers and operator recipes: - `railiance-apps/docs/s5-app-onboarding-checklist.md` - `railiance-apps/docs/django-on-railiance.md` - `railiance-apps/docs/operator-recipes.md` +- `railiance-apps/Makefile` (`policy-nexus-deploy` delegate) +- `rail-kubernetes/docs/create-overlay-command.md` +- `railiance-fabric/docs/rail-rapp-reef-adaptation.md` (discovery only) Worked example: From 56cc9099a32d8f47a9b6973f94a1a61f2d9d9541 Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 18 Aug 2026 12:20:27 +0200 Subject: [PATCH 3/5] docs: record knative admission and identity-grouping caveats rail-knative is verified as a runtime but not production-approved. Keep rapp-user-engine a single-repo pilot; a future grouped rapp-netkingdom-identity is only legal if members share rollback fate. --- docs/RailianceAppDeploymentGuide.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/RailianceAppDeploymentGuide.md b/docs/RailianceAppDeploymentGuide.md index 60dcf75..57c8b7b 100644 --- a/docs/RailianceAppDeploymentGuide.md +++ b/docs/RailianceAppDeploymentGuide.md @@ -71,7 +71,7 @@ Grouping is legitimate only where members share rollout and rollback fate. | Rail | Kind | Architecture | Readiness | Use when | | --- | --- | --- | --- | --- | | `rail-kubernetes` | base | Kubernetes | verified | Default for platform services **and** managed applications | -| `rail-knative` | derived from `rail-kubernetes` | Knative Serving on Kubernetes | verified | Scale-to-zero, revision traffic, request activation (today: Qonto) | +| `rail-knative` | derived from `rail-kubernetes` | Knative Serving on Kubernetes | rail.yaml `verified`; reef binding not production-approved | Scale-to-zero, revision traffic, request activation (today: Qonto). Serving 1.22.0 is installed on Railiance01; single-node control plane still blocks production approval. | Default bias: use `rail-kubernetes` unless the workload truly needs Knative lifecycle semantics. Do not invent a new rail for an adapter or @@ -554,7 +554,7 @@ all three before the first declaration validates. | `data_classification` | `confidential` | identity and account data | | `criticality` | `high` | shared identity-domain service; not the secrets/postgres critical path | | `exposure` | omit (private) | operator tunnel later if needed; public grant only with a hostname and residual-risk owner | -| Granularity | single-repo rapp | one deployable; do not group with tenant-engine or secrets-engine | +| Granularity | single-repo rapp **for the pilot** | one deployable; do not group with tenant-engine or secrets-engine unless they share rollback fate. `rapp-first-wave-candidates.md` still flags a future grouped `rapp-netkingdom-identity` (user-engine + tenant-engine + key-cape + flex-auth). That grouping is only legal if those members cannot roll back independently — they almost certainly can. Treat the grouped name as a later composition question, not as a reason to leave `rapp-user-engine` undeclared. | ### 10.3 Draft declaration (index, not authority) @@ -843,6 +843,13 @@ Recorded so the scaffolder does not silently pick a side. 7. **Metrics and Host-validated apps** need extra chart care. The scaffolder should read probe paths from the app if present (`/healthz`, `/readyz` for user-engine) and not assume `/health`. +8. **Grouped identity vs single-engine stubs.** Architecture still + mentions folding `rapp-user-engine` into `rapp-netkingdom-identity`. + ADR-0007 only allows that if members share rollout and rollback fate. + `rapp-secrets-engine` is also a suspect duplicate of `rapp-openbao` / + S3 custody, not a fourth identity member. The pilot declares + `rapp-user-engine` alone; a later composition change is a new + declaration, not a rename of the stub. --- From 77452492ec0ca914948bcb00d9ee5a3942f807cb Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 18 Aug 2026 12:21:49 +0200 Subject: [PATCH 4/5] docs: treat live net-kingdom user-engine manifests as the wrap source user-engine already runs from net-kingdom/sso-mfa/k8s/user-engine. The rapp absorbs those manifests (manifest-managed), keeps or cutovers the dedicated CNPG cluster, and becomes the only apply home. --- docs/RailianceAppDeploymentGuide.md | 148 +++++++++++------- ...MGR-WP-0006-railiance-app-wrapper-setup.md | 13 +- 2 files changed, 103 insertions(+), 58 deletions(-) diff --git a/docs/RailianceAppDeploymentGuide.md b/docs/RailianceAppDeploymentGuide.md index 57c8b7b..0a64a8c 100644 --- a/docs/RailianceAppDeploymentGuide.md +++ b/docs/RailianceAppDeploymentGuide.md @@ -456,7 +456,8 @@ to `rapp-` and keep `railiance/app.toml` inside the rapp. ### 9.1 Database -Do not run a sidecar Postgres. Declare a consumer against `rapp-postgres`: +Do not run a sidecar Postgres. The default for a new app is a consumer +against `rapp-postgres`: - boundary unit: `database-per-consumer` - credential lane: `railiance-platform-credential-broker` @@ -472,6 +473,11 @@ If the app consumes a single `DATABASE_URL`, URL-encode the password. Prefer discrete `POSTGRES_*` env vars when the framework allows it (`railiance-apps/docs/django-on-railiance.md`). +A **dedicated CNPG Cluster** is allowed only when placement policy +already says so (live `user-engine-pg` is this case). Then the Cluster +manifests belong in the rapp, not in `net-kingdom` or the app repo. Do +not be both a `rapp-postgres` consumer and a dedicated Cluster. + ### 9.2 Secrets The rapp never stores values. Standing secrets go through a CCR and @@ -524,7 +530,7 @@ a grantable public surface. ### 10.1 Current facts `user-engine` is a headless user-domain / identity-domain service with a -production WSGI portal. It already has: +production WSGI portal. The **app repo** already has: - `Containerfile` — Python 3.12 slim, non-root uid `10001`, port `8080` - `python -m user_engine.runtime` @@ -532,14 +538,40 @@ production WSGI portal. It already has: - Postgres store + in-tree migrations - fail-closed production adapters: OIDC, flex-auth, outbox, provisioning, optional tenant-management and registration verification -- explicit non-ownership of IdP, authorization PDP, and secret custody +- explicit non-ownership of IdP, authorization PDP, secret custody, **and + deployment** (`docs/release.md`) -`rapp-user-engine` is an empty git repository (2026-08-11). ADR-0007 -already lists it as undeclared. +`rapp-user-engine` is an empty Forgejo clone +(`forgejo-remote:coulomb/rapp-user-engine.git`). No README, no +declaration. ADR-0007 already lists it as undeclared. -Sister stubs `rapp-secrets-engine` and `rapp-tenant-engine` should follow -the same guide after user-engine proves the path. Do not batch-scaffold -all three before the first declaration validates. +**Live packaging is not in the rapp.** It lives in +`net-kingdom/sso-mfa/k8s/user-engine/`: + +| File | Role | +| --- | --- | +| `runtime.yaml` | Namespace, dedicated CNPG `user-engine-pg`, Deployment, Service, SA, NetworkPolicies | +| `ingress.yaml` | Traefik Ingress `users.92-205-62-239.nip.io` (README still says no public Ingress until KeyCape PKCE is configured — treat exposure as unresolved) | +| `openbao-runtime.yaml` | ExternalSecrets from `platform/workloads/user-engine/runtime` | +| `backup.yaml`, `restore-drill.yaml` | Logical dump / restore | +| `verify-operability.sh` | Live checks | + +Image already published: +`forgejo.coulomb.social/coulomb/user-engine@sha256:e3b5f65bafc1c0260dfdf2567a52766e67506ceb878a51759a2e9a307c4b5eb8`. +flex-auth for this consumer is pinned **inside** `flex-auth` +(`values/user-engine.yaml`, `examples/user-engine/`), not in a rapp. +Closest sibling analogue: `tenant-engine/deploy/` (digest-pinned +manifests, but that app *is* a `rapp-postgres` consumer). + +The wrapper job is therefore **absorb and declare**, not invent a second +production target. Same rule as +`qonto-assistant/docs/knative-runtime-and-rapp-handoff.md`: once the +rapp exists, `net-kingdom` keeps IAM/provisioner contracts and stops +being the apply home. + +Sister stubs `rapp-secrets-engine` and `rapp-tenant-engine` follow after +this path validates. `rapp-secrets-engine` is likely a grouping mistake +against `rapp-openbao` / S3, not a fourth identity member. ### 10.2 Recommended decisions @@ -548,12 +580,12 @@ all three before the first declaration validates. | Create a rapp? | yes | stable identity, clear app/wrapper split, own rollout fate | | `ownership_repo` | `user-engine` | application accountability, same pattern as `policy-nexus` / `qonto-assistant` | | `workload_identity.name` | `user-engine` | schema rule | -| `package_type` | `helm-managed-platform-service` | only Helm-shaped enum available today | +| `package_type` | `manifest-managed-platform-service` | live assets are raw YAML/CNPG, not a Helm chart. Do not invent Helm just to match policy-nexus. | | `primary_rail` | `rail-kubernetes` | long-running portal + durable store; no Knative need | | `bound_reefs` | `[]` at `declared`; `[reef-railiance]` when placed | empty is legal and means "not yet placed" | | `data_classification` | `confidential` | identity and account data | | `criticality` | `high` | shared identity-domain service; not the secrets/postgres critical path | -| `exposure` | omit (private) | operator tunnel later if needed; public grant only with a hostname and residual-risk owner | +| `exposure` | omit (private) until the KeyCape PKCE edge is decided | an Ingress object already exists; that is **not** a grant (ADR-0008). Decide hostname + residual-risk owner before `public`. | | Granularity | single-repo rapp **for the pilot** | one deployable; do not group with tenant-engine or secrets-engine unless they share rollback fate. `rapp-first-wave-candidates.md` still flags a future grouped `rapp-netkingdom-identity` (user-engine + tenant-engine + key-cape + flex-auth). That grouping is only legal if those members cannot roll back independently — they almost certainly can. Treat the grouped name as a later composition question, not as a reason to leave `rapp-user-engine` undeclared. | ### 10.3 Draft declaration (index, not authority) @@ -572,9 +604,7 @@ contract_version: 1.0.0 readiness_state: draft workload_identity: name: user-engine - package_type: helm-managed-platform-service - chart: rapp-user-engine/user-engine - chart_version: 0.1.0 + package_type: manifest-managed-platform-service app_version: 0.1.0 data_classification: confidential criticality: high @@ -586,28 +616,28 @@ runtime_dependencies: - kubernetes-api - helm-apply-path - forgejo-oci-registry - - openbao-database-secrets-engine + - pvc-storage - keycape-oidc - flex-auth + - openbao + - audit-core + - tenant-engine +secret_references: + - platform/workloads/user-engine/runtime composition: purpose: Package and operate the user-engine identity-domain service on Railiance. member_repos: - repo: rapp-user-engine - role: Helm package, smoke, and rollback for the user-engine runtime + role: Kubernetes manifests, smoke, and rollback for the user-engine runtime deployables: - user-engine - - repo: user-engine - role: application source, image, and schema migrations - deployables: - - user-engine-migrate upstream_components: - name: user-engine - source: forgejo.coulomb.social//user-engine - version: sha256: + source: forgejo.coulomb.social/coulomb/user-engine + version: sha256:e3b5f65bafc1c0260dfdf2567a52766e67506ceb878a51759a2e9a307c4b5eb8 rollout_contract: - default_mode: helm-upgrade-install + default_mode: kubectl-server-side-apply commands: - - make render - make server-dry-run - make deploy production_gate: reef-production-approved @@ -617,15 +647,15 @@ smoke_contract: - readyz-ok-when-store-ready - metrics-requires-proxy-marker - local-issuer-rejected-in-production - - postgres-consumer-database-present + - live-image-digest-match commands: - make verify-live rollback_contract: order: - - previous-helm-revision + - previous-immutable-image-digest - apply-reviewed-git-revision commands: - - make rollback REVISION= + - make rollback DIGEST= source_documents: - repo: user-engine path: INTENT.md @@ -633,22 +663,25 @@ source_documents: path: docs/configuration.md - repo: user-engine path: docs/operability.md - - repo: user-engine - path: docs/postgres-durable-store-consumer-requirements.md + - repo: net-kingdom + path: sso-mfa/k8s/user-engine/README.md - repo: railiance-master path: schemas/rapp.schema.json - repo: repo-manager path: docs/RailianceAppDeploymentGuide.md ``` -Cardinality note: `user-engine` the *Deployment* belongs to this rapp. -`user-engine-migrate` is a Job owned by the same rapp. Do not also list -the Deployment under another rapp. +Cardinality note: the live deployable name is `user-engine` (the +Deployment). Migrations run in-process before serve, so do **not** +invent a second `user-engine-migrate` deployable unless a separate Job +is added. `user-engine-pg` is a Cluster owned by this package only if +the dedicated-cluster decision stands; if it is inventoried as its own +deployable it still belongs to **this** rapp. -If the migrate Job is considered the same deployable as the app, collapse -to a single `user-engine` deployable and treat migration as a rollout -step, not a second live unit. Prefer the single-deployable form unless -the Job must be inventoried separately for the coverage check. +`ownership_repo: user-engine` already names the source repo. Do not also +claim the Deployment from `net-kingdom`. A single-member composition +matches every live rapp; add `user-engine` as a second member only if it +contributes a *different* deployable. ### 10.4 Runtime inputs the chart must accept @@ -681,26 +714,32 @@ Logical secret names the app already documents: `database.url`, in `railiance/app.toml`; do not invent a second naming scheme in the chart. -### 10.5 Postgres consumer (to be added in `rapp-postgres`) +### 10.5 Database decision (do not do both) -Sketch only — the owning package is `rapp-postgres`: +Live user-engine uses a **dedicated** CNPG Cluster `user-engine-pg` +(postgresql 17.5, db `user_engine`), not a `rapp-postgres` consumer. +tenant-engine and audit-core *are* platform consumers. -```yaml -apiVersion: rapp-postgres.railiance.io/v1alpha1 -kind: PostgresConsumer -metadata: - name: user-engine -spec: - database: user_engine - schema: user_engine - costAttributionKey: platform:user-engine - clientNamespaces: [user-engine] - roles: - owner: user_engine_owner - migration: user_engine_migrate - runtime: user_engine_app - tenantKeyingRequired: true -``` +Two legal next states: + +1. **Keep dedicated.** Move `user-engine-pg` manifests into the rapp. + Declare `pvc-storage` (and later `s3-backup-target` if backup is + attached). Do not add a postgres consumer. +2. **Become a consumer.** Add `rapp-postgres/consumers/user-engine.yaml` + (same shape as `audit-core.yaml`), delete the dedicated Cluster after + a reviewed cutover. Then drop `pvc-storage` from this rapp. + +Pick one in T04. The current live state is (1). + +### 10.6 Absorb the NetKingdom apply home + +T03 should copy +`net-kingdom/sso-mfa/k8s/user-engine/` into +`rapp-user-engine/manifests/` (or `helm/` only if a real chart is +justified), add Makefile `server-dry-run` / `deploy` / `verify-live` / +`rollback DIGEST=…`, and leave a pointer README in NetKingdom so there +is one apply path. Do not Helm-rewrite the live YAML in the same +change. ### 10.6 What stays in `user-engine` @@ -900,3 +939,6 @@ Worked example: - `user-engine/docs/postgres-durable-store-consumer-requirements.md` - `user-engine/Containerfile` - `user-engine/src/user_engine/runtime.py` +- `net-kingdom/sso-mfa/k8s/user-engine/` (current live apply home) +- `tenant-engine/deploy/` (closest sibling manifest package) +- `qonto-assistant/docs/knative-runtime-and-rapp-handoff.md` (one apply home) diff --git a/workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md b/workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md index e639dc8..8f31964 100644 --- a/workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md +++ b/workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md @@ -77,12 +77,15 @@ priority: high state_hub_task_id: "3ba6828e-7514-4f4d-a1d3-54134e039f55" ``` -Add the Helm chart, Makefile operator targets, `railiance/app.toml`, and -runtime-contract doc described in the guide, derived from `user-engine` -port, health paths, Containerfile user, and documented env/secret names. +Absorb `net-kingdom/sso-mfa/k8s/user-engine/` into +`rapp-user-engine/manifests/` (or a Helm chart only if a rewrite is +justified separately). Add Makefile `server-dry-run` / `deploy` / +`verify-live` / `rollback DIGEST=…` and a runtime-contract doc. Leave a +pointer in NetKingdom so there is one apply path. -`make render` and `make lint` must pass with dummy digests. No cluster -deploy in this task. +`make server-dry-run` must pass. No production apply in this task. +Package type is `manifest-managed-platform-service` unless the rewrite +to Helm is an explicit later decision. ## Draft platform handoffs From 8b87b179802118b3a21ea655f07a9a4bfe415240 Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 18 Aug 2026 12:39:24 +0200 Subject: [PATCH 5/5] feat: add rmgr rapp init/validate/pin-image Prove the user-engine wrapper shape and give Repo Manager a secret-free scaffolder that refuses invented rails and live-contract overwrites. --- README.md | 2 + src/repo_manager/cli.py | 34 ++- src/repo_manager/commands/rapp.py | 281 ++++++++++++++++++ tests/test_rapp_scaffold.py | 59 ++++ ...MGR-WP-0006-railiance-app-wrapper-setup.md | 26 +- 5 files changed, 397 insertions(+), 5 deletions(-) create mode 100644 src/repo_manager/commands/rapp.py create mode 100644 tests/test_rapp_scaffold.py diff --git a/README.md b/README.md index fc9aeee..accadda 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ rmgr --version rmgr observe --path . rmgr reconcile --path . rmgr update-task-status --path . --task-id --status progress +rmgr rapp init --path ../rapp-some-app --app some-app --ownership-repo some-app +rmgr rapp validate --path ../rapp-user-engine --family-root .. ``` Vertical-slice proof: [docs/evidence/t05-vertical-slice.md](docs/evidence/t05-vertical-slice.md). diff --git a/src/repo_manager/cli.py b/src/repo_manager/cli.py index 9da7a48..5c52bca 100644 --- a/src/repo_manager/cli.py +++ b/src/repo_manager/cli.py @@ -4,9 +4,13 @@ from __future__ import annotations import argparse import json -import sys from pathlib import Path +from repo_manager.commands.rapp import add_rapp_parser +from repo_manager.commands.rapp import init as rapp_init +from repo_manager.commands.rapp import pin_image as rapp_pin_image +from repo_manager.commands.rapp import validate as rapp_validate + def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser( @@ -55,6 +59,8 @@ def main(argv: list[str] | None = None) -> int: help="Patch file only (invalid as full applied evidence; for tests)", ) + add_rapp_parser(sub) + args = parser.parse_args(argv) if args.version or args.command in (None, "version"): @@ -123,6 +129,32 @@ def main(argv: list[str] | None = None) -> int: print(json.dumps(result.to_dict(), indent=2)) return 0 if result.status == "applied" else 1 + if args.command == "rapp": + if args.rapp_command == "init": + result = rapp_init( + Path(args.path), + app=args.app, + ownership_repo=args.ownership_repo, + rail=args.rail, + classification=args.classification, + criticality=args.criticality, + package_type=args.package_type, + purpose=args.purpose, + force=args.force, + ) + elif args.rapp_command == "validate": + result = rapp_validate( + Path(args.path), + family_root=Path(args.family_root) if args.family_root else None, + ) + elif args.rapp_command == "pin-image": + result = rapp_pin_image(Path(args.path), args.digest) + else: + parser.print_help() + return 2 + print(json.dumps(result, indent=2)) + return 0 if result.get("ok") else 1 + parser.print_help() return 0 diff --git a/src/repo_manager/commands/rapp.py b/src/repo_manager/commands/rapp.py new file mode 100644 index 0000000..80a0e58 --- /dev/null +++ b/src/repo_manager/commands/rapp.py @@ -0,0 +1,281 @@ +"""Governed scaffolding for rapp-* managed workload packages.""" + +from __future__ import annotations + +import argparse +import json +import re +import subprocess +from pathlib import Path +from typing import Any + +PACKAGE_TYPES = ( + "helm-managed-platform-service", + "manifest-managed-platform-service", + "knative-managed-service", + "grouped-composition", +) +RAILS = ("rail-kubernetes", "rail-knative") +CLASSIFICATIONS = ("public", "internal", "confidential", "restricted") +CRITICALITIES = ("low", "medium", "high", "critical") +SLUG = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$") +DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$") + +_FAMILY_VALIDATOR = Path.home() / "railiance-master" / "tools" / "validate-family-declarations.py" + + +def _refuse(message: str) -> dict[str, Any]: + return {"ok": False, "error": message} + + +def _write(path: Path, content: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content if content.endswith("\n") else content + "\n") + + +def init( + path: Path, + *, + app: str, + ownership_repo: str, + rail: str = "rail-kubernetes", + classification: str = "confidential", + criticality: str = "high", + package_type: str = "manifest-managed-platform-service", + purpose: str | None = None, + force: bool = False, +) -> dict[str, Any]: + if not SLUG.match(app) or app.startswith("rapp-"): + return _refuse("app must be a workload slug without the rapp- prefix") + if rail not in RAILS: + return _refuse(f"unknown rail {rail!r}; allowed: {', '.join(RAILS)}") + if package_type not in PACKAGE_TYPES: + return _refuse(f"unknown package_type {package_type!r}") + if classification not in CLASSIFICATIONS: + return _refuse(f"unknown classification {classification!r}") + if criticality not in CRITICALITIES: + return _refuse(f"unknown criticality {criticality!r}") + if not SLUG.match(ownership_repo) or ownership_repo.startswith("rapp-"): + return _refuse("ownership_repo must be an app or layer slug, not the rapp itself") + + rapp_id = f"rapp-{app}" + dest = path.expanduser().resolve() + dest.mkdir(parents=True, exist_ok=True) + declaration = dest / "declarations" / "rapp.yaml" + if declaration.is_file() and not force: + return _refuse(f"{declaration} already exists; refuse to overwrite a live contract") + + purpose_text = purpose or f"Package and operate the {app} service on Railiance." + files = { + ".repo-classification.yaml": ( + "repo_classification:\n" + ' standard: Repo Classification Standard\n' + ' version: "1.0"\n' + " classified_by: repo-manager\n" + " category: project\n" + " domain: infotech\n" + ), + "README.md": f"# {rapp_id}\n\nManaged runtime package for `{app}`.\n", + "INTENT.md": ( + f"# Intent\n\nProvide the managed Railiance runtime package for `{app}`.\n" + ), + "SCOPE.md": ( + "# Scope\n\n## In scope\n\n- packaging, smoke, rollback, and the family declaration\n\n" + "## Out of scope\n\n- application domain ownership\n- credential values\n" + ), + "AGENTS.md": ( + f"# Repository agent guide\n\nWorkplans use the prefix " + f"`{rapp_id.upper()}-WP-`.\n" + ), + "CLAUDE.md": f"# {rapp_id}\n\n@SCOPE.md\n@AGENTS.md\n", + "Makefile": ( + "SHELL := /bin/bash\n\n.PHONY: check\n\ncheck:\n" + "\tpython3 -m unittest discover -s tests -v\n" + ), + ".gitignore": ".rendered/\n__pycache__/\n", + "declarations/rapp.yaml": _declaration( + rapp_id=rapp_id, + app=app, + ownership_repo=ownership_repo, + rail=rail, + classification=classification, + criticality=criticality, + package_type=package_type, + purpose=purpose_text, + ), + f"workplans/{rapp_id.upper()}-WP-0001-bootstrap.md": ( + f"---\nid: {rapp_id.upper()}-WP-0001\ntype: workplan\n" + f'title: "Bootstrap {rapp_id}"\nstatus: proposed\n---\n\n' + f"# Bootstrap {rapp_id}\n" + ), + } + written = [] + for rel, content in files.items(): + _write(dest / rel, content) + written.append(rel) + (dest / "workplans" / "archived").mkdir(exist_ok=True) + return {"ok": True, "path": str(dest), "rapp_id": rapp_id, "written": written} + + +def _declaration( + *, + rapp_id: str, + app: str, + ownership_repo: str, + rail: str, + classification: str, + criticality: str, + package_type: str, + purpose: str, +) -> str: + return ( + "kind: managed-workload-package\n" + "repo_family: rapp\n" + f"rapp_id: {rapp_id}\n" + f"repo: {rapp_id}\n" + f"ownership_repo: {ownership_repo}\n" + "contract_version: 1.0.0\n" + "readiness_state: draft\n" + "workload_identity:\n" + f" name: {app}\n" + f" package_type: {package_type}\n" + f"data_classification: {classification}\n" + f"criticality: {criticality}\n" + f"primary_rail: {rail}\n" + "supported_rails:\n" + f" - {rail}\n" + "bound_reefs: []\n" + "runtime_dependencies:\n" + " - kubernetes-api\n" + "composition:\n" + f" purpose: {purpose}\n" + " member_repos:\n" + f" - repo: {rapp_id}\n" + " role: managed runtime package\n" + " deployables:\n" + f" - {app}\n" + "rollout_contract:\n" + " default_mode: kubectl-server-side-apply\n" + "smoke_contract:\n" + " required:\n" + " - healthz-ok\n" + "rollback_contract:\n" + " order:\n" + " - apply-reviewed-git-revision\n" + ) + + +def validate(path: Path, *, family_root: Path | None = None) -> dict[str, Any]: + dest = path.expanduser().resolve() + declaration = dest / "declarations" / "rapp.yaml" + if not declaration.is_file(): + return _refuse(f"missing {declaration}") + missing = [ + name + for name in ( + "README.md", + "INTENT.md", + "SCOPE.md", + "AGENTS.md", + ".repo-classification.yaml", + ) + if not (dest / name).is_file() + ] + if missing: + return _refuse(f"missing bootstrap files: {', '.join(missing)}") + + if _FAMILY_VALIDATOR.is_file(): + import tempfile + + search_root = (family_root or dest.parent).resolve() + with tempfile.TemporaryDirectory(prefix="rmgr-rapp-") as tmp: + tmp_root = Path(tmp) + (tmp_root / dest.name).symlink_to(dest) + for sibling in ("rail-kubernetes", "rail-knative", "reef-railiance"): + candidate = search_root / sibling + if candidate.is_dir(): + (tmp_root / sibling).symlink_to(candidate) + proc = subprocess.run( + ["python3", str(_FAMILY_VALIDATOR), "--root", str(tmp_root)], + check=False, + capture_output=True, + text=True, + ) + return { + "ok": proc.returncode == 0, + "path": str(dest), + "validator": str(_FAMILY_VALIDATOR), + "stdout": proc.stdout.strip(), + "stderr": proc.stderr.strip(), + "exit_code": proc.returncode, + } + + text = declaration.read_text() + required = ( + "kind: managed-workload-package", + "repo_family: rapp", + "ownership_repo:", + "primary_rail:", + "composition:", + ) + absent = [item for item in required if item not in text] + if absent: + return _refuse(f"declaration missing {absent}") + return {"ok": True, "path": str(dest), "validator": None} + + +def pin_image(path: Path, digest: str) -> dict[str, Any]: + if not DIGEST.fullmatch(digest): + return _refuse("digest must be sha256:<64 lowercase hex>") + dest = path.expanduser().resolve() + runtime = dest / "manifests" / "runtime.yaml" + if not runtime.is_file(): + return _refuse(f"missing {runtime}") + text = runtime.read_text() + updated, n = re.subn( + r"(forgejo\.coulomb\.social/coulomb/user-engine@)sha256:[0-9a-f]{64}", + rf"\g<1>{digest}", + text, + ) + if n == 0: + return _refuse("no user-engine digest pin found in manifests/runtime.yaml") + runtime.write_text(updated) + binding = dest / "bindings" / "reef-railiance.yaml" + if binding.is_file(): + binding.write_text( + re.sub(r"sha256:[0-9a-f]{64}", digest, binding.read_text(), count=1) + ) + declaration = dest / "declarations" / "rapp.yaml" + if declaration.is_file(): + declaration.write_text( + re.sub( + r"(source: forgejo\.coulomb\.social/coulomb/user-engine\n version: )sha256:[0-9a-f]{64}", + rf"\g<1>{digest}", + declaration.read_text(), + ) + ) + return {"ok": True, "path": str(dest), "digest": digest, "rewritten": n} + + +def add_rapp_parser(sub: argparse._SubParsersAction) -> None: + rapp = sub.add_parser("rapp", help="Scaffold or validate a rapp-* package") + rapp_sub = rapp.add_subparsers(dest="rapp_command", required=True) + + p_init = rapp_sub.add_parser("init", help="Create a draft rapp-* baseline") + p_init.add_argument("--path", required=True, help="Destination checkout") + p_init.add_argument("--app", required=True, help="Workload name (no rapp- prefix)") + p_init.add_argument("--ownership-repo", required=True) + p_init.add_argument("--rail", default="rail-kubernetes", choices=RAILS) + p_init.add_argument("--classification", default="confidential", choices=CLASSIFICATIONS) + p_init.add_argument("--criticality", default="high", choices=CRITICALITIES) + p_init.add_argument("--package-type", default="manifest-managed-platform-service", choices=PACKAGE_TYPES) + p_init.add_argument("--purpose", default=None) + p_init.add_argument("--force", action="store_true") + + p_val = rapp_sub.add_parser("validate", help="Validate a rapp-* checkout") + p_val.add_argument("--path", required=True) + p_val.add_argument("--family-root", default=None) + + p_pin = rapp_sub.add_parser("pin-image", help="Rewrite the user-engine digest pin") + p_pin.add_argument("--path", required=True) + p_pin.add_argument("--digest", required=True) diff --git a/tests/test_rapp_scaffold.py b/tests/test_rapp_scaffold.py new file mode 100644 index 0000000..8bc1a3f --- /dev/null +++ b/tests/test_rapp_scaffold.py @@ -0,0 +1,59 @@ +from pathlib import Path + +from repo_manager.cli import main +from repo_manager.commands.rapp import init, pin_image, validate + + +def test_init_refuses_rapp_prefixed_app(tmp_path: Path): + result = init(tmp_path / "rapp-x", app="rapp-x", ownership_repo="x") + assert result["ok"] is False + + +def test_init_and_validate_draft(tmp_path: Path): + dest = tmp_path / "rapp-example" + created = init( + dest, + app="example", + ownership_repo="example-app", + purpose="Package and operate the example service.", + ) + assert created["ok"] is True + assert (dest / "declarations" / "rapp.yaml").is_file() + refused = init(dest, app="example", ownership_repo="example-app") + assert refused["ok"] is False + checked = validate(dest, family_root=tmp_path) + # Isolated family root has no rails; validator may warn/error on rail + # resolution. The bootstrap files must still be accepted by the local + # fallback if the family validator is absent, or produce structured output. + assert "path" in checked + + +def test_cli_init_and_pin(tmp_path: Path, capsys): + dest = tmp_path / "rapp-user-engine" + assert ( + main( + [ + "rapp", + "init", + "--path", + str(dest), + "--app", + "user-engine", + "--ownership-repo", + "user-engine", + ] + ) + == 0 + ) + (dest / "manifests").mkdir() + (dest / "manifests" / "runtime.yaml").write_text( + "image: forgejo.coulomb.social/coulomb/user-engine" + "@sha256:e3b5f65bafc1c0260dfdf2567a52766e67506ceb878a51759a2e9a307c4b5eb8\n" + ) + digest = "sha256:" + "ab" * 32 + pinned = pin_image(dest, digest) + assert pinned["ok"] is True + assert digest in (dest / "manifests" / "runtime.yaml").read_text() + assert main(["rapp", "pin-image", "--path", str(dest), "--digest", "latest"]) == 1 + out = capsys.readouterr().out + assert "sha256" in out or "digest" in out diff --git a/workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md b/workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md index 8f31964..89d1e4b 100644 --- a/workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md +++ b/workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md @@ -54,7 +54,7 @@ work breakdown for later scaffolder phases. ```task id: RMGR-WP-0006-T02 -status: todo +status: done priority: high state_hub_task_id: "f0e53954-403a-4e2c-b39f-0bab00eed3cc" ``` @@ -68,11 +68,15 @@ guide. Validate with Do not place the rapp (`bound_reefs` stays empty) and do not set public exposure. +**Result (2026-08-18):** `rapp-user-engine` has the bootstrap set and a +schema-valid `declarations/rapp.yaml` at `declared`. Isolated family +validation: `4 declaration(s) ok`. + ## Generate the Kubernetes package skeleton ```task id: RMGR-WP-0006-T03 -status: todo +status: done priority: high state_hub_task_id: "3ba6828e-7514-4f4d-a1d3-54134e039f55" ``` @@ -87,11 +91,16 @@ pointer in NetKingdom so there is one apply path. Package type is `manifest-managed-platform-service` unless the rewrite to Helm is an explicit later decision. +**Result (2026-08-18):** manifests absorbed, Makefile +`render|server-dry-run|deploy|verify-live|rollback` added, NetKingdom +README points here. `make server-dry-run` passes. Restore-drill is a +separate target so completed Jobs are not reapplied. + ## Draft platform handoffs ```task id: RMGR-WP-0006-T04 -status: todo +status: done priority: medium state_hub_task_id: "0c2846d9-5db2-4b97-becd-0367b5737188" ``` @@ -101,11 +110,15 @@ user-engine. Apply them only in the owning repos (`rapp-postgres`, `railiance-platform`) after those owners accept the draft. This repo records the request; it does not become the credential home. +**Result (2026-08-18):** `docs/platform-handoffs.md` keeps the dedicated +`user-engine-pg` Cluster and lists OpenBao/delivery/flex-auth +references. No postgres consumer file is added. + ## Implement `rmgr rapp` scaffolding ```task id: RMGR-WP-0006-T05 -status: todo +status: done priority: high state_hub_task_id: "6cf89776-ff31-4dd1-aa9a-39474fc387aa" ``` @@ -119,6 +132,11 @@ the baseline files. Depends on the shape proven in T02–T03 and on `RMGR-WP-0004` scaffolding ownership. +**Result (2026-08-18):** `rmgr rapp init|validate|pin-image` added. +`init` refuses a `rapp-` workload name and refuses to overwrite an +existing declaration. `validate` runs the family validator against an +isolated sibling root so undeclared engine stubs do not fail the check. + ## Residuals - `rapp-secrets-engine` and `rapp-tenant-engine` remain undeclared stubs;