feat: greenfield rapp wrap path and rail-sequence rules

Add the §0 playbook and kubernetes-then-knative gate to the guide.
Implement rmgr rapp skeleton/wrap/place, draft postgres consumers,
and copy the fleet image workflow when missing.
This commit is contained in:
tegwick 2026-08-18 13:03:16 +02:00
parent 4743435f04
commit f953b1ebf5
6 changed files with 963 additions and 97 deletions

View file

@ -12,7 +12,55 @@ schema wins:
- [`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`.
Worked example (absorb path): `user-engine``rapp-user-engine` (§10).
Greenfield default: `rmgr rapp wrap` (§0). Rail choice: §3.1.
`RMGR-WP-0006` proved the contract. `RMGR-WP-0007` is the wrap path.
---
## 0. Greenfield playbook
Use this section to wrap an app that has **no rapp yet**. Do not start in
§10 unless you are absorbing an existing apply home.
```text
decide (§4, §3.1) → rmgr rapp wrap → human reviews drafts
→ CI publishes a digest → rmgr rapp pin-image
→ rmgr rapp place → operator deploy
```
```bash
rmgr rapp wrap \
--path ../rapp-<app> \
--app <app> \
--ownership-repo <app> \
--from-app ../<app>
rmgr rapp validate --path ../rapp-<app> --family-root ..
# after CI prints a digest:
rmgr rapp pin-image --path ../rapp-<app> --digest sha256:<64 hex>
# only when you intend to bind compute:
rmgr rapp place --path ../rapp-<app> --reef reef-railiance
```
`wrap` runs init → skeleton (or absorb `deploy/`) → app `image.yaml` if
missing → postgres consumer **draft** → validate. It stops. It does not
set `bound_reefs`, apply to a cluster, or grant public exposure.
Defaults:
| Decision | Default |
| --- | --- |
| Rail | `rail-kubernetes` (§3.1). Not Knative. |
| Package type | `manifest-managed-platform-service`. Helm only if a chart already exists. |
| Database | `rapp-postgres` consumer draft. No dedicated Cluster. |
| Image | fleet `.forgejo/workflows/image.yaml` in the **app** repo. Pin by digest in the rapp. |
| `railiance/app.toml` | optional; not part of the first wrap |
| Exposure | omitted (private) |
Still human: the purpose sentence, classification/criticality, NetworkPolicy
review, CCR / consumer **apply**, production gate, any public grant.
---
@ -25,14 +73,14 @@ 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.
1. How to wrap a new app from scratch (§0).
2. Which rail to start on, and when Knative is justified (§3.1).
3. What a managed Railiance application **is**.
4. Which files, declarations, and operator surfaces a wrapper needs.
5. How `user-engine` was absorbed as `rapp-user-engine` (§10).
It is the first deliverable of `RMGR-WP-0006`. Implementation of the
scaffolder comes after this shape is accepted.
`RMGR-WP-0006` recorded the contract and the first wrap.
`RMGR-WP-0007` is the greenfield command path.
---
@ -73,14 +121,73 @@ Grouping is legitimate only where members share rollout and rollback fate.
| `rail-kubernetes` | base | Kubernetes | verified | Default for platform services **and** managed applications |
| `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
values profile.
This is a **default plus a specialization gate**, not a maturity ladder.
An app does not graduate from Kubernetes to Knative.
```text
rail-kubernetes ← start here, stay here unless proven otherwise
└── rail-knative ← only if the execution model itself must change
```
`rail-kubernetes` is the base rail. `rail-knative` is derived from it:
same identity, smoke, promotion vocabulary, and recovery; it only
overrides activation, scale-to-zero, concurrency, revision traffic,
cold-start, and revision rollback
(`railiance-master/docs/rail-composition-contract.md`).
There is no next rail after Knative. Names such as `rail-keda`,
`rail-fission`, and `rail-nuclio` are examples of *when a new rail would
be justified*, not a planned path. Do not invent a rail for an adapter
or a values profile.
Do not mix rail choice with the other progressions:
| Sequence | What it is |
| --- | --- |
| `draft``declared``deployed``verified` | package readiness |
| omitted / `private``operator``public` | who may reach the listener |
| kubernetes → knative | **how the process is executed** |
**Stay on Kubernetes when** the app is a long-running portal or API; it
has a durable store, in-process migrations, or startup that cannot die
between requests; ordinary Deployment rolling updates and digest
rollback are enough; “scale down at night” can be an HPA / replica
count; or no cold-start SLO has been measured.
**Move to Knative only if all of these hold:**
1. The semantics are intrinsic: request activation, scale-to-zero,
revision traffic splits, or previous-revision rollback — not just
fewer replicas.
2. A Kubernetes profile cannot say it safely. If a Deployment + Service
+ HPA is enough, it is not a new rail.
3. The workload can die between requests. No migration-on-boot that must
finish before the first probe; no sticky in-memory session that
cannot cold-start.
4. Callers tolerate cold-start. Measure activator/buffering timeouts,
concurrency, and retry ownership before those numbers are
load-bearing (`docs/qonto-knative-runtime-contract.md`).
5. Egress is restricted. Unrestricted HTTPS egress is not
production-approved for critical Knative workloads.
6. The reef will admit it. Knative Serving is installed and *verified*
on Railiance01; **production approval is still blocked** (single-node
control plane). Today only `rapp-qonto` uses this rail.
Qonto is the worked example: internet-reachable, scales to zero, holds a
bank credential, needs revision canaries. user-engine stays on
Kubernetes — long-running portal plus Postgres.
New wraps set `primary_rail: rail-kubernetes`. Change it only when the
app owner can write: *this process must not exist until a request
arrives, and rollback is a previous Knative revision, not a previous
image digest.* If that sentence needs “and also it has a database that
migrates on start,” keep Kubernetes.
`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-*`.
the compatibility overlay-repo pattern. That overlay is the
migration-era wrapper. `app.toml` is **not** required on the first wrap.
### 3.2 Reefs
@ -103,17 +210,17 @@ admit it to production and does not make it public (ADR-0008).
| `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-user-engine` | yes | manifest-managed-platform-service | kubernetes | `user-engine` | verified | First-party absorb wrap. Apply home for the portal. |
| `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.
wrapper is proven once as an absorb (`rapp-user-engine`) and once as a
static Helm site (`rapp-policy-nexus`). `rapp-qonto` is drifted.
`rapp-vergabe-teilnahme` was never extracted. `rapp-tenant-engine` is
the greenfield/absorb pilot for `RMGR-WP-0007`.
### 3.4 Ownership repos that still hold wrappers
@ -777,8 +884,10 @@ Use this when filling a stub or extracting a wrapper from
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 `railiance/app.toml` only if you need Stage 1/2/3 rail
promotion. It is not part of the first wrap.
- [ ] Add the Helm chart or manifest set. Pin images by digest. Prefer
`rmgr rapp wrap` / `skeleton` over hand-copying.
- [ ] Add Makefile targets from §8.1.
- [ ] Name smoke **outcomes** and rollback **order**.
- [ ] Leave `exposure` unset.
@ -816,42 +925,36 @@ Use this when filling a stub or extracting a wrapper from
---
## 12. What Repo Manager should automate
## 12. What Repo Manager automates
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).
Implemented under `RMGR-WP-0006` (P1, P4) and `RMGR-WP-0007` (P2, P3, P5,
compose).
| Phase | Repo Manager does | Still human |
| Phase | Command | Still human |
| --- | --- | --- |
| **P0 — this guide** | Record the shape. | Accept or amend the shape. |
| **P1 — bootstrap stub** | Create or complete `rapp-<app>` 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):
| **P0 — this guide** | — | Accept or amend the shape. |
| **P1 — bootstrap** | `rmgr rapp init` | Purpose sentence, classification. |
| **P2 — package skeleton** | `rmgr rapp skeleton --from-app` | NetworkPolicy and resource review. |
| **P3 — platform drafts** | consumer draft + app `image.yaml` | CCR / consumer **apply**. |
| **P4 — validate** | `rmgr rapp validate` | Promote `draft``declared`. |
| **P5 — place** | `rmgr rapp place --reef reef-railiance` | Production gate, exposure grant. |
```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 ..
rmgr rapp wrap --path ../rapp-<app> --app <app> \
--ownership-repo <app> --from-app ../<app>
rmgr rapp validate --path ../rapp-<app> --family-root ..
rmgr rapp pin-image --path ../rapp-<app> --digest sha256:<64 hex>
rmgr rapp place --path ../rapp-<app> --reef reef-railiance
```
Constraints on the scaffolder:
Constraints:
- Files stay authoritative. The hub is not written to except via
`fix-consistency` after the files exist.
- Files stay authoritative. The hub is only updated via `fix-consistency`.
- 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.
- `init` / `wrap` refuse to overwrite a live declaration.
- `tenant-engine` is the `RMGR-WP-0007` pilot. `rapp-secrets-engine` is
not a wrap target.
---