Onboard tenant-engine to the staged-promotion contract
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 50s

TEN-WP-0008. railiance/app.toml declares criticality=high, empty secrets,
isolated canary, and the live PostgreSQL digest as previous_stable.
Manifests render through kustomize (deploy/ and deploy/canary/). Stage 1
passed. Stage 2/3 Helm-only CLI gap requested as RAIL-BS-IN-0001 rather
than a dummy chart.

Assistant: grok
Assistant-Session: 01a04cea-e5e8-7081-a0fc-808ebbc35fa9
This commit is contained in:
tegwick 2026-08-29 14:51:27 +02:00
parent f9f8e0c54f
commit 6644ad8402
19 changed files with 1053 additions and 18 deletions

70
railiance/README.md Normal file
View file

@ -0,0 +1,70 @@
# Railiance staged promotion — tenant-engine
TEN-WP-0008 onboards this repo to `railiance.app.v1` without changing the
contract. Production is described, not redeployed.
## T02 decision: kustomize, not Helm, not a dummy chart
The schema accepts any stage `commands` and check `type`s including
`command` and `kubernetes`. This workload ships a multi-document
manifest, now rendered by `deploy/kustomization.yaml` (`kubectl apply -k
deploy/`). That is option 2 in TEN-WP-0008-T02: converting to kustomize is
cheap and matches the small-headless-service precedent
(`qonto-assistant`).
What the schema does **not** require, and what `bin/railiance
deploy|promote --apply` still requires, is a Helm chart plus
`values/stage3-production.yaml`. Plan mode of those commands fails closed
on "no Stage 2 helm check declared" / missing chart. That is a CLI gap,
not a missing field in `app.toml`. Per the workplan we do not paper over
it with a chart we would never apply. Requested of `railiance-bootstrap`
as `RAIL-BS-IN-0001`.
Stage commands in `app.toml` are therefore the kubectl invocations that
actually work against this shape. Stage 1 (`bin/railiance run`) validates
them by rendering both overlays locally.
## Canary
TEN-WP-0009 removed the ReadWriteOnce PVC and switched the Deployment to
`RollingUpdate`, so a second pod is storage-feasible. `canary_mode =
"isolated"` is a distinct Deployment/Service (`tenant-engine-canary`)
whose selector does not match the production Service. It does not take
`user-engine` traffic. Overlay: `deploy/canary/`.
## Recorded stable (T03)
`[stages.stage3].previous_stable` is the contract's copy of "what should
be running":
```
tenant-engine@sha256:a8e8086ffc5b772c1391b166f5e1884b90f7d327b152c205eceae129df555c24
```
That is the TEN-WP-0009 PostgreSQL runtime pin, the same digest
`deploy/base/tenant-engine.yaml` and `make verify-pin` use. Onboarding does
not ship a newer image.
## Rollback ladder
| Digest | Role |
| --- | --- |
| `a8e8086ffc…` | current stable (PostgreSQL) |
| `023ac49f5c…` | previous PostgreSQL image (leased-login ownership defect) |
| `7dc9d7bf1f…` | last SQLite/PVC image — **not** a rollback target after any PostgreSQL write |
Older TEN-WP-0007 pins (`44ca65f3…` / `08be0b1d…` / `2249e8c6…`) remain
the lifecycle/guardrail feature ladder: rolling past a pre-lifecycle
digest removes routes `user-engine` depends on.
## Secrets
`secrets.references = []` is deliberate. Postgres URL and flex-auth
caller token are projected by those platforms from OpenBao. This repo
does not catalogue them.
## Criticality
`high`. Argument is in `[app].description`. Not `critical`: not
internet-facing, holds no application secret, fail-closed on outage.
Not `medium`: flex-auth and key-cape are live consumers.

242
railiance/app.toml Normal file
View file

@ -0,0 +1,242 @@
schema_version = "railiance.app.v1"
# TEN-WP-0008. Modelled on qonto-assistant (small headless service) for
# stage commands, and on flex-auth for an empty secrets list and isolated
# canary. Stage 2/3 *commands* are kubectl against this repo's kustomize
# overlays. `bin/railiance deploy|promote --apply` currently requires a Helm
# chart the schema does not; that gap is requested of railiance-bootstrap
# rather than papered over with a dummy chart. See railiance/README.md.
[app]
id = "tenant-engine"
name = "tenant-engine"
repo = "tenant-engine"
owner = "infotech"
criticality = "high"
description = """
Canonical PIP for tenant-as-an-entity facts. flex-auth consults it live on
high-stakes decisions and key-cape consults it at token issuance. It holds
no application secrets and is not internet-facing, but an outage makes the
PDP fail closed fleet-wide. high, not critical: the PDP itself is high,
this is a data source whose unavailability is deny rather than an
unauthorized allow, and it does not hold a credential of its own.
"""
[source]
revision = "git:main"
artifact = "image"
digest_policy = "required"
[rollback]
strategy = "image-digest"
command = "kubectl -n tenant-engine rollout undo deploy/tenant-engine"
verification = """
make verify-pin agrees repo, spec, and running pod; GET /health returns
200 with store_backend=postgresql; lifecycle routes still present.
Do not roll back to sha256:7dc9d7bf1fcbb3f3ad7035366f4cd44038322995a126b4423142e446ca5c9cd5
(SQLite/PVC) after any PostgreSQL write. The TEN-WP-0007 ladder still
holds for older images: rolling past a pre-lifecycle digest removes
GET/PATCH/retire/reactivate that user-engine depends on.
"""
[[platform.dependencies]]
name = "platform-pg"
kind = "postgres"
required = true
stage = "stage2"
evidence = "rapp-postgres consumer tenant-engine can reach platform-pg:5432; /health reports store_backend=postgresql."
[[platform.dependencies]]
name = "flex-auth-tenant-engine"
kind = "identity"
required = true
stage = "stage2"
evidence = "http://flex-auth-tenant-engine.flex-auth.svc.cluster.local:8080/healthz returns 200."
[[platform.dependencies]]
name = "forgejo-oci"
kind = "other"
required = true
stage = "stage2"
evidence = "Image forgejo.coulomb.social/coulomb/tenant-engine is pullable by digest."
[[platform.dependencies]]
name = "railiance01-k3s"
kind = "network"
required = true
stage = "stage2"
evidence = "kubectl -n tenant-engine get ns reports Active on railiance01."
[secrets]
# Runtime secret custody is OpenBao's. Postgres URL and flex-auth caller
# token are projected files owned by those platforms, not entries this
# repo catalogues. An empty list is the correct answer (TEN-WP-0008-T01).
references = []
[[observability.health_endpoints]]
name = "local-health"
url = "http://127.0.0.1:8090/health"
stage = "stage1"
expected_status = 200
[[observability.health_endpoints]]
name = "cluster-health"
url = "http://tenant-engine.tenant-engine.svc.cluster.local:8090/health"
stage = "stage2"
expected_status = 200
[[observability.health_endpoints]]
name = "canary-health"
url = "http://tenant-engine-canary.tenant-engine.svc.cluster.local:8090/health"
stage = "stage2"
expected_status = 200
[stages.stage1]
enabled = true
namespace = "local"
release = "tenant-engine-local"
commands = [
"make test",
"make validate-app-toml",
"kubectl kustomize deploy",
"kubectl kustomize deploy/canary",
]
checks = ["unit-tests", "contract-schema", "kustomize-production", "kustomize-canary"]
evidence = ["pytest output", "app.toml schema validation", "kustomize render of deploy/ and deploy/canary"]
requires_approval = false
[stages.stage2]
enabled = true
namespace = "tenant-engine"
release = "tenant-engine-canary"
commands = [
"kubectl apply -k deploy/canary --dry-run=server",
"kubectl apply -k deploy/canary",
]
checks = ["server-dry-run", "canary-ready", "canary-health", "operator-approval"]
evidence = ["server-side dry-run of isolated canary", "deploy/tenant-engine-canary Available", "canary /health 200", "State Hub approval id"]
requires_approval = true
canary_mode = "isolated"
observation_minutes = 15
[stages.stage3]
enabled = true
namespace = "tenant-engine"
release = "tenant-engine"
commands = [
"kubectl apply -k deploy --dry-run=server",
"kubectl apply -k deploy",
]
checks = ["stage2-accepted", "rollback-target", "cluster-health", "operator-approval"]
evidence = ["promotion commit", "new stable digest in previous_stable and deploy/base/tenant-engine.yaml", "post-promotion make verify-pin"]
requires_approval = true
promotion_mode = "release-replace"
# TEN-WP-0008-T03: the contract's recorded stable, not only the manifest pin.
# Current production (TEN-WP-0009 PostgreSQL runtime), CI-built from d2a7fe3.
previous_stable = "tenant-engine@sha256:a8e8086ffc5b772c1391b166f5e1884b90f7d327b152c205eceae129df555c24"
[[checks]]
id = "unit-tests"
type = "command"
stage = "stage1"
description = "Run repository unit tests."
required = true
run = "make test"
timeout_seconds = 600
[[checks]]
id = "contract-schema"
type = "command"
stage = "stage1"
description = "Validate railiance/app.toml against railiance.app.v1."
required = true
run = "make validate-app-toml"
timeout_seconds = 60
[[checks]]
id = "kustomize-production"
type = "command"
stage = "stage1"
description = "Render the production kustomization locally, no cluster contact."
required = true
run = "kubectl kustomize deploy"
[[checks]]
id = "kustomize-canary"
type = "command"
stage = "stage1"
description = "Render the isolated canary kustomization locally, no cluster contact."
required = true
run = "kubectl kustomize deploy/canary"
[[checks]]
id = "local-health"
type = "http"
stage = "stage1"
description = "Confirm a locally served /health when one is running."
required = false
url = "http://127.0.0.1:8090/health"
expected_status = 200
timeout_seconds = 10
[[checks]]
id = "server-dry-run"
type = "command"
stage = "stage2"
description = "Server-side dry run of the isolated canary overlay before apply."
required = true
run = "kubectl apply -k deploy/canary --dry-run=server"
[[checks]]
id = "canary-ready"
type = "kubernetes"
stage = "stage2"
description = "Isolated canary Deployment reaches Available."
required = true
namespace = "tenant-engine"
resource = "deploy/tenant-engine-canary"
condition = "Available"
[[checks]]
id = "canary-health"
type = "http"
stage = "stage2"
description = "Isolated canary Service /health returns 200."
required = true
url = "http://tenant-engine-canary.tenant-engine.svc.cluster.local:8090/health"
expected_status = 200
timeout_seconds = 10
[[checks]]
id = "cluster-health"
type = "http"
stage = "stage2"
description = "Production Service /health returns 200."
required = true
url = "http://tenant-engine.tenant-engine.svc.cluster.local:8090/health"
expected_status = 200
timeout_seconds = 10
[[checks]]
id = "operator-approval"
type = "manual"
stage = "stage2"
description = "Human approval recorded before the isolated canary is applied and before the production pin moves."
required = true
evidence_required = "State Hub approval note id, candidate digest, rollback target (previous_stable)."
[[checks]]
id = "stage2-accepted"
type = "manual"
stage = "stage3"
description = "Stage 2 gates passed for the same candidate artifact."
required = true
evidence_required = "State Hub Stage 2 acceptance progress id."
[[checks]]
id = "rollback-target"
type = "manual"
stage = "stage3"
description = "Previous stable digest is recorded in previous_stable before promotion."
required = true
evidence_required = "tenant-engine image digest currently in previous_stable and deploy/base/tenant-engine.yaml."