package hostbox import ( "context" "errors" "path/filepath" "strings" "testing" "github.com/carlosframework/platform/internal/hostguard" ) func roleLaunchFixture(t *testing.T) (*LaunchJournal, LaunchReservation) { t.Helper() j, err := OpenLaunchJournal(filepath.Join(t.TempDir(), "launch.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { j.Close() }) target := hostguard.FenceTarget{Region: "eu", Host: "app.example.test", Incarnation: "inc", Stamp: 3, Session: 4, Op: "", Phase: "", Box: "box", Enrollment: "enroll", BootID: "boot", ControllerID: "controller", ClaimETag: "etag", DirectoryRevision: "7"} if err = j.Prelaunch(context.Background(), LaunchInventory{ControllerPID: 41, ProcessStart: "start-41", Target: target, Groups: []LaunchGroup{{RelativePath: "carlos/claim-parent", Device: 1, Inode: 9}}, NoState: true}); err != nil { t.Fatal(err) } r := LaunchReservation{ID: "attempt-1:main", Role: "main", InvocationID: "invocation-1", Inputs: "inputs-1", Target: target, Group: LaunchGroup{RelativePath: "carlos.slice/carlos-instances.slice/carlos-instances-app.slice/carlos-instance@app.service"}, Executable: LaunchFile{Device: 5, Inode: 6}} return j, r } func TestRoleLaunchRequestDecodeIsStrict(t *testing.T) { _, r := roleLaunchFixture(t) good, err := EncodeRoleLaunchRequest(RoleLaunchRequest{V: RoleLaunchV, Op: RoleLaunchStart, Reservation: &r, Executable: "/opt/carlos/versions/v1/app", MemoryMax: 1 << 29}) if err != nil { t.Fatal(err) } if q, err := DecodeRoleLaunchRequest(strings.NewReader(string(good))); err != nil || q.Reservation == nil || *q.Reservation != r { t.Fatalf("round trip=%+v %v", q, err) } for name, body := range map[string]string{ "unknown field": strings.Replace(string(good), `"v":1`, `"v":1,"argv":["/bin/sh"]`, 1), "trailing value": string(good) + `{}`, "wrong version": strings.Replace(string(good), `"v":1`, `"v":2`, 1), "unknown op": strings.Replace(string(good), `"op":"start"`, `"op":"exec"`, 1), "oversize": string(good) + strings.Repeat(" ", RoleLaunchLimit), "relative build": strings.Replace(string(good), `/opt/carlos/versions/v1/app`, `opt/app`, 1), "capability carries": `{"v":1,"op":"capability","executable":"/x"}`, "stop without id": `{"v":1,"op":"stop"}`, } { if _, err := DecodeRoleLaunchRequest(strings.NewReader(body)); !errors.Is(err, ErrRoleLaunchShape) { t.Fatalf("%s accepted: %v", name, err) } } for name, mutate := range map[string]func(*LaunchReservation){ "sidecar role": func(r *LaunchReservation) { r.Role = "sidecar" }, "agent group": func(r *LaunchReservation) { r.Group.Device = 1 }, "agent state": func(r *LaunchReservation) { r.State = UnitStarted }, "agent pid": func(r *LaunchReservation) { r.PID = 7 }, "agent parent": func(r *LaunchReservation) { r.Parent = LaunchGroup{RelativePath: "x", Device: 1, Inode: 1} }, "missing executable": func(r *LaunchReservation) { r.Executable = LaunchFile{} }, "missing session": func(r *LaunchReservation) { r.Target.Session = 0 }, "missing controller": func(r *LaunchReservation) { r.Target.ControllerID = "" }, "missing inputs": func(r *LaunchReservation) { r.Inputs = "" }, "unit invocation set": func(r *LaunchReservation) { r.Group.InvocationID = "forged" }, } { bad := r mutate(&bad) body, err := EncodeRoleLaunchRequest(RoleLaunchRequest{V: RoleLaunchV, Op: RoleLaunchStart, Reservation: &bad, Executable: "/opt/carlos/versions/v1/app", MemoryMax: 1 << 29}) if err != nil { t.Fatal(err) } if _, err := DecodeRoleLaunchRequest(strings.NewReader(string(body))); !errors.Is(err, ErrRoleLaunchShape) { t.Fatalf("%s accepted: %v", name, err) } } } func TestRoleLaunchGrantMustNameTheReservation(t *testing.T) { _, r := roleLaunchFixture(t) g := RoleLaunchGrant{ReservationID: r.ID, Unit: "carlos-instance@app.service", UnitInvocationID: "unit-1", PID: 99, ProcessStart: "123", Group: LaunchGroup{RelativePath: r.Group.RelativePath, Device: 3, Inode: 4, InvocationID: "unit-1"}} if err := g.Check(r); err != nil { t.Fatal(err) } for name, mutate := range map[string]func(*RoleLaunchGrant){ "other reservation": func(g *RoleLaunchGrant) { g.ReservationID = "attempt-2:main" }, "other group": func(g *RoleLaunchGrant) { g.Group.RelativePath = "carlos.slice/x.service" }, "no identity": func(g *RoleLaunchGrant) { g.Group.Inode = 0 }, "no pid": func(g *RoleLaunchGrant) { g.PID = 0 }, "invocation split": func(g *RoleLaunchGrant) { g.Group.InvocationID = "unit-2" }, "no start": func(g *RoleLaunchGrant) { g.ProcessStart = "" }, } { bad := g mutate(&bad) if err := bad.Check(r); err == nil { t.Fatalf("%s accepted", name) } } } func TestUnitReservationLifecycleIsRootOnlyAndSingleUse(t *testing.T) { ctx := context.Background() j, r := roleLaunchFixture(t) if err := j.IntendUnit(ctx, r); err != nil { t.Fatal(err) } // Replays: the same reservation, the same attempt invocation under another // ID, and a second live unit for the record all refuse. if err := j.IntendUnit(ctx, r); !errors.Is(err, ErrChanged) { t.Fatalf("replayed intention: %v", err) } other := r other.ID = "attempt-1b:main" if err := j.IntendUnit(ctx, other); !errors.Is(err, ErrChanged) { t.Fatalf("same invocation accepted: %v", err) } other.InvocationID = "invocation-2" if err := j.IntendUnit(ctx, other); !errors.Is(err, ErrChanged) { t.Fatalf("second live unit accepted: %v", err) } // The stop journal never reports a pending unit as settled. sj, err := j.ReadStopKernelJournal(ctx, r.Target) if err != nil || sj.Settled { t.Fatalf("pending unit settled=%v %v", sj.Settled, err) } // The agent's journal operations cannot move a unit reservation. agent := r agent.Group = LaunchGroup{RelativePath: "carlos/claim-parent/role-x", Device: 1, Inode: 2, InvocationID: r.InvocationID} if err := j.Reserve(ctx, agent); err == nil { t.Fatal("agent reserve rebound a unit reservation") } if err := j.Started(ctx, r, 99, "123"); err == nil { t.Fatal("agent started a unit reservation") } if err := j.Settled(ctx, r); err == nil { t.Fatal("agent settled a unit reservation") } group := LaunchGroup{RelativePath: r.Group.RelativePath, Device: 3, Inode: 4, InvocationID: "unit-1"} wrong := r wrong.Executable.Inode++ if err := j.StartedUnit(ctx, wrong, group, 99, "123"); !errors.Is(err, ErrChanged) { t.Fatalf("different executable started: %v", err) } if err := j.StartedUnit(ctx, r, LaunchGroup{RelativePath: "carlos.slice/other.service", Device: 3, Inode: 4, InvocationID: "unit-1"}, 99, "123"); err == nil { t.Fatal("other group started") } if err := j.StartedUnit(ctx, r, group, 99, "123"); err != nil { t.Fatal(err) } if err := j.StartedUnit(ctx, r, group, 99, "123"); !errors.Is(err, ErrChanged) { t.Fatalf("restarted: %v", err) } got, err := j.UnitReservation(ctx, r.Target, r.ID) if err != nil || got.State != UnitStarted || got.Group != group || got.PID != 99 || got.ProcessStart != "123" { t.Fatalf("started=%+v %v", got, err) } if sj, err = j.ReadStopKernelJournal(ctx, r.Target); err != nil || sj.Settled || len(sj.Cgroups) != 1 { t.Fatalf("started unit leaked into the agent cgroup scan or settled: %+v %v", sj, err) } if err := j.SettleUnit(ctx, r.Target, r.ID); err != nil { t.Fatal(err) } if err := j.SettleUnit(ctx, r.Target, r.ID); err != nil { t.Fatalf("settle not idempotent: %v", err) } if sj, err = j.ReadStopKernelJournal(ctx, r.Target); err != nil || !sj.Settled { t.Fatalf("settled unit still pending: %+v %v", sj, err) } // A settled ID is spent for good. if err := j.IntendUnit(ctx, r); !errors.Is(err, ErrChanged) { t.Fatalf("spent reservation reused: %v", err) } if err := j.IntendUnit(ctx, other); err != nil { t.Fatalf("next attempt after settlement: %v", err) } } func TestUnitTombstoneBurnsAnUnseenReservation(t *testing.T) { ctx := context.Background() j, r := roleLaunchFixture(t) if err := j.TombstoneUnit(ctx, r); err != nil { t.Fatal(err) } if err := j.IntendUnit(ctx, r); !errors.Is(err, ErrChanged) { t.Fatalf("start after its stop accepted: %v", err) } got, err := j.UnitReservation(ctx, r.Target, r.ID) if err != nil || got.State != UnitSettled || got.PID != 0 || !got.NeverIntended { t.Fatalf("tombstone=%+v %v", got, err) } } func TestUnitJournalRefusesRemoteAndMissingRecords(t *testing.T) { ctx := context.Background() _, r := roleLaunchFixture(t) remote, _ := RemoteJournals(func(context.Context, JournalRequest) (JournalResponse, error) { t.Fatal("unit operation reached the agent journal transport") return JournalResponse{}, nil }) if err := remote.IntendUnit(ctx, r); !errors.Is(err, ErrUnsupported) { t.Fatalf("remote intention: %v", err) } if err := remote.TombstoneUnit(ctx, r); !errors.Is(err, ErrUnsupported) { t.Fatalf("remote tombstone: %v", err) } j, err := OpenLaunchJournal(filepath.Join(t.TempDir(), "launch.db")) if err != nil { t.Fatal(err) } defer j.Close() if err := j.IntendUnit(ctx, r); err == nil { t.Fatal("intention without the prelaunch record") } } func TestPlan14NeverIntendedIsRootOnly(t *testing.T) { j, r := roleLaunchFixture(t) r.NeverIntended = true if unitReservationShape(r) { t.Fatal("agent set root cancellation proof") } if err := j.TombstoneUnit(t.Context(), r); err == nil { t.Fatal("forged cancellation proof accepted") } if err := j.IntendRole(t.Context(), r); err == nil { t.Fatal("forged legacy intention accepted") } if err := j.Reserve(t.Context(), r); err == nil { t.Fatal("forged legacy reservation accepted") } }