package hostbox import ( "context" "path/filepath" "testing" "github.com/carlosframework/platform/internal/hostguard" ) func TestLaunchJournalDurableExactReservations(t *testing.T) { ctx := context.Background() file := filepath.Join(t.TempDir(), "launch.db") j, err := OpenLaunchJournal(file) if err != nil { t.Fatal(err) } target := hostguard.FenceTarget{Host: "app.example.test", Incarnation: "inc", Stamp: 1, Session: 1, Box: "box", Enrollment: "enroll", BootID: "boot", ControllerID: "controller"} if err = j.Create(ctx, LaunchInventory{ControllerPID: 123, ProcessStart: "456", Target: target, Groups: []LaunchGroup{{RelativePath: "host", Device: 1, Inode: 1}}, NoState: true}); err != nil { t.Fatal(err) } reservation := LaunchReservation{ID: "launch", Role: "main", InvocationID: "invocation", Inputs: "inputs", Target: target, Group: LaunchGroup{RelativePath: "host/main", Device: 1, Inode: 2, InvocationID: "invocation"}, Executable: LaunchFile{Device: 1, Inode: 3}} if err = j.Reserve(ctx, reservation); err != nil { t.Fatal(err) } if err = j.Close(); err != nil { t.Fatal(err) } j, err = OpenLaunchJournal(file) if err != nil { t.Fatal(err) } defer j.Close() got, err := j.Read(ctx, target) if err != nil || len(got.Reservations) != 1 || got.Reservations[0].State != "reserved" { t.Fatalf("recovery=%+v %v", got, err) } wrong := reservation wrong.Executable.Inode++ if err = j.Started(ctx, wrong, 123, "12345"); err == nil { t.Fatal("different executable accepted") } if err = j.Reserve(ctx, reservation); err == nil { t.Fatal("launch ID reused") } if err = j.Started(ctx, reservation, 123, "12345"); err != nil { t.Fatal(err) } if err = j.Settled(ctx, reservation); err != nil { t.Fatal(err) } target.Session++ if _, err = j.Read(ctx, target); err == nil { t.Fatal("successor read predecessor inventory") } } func TestLaunchJournalPrelaunchRecordIsRecoverableWithoutEffects(t *testing.T) { ctx := context.Background() file := filepath.Join(t.TempDir(), "launch.db") j, err := OpenLaunchJournal(file) if err != nil { t.Fatal(err) } former := hostguard.FenceTarget{Host: "app.example.test", Incarnation: "inc", Stamp: 7, Session: 7, Box: "box", Enrollment: "enroll", BootID: "boot-1", ControllerID: "controller-1"} parent := LaunchGroup{RelativePath: "carlos/claim-parent", Device: 1, Inode: 9} for _, bad := range []LaunchInventory{ {ControllerPID: 41, ProcessStart: "start-1", Target: former, Groups: []LaunchGroup{parent}, Files: []LaunchFile{{Device: 1, Inode: 2}}}, {ControllerPID: 41, ProcessStart: "start-1", Target: former, Groups: []LaunchGroup{parent}, NoState: true, ProducerIDs: []string{"replicator"}}, {ControllerPID: 41, ProcessStart: "start-1", Target: former, Groups: []LaunchGroup{parent, {RelativePath: "carlos/claim-parent/role", Device: 1, Inode: 10}}, NoState: true}, {ControllerPID: 41, ProcessStart: "start-1", Target: former, NoState: true}, } { if err = j.Prelaunch(ctx, bad); err == nil { t.Fatalf("pre-launch record accepted effects or missing containment: %+v", bad) } } if err = j.Prelaunch(ctx, LaunchInventory{ControllerPID: 41, ProcessStart: "start-1", Target: former, Groups: []LaunchGroup{parent}, NoState: true}); err != nil { t.Fatal(err) } successor := former successor.Session, successor.Stamp, successor.BootID, successor.ControllerID = 8, 8, "boot-2", "controller-2" if err = j.Prelaunch(ctx, LaunchInventory{ControllerPID: 42, ProcessStart: "start-2", Target: successor, Groups: []LaunchGroup{{RelativePath: "carlos/claim-successor", Device: 1, Inode: 11}}, NoState: true}); err != nil { t.Fatal(err) } if err = j.Close(); err != nil { t.Fatal(err) } // A restarted controller reopens the journal and finds the former tuple. j, err = OpenLaunchJournal(file) if err != nil { t.Fatal(err) } defer j.Close() found, err := j.SessionLaunches(ctx, former.Host, former.Incarnation, former.Stamp, former.Session, former.Box, former.Enrollment) if err != nil || len(found) != 1 || !SameLaunchOwner(found[0].Target, former) || found[0].ControllerPID != 41 || found[0].ProcessStart != "start-1" { t.Fatalf("session records = %+v, %v", found, err) } stop, err := j.ReadStopKernelJournal(ctx, former) if err != nil || !stop.Settled || len(stop.Cgroups) != 1 || len(stop.Files) != 0 || len(stop.ProducerIDs) != 0 { t.Fatalf("pre-launch stop journal = %+v, %v", stop, err) } // The record is not an installation effect, so cold handover still accepts it. if effects, err := j.HasInstallationEffects(ctx, former.Host, former.Incarnation, former.Stamp); err != nil || effects { t.Fatalf("pre-launch record counted as installation effect: %v %v", effects, err) } if err = j.SettleRecovered(ctx, former); err != nil { t.Fatal(err) } other, err := j.Read(ctx, successor) if err != nil || len(other.Reservations) != 0 || !other.NoState { t.Fatalf("recovering the former tuple changed its successor: %+v %v", other, err) } if found, err = j.SessionLaunches(ctx, former.Host, former.Incarnation, former.Stamp, 99, former.Box, former.Enrollment); err != nil || len(found) != 0 { t.Fatalf("unrelated session matched: %+v %v", found, err) } if err = j.RecordFile(ctx, former, LaunchFile{Device: 1, Inode: 12}); err != nil { t.Fatal(err) } if got, err := j.Read(ctx, former); err != nil || got.NoState || len(got.Files) != 1 { t.Fatalf("launch files after pre-launch = %+v %v", got, err) } } func TestClaimReviewCompletedProducersAreInstallationEffects(t *testing.T) { j, err := OpenLaunchJournal(filepath.Join(t.TempDir(), "launch.db")) if err != nil { t.Fatal(err) } defer j.Close() target := hostguard.FenceTarget{Host: "app.test", Incarnation: "inc", Stamp: 2, Session: 2, Box: "box", Enrollment: "enroll", BootID: "boot", ControllerID: "controller"} if err := j.Prelaunch(t.Context(), LaunchInventory{ControllerPID: 123, ProcessStart: "456", Target: target, Groups: []LaunchGroup{{RelativePath: "host", Device: 1, Inode: 1}}, NoState: true}); err != nil { t.Fatal(err) } if err := j.RecordProducer(t.Context(), target, "delivery"); err != nil { t.Fatal(err) } if _, err := j.CompleteProducer(t.Context(), target, "delivery", ProducerCompletion{}); err != nil { t.Fatal(err) } if effects, err := j.HasInstallationEffects(t.Context(), target.Host, target.Incarnation, target.Stamp); err != nil || !effects { t.Fatalf("completed producer lost from handover check: effects=%v err=%v", effects, err) } if _, err := j.db.Exec(`UPDATE launches SET body=json_set(body,'$.Completed.Digest','broken')`); err != nil { t.Fatal(err) } if _, err := j.HasInstallationEffects(t.Context(), target.Host, target.Incarnation, target.Stamp); err == nil { t.Fatal("invalid completion summary accepted") } }