f3146ac7bf
DR recipe slice (controller half), grounded in SPIKE-dr-recipe-2026-06-16. The
controller emitter is the BOUNDARY enforcement point: v1 ships an explicit
allowlist {catalog_ref, enabled, storage_bindings} and reads NOTHING from
AppConfig.Env, so no ENC:/token/password can leak. storage_bindings parsed from
the compose (${HDD_PATH}/${USERDATA_PATH} volume binds -> {container_path,
drive, subpath}).
Load-bearing tests: TestBuildAppRecipe_NoSecrets (synthetic-secret app -> none
leak) + TestBuildAppRecipe_AllowlistIsLoadBearing (red-proof companion) +
TestAppStorageBindings + TestBuildDRRecipeAppHalf. Red-proofed live: forcing the
emitter to dump Env makes the boundary test fail. recipe_version=1.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
59 lines
3.3 KiB
Markdown
59 lines
3.3 KiB
Markdown
# REPORT — controller v0.73.0: DR recipe (customer + apps half)
|
|
|
|
**TASK — DR recipe slice (controller half).** Emit the secret-free customer + apps half of the
|
|
reconstruction recipe as an additive hub-report section. Grounded in `SPIKE-dr-recipe-2026-06-16.md`.
|
|
**The controller emitter is the boundary enforcement point** — it is the component that distinguishes
|
|
secret from non-secret deploy fields, so v1 ships only an allowlist and the no-secrets boundary test
|
|
lives here.
|
|
|
|
## Implementation
|
|
|
|
- `internal/report/dr_recipe.go`:
|
|
- `DRRecipeAppHalf{recipe_version, customer{id,display,domain}, apps[]}`.
|
|
- `BuildDRRecipeAppHalf(custID, custName, domain, stacks, composeReader)` — pure given the reader;
|
|
one `AppRecipe` per DEPLOYED, non-protected stack.
|
|
- `AppRecipe{catalog_ref, enabled, storage_bindings}` — the **entire v1 surface**. `buildAppRecipe`
|
|
reads NOTHING from `AppConfig.Env`.
|
|
- `appStorageBindings(composeYAML, hddPath)` — pure compose parser; each `${HDD_PATH}`/`${USERDATA_PATH}`
|
|
volume bind → `{container_path, drive=basename(HDD_PATH), subpath}`; named volumes excluded.
|
|
- `readComposeFile` — the production reader (best-effort; unreadable → no bindings, never a failure).
|
|
- `Report.DRRecipe *DRRecipeAppHalf` wired into `BuildReport` (customer fields + `GetStacks()`).
|
|
|
|
## The boundary (the Phase-1 lesson)
|
|
|
|
The recipe carries ONLY `{catalog_ref, enabled, storage_bindings}` — identifiers/paths. It NEVER touches
|
|
`AppConfig.Env`, where the controller keeps `ENC:` secrets. This is an **allowlist** (a new field is
|
|
excluded by default), the inverse of the retired infra-backup that shipped `restic_password` /
|
|
`cf_api_token`. Secrets stay in the PBS whole-CT snapshot + escrow, recovered with R, never here.
|
|
|
|
## Tests (the load-bearing boundary test + companion)
|
|
|
|
- `TestBuildAppRecipe_NoSecrets` — emit a recipe for an app whose `Env` carries an `ENC:` value + a
|
|
token-shaped value; assert NONE of the values and NO credential-shaped key survive; assert the
|
|
allowlisted facts DID emit (non-vacuous).
|
|
- `TestBuildAppRecipe_AllowlistIsLoadBearing` — the companion/red-proof: a guard-removed shape leaks the
|
|
token; the production emitter does not.
|
|
- `TestAppStorageBindings` (+ `_NoHDD`) — pins the compose parse (roms + resources bindings; named volume
|
|
excluded; rootfs app → 0 bindings).
|
|
- `TestBuildDRRecipeAppHalf` — assemble-correctness (deployed + non-protected only) + whole-half secret
|
|
sweep.
|
|
- **Live red-proof:** forcing `buildAppRecipe` to dump `Env` made `TestBuildAppRecipe_NoSecrets` and the
|
|
companion FAIL (caught the token value + the `DB_PASSWORD`/`SECRET_KEY`/`IGDB_CLIENT_SECRET` keys);
|
|
reverted → green.
|
|
|
|
## Versioning
|
|
|
|
`recipe_version=1`; read is ignore-unknown for forward-compat. The hub assembles this half with the
|
|
agent's storage/guest/PBS half (agent v0.38.0) into one customer recipe.
|
|
|
|
## Gate / deploy
|
|
|
|
`go build`, `go vet`, `go test ./...` all green (local + build server). Built + pushed image v0.73.0;
|
|
deployed to guest 9201 (bootstrap-managed).
|
|
|
|
## Deferred (NOT in this slice)
|
|
|
|
Free-form non-secret deploy fields (the SPIKE's `non_secret_deploy_fields`) — v1 is the three-field
|
|
allowlist only; additional fields land incrementally behind the same allowlist test. No re-provisioning
|
|
automation; no recovery-mode consumption.
|