//go:build linux package host import ( "context" "database/sql" "errors" "fmt" "github.com/carlosframework/platform/internal/hostclaim" "os" "path/filepath" "reflect" "testing" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostguard" ) func TestServingHandoverRebootDirectoryProof(t *testing.T) { for _, fault := range []string{"", "same-boot", "same-boot-renumber", "missing-boot", "boot-read", "foreign-host", "inode", "handle", "mount", "missing-record"} { t.Run(fault, func(t *testing.T) { f, r := servingRebootPreparationFixture(t, "complete") if err := f.p.commitServingReboot(t.Context(), r.Authorization.ID); err != nil { t.Fatal(err) } fs := f.p.ledger.fs.(*servingParkFS) prior := hostguard.FenceTarget{Host: r.Authorization.Successor.Host, Box: r.Authorization.Successor.Box, Incarnation: r.Authorization.Successor.Incarnation, Stamp: r.Authorization.Successor.Stamp, BootID: r.Source.BootID} e := hostbox.StopInventoryEntry{Kind: "path", ManagedRoot: "data", RelativePath: "instances/" + prior.Host, Device: r.Source.Directory.Device, Inode: r.Source.Directory.Inode, Type: 0040000} a := hostbox.Artifact{Dev: r.Source.Directory.Device, Ino: e.Inode, Type: e.Type} switch fault { case "same-boot", "same-boot-renumber": prior.BootID = servingOtherBoot if fault == "same-boot-renumber" { e.Device++ } case "missing-boot": prior.BootID = "" case "boot-read": fs.boot = func() (string, error) { return "", os.ErrPermission } case "foreign-host": prior.Host = "foreign.example" case "inode": e.Inode++ case "handle": original := fs.handle fs.handle = func(d *os.File) (servingParkPersistentDirectory, error) { p, err := original(d) if filepath.Base(d.Name()) == prior.Host { p.Handle += "00" } return p, err } case "mount": binding, err := servingParkReadBinding(f.disk, "namespace") if err != nil { t.Fatal(err) } binding.Parent.Mount++ if err := f.p.ledger.write("current-namespace.json", binding); err != nil { t.Fatal(err) } case "missing-record": if err := f.disk.Remove("retirement-" + r.Authorization.ID + ".json"); err != nil { t.Fatal(err) } } h := servingTransportRoot(t, f) err := h.verifyServingHandoverDirectory(t.Context(), prior, e, a) if (err == nil) != (fault == "" || fault == "same-boot") { t.Fatalf("fault %q: %v", fault, err) } }) } } func TestServingHandoverRebootEqualNumbers(t *testing.T) { for _, changed := range []bool{false, true} { t.Run(fmt.Sprint(changed), func(t *testing.T) { f := newServingRootFixture(t, true) root := servingHandoverRoot(t, f) next := func() hostbox.ColdSuccessor { return hostbox.ColdSuccessor{Host: f.current.Host, Incarnation: f.current.Incarnation, Box: f.actor.Box, Enrollment: f.actor.Enrollment, DirectoryRevision: "7", ETag: f.f.read().ETag, Generation: f.current.Generation, Stamp: f.current.Stamp, Session: f.current.Session} } if err := root.servingHandover().Run(t.Context(), f.receipt.Target, next()); err != nil { t.Fatal(err) } a, err := f.park() if err != nil { t.Fatal(err) } f.tags(f.current.Stamp) if err := f.p.completeServingDatabasePark(t.Context(), f.conn, a.ID, f.current); err != nil { t.Fatal(err) } prior := servingStop(t, f.f, f.f.read(), f.receipt.Target.BootID).Target o := f.f.read() admitted, err := hostclaim.Update(t.Context(), f.f.claims, o, hostclaim.AdmitWriter(hostclaim.Admission{ObservedSession: f.current.Session, ObservedGeneration: f.current.Generation, Box: f.actor.Box, Region: f.actor.Region, PredecessorRegion: f.actor.Region, Mode: "cold", NewGeneration: "G3"})) if err != nil { t.Fatal(err) } f.current, err = servingWriterIdentity(admitted, f.actor.Box, f.actor.Region) if err != nil { t.Fatal(err) } fs := f.p.ledger.fs.(*servingParkFS) fs.boot = func() (string, error) { return servingOtherBoot, nil } if prior.BootID == servingOtherBoot { t.Fatal("boots must differ") } if err := f.p.commitServingReboot(t.Context(), a.ID); err != nil { t.Fatal(err) } if changed { handle := fs.handle fs.handle = func(d *os.File) (servingParkPersistentDirectory, error) { p, err := handle(d) if filepath.Base(d.Name()) == prior.Host { p.Handle += "00" } return p, err } } h := root.servingHandover() verify := h.VerifyDirectory calls := 0 h.VerifyDirectory = func(ctx context.Context, p hostguard.FenceTarget, e hostbox.StopInventoryEntry, a hostbox.Artifact) error { calls++ if a.Dev != e.Device || a.Ino != e.Inode || a.Type != e.Type { t.Fatal("numeric identities must be equal") } return verify(ctx, p, e, a) } before, err := h.Registry.Get(prior.Host) if err != nil { t.Fatal(err) } n := next() err = h.Run(t.Context(), prior, n) if changed { if !errors.Is(err, hostbox.ErrChanged) { t.Fatal("changed persistent evidence accepted", err) } after, err := h.Registry.Get(prior.Host) if err != nil || !reflect.DeepEqual(before, after) { t.Fatal("registry changed", err) } if _, err := h.Inventory.ReadInstallation(t.Context(), n.Host, n.Incarnation, n.Stamp); !errors.Is(err, sql.ErrNoRows) { t.Fatal("successor inventory written", err) } } else { if err != nil { t.Fatal("unchanged filesystem refused", err) } if err := h.Run(t.Context(), prior, n); err != nil { t.Fatal("replay", err) } if calls < 5 { t.Fatal("missing boundary verification", calls) } } if calls == 0 { t.Fatal("persistent verifier bypassed") } }) } } func servingRecoveryFilesystem(t *testing.T, data string) (string, *servingParkRuntime) { t.Helper() disk := &rollbackDisk{dir: t.TempDir()} fs := newServingParkFS(data, disk) boot, err := fs.currentBoot() if err != nil { t.Fatal(err) } return boot, &servingParkRuntime{authority: &servingParkRoot{ledger: &servingParkLedger{state: disk, fs: fs}}} }