//go:build linux package hostbox import ( "context" "encoding/json" "errors" "os" "os/exec" "path/filepath" "testing" "time" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rootstate" ) type plan14ColdManifest struct { Base, Data, Locks string Prior hostguard.FenceTarget Next ColdSuccessor } type plan14ColdDisk struct { rootstate.Store dir string before, after func([]byte) } func (d *plan14ColdDisk) Read(n string) ([]byte, error) { return os.ReadFile(filepath.Join(d.dir, n)) } func (d *plan14ColdDisk) Write(n string, b []byte) error { if d.before != nil { d.before(b) } f, err := os.CreateTemp(d.dir, "pending-") if err != nil { return err } defer os.Remove(f.Name()) _, err = f.Write(b) if err == nil { err = f.Sync() } err = errors.Join(err, f.Close()) if err != nil { return err } if err = os.Rename(f.Name(), filepath.Join(d.dir, n)); err != nil { return err } parent, err := os.Open(d.dir) if err != nil { return err } err = errors.Join(parent.Sync(), parent.Close()) if err != nil { return err } if d.after != nil { d.after(b) } return nil } func (d *plan14ColdDisk) Names() ([]string, error) { es, err := os.ReadDir(d.dir) var names []string for _, e := range es { names = append(names, e.Name()) } return names, err } func (d *plan14ColdDisk) Lock(context.Context, string) (func(), error) { return func() {}, nil } func (d *plan14ColdDisk) Close() error { return nil } func plan14ColdSetup(t *testing.T) plan14ColdManifest { t.Setenv("CARLOS_HOST_CLAIM", "1") source, store, initial := workFixture(t) ctx := t.Context() base := t.TempDir() rootDir := filepath.Join(base, "root") if err := os.Mkdir(rootDir, 0700); err != nil { t.Fatal(err) } disk := &plan14ColdDisk{dir: rootDir} admit := func(old hostclaim.Observed, generation string) hostclaim.Observed { next, err := hostclaim.Update(ctx, store, old, hostclaim.AdmitWriter(hostclaim.Admission{ObservedOpID: old.Claim.Op.ID, ObservedPhase: old.Claim.Op.Phase, ObservedSession: old.Claim.Holder.Session, ObservedGeneration: old.Claim.Holder.Generation, Box: "box", Region: "ie", Mode: "cold", NewGeneration: generation})) if err != nil { t.Fatal(err) } return next } old := admit(initial, "first") if err := source.Guard.Activate(ctx, old.Host, "box", nil); err != nil { t.Fatal(err) } source.Guard.Root = hostguard.NewRootAuthority(disk) holder := old.Claim.Holder if err := source.Guard.EstablishRoot(ctx, hostguard.RootInstallation{Host: old.Host, Incarnation: holder.Incarnation, Stamp: holder.Stamp, Account: holder.Account, App: holder.App, Record: holder.Key, Box: "box", Enrollment: "enrollment", Migration: 1, InventoryRevision: 1}, func(*hostclaim.Claim) error { return nil }); err != nil { t.Fatal(err) } prior := hostguard.FenceTarget{FenceID: "fence", Region: "ie", Host: old.Host, Incarnation: old.Claim.Holder.Incarnation, Stamp: old.Claim.Holder.Stamp, Session: old.Claim.Holder.Session, Box: "box", Enrollment: "enrollment", BootID: "boot", ControllerID: "controller", ClaimETag: old.ETag, DirectoryRevision: "1", Op: old.Claim.Op.ID, Phase: old.Claim.Op.Phase} if _, err := source.Guard.BeginFence(ctx, prior); err != nil { t.Fatal(err) } if err := source.Guard.CompleteFence(ctx, prior, []byte("verified-root-receipt"), time.Now().UTC(), func(context.Context, hostguard.FenceTarget, []byte, time.Time) error { return nil }); err != nil { t.Fatal(err) } reg, err := registry.Open(filepath.Join(base, "registry.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { reg.Close() }) inventory, err := OpenStopInventoryDB(filepath.Join(base, "inventory.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { inventory.Close() }) launches, err := OpenLaunchJournal(filepath.Join(base, "launches.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { launches.Close() }) owner := registry.ClaimOwnership{ClaimIncarnation: prior.Incarnation, ClaimStamp: prior.Stamp, ClaimOwner: "old-owner", ClaimRowID: "old-route"} dbPath := filepath.Join(source.Guard.DataDir, old.Host+".db") if err = os.WriteFile(dbPath, []byte("untrusted-local-database"), 0600); err != nil { t.Fatal(err) } if err = reg.Upsert(registry.Route{Host: old.Host, Kind: "instance", Socket: "/run/example.sock", DBPath: dbPath, ClaimOwnership: owner, HoldOp: "held-operation"}); err != nil { t.Fatal(err) } if err = reg.UpsertTimer(registry.Timer{Host: old.Host, Name: "daily", Kind: "cron", Spec: "0 0 * * *", NextAt: 123, Attempts: 2}); err != nil { t.Fatal(err) } _, err = reg.GetTimer(old.Host, "daily") if err != nil { t.Fatal(err) } parker := TypedParker{CapturePersistent: true, Roots: map[string]string{"data": source.Guard.DataDir}, ParkedRoot: source.Guard.DataDir, Incarnation: prior.Incarnation, Host: prior.Host, Retirement: "capture"} artifact, err := parker.CaptureArtifact("data", old.Host+".db", owner.ClaimOwner, ArtifactPath) if err != nil { t.Fatal(err) } in := StopInventory{Complete: true, Host: old.Host, Incarnation: prior.Incarnation, InstallationStamp: prior.Stamp, Box: "box", Enrollment: "enrollment", DirectoryRevision: "1", Entries: []StopInventoryEntry{{Kind: "route", ArtifactID: owner.ClaimRowID, ManagedRoot: "registry", RelativePath: old.Host, RowIdentity: owner.ClaimRowID, Ownership: owner.ClaimOwner, Materialization: "materialized"}, {Kind: "path", ArtifactID: "database", ManagedRoot: "data", RelativePath: artifact.Name, Ownership: artifact.Owner, Materialization: "materialized", Identity: artifact.Identity, Device: artifact.Dev, Inode: artifact.Ino, Type: artifact.Type}}} if err = inventory.WriteStopInventory(ctx, in); err != nil { t.Fatal(err) } successor := admit(old, "second") next := ColdSuccessor{Host: old.Host, Incarnation: prior.Incarnation, Stamp: successor.Claim.Holder.Stamp, Session: successor.Claim.Holder.Session, Generation: successor.Claim.Holder.Generation, Box: "box", Enrollment: "enrollment", DirectoryRevision: "1", ETag: successor.ETag, Op: successor.Claim.Op.ID, Phase: successor.Claim.Op.Phase} handover := ColdHandover{Guard: source.Guard, Claims: store, Registry: reg, Inventory: inventory, Launches: launches, Roots: map[string]string{"data": source.Guard.DataDir}, JournalDir: filepath.Join(base, "handover"), VerifyReceipt: func(context.Context, hostguard.FenceReceipt) error { return nil }} durable := release.NewDirStore(filepath.Join(base, "deployment")) keys, err := store.List(ctx, "") if err != nil { t.Fatal(err) } for _, key := range keys { body, _, err := store.Get(ctx, key) if err != nil { t.Fatal(err) } if _, err = durable.Put(ctx, key, body, ""); err != nil { t.Fatal(err) } } manifest := plan14ColdManifest{Base: base, Data: source.Guard.DataDir, Locks: source.Guard.LockDir, Prior: prior, Next: next} raw, _ := json.Marshal(manifest) if err = os.WriteFile(filepath.Join(base, "fixture.json"), raw, 0600); err != nil { t.Fatal(err) } _ = handover return manifest } func plan14ColdOpen(t *testing.T, m plan14ColdManifest) (ColdHandover, *plan14ColdDisk) { t.Helper() reg, err := registry.Open(filepath.Join(m.Base, "registry.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { reg.Close() }) inventory, err := OpenStopInventoryDB(filepath.Join(m.Base, "inventory.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { inventory.Close() }) launches, err := OpenLaunchJournal(filepath.Join(m.Base, "launches.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { launches.Close() }) disk := &plan14ColdDisk{dir: filepath.Join(m.Base, "root")} store := release.NewDirStore(filepath.Join(m.Base, "deployment")) g := hostguard.Guard{DataDir: m.Data, LockDir: m.Locks, Claims: hostguard.StoreReader{Store: store}, Root: hostguard.NewRootAuthorityWithInventory(disk, func(ctx context.Context, host string, c hostguard.Captured) (uint64, error) { _, revision, err := inventory.Snapshot(ctx, host, c) return revision, err })} return ColdHandover{Guard: g, Claims: store, Registry: reg, Inventory: inventory, Launches: launches, Roots: map[string]string{"data": m.Data}, JournalDir: filepath.Join(m.Base, "handover"), VerifyReceipt: func(_ context.Context, r hostguard.FenceReceipt) error { if string(r.Bytes) != "verified-root-receipt" || r.Target != m.Prior { return ErrChanged } return nil }}, disk } func TestPlan14ColdHandoverProcessBoundaries(t *testing.T) { if phase := os.Getenv("PLAN14_COLD_PHASE"); phase != "" { t.Setenv("CARLOS_HOST_CLAIM", "1") raw, err := os.ReadFile(filepath.Join(os.Getenv("PLAN14_COLD_DIR"), "fixture.json")) if err != nil { t.Fatal(err) } var m plan14ColdManifest if err = json.Unmarshal(raw, &m); err != nil { t.Fatal(err) } h, disk := plan14ColdOpen(t, m) h.checkpoint = func(point string) { if phase == point { os.Exit(73) } } classify := func(raw []byte) string { var r struct { Barrier *hostguard.FenceTarget State struct { Stamp uint64 `json:"active_stamp"` } } if json.Unmarshal(raw, &r) != nil { t.Fatal("bad root") } if r.State.Stamp == 0 { return "retired" } if r.Barrier != nil { return "barrier" } if r.State.Stamp == m.Next.Stamp { return "commit" } return "old" } disk.before = func(raw []byte) { if phase == "before-root-"+classify(raw) { os.Exit(73) } } disk.after = func(raw []byte) { if phase == "after-root-"+classify(raw) { os.Exit(73) } } err = h.Run(t.Context(), m.Prior, m.Next) t.Fatal("boundary not reached", phase, err) } for _, phase := range []string{"intent", "parked", "inventory-intent", "rows", "inventory-materialized", "complete-journal", "before-root-barrier", "after-root-barrier", "before-root-retired", "after-root-retired", "before-root-commit", "after-root-commit"} { t.Run(phase, func(t *testing.T) { m := plan14ColdSetup(t) cmd := exec.CommandContext(t.Context(), os.Args[0], "-test.run=^TestPlan14ColdHandoverProcessBoundaries$") cmd.Dir = m.Base cmd.Env = append(os.Environ(), "PLAN14_COLD_PHASE="+phase, "PLAN14_COLD_DIR="+m.Base) output, err := cmd.CombinedOutput() exit, ok := err.(*exec.ExitError) if !ok || exit.ExitCode() != 73 { t.Fatalf("crash: %v %s", err, output) } h, _ := plan14ColdOpen(t, m) effects := 0 allow := func(*hostclaim.Claim) error { return nil } prior := hostguard.Captured{Incarnation: m.Prior.Incarnation, Stamp: m.Prior.Stamp} next := hostguard.Captured{Incarnation: m.Next.Incarnation, Stamp: m.Next.Stamp} if err = h.Guard.With(t.Context(), m.Prior.Host, prior, allow, func() error { effects++; return nil }); err == nil || effects != 0 { t.Fatal("predecessor granted", err, effects) } if phase != "after-root-commit" { if err = h.Guard.With(t.Context(), m.Next.Host, next, allow, func() error { effects++; return nil }); err == nil || effects != 0 { t.Fatal("unfinished successor granted", err, effects) } } if err = h.Recover(t.Context()); err != nil { t.Fatal("resume", err) } if err = h.Run(t.Context(), m.Prior, m.Next); err != nil { t.Fatal("replay", err) } if err = h.Guard.With(t.Context(), m.Next.Host, next, allow, func() error { effects++; return nil }); err != nil || effects != 1 { t.Fatal("successor", err, effects) } receipt, err := h.Guard.FenceReceiptFor(t.Context(), m.Prior) if err != nil || h.VerifyReceipt(t.Context(), receipt) != nil || receipt.Target.BootID != "boot" { t.Fatal("old receipt changed", receipt, err) } if _, err = os.Stat(filepath.Join(m.Data, m.Prior.Host+".db")); !os.IsNotExist(err) { t.Fatal("old data reused", err) } row, err := h.Registry.Get(m.Next.Host) if err != nil || row.ClaimStamp != m.Next.Stamp { t.Fatal("rows", row, err) } view, err := h.Guard.Root.View(t.Context(), m.Next.Host, next) if err != nil || view.Barrier != nil || view.Installation.InventoryRevision == 0 { t.Fatal("root commit", view, err) } }) } }