Offsite tier policy engine: mandatory userdata, raw-data quota, restore rework (Task 3a, v0.134.0)

Each toggled app's offsite push = one multi-path restic snapshot (recovery unit + TierOffsite
mandatory userdata via ComputeCaptureSet); legacy/undeployed stay unit-only. Loud capture gaps
(SP-3.4: restic 0.14.0 silently skips missing paths). Quota = stats --mode raw-data (SP-1;
displayed size drops once). Pre-push enlargement gate blocks the userdata enlargement over-quota
(unit-only push continues; EnlargedBlocked; edge-triggered notify). forget --group-by host,tags
on both sites (SP-2). Restore reworked: scratch off the rootfs + headroom gate (F-A1), unit-only
default via --include, size-first full, place-to-live missing-only merge (never --delete).
UI: unit/full-two-step/place actions + per-app blocked note; route POST /backup/offbox/place.
HUB FLAG: offbox_enlarge_blocked event needs hub allowlist for push delivery.
+13 tests; all 10 §10 red-proofs verified. No tier-2/.fab/hub/agent changes.
This commit is contained in:
2026-07-14 22:51:54 +02:00
parent 0c6e151c1c
commit 2d20859858
17 changed files with 1413 additions and 109 deletions
+15
View File
@@ -36,6 +36,21 @@ type Manager struct {
offboxRunner offboxRunner
offboxNotify func(dur time.Duration, snapshots int, err error)
// offboxSizer (3a) — the mandatory-set byte estimator for the pre-push enlargement gate, overridable
// in tests so the gate is unit-testable without a real du. Nil → the real dirSizeBytes (du -sb).
offboxSizer func(path string) int64
// offboxEnlargeBlockedNotify (3a), if set, is called ONCE per app that NEWLY enters the
// quota-blocked (enlargement-refused) state — edge-triggered against the persisted EnlargedBlocked
// set so a nightly schedule can't re-notify a persistently-blocked app (the hub owns cooldown; the
// controller must not add a timer). Wired in cmd/controller/main.go.
offboxEnlargeBlockedNotify func(stack string, estBytes int64, usedGB, quotaGB int)
// offboxPlaceCopier (3a) — the place-to-live missing-only merge seam (nil → rsyncRestoreMissing,
// the `-a --ignore-existing` additive copy). Never rsyncMirror (--delete trap).
offboxPlaceCopier func(src, dst string) (int, error)
// offboxFreeFn (3a) — the free-space probe for the restore headroom gate, overridable in tests (the
// Windows `go test` host has no `df`). Nil → the real diskFreeBytes (df --output=avail).
offboxFreeFn func(path string) int64
// F17 restore seams — overridable in tests so the .sql re-import orchestration can be unit-tested
// without Docker. Default to the real DiscoverDatabases / ImportDump (lazy-init in reimportDBDumps).
discoverDBs func(ctx context.Context) ([]DiscoveredDB, error)