//go:build linux package host import ( "context" "os" "path/filepath" "strings" "testing" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostguard" ) type seedAbsentJournal struct { rebootJournalFixture raw hostbox.LaunchInventory after func() } func (j *seedAbsentJournal) Read(context.Context, hostguard.FenceTarget) (hostbox.LaunchInventory, error) { return j.raw, nil } func (j *seedAbsentJournal) SettleRecovered(context.Context, hostguard.FenceTarget) error { j.j.Settled = true if j.after != nil { j.after() } return nil } func TestFilesSeedKilledUnlaunchedRecovery(t *testing.T) { for _, fault := range []string{"none", "live-controller", "reused-parent", "reappears", "role", "boot-change", "descriptor", "foreign-parent", "no-journal"} { t.Run(fault, func(t *testing.T) { root, proc := t.TempDir(), t.TempDir() os.MkdirAll(filepath.Join(proc, "sys/kernel/random"), 0700) os.WriteFile(filepath.Join(proc, "sys/kernel/random/boot_id"), []byte("boot\n"), 0600) target := hostguard.FenceTarget{Host: "seed.example", Incarnation: "inc", Stamp: 11, Session: 13, Box: "box", Enrollment: "enroll", BootID: "boot", ControllerID: "controller", FenceID: "fence"} agent := "/sys/fs/cgroup/system.slice/carlos-edge.service" parent, err := claimContainmentParent(agent, target) if err != nil { t.Fatal(err) } group := hostbox.StopCgroupIdentity{RelativePath: parent, Device: 1, Inode: 2} j := &seedAbsentJournal{rebootJournalFixture: rebootJournalFixture{j: hostbox.StopKernelJournal{V: 1, Target: target, ControllerPID: 42, ProcessStart: "123", Cgroups: []hostbox.StopCgroupIdentity{group}}}, raw: hostbox.LaunchInventory{Target: target, Groups: []hostbox.LaunchGroup{{RelativePath: parent, Device: 1, Inode: 2}}, NoState: true}} state := t.TempDir() k := LinuxStopKernel{AgentCgroup: func(context.Context) (string, error) { return agent, nil }, Journal: j, CgroupRoot: root, ProcRoot: proc, ManagedRoots: map[string]string{"data": state}} switch fault { case "live-controller": os.MkdirAll(filepath.Join(proc, "42"), 0700) os.WriteFile(filepath.Join(proc, "42/stat"), []byte("42 (fixture) S "+strings.Repeat("0 ", 18)+"123 0"), 0600) case "reused-parent": os.MkdirAll(filepath.Join(root, parent), 0700) case "reappears": j.after = func() { os.MkdirAll(filepath.Join(root, parent), 0700) } case "role": j.j.UnitReservationIDs = []string{"main"} case "boot-change": os.WriteFile(filepath.Join(proc, "sys/kernel/random/boot_id"), []byte("other"), 0600) case "descriptor": os.MkdirAll(filepath.Join(proc, "42/fd"), 0700) os.WriteFile(filepath.Join(proc, "42/stat"), []byte("42 (fixture) S "+strings.Repeat("0 ", 18)+"456 0"), 0600) os.Symlink(filepath.Join(state, "data.db")+" (deleted)", filepath.Join(proc, "42/fd/3")) case "foreign-parent": j.j.Cgroups[0].RelativePath = "other/group" case "no-journal": k.Journal = &j.rebootJournalFixture } in := StopInventory{Complete: true, Entries: []hostbox.StopInventoryEntry{{Kind: "path", ManagedRoot: "data", RelativePath: "data.db"}}} evidence, _, err := k.recoverUnlaunchedAbsent(t.Context(), target, in, j.j, j) if fault == "none" { if err != nil || !j.j.Settled || evidence.ObservedBootID != "boot" { t.Fatal(err, evidence) } return } if err == nil { t.Fatal("unsafe recovery accepted", fault) } if fault != "reappears" && j.j.Settled { t.Fatal("refusal settled journal") } }) } }