carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package hostbox
2
3import (
4 "context"
5 "errors"
6 "path/filepath"
7 "strings"
8 "testing"
9
10 "github.com/carlosframework/platform/internal/hostguard"
11)
12
13func roleLaunchFixture(t *testing.T) (*LaunchJournal, LaunchReservation) {
14 t.Helper()
15 j, err := OpenLaunchJournal(filepath.Join(t.TempDir(), "launch.db"))
16 if err != nil {
17 t.Fatal(err)
18 }
19 t.Cleanup(func() { j.Close() })
20 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"}
21 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 {
22 t.Fatal(err)
23 }
24 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}}
25 return j, r
26}
27
28func TestRoleLaunchRequestDecodeIsStrict(t *testing.T) {
29 _, r := roleLaunchFixture(t)
30 good, err := EncodeRoleLaunchRequest(RoleLaunchRequest{V: RoleLaunchV, Op: RoleLaunchStart, Reservation: &r, Executable: "/opt/carlos/versions/v1/app", MemoryMax: 1 << 29})
31 if err != nil {
32 t.Fatal(err)
33 }
34 if q, err := DecodeRoleLaunchRequest(strings.NewReader(string(good))); err != nil || q.Reservation == nil || *q.Reservation != r {
35 t.Fatalf("round trip=%+v %v", q, err)
36 }
37 for name, body := range map[string]string{
38 "unknown field": strings.Replace(string(good), `"v":1`, `"v":1,"argv":["/bin/sh"]`, 1),
39 "trailing value": string(good) + `{}`,
40 "wrong version": strings.Replace(string(good), `"v":1`, `"v":2`, 1),
41 "unknown op": strings.Replace(string(good), `"op":"start"`, `"op":"exec"`, 1),
42 "oversize": string(good) + strings.Repeat(" ", RoleLaunchLimit),
43 "relative build": strings.Replace(string(good), `/opt/carlos/versions/v1/app`, `opt/app`, 1),
44 "capability carries": `{"v":1,"op":"capability","executable":"/x"}`,
45 "stop without id": `{"v":1,"op":"stop"}`,
46 } {
47 if _, err := DecodeRoleLaunchRequest(strings.NewReader(body)); !errors.Is(err, ErrRoleLaunchShape) {
48 t.Fatalf("%s accepted: %v", name, err)
49 }
50 }
51 for name, mutate := range map[string]func(*LaunchReservation){
52 "sidecar role": func(r *LaunchReservation) { r.Role = "sidecar" },
53 "agent group": func(r *LaunchReservation) { r.Group.Device = 1 },
54 "agent state": func(r *LaunchReservation) { r.State = UnitStarted },
55 "agent pid": func(r *LaunchReservation) { r.PID = 7 },
56 "agent parent": func(r *LaunchReservation) { r.Parent = LaunchGroup{RelativePath: "x", Device: 1, Inode: 1} },
57 "missing executable": func(r *LaunchReservation) { r.Executable = LaunchFile{} },
58 "missing session": func(r *LaunchReservation) { r.Target.Session = 0 },
59 "missing controller": func(r *LaunchReservation) { r.Target.ControllerID = "" },
60 "missing inputs": func(r *LaunchReservation) { r.Inputs = "" },
61 "unit invocation set": func(r *LaunchReservation) { r.Group.InvocationID = "forged" },
62 } {
63 bad := r
64 mutate(&bad)
65 body, err := EncodeRoleLaunchRequest(RoleLaunchRequest{V: RoleLaunchV, Op: RoleLaunchStart, Reservation: &bad, Executable: "/opt/carlos/versions/v1/app", MemoryMax: 1 << 29})
66 if err != nil {
67 t.Fatal(err)
68 }
69 if _, err := DecodeRoleLaunchRequest(strings.NewReader(string(body))); !errors.Is(err, ErrRoleLaunchShape) {
70 t.Fatalf("%s accepted: %v", name, err)
71 }
72 }
73}
74
75func TestRoleLaunchGrantMustNameTheReservation(t *testing.T) {
76 _, r := roleLaunchFixture(t)
77 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"}}
78 if err := g.Check(r); err != nil {
79 t.Fatal(err)
80 }
81 for name, mutate := range map[string]func(*RoleLaunchGrant){
82 "other reservation": func(g *RoleLaunchGrant) { g.ReservationID = "attempt-2:main" },
83 "other group": func(g *RoleLaunchGrant) { g.Group.RelativePath = "carlos.slice/x.service" },
84 "no identity": func(g *RoleLaunchGrant) { g.Group.Inode = 0 },
85 "no pid": func(g *RoleLaunchGrant) { g.PID = 0 },
86 "invocation split": func(g *RoleLaunchGrant) { g.Group.InvocationID = "unit-2" },
87 "no start": func(g *RoleLaunchGrant) { g.ProcessStart = "" },
88 } {
89 bad := g
90 mutate(&bad)
91 if err := bad.Check(r); err == nil {
92 t.Fatalf("%s accepted", name)
93 }
94 }
95}
96
97func TestUnitReservationLifecycleIsRootOnlyAndSingleUse(t *testing.T) {
98 ctx := context.Background()
99 j, r := roleLaunchFixture(t)
100 if err := j.IntendUnit(ctx, r); err != nil {
101 t.Fatal(err)
102 }
103 // Replays: the same reservation, the same attempt invocation under another
104 // ID, and a second live unit for the record all refuse.
105 if err := j.IntendUnit(ctx, r); !errors.Is(err, ErrChanged) {
106 t.Fatalf("replayed intention: %v", err)
107 }
108 other := r
109 other.ID = "attempt-1b:main"
110 if err := j.IntendUnit(ctx, other); !errors.Is(err, ErrChanged) {
111 t.Fatalf("same invocation accepted: %v", err)
112 }
113 other.InvocationID = "invocation-2"
114 if err := j.IntendUnit(ctx, other); !errors.Is(err, ErrChanged) {
115 t.Fatalf("second live unit accepted: %v", err)
116 }
117 // The stop journal never reports a pending unit as settled.
118 sj, err := j.ReadStopKernelJournal(ctx, r.Target)
119 if err != nil || sj.Settled {
120 t.Fatalf("pending unit settled=%v %v", sj.Settled, err)
121 }
122 // The agent's journal operations cannot move a unit reservation.
123 agent := r
124 agent.Group = LaunchGroup{RelativePath: "carlos/claim-parent/role-x", Device: 1, Inode: 2, InvocationID: r.InvocationID}
125 if err := j.Reserve(ctx, agent); err == nil {
126 t.Fatal("agent reserve rebound a unit reservation")
127 }
128 if err := j.Started(ctx, r, 99, "123"); err == nil {
129 t.Fatal("agent started a unit reservation")
130 }
131 if err := j.Settled(ctx, r); err == nil {
132 t.Fatal("agent settled a unit reservation")
133 }
134 group := LaunchGroup{RelativePath: r.Group.RelativePath, Device: 3, Inode: 4, InvocationID: "unit-1"}
135 wrong := r
136 wrong.Executable.Inode++
137 if err := j.StartedUnit(ctx, wrong, group, 99, "123"); !errors.Is(err, ErrChanged) {
138 t.Fatalf("different executable started: %v", err)
139 }
140 if err := j.StartedUnit(ctx, r, LaunchGroup{RelativePath: "carlos.slice/other.service", Device: 3, Inode: 4, InvocationID: "unit-1"}, 99, "123"); err == nil {
141 t.Fatal("other group started")
142 }
143 if err := j.StartedUnit(ctx, r, group, 99, "123"); err != nil {
144 t.Fatal(err)
145 }
146 if err := j.StartedUnit(ctx, r, group, 99, "123"); !errors.Is(err, ErrChanged) {
147 t.Fatalf("restarted: %v", err)
148 }
149 got, err := j.UnitReservation(ctx, r.Target, r.ID)
150 if err != nil || got.State != UnitStarted || got.Group != group || got.PID != 99 || got.ProcessStart != "123" {
151 t.Fatalf("started=%+v %v", got, err)
152 }
153 if sj, err = j.ReadStopKernelJournal(ctx, r.Target); err != nil || sj.Settled || len(sj.Cgroups) != 1 {
154 t.Fatalf("started unit leaked into the agent cgroup scan or settled: %+v %v", sj, err)
155 }
156 if err := j.SettleUnit(ctx, r.Target, r.ID); err != nil {
157 t.Fatal(err)
158 }
159 if err := j.SettleUnit(ctx, r.Target, r.ID); err != nil {
160 t.Fatalf("settle not idempotent: %v", err)
161 }
162 if sj, err = j.ReadStopKernelJournal(ctx, r.Target); err != nil || !sj.Settled {
163 t.Fatalf("settled unit still pending: %+v %v", sj, err)
164 }
165 // A settled ID is spent for good.
166 if err := j.IntendUnit(ctx, r); !errors.Is(err, ErrChanged) {
167 t.Fatalf("spent reservation reused: %v", err)
168 }
169 if err := j.IntendUnit(ctx, other); err != nil {
170 t.Fatalf("next attempt after settlement: %v", err)
171 }
172}
173
174func TestUnitTombstoneBurnsAnUnseenReservation(t *testing.T) {
175 ctx := context.Background()
176 j, r := roleLaunchFixture(t)
177 if err := j.TombstoneUnit(ctx, r); err != nil {
178 t.Fatal(err)
179 }
180 if err := j.IntendUnit(ctx, r); !errors.Is(err, ErrChanged) {
181 t.Fatalf("start after its stop accepted: %v", err)
182 }
183 got, err := j.UnitReservation(ctx, r.Target, r.ID)
184 if err != nil || got.State != UnitSettled || got.PID != 0 || !got.NeverIntended {
185 t.Fatalf("tombstone=%+v %v", got, err)
186 }
187}
188
189func TestUnitJournalRefusesRemoteAndMissingRecords(t *testing.T) {
190 ctx := context.Background()
191 _, r := roleLaunchFixture(t)
192 remote, _ := RemoteJournals(func(context.Context, JournalRequest) (JournalResponse, error) {
193 t.Fatal("unit operation reached the agent journal transport")
194 return JournalResponse{}, nil
195 })
196 if err := remote.IntendUnit(ctx, r); !errors.Is(err, ErrUnsupported) {
197 t.Fatalf("remote intention: %v", err)
198 }
199 if err := remote.TombstoneUnit(ctx, r); !errors.Is(err, ErrUnsupported) {
200 t.Fatalf("remote tombstone: %v", err)
201 }
202 j, err := OpenLaunchJournal(filepath.Join(t.TempDir(), "launch.db"))
203 if err != nil {
204 t.Fatal(err)
205 }
206 defer j.Close()
207 if err := j.IntendUnit(ctx, r); err == nil {
208 t.Fatal("intention without the prelaunch record")
209 }
210}
211
212func TestPlan14NeverIntendedIsRootOnly(t *testing.T) {
213 j, r := roleLaunchFixture(t)
214 r.NeverIntended = true
215 if unitReservationShape(r) {
216 t.Fatal("agent set root cancellation proof")
217 }
218 if err := j.TombstoneUnit(t.Context(), r); err == nil {
219 t.Fatal("forged cancellation proof accepted")
220 }
221 if err := j.IntendRole(t.Context(), r); err == nil {
222 t.Fatal("forged legacy intention accepted")
223 }
224 if err := j.Reserve(t.Context(), r); err == nil {
225 t.Fatal("forged legacy reservation accepted")
226 }
227}
228