Compare commits
5 commits
c7fbd57800
...
8b87b17980
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b87b17980 | |||
| 77452492ec | |||
| 56cc9099a3 | |||
| f285b2f315 | |||
| 5197aa7425 |
9 changed files with 1499 additions and 1 deletions
|
|
@ -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]"
|
||||
|
|
@ -32,6 +33,8 @@ rmgr --version
|
|||
rmgr observe --path .
|
||||
rmgr reconcile --path .
|
||||
rmgr update-task-status --path . --task-id <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).
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
944
docs/RailianceAppDeploymentGuide.md
Normal file
944
docs/RailianceAppDeploymentGuide.md
Normal file
|
|
@ -0,0 +1,944 @@
|
|||
# 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-<app>` 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. 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):
|
||||
|
||||
- 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 | 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.
|
||||
|
||||
`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`.
|
||||
|
||||
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 `<app>-railiance-overlay/`. Graduate it to Path A; do not start new overlays. |
|
||||
| **C. S5 chart in `railiance-apps`** | `charts/<app>/` + `helm/<app>-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
|
||||
|
||||
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. 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.
|
||||
|
||||
---
|
||||
|
||||
## 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/<app>.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-<app>/
|
||||
.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/<app>/ # 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: <workload 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-<app>` 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
|
||||
`<app>-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 <id> --upstream-url <url> …
|
||||
bin/railiance run <overlay-dir> # Stage 1
|
||||
bin/railiance deploy --stage 2 <overlay-dir> # Stage 2 canary
|
||||
bin/railiance promote <overlay-dir> # Stage 3
|
||||
bin/railiance rollback <overlay-dir>
|
||||
```
|
||||
|
||||
`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-<app>` and keep `railiance/app.toml` inside the rapp.
|
||||
|
||||
---
|
||||
|
||||
## 9. Platform handoffs every app wrapper needs
|
||||
|
||||
### 9.1 Database
|
||||
|
||||
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`
|
||||
- roles split: owner / migration / runtime
|
||||
- tenant-keying required even for a single current tenant
|
||||
|
||||
The consumer file lives in `rapp-postgres/consumers/<app>.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`).
|
||||
|
||||
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
|
||||
`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. The **app repo** 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, secret custody, **and
|
||||
deployment** (`docs/release.md`)
|
||||
|
||||
`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.
|
||||
|
||||
**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
|
||||
|
||||
| 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` | `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) 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)
|
||||
|
||||
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: manifest-managed-platform-service
|
||||
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
|
||||
- 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: Kubernetes manifests, smoke, and rollback for the user-engine runtime
|
||||
deployables:
|
||||
- user-engine
|
||||
upstream_components:
|
||||
- name: user-engine
|
||||
source: forgejo.coulomb.social/coulomb/user-engine
|
||||
version: sha256:e3b5f65bafc1c0260dfdf2567a52766e67506ceb878a51759a2e9a307c4b5eb8
|
||||
rollout_contract:
|
||||
default_mode: kubectl-server-side-apply
|
||||
commands:
|
||||
- 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
|
||||
- live-image-digest-match
|
||||
commands:
|
||||
- make verify-live
|
||||
rollback_contract:
|
||||
order:
|
||||
- previous-immutable-image-digest
|
||||
- apply-reviewed-git-revision
|
||||
commands:
|
||||
- make rollback DIGEST=<previous-image-digest>
|
||||
source_documents:
|
||||
- repo: user-engine
|
||||
path: INTENT.md
|
||||
- repo: user-engine
|
||||
path: docs/configuration.md
|
||||
- repo: user-engine
|
||||
path: docs/operability.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: 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.
|
||||
|
||||
`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
|
||||
|
||||
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 Database decision (do not do both)
|
||||
|
||||
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.
|
||||
|
||||
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`
|
||||
|
||||
- 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-<app>` 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-<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):
|
||||
|
||||
```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. 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
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 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`
|
||||
- `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:
|
||||
|
||||
- `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`
|
||||
- `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)
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
281
src/repo_manager/commands/rapp.py
Normal file
281
src/repo_manager/commands/rapp.py
Normal file
|
|
@ -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)
|
||||
59
tests/test_rapp_scaffold.py
Normal file
59
tests/test_rapp_scaffold.py
Normal file
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
147
workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md
Normal file
147
workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
---
|
||||
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-<app>` 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: done
|
||||
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.
|
||||
|
||||
**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: done
|
||||
priority: high
|
||||
state_hub_task_id: "3ba6828e-7514-4f4d-a1d3-54134e039f55"
|
||||
```
|
||||
|
||||
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 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.
|
||||
|
||||
**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: done
|
||||
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.
|
||||
|
||||
**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: done
|
||||
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.
|
||||
|
||||
**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;
|
||||
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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue