//go:build linux package host import ( "context" "encoding/json" "testing" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/instancectl" ) func TestServingParkRequiresRootObservedTenantStart(t *testing.T) { t.Parallel() for _, state := range []string{"prelaunch", "intended", "started", "settled-unobserved", "settled-started", "tombstoned", "cancelled", "missing", "remote"} { for _, database := range []bool{true, false} { t.Run(state+map[bool]string{true: "/database", false: "/no-database"}[database], func(t *testing.T) { f, _, _, in := servingRetirementFixture(t) admitted := servingAdmit(t, f, &in, "G") receipt := servingStop(t, f, admitted) target := receipt.Target if state != "missing" { if err := f.journal.Prelaunch(t.Context(), hostbox.LaunchInventory{Target: target, ControllerPID: 77, ProcessStart: "controller-start", NoState: true, Groups: []hostbox.LaunchGroup{{RelativePath: "carlos/parent", Device: 1, Inode: 2}}}); err != nil { t.Fatal(err) } } reservation := hostbox.LaunchReservation{ID: "attempt:main", Role: "main", InvocationID: "attempt-invocation", Inputs: "inputs", Target: target, Group: hostbox.LaunchGroup{RelativePath: instancectl.ClaimUnitControlGroup(target.Host)}, Executable: hostbox.LaunchFile{Device: 1, Inode: 3}} switch state { case "intended", "started", "settled-unobserved", "settled-started": if err := f.journal.IntendUnit(t.Context(), reservation); err != nil { t.Fatal(err) } case "tombstoned", "cancelled": if err := f.journal.TombstoneUnit(t.Context(), reservation); err != nil { t.Fatal(err) } } if state == "started" || state == "settled-started" { group := hostbox.LaunchGroup{RelativePath: reservation.Group.RelativePath, Device: 1, Inode: 4, InvocationID: "unit-invocation"} if err := f.journal.StartedUnit(t.Context(), reservation, group, 99, "tenant-start"); err != nil { t.Fatal(err) } } if state == "settled-started" || state == "settled-unobserved" { if err := f.journal.SettleUnit(t.Context(), target, reservation.ID); err != nil { t.Fatal(err) } } if state == "cancelled" { if err := f.journal.CloseWriters(t.Context(), target); err != nil { t.Fatal(err) } } journal := f.journal remoteCalls := 0 if state == "remote" { journal, _ = hostbox.RemoteJournals(func(context.Context, hostbox.JournalRequest) (hostbox.JournalResponse, error) { remoteCalls++ return hostbox.JournalResponse{}, nil }) } prior, err := servingWriterIdentity(admitted, "box-1", "ie") if err != nil { t.Fatal(err) } receipts := []hostguard.FenceReceipt{receipt} if state == "cancelled" { recovered := receipt recovered.Target.ControllerID = "recovery-controller" recovered.Target.FenceID = "recovery-fence" var proof StopReceipt if err := json.Unmarshal(receipt.Bytes, &proof); err != nil { t.Fatal(err) } proof.ControllerID, proof.FenceID = recovered.Target.ControllerID, recovered.Target.FenceID recovered.Bytes = mustMarshalStopReceipt(proof) receipts = append([]hostguard.FenceReceipt{recovered}, receipts...) } got, err := servingGenerationParkDisposition(t.Context(), journal, prior, receipts, database) want := servingParkRefuse if !database { want = servingParkArtifactFree } else if state == "prelaunch" || state == "cancelled" { want = servingParkDiscard } else if state == "settled-started" { want = servingParkRetain } if got != want || (err != nil) != (want == servingParkRefuse) { t.Fatalf("got %v,%v want %v", got, err, want) } if remoteCalls != 0 { t.Fatal("root retirement trusted agent journal") } }) } } }