//go:build unix package hostbox import ( "context" "os" "path/filepath" "strconv" "testing" "time" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/park" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) // relocationReconcile is the box "box" as a relocation target: the // production adapter with the real census and park.Local, region HA's // relocation-aware reconciler (FenceClosed and HoldRow set), and the // target's row minted held by MintHeld at the install stamp. type relocationReconcile struct { f admissionFixture reg *registry.Registry adapter ReconcileAdapter cfg provision.ReconcileConfig captured hostguard.Captured holds []string } func newRelocationReconcile(t *testing.T) *relocationReconcile { t.Helper() ctx := t.Context() f := newAdmissionFixture(t, false) r := &relocationReconcile{f: f} var err error if r.reg, err = registry.Open(filepath.Join(t.TempDir(), "registry.db")); err != nil { t.Fatal(err) } t.Cleanup(func() { r.reg.Close() }) inv, err := OpenStopInventoryDB(filepath.Join(t.TempDir(), "inventory.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { inv.Close() }) launches, err := OpenLaunchJournal(filepath.Join(t.TempDir(), "launches.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { launches.Close() }) census := &EmptyInstallationCensus{Inventory: inv, Registry: r.reg, Launches: launches, Box: "box", Enrollment: "en", DirectoryRevision: "1", Roots: map[string]string{"data": f.data, "opt": t.TempDir(), "units": t.TempDir()}, SharedPaths: map[string][]string{"data": {"hostguard"}}} guard := hostguard.Guard{DataDir: f.data, LockDir: t.TempDir(), Claims: hostguard.StoreReader{Store: f.store}} source := WorkSource{Guard: guard, Box: "box", Region: "ie", Now: time.Now, Policy: func(context.Context, *hostclaim.Claim, Selection, hostwrite.Identity) error { return nil }, Local: func(context.Context, *hostclaim.Claim, Selection) error { return nil }} r.adapter = ReconcileAdapter{Registry: r.reg, Inventory: inv, Census: census, Box: "box", Enrollment: "en", DirectoryRevision: "1", Reconciler: Reconciler{Claims: f.store, Box: "box", Region: "ie", Guard: guard, Writes: hostwrite.Config{Enabled: true, Selector: source}, Local: park.Local{Guard: guard, Claims: f.store, DataDir: f.data, Box: "box"}, Now: func() time.Time { return time.Unix(100, 0) }, CheckRecord: func(context.Context, RecordSelection) error { return nil }, CheckLocal: census.CheckLocal, FenceClosed: func(context.Context, hostclaim.Observed) (bool, error) { return false, nil }, HoldRow: func(_ context.Context, host, op string) error { r.holds = append(r.holds, op) row, err := r.reg.Get(host) if err != nil || row == nil { return ErrChanged } if row.HoldOp == op { return nil } return r.reg.SetHold(host, "relocating", op) }}} r.cfg = provision.ReconcileConfig{DataDir: f.data, Region: "ie"} r.cfg.Layout = func(context.Context, release.Store) (string, error) { return release.LayoutModeNested, nil } r.adapter.Configure(&r.cfg) // The installing CAS: this box executes rel-1 at the install stamp. o, err := hostclaim.Read(ctx, f.store, f.host) if err != nil { t.Fatal(err) } if o, err = hostclaim.Update(ctx, f.store, o, func(c *hostclaim.Claim) error { c.Holder.Stamp = c.Gen + 1 c.Op = hostclaim.Op{Kind: hostclaim.KindRelocating, ID: "rel-1", Phase: hostclaim.PhaseInstalling, Exec: "box", Participants: []string{"src", "box"}, Intent: map[string]string{"region": "ie", "source_box": "src", "target_box": "box"}, Journal: map[string]string{"install_stamp": strconv.FormatUint(c.Holder.Stamp, 10)}} return nil }); err != nil { t.Fatal(err) } if err := guard.Activate(ctx, f.host, "box", nil); err != nil { t.Fatal(err) } r.captured = hostguard.Captured{Incarnation: o.Claim.Holder.Incarnation, Stamp: o.Claim.Holder.Stamp} record := provision.Record{Schema: 1, Host: f.host, Slug: "shop", App: "app", Account: f.account, Region: "ie", Channel: "stable", CreatedAt: time.Date(2026, 10, 7, 0, 0, 0, 0, time.UTC)} rec := provision.ClaimRecord{Host: f.host, Key: f.key, Family: "instance", Account: f.account, App: "app", CreatedAt: record.CreatedAt, Value: record, Route: provision.RouteFor(record, f.data, "", true, nil)} if err := r.adapter.MintHeld(ctx, rec, r.captured, "relocating", "rel-1"); err != nil { t.Fatal(err) } return r } func (r *relocationReconcile) phase(t *testing.T, tr func(*hostclaim.Claim)) hostclaim.Observed { t.Helper() o, err := hostclaim.Read(t.Context(), r.f.store, r.f.host) if err != nil { t.Fatal(err) } if o, err = hostclaim.Update(t.Context(), r.f.store, o, func(c *hostclaim.Claim) error { tr(c); return nil }); err != nil { t.Fatal(err) } return o } func targetGuardFiles(t *testing.T, data string) map[string]string { t.Helper() out := map[string]string{} entries, err := os.ReadDir(filepath.Join(data, "hostguard")) if err != nil { t.Fatal(err) } for _, e := range entries { raw, err := os.ReadFile(filepath.Join(data, "hostguard", e.Name())) if err != nil { t.Fatal(err) } out[e.Name()] = string(raw) } return out } // Ruling 31: under a relocating claim reconcile never activates the // target. The target's held row (minted by its install) is left to the // install at installing, installed and probing: stage A's check refuses // the executor's reconcile before any write (claim Plan 11 admits no // reconcile action under relocating), so the row, the guard record and // the claim are unchanged and no hold is written. func TestReconcileLeavesARelocatingTargetsRowToItsInstall(t *testing.T) { ctx := t.Context() r := newRelocationReconcile(t) before, err := r.reg.Get(r.f.host) if err != nil || before == nil { t.Fatal(err) } for _, phase := range []string{hostclaim.PhaseInstalling, hostclaim.PhaseInstalled, hostclaim.PhaseRelocatingProbing} { o := r.phase(t, func(c *hostclaim.Claim) { c.Op.Phase = phase }) guard := targetGuardFiles(t, r.f.data) r.holds = nil if err := r.f.pass(ctx, r.f.store, r.reg, r.cfg); err != nil { t.Fatalf("%s: %v", phase, err) } row, err := r.reg.Get(r.f.host) if err != nil || row == nil || *row != *before { t.Fatalf("%s: the row changed: %+v %v", phase, row, err) } if len(r.holds) != 0 { t.Fatalf("%s: a hold was written: %v", phase, r.holds) } if got := targetGuardFiles(t, r.f.data); len(got) != len(guard) || got[r.f.host+".json"] != guard[r.f.host+".json"] { t.Fatalf("%s: the guard record changed", phase) } after, err := hostclaim.Read(ctx, r.f.store, r.f.host) if err != nil || after.ETag != o.ETag { t.Fatalf("%s: the claim was written: %v", phase, err) } } } // Rev 1 finding 5: after completion the target serves at the install // stamp; relocationReleaseHold releases the row's hold (registry // ReleaseHold, under the op); reconcile's real adapter then finds the // installation's inventory coherent and keeps the row, ready and owned by // the same installation. func TestReconcileAdapterClearsTheHoldAfterCompletion(t *testing.T) { ctx := t.Context() r := newRelocationReconcile(t) minted, err := r.reg.Get(r.f.host) if err != nil || minted == nil { t.Fatal(err) } r.phase(t, func(c *hostclaim.Claim) { c.Op = hostclaim.Op{Kind: hostclaim.KindServing, Exec: "box", Participants: []string{"box"}} c.Holder.WriterBox = "box" }) if changed, err := r.reg.ReleaseHold(r.f.host, "rel-1"); err != nil || !changed { t.Fatalf("release hold: %v %v", changed, err) } if err := r.f.pass(ctx, r.f.store, r.reg, r.cfg); err != nil { t.Fatalf("the serving reconcile: %v", err) } row, err := r.reg.Get(r.f.host) if err != nil || row == nil || row.Status == "relocating" || row.HoldOp != "" || row.ClaimOwnership != minted.ClaimOwnership || !provision.NestedPaths(r.f.data, *row) { t.Fatalf("after completion: %+v %v (minted %+v)", row, err, minted.ClaimOwnership) } if len(r.holds) != 0 { t.Fatalf("a serving claim took the hold-only path: %v", r.holds) } }