diff --git a/README.md b/README.md index accadda..b812c9b 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,6 @@ 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]" @@ -33,8 +32,6 @@ rmgr --version rmgr observe --path . rmgr reconcile --path . rmgr update-task-status --path . --task-id --status progress -rmgr rapp init --path ../rapp-some-app --app some-app --ownership-repo some-app -rmgr rapp validate --path ../rapp-user-engine --family-root .. ``` Vertical-slice proof: [docs/evidence/t05-vertical-slice.md](docs/evidence/t05-vertical-slice.md). diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 3482498..5a4b673 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -12,8 +12,6 @@ | 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 | @@ -36,20 +34,3 @@ | 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 deleted file mode 100644 index 0a64a8c..0000000 --- a/docs/RailianceAppDeploymentGuide.md +++ /dev/null @@ -1,944 +0,0 @@ -# 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. 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 `-railiance-overlay/`. Graduate it to Path A; do not start new overlays. | -| **C. S5 chart in `railiance-apps`** | `charts//` + `helm/-values.yaml` | How user-facing apps actually ship today. Stay here only for an already-extracted-or-not decision; new managed apps should leave as Path A. | - -`flex-auth` is a live Path B example (`flex-auth/railiance/app.toml`) -with no `rapp-*` repo. `vergabe-teilnahme` is a live Path C example. -`rapp-policy-nexus` is the only live A←C bridge. - -There is no `railiance register-rapp`. A rapp exists when -`declarations/rapp.yaml` is committed and -`validate-family-declarations.py` accepts it. - ---- - -## 4. Decide before creating a rapp - -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/.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`. The existing -scaffold is: - -```bash -# from rail-kubernetes; cluster/bootstrap only keep shims -bin/railiance create-overlay --app-id --upstream-url … -bin/railiance run # Stage 1 -bin/railiance deploy --stage 2 # Stage 2 canary -bin/railiance promote # Stage 3 -bin/railiance rollback -``` - -`create-overlay` writes local files only. It does not create -`declarations/rapp.yaml`, a Forgejo remote, or a family classification. -Repo Manager's later `rmgr rapp init` replaces this for first-party apps. - -New work should not create overlay repos. If you find one, graduate it -to `rapp-` and keep `railiance/app.toml` inside the rapp. - ---- - -## 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/.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= -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-` 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. 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) diff --git a/src/repo_manager/cli.py b/src/repo_manager/cli.py index 5c52bca..9da7a48 100644 --- a/src/repo_manager/cli.py +++ b/src/repo_manager/cli.py @@ -4,13 +4,9 @@ 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( @@ -59,8 +55,6 @@ 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"): @@ -129,32 +123,6 @@ def main(argv: list[str] | None = None) -> int: print(json.dumps(result.to_dict(), indent=2)) return 0 if result.status == "applied" else 1 - if args.command == "rapp": - if args.rapp_command == "init": - result = rapp_init( - Path(args.path), - app=args.app, - ownership_repo=args.ownership_repo, - rail=args.rail, - classification=args.classification, - criticality=args.criticality, - package_type=args.package_type, - purpose=args.purpose, - force=args.force, - ) - elif args.rapp_command == "validate": - result = rapp_validate( - Path(args.path), - family_root=Path(args.family_root) if args.family_root else None, - ) - elif args.rapp_command == "pin-image": - result = rapp_pin_image(Path(args.path), args.digest) - else: - parser.print_help() - return 2 - print(json.dumps(result, indent=2)) - return 0 if result.get("ok") else 1 - parser.print_help() return 0 diff --git a/src/repo_manager/commands/rapp.py b/src/repo_manager/commands/rapp.py deleted file mode 100644 index 80a0e58..0000000 --- a/src/repo_manager/commands/rapp.py +++ /dev/null @@ -1,281 +0,0 @@ -"""Governed scaffolding for rapp-* managed workload packages.""" - -from __future__ import annotations - -import argparse -import json -import re -import subprocess -from pathlib import Path -from typing import Any - -PACKAGE_TYPES = ( - "helm-managed-platform-service", - "manifest-managed-platform-service", - "knative-managed-service", - "grouped-composition", -) -RAILS = ("rail-kubernetes", "rail-knative") -CLASSIFICATIONS = ("public", "internal", "confidential", "restricted") -CRITICALITIES = ("low", "medium", "high", "critical") -SLUG = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$") -DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$") - -_FAMILY_VALIDATOR = Path.home() / "railiance-master" / "tools" / "validate-family-declarations.py" - - -def _refuse(message: str) -> dict[str, Any]: - return {"ok": False, "error": message} - - -def _write(path: Path, content: str) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(content if content.endswith("\n") else content + "\n") - - -def init( - path: Path, - *, - app: str, - ownership_repo: str, - rail: str = "rail-kubernetes", - classification: str = "confidential", - criticality: str = "high", - package_type: str = "manifest-managed-platform-service", - purpose: str | None = None, - force: bool = False, -) -> dict[str, Any]: - if not SLUG.match(app) or app.startswith("rapp-"): - return _refuse("app must be a workload slug without the rapp- prefix") - if rail not in RAILS: - return _refuse(f"unknown rail {rail!r}; allowed: {', '.join(RAILS)}") - if package_type not in PACKAGE_TYPES: - return _refuse(f"unknown package_type {package_type!r}") - if classification not in CLASSIFICATIONS: - return _refuse(f"unknown classification {classification!r}") - if criticality not in CRITICALITIES: - return _refuse(f"unknown criticality {criticality!r}") - if not SLUG.match(ownership_repo) or ownership_repo.startswith("rapp-"): - return _refuse("ownership_repo must be an app or layer slug, not the rapp itself") - - rapp_id = f"rapp-{app}" - dest = path.expanduser().resolve() - dest.mkdir(parents=True, exist_ok=True) - declaration = dest / "declarations" / "rapp.yaml" - if declaration.is_file() and not force: - return _refuse(f"{declaration} already exists; refuse to overwrite a live contract") - - purpose_text = purpose or f"Package and operate the {app} service on Railiance." - files = { - ".repo-classification.yaml": ( - "repo_classification:\n" - ' standard: Repo Classification Standard\n' - ' version: "1.0"\n' - " classified_by: repo-manager\n" - " category: project\n" - " domain: infotech\n" - ), - "README.md": f"# {rapp_id}\n\nManaged runtime package for `{app}`.\n", - "INTENT.md": ( - f"# Intent\n\nProvide the managed Railiance runtime package for `{app}`.\n" - ), - "SCOPE.md": ( - "# Scope\n\n## In scope\n\n- packaging, smoke, rollback, and the family declaration\n\n" - "## Out of scope\n\n- application domain ownership\n- credential values\n" - ), - "AGENTS.md": ( - f"# Repository agent guide\n\nWorkplans use the prefix " - f"`{rapp_id.upper()}-WP-`.\n" - ), - "CLAUDE.md": f"# {rapp_id}\n\n@SCOPE.md\n@AGENTS.md\n", - "Makefile": ( - "SHELL := /bin/bash\n\n.PHONY: check\n\ncheck:\n" - "\tpython3 -m unittest discover -s tests -v\n" - ), - ".gitignore": ".rendered/\n__pycache__/\n", - "declarations/rapp.yaml": _declaration( - rapp_id=rapp_id, - app=app, - ownership_repo=ownership_repo, - rail=rail, - classification=classification, - criticality=criticality, - package_type=package_type, - purpose=purpose_text, - ), - f"workplans/{rapp_id.upper()}-WP-0001-bootstrap.md": ( - f"---\nid: {rapp_id.upper()}-WP-0001\ntype: workplan\n" - f'title: "Bootstrap {rapp_id}"\nstatus: proposed\n---\n\n' - f"# Bootstrap {rapp_id}\n" - ), - } - written = [] - for rel, content in files.items(): - _write(dest / rel, content) - written.append(rel) - (dest / "workplans" / "archived").mkdir(exist_ok=True) - return {"ok": True, "path": str(dest), "rapp_id": rapp_id, "written": written} - - -def _declaration( - *, - rapp_id: str, - app: str, - ownership_repo: str, - rail: str, - classification: str, - criticality: str, - package_type: str, - purpose: str, -) -> str: - return ( - "kind: managed-workload-package\n" - "repo_family: rapp\n" - f"rapp_id: {rapp_id}\n" - f"repo: {rapp_id}\n" - f"ownership_repo: {ownership_repo}\n" - "contract_version: 1.0.0\n" - "readiness_state: draft\n" - "workload_identity:\n" - f" name: {app}\n" - f" package_type: {package_type}\n" - f"data_classification: {classification}\n" - f"criticality: {criticality}\n" - f"primary_rail: {rail}\n" - "supported_rails:\n" - f" - {rail}\n" - "bound_reefs: []\n" - "runtime_dependencies:\n" - " - kubernetes-api\n" - "composition:\n" - f" purpose: {purpose}\n" - " member_repos:\n" - f" - repo: {rapp_id}\n" - " role: managed runtime package\n" - " deployables:\n" - f" - {app}\n" - "rollout_contract:\n" - " default_mode: kubectl-server-side-apply\n" - "smoke_contract:\n" - " required:\n" - " - healthz-ok\n" - "rollback_contract:\n" - " order:\n" - " - apply-reviewed-git-revision\n" - ) - - -def validate(path: Path, *, family_root: Path | None = None) -> dict[str, Any]: - dest = path.expanduser().resolve() - declaration = dest / "declarations" / "rapp.yaml" - if not declaration.is_file(): - return _refuse(f"missing {declaration}") - missing = [ - name - for name in ( - "README.md", - "INTENT.md", - "SCOPE.md", - "AGENTS.md", - ".repo-classification.yaml", - ) - if not (dest / name).is_file() - ] - if missing: - return _refuse(f"missing bootstrap files: {', '.join(missing)}") - - if _FAMILY_VALIDATOR.is_file(): - import tempfile - - search_root = (family_root or dest.parent).resolve() - with tempfile.TemporaryDirectory(prefix="rmgr-rapp-") as tmp: - tmp_root = Path(tmp) - (tmp_root / dest.name).symlink_to(dest) - for sibling in ("rail-kubernetes", "rail-knative", "reef-railiance"): - candidate = search_root / sibling - if candidate.is_dir(): - (tmp_root / sibling).symlink_to(candidate) - proc = subprocess.run( - ["python3", str(_FAMILY_VALIDATOR), "--root", str(tmp_root)], - check=False, - capture_output=True, - text=True, - ) - return { - "ok": proc.returncode == 0, - "path": str(dest), - "validator": str(_FAMILY_VALIDATOR), - "stdout": proc.stdout.strip(), - "stderr": proc.stderr.strip(), - "exit_code": proc.returncode, - } - - text = declaration.read_text() - required = ( - "kind: managed-workload-package", - "repo_family: rapp", - "ownership_repo:", - "primary_rail:", - "composition:", - ) - absent = [item for item in required if item not in text] - if absent: - return _refuse(f"declaration missing {absent}") - return {"ok": True, "path": str(dest), "validator": None} - - -def pin_image(path: Path, digest: str) -> dict[str, Any]: - if not DIGEST.fullmatch(digest): - return _refuse("digest must be sha256:<64 lowercase hex>") - dest = path.expanduser().resolve() - runtime = dest / "manifests" / "runtime.yaml" - if not runtime.is_file(): - return _refuse(f"missing {runtime}") - text = runtime.read_text() - updated, n = re.subn( - r"(forgejo\.coulomb\.social/coulomb/user-engine@)sha256:[0-9a-f]{64}", - rf"\g<1>{digest}", - text, - ) - if n == 0: - return _refuse("no user-engine digest pin found in manifests/runtime.yaml") - runtime.write_text(updated) - binding = dest / "bindings" / "reef-railiance.yaml" - if binding.is_file(): - binding.write_text( - re.sub(r"sha256:[0-9a-f]{64}", digest, binding.read_text(), count=1) - ) - declaration = dest / "declarations" / "rapp.yaml" - if declaration.is_file(): - declaration.write_text( - re.sub( - r"(source: forgejo\.coulomb\.social/coulomb/user-engine\n version: )sha256:[0-9a-f]{64}", - rf"\g<1>{digest}", - declaration.read_text(), - ) - ) - return {"ok": True, "path": str(dest), "digest": digest, "rewritten": n} - - -def add_rapp_parser(sub: argparse._SubParsersAction) -> None: - rapp = sub.add_parser("rapp", help="Scaffold or validate a rapp-* package") - rapp_sub = rapp.add_subparsers(dest="rapp_command", required=True) - - p_init = rapp_sub.add_parser("init", help="Create a draft rapp-* baseline") - p_init.add_argument("--path", required=True, help="Destination checkout") - p_init.add_argument("--app", required=True, help="Workload name (no rapp- prefix)") - p_init.add_argument("--ownership-repo", required=True) - p_init.add_argument("--rail", default="rail-kubernetes", choices=RAILS) - p_init.add_argument("--classification", default="confidential", choices=CLASSIFICATIONS) - p_init.add_argument("--criticality", default="high", choices=CRITICALITIES) - p_init.add_argument("--package-type", default="manifest-managed-platform-service", choices=PACKAGE_TYPES) - p_init.add_argument("--purpose", default=None) - p_init.add_argument("--force", action="store_true") - - p_val = rapp_sub.add_parser("validate", help="Validate a rapp-* checkout") - p_val.add_argument("--path", required=True) - p_val.add_argument("--family-root", default=None) - - p_pin = rapp_sub.add_parser("pin-image", help="Rewrite the user-engine digest pin") - p_pin.add_argument("--path", required=True) - p_pin.add_argument("--digest", required=True) diff --git a/tests/test_rapp_scaffold.py b/tests/test_rapp_scaffold.py deleted file mode 100644 index 8bc1a3f..0000000 --- a/tests/test_rapp_scaffold.py +++ /dev/null @@ -1,59 +0,0 @@ -from pathlib import Path - -from repo_manager.cli import main -from repo_manager.commands.rapp import init, pin_image, validate - - -def test_init_refuses_rapp_prefixed_app(tmp_path: Path): - result = init(tmp_path / "rapp-x", app="rapp-x", ownership_repo="x") - assert result["ok"] is False - - -def test_init_and_validate_draft(tmp_path: Path): - dest = tmp_path / "rapp-example" - created = init( - dest, - app="example", - ownership_repo="example-app", - purpose="Package and operate the example service.", - ) - assert created["ok"] is True - assert (dest / "declarations" / "rapp.yaml").is_file() - refused = init(dest, app="example", ownership_repo="example-app") - assert refused["ok"] is False - checked = validate(dest, family_root=tmp_path) - # Isolated family root has no rails; validator may warn/error on rail - # resolution. The bootstrap files must still be accepted by the local - # fallback if the family validator is absent, or produce structured output. - assert "path" in checked - - -def test_cli_init_and_pin(tmp_path: Path, capsys): - dest = tmp_path / "rapp-user-engine" - assert ( - main( - [ - "rapp", - "init", - "--path", - str(dest), - "--app", - "user-engine", - "--ownership-repo", - "user-engine", - ] - ) - == 0 - ) - (dest / "manifests").mkdir() - (dest / "manifests" / "runtime.yaml").write_text( - "image: forgejo.coulomb.social/coulomb/user-engine" - "@sha256:e3b5f65bafc1c0260dfdf2567a52766e67506ceb878a51759a2e9a307c4b5eb8\n" - ) - digest = "sha256:" + "ab" * 32 - pinned = pin_image(dest, digest) - assert pinned["ok"] is True - assert digest in (dest / "manifests" / "runtime.yaml").read_text() - assert main(["rapp", "pin-image", "--path", str(dest), "--digest", "latest"]) == 1 - out = capsys.readouterr().out - assert "sha256" in out or "digest" in out diff --git a/workplans/RMGR-WP-0004-repository-standards-conformance.md b/workplans/RMGR-WP-0004-repository-standards-conformance.md index 7611d24..a1ae7b5 100644 --- a/workplans/RMGR-WP-0004-repository-standards-conformance.md +++ b/workplans/RMGR-WP-0004-repository-standards-conformance.md @@ -198,7 +198,6 @@ 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 @@ -240,7 +239,6 @@ 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 6d65df2..68ecf1a 100644 --- a/workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md +++ b/workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md @@ -16,7 +16,6 @@ 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 @@ -52,7 +51,6 @@ 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 @@ -75,7 +73,6 @@ 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 @@ -97,7 +94,6 @@ 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 @@ -120,7 +116,6 @@ 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 @@ -140,7 +135,6 @@ 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 @@ -156,7 +150,6 @@ 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 @@ -178,7 +171,6 @@ 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: @@ -205,7 +197,6 @@ 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 @@ -236,7 +227,6 @@ 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 @@ -255,7 +245,6 @@ 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 deleted file mode 100644 index 89d1e4b..0000000 --- a/workplans/RMGR-WP-0006-railiance-app-wrapper-setup.md +++ /dev/null @@ -1,147 +0,0 @@ ---- -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: 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.