v0.39.0 — DR-recipe completion: live PBS coord + drop role/restic_repo_coord from v1 drive shape

Live PBS coord: new internal/pbs/live_reporter.go (LiveSnapshotReporter implements
hub.PBSReporter via the cheap Client.Snapshots() list with last-known-good fallback,
bounded by an 8s timeout, list-only — never triggers a verify). Closes the gap where
the recipe's pbs block was omitted whenever the verify-loop SnapshotStore was empty
(one-shot collect + the first ~6h after a daemon restart). SnapshotStore.Get added
(per-datastore LKG). Wired into the collector in both runDaemon and runSelftestHub;
the verify loop keeps Recording into the SAME shared store via one hoisted pbsTargets.

v1 host-half drive shape: dropped drives[].role (hub/operator-owned manifest concept,
not host-derivable) and drives[].restic_repo_coord (named a backup tier that doesn't
exist). Drive shape is now {durable_id, mount_path, intent, fs_type?, total_bytes}.
Hub reads drives as json.RawMessage → no hub struct change; goldens re-pinned
byte-identical (agent + hub copies).

Tests: live_reporter_test.go (T1 load-bearing coord-without-verify + T2..T6),
TestDRRecipeHostHalf_V1DriveShape; each companion demonstrated to fail pre-fix then
reverted. go build/vet/test green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 20:25:09 +02:00
parent 8abc1b8852
commit fbe113011d
8 changed files with 414 additions and 23 deletions
+39 -3
View File
@@ -54,7 +54,7 @@ func TestBuildDRRecipeHostHalf(t *testing.T) {
t.Errorf("drive %s intent=%q, want enrolled", d.DurableID, d.Intent)
}
}
if d, ok := byDur["uuid:da9e7089"]; !ok || d.Role != "bulk-data" || d.MountPath != "/mnt/felhom-usb" || d.TotalBytes != 931<<30 {
if d, ok := byDur["uuid:da9e7089"]; !ok || d.MountPath != "/mnt/felhom-usb" || d.TotalBytes != 931<<30 {
t.Errorf("felhom-usb drive wrong: %+v", d)
}
if _, ok := byDur["uuid:81a26531"]; !ok {
@@ -77,6 +77,42 @@ func TestBuildDRRecipeHostHalf_NoPBS(t *testing.T) {
}
}
// TestDRRecipeHostHalf_V1DriveShape pins the v1 host-half drive shape: a drive object carries ONLY
// {durable_id, mount_path, intent, total_bytes} (fs_type is omitempty) — and specifically NEITHER the
// dropped "role" NOR "restic_repo_coord" keys. Re-adding either field to DRDrive makes this fail
// (the companion: `Role string \`json:"role"\`` reintroduces the "role" key → caught here).
func TestDRRecipeHostHalf_V1DriveShape(t *testing.T) {
h := BuildDRRecipeHostHalf(
nil,
[]StorageTarget{
{Name: "felhom-usb", Type: StorageTypeUSB, DurableID: "uuid:da9e7089", Role: "bulk-data",
MountPath: "/mnt/felhom-usb", TotalBytes: 931 << 30},
},
nil,
)
if len(h.Drives) != 1 {
t.Fatalf("want 1 drive, got %d", len(h.Drives))
}
b, err := json.Marshal(h.Drives[0])
if err != nil {
t.Fatal(err)
}
var keys map[string]json.RawMessage
if err := json.Unmarshal(b, &keys); err != nil {
t.Fatal(err)
}
for _, banned := range []string{"role", "restic_repo_coord"} {
if _, ok := keys[banned]; ok {
t.Errorf("v1 drive must NOT carry %q key (it was dropped); got %s", banned, b)
}
}
for _, want := range []string{"durable_id", "mount_path", "intent", "total_bytes"} {
if _, ok := keys[want]; !ok {
t.Errorf("v1 drive missing required key %q; got %s", want, b)
}
}
}
// TestDRRecipeHostHalf_NoSecrets is the agent-side boundary assertion (the lighter mirror of the
// controller's load-bearing boundary test): a fully-populated host-half must carry NO field whose
// name smells like a credential. If a future field leaks a key/token/hash in, this fails.
@@ -97,8 +133,8 @@ func TestDRRecipeHostHalf_NoSecrets(t *testing.T) {
}
// assertNoSecretKeys walks decoded JSON and fails on any object key matching secretNameRe. Shared by
// the agent boundary assertions. (durable_id/repo_id/latest_snapshot_id/restic_repo_coord are
// identifiers/coordinates — none match the credential regex.)
// the agent boundary assertions. (durable_id/repo_id/latest_snapshot_id are identifiers/coordinates —
// none match the credential regex.)
func assertNoSecretKeys(t *testing.T, jsonBytes []byte) {
t.Helper()
var v any