# Draft section for `09-update-architecture.md` — the standing method for update drills *(To be inserted after §6.4. Written here first so the audit and the architecture stay in step.)* --- ## 6.5 The drill catalog and the image store — the standing method for update drills **Why this section exists.** On 2026-09-21 an afternoon session put a deliberately broken image into the LIVE catalog for thirteen minutes to produce a failing edge. It was reverted and nothing reached a customer, but the method was wrong and the brief that asked for it said so. This is the method that replaces it, proven the same night. **The rule, and it has no exception:** *nothing broken, dummy, cross-repo or engine-major ever enters the live catalog — not as a fallback, not for thirteen minutes. If a leg cannot be done without that, the leg is skipped and named.* ### The two mechanisms | | what it is | what it makes possible | |---|---|---| | **the drill catalog** | `admin/app-catalog-drill` on Gitea — private, a copy of the live catalog's `main` | a scratch box can be pointed at a catalog where a failing edge is *committable*, because it carries none of the live repo's gates | | **the image store** | a `registry:2` container on the scratch guest at `127.0.0.1:5000` | an edge that **passes the within-a-major test and still fails** — the one shape a real catalog move cannot produce | **The image store is not a convenience.** `09` §3b Q4 could not be measured for a year of drills because the only failing edges available were across-a-major, and the within-a-major rule — correctly — refuses those before the guarded update is ever reached. *The rule that makes automatic updates safe is the same rule that refuses the obvious way to break one.* Measuring an unattended HOLD needs `drill/:X.Y.Z` (the real image, retagged) against `drill/:X.Y.(Z+1)` (a built image that starts, stays up and never serves) — same repository, same major, plain version tags. A third flavour, a tag simply **absent** from the store, gives the pull-failure leg. `stacks.CompareImageRefs` orders a `host:port/` reference correctly: `splitImageRef` takes the last colon and rejects it only when a `/` follows, so a registry port is never read as a tag. **Proven by running it**, four positive cases and a negative control, 2026-09-21. ### Pointing a box at the drill catalog — the step that is NOT obvious **`git.repo_url` alone is inert.** `Syncer.gitCloneOrPull` clones only when the cache has no `.git`; otherwise it fetches from the remote the clone already stores. The cache directory must be removed as well, or the box goes on following the live catalog and reports success. Filed as **R-615**; until it is fixed, the drill procedure is: 1. save `controller.yaml` as `controller.yaml.pre-update-night`; 2. set `git.repo_url` (and `username`/`token` — the drill repo is private); 3. **remove `/catalog-cache`**; 4. restart the controller, sync, **rescan** (R-607: a sync can answer „nincs változás" while the catalog has moved, and the badge answers from the stale value until the rescan); 5. **three controls, all quoted in the report** — the drill bump appears on the scratch box; the other boxes' caches are unchanged; the live catalog's `main` hash is unchanged. ### What the drill must leave behind - `controller.yaml` restored from the saved copy, the controller restarted, and `git.repo_url` **read back and quoted** as the live catalog. - The registry container and its volume removed; drill images removed **by name**. Never `prune`. - The drill repo **kept**, private, reset to the live catalog's `main`, so the next drill starts clean. - A diff of every `image:` line against the live catalog's `main` — expected: identical. ### The fence Only a scratch guest is ever pointed at the drill catalog. The drill repo's README says so, and no customer box has credentials for it. The store listens on the guest's loopback only.