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.