feat(registry-manifest): phase 1 — orchestrator consumes manifests from signed catalog

Workstream B phase 1 (node-side consume). The signed app-catalog can now carry a
full manifest per entry; the orchestrator overlays it over the disk manifest
(origin-wins) with disk as the migration fallback. Moves apps toward
registry-distributed manifests with no OTA-shipped disk file.

- app_catalog: `manifest: Option<Value>` on AppCatalogEntry (forward-compatible,
  covered by the existing release-root signature over the raw JSON);
  `catalog_manifest_values()` accessor.
- prod_orchestrator: `load_manifests` overlays catalog manifests after the disk
  walk; `catalog_manifest_to_overlay()` returns None (→ disk fallback) on
  unparseable value / app-id mismatch / failed validate() / build source
  (build contexts aren't registry-distributed yet — phase 1 is image-only).
- manifest_dir stays PathBuf (build-only field); image-only apps never read it.
- 6 unit tests; compiles clean. No-op until a catalog embeds a manifest, so
  existing nodes are unaffected.

See docs/registry-manifest-design.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-06-21 05:30:38 -04:00
co-authored by Claude Opus 4.8
parent 192238cbb8
commit 220666d3a9
4 changed files with 157 additions and 11 deletions
+14 -9
View File
@@ -83,17 +83,22 @@ Extend (not replace) the disk walk:
4. A catalog manifest that fails parse/validate is logged and skipped → disk
fallback used (one bad entry never blocks the fleet, same as the disk walk).
### `manifest_dir` for registry manifests
### `manifest_dir` for registry manifests — IMPLEMENTED
`LoadedManifest.manifest_dir` is used for **build contexts** and **generated files**
(`GeneratedFile`) that live next to a disk manifest. Registry manifests have no dir.
`LoadedManifest.manifest_dir` is used **only** in the `ResolvedSource::Build` branch
(relative `container.build.context` resolution — two call sites). Image-only apps
(`ResolvedSource::Pull`) never read it.
- **Phase 1 scope = image-only apps** (no `build:`, no `generated_files:` needing a
source dir). immich, grafana, the fedimint apps, postgres/redis all qualify.
- Represent the absent dir explicitly: `manifest_dir: Option<PathBuf>` (or a
sentinel under `<data_dir>/registry-apps/<app_id>/` materialized on demand).
Companion build apps (bitcoin-ui, …) keep their disk path until a later phase
teaches the catalog to carry build contexts (content-addressed, per the DHT plan).
**Decision (phase 1, shipped):** keep `manifest_dir: PathBuf` (no `Option` ripple
through the codebase). A catalog manifest with a **build source is skipped** so its
disk manifest stays in effect — build contexts aren't registry-distributed until a
later phase (content-addressed, per the DHT plan). For an accepted (image-only)
catalog manifest, `manifest_dir` = the disk app dir if the app also exists on disk,
else a sentinel `<manifests_dir>/<app_id>` (never read for image-only apps).
This is enforced by `catalog_manifest_to_overlay(app_id, value) -> Option<AppManifest>`
in `prod_orchestrator.rs`, which returns `None` (→ disk fallback) for: unparseable
value, embedded-id ≠ catalog-key, failed `validate()`, or a build source.
## 5. Publishing (publish-side generator)