carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package host
4
5import (
6 "context"
7 "errors"
8 "os"
9 "path/filepath"
10 "reflect"
11 "testing"
12 "time"
13
14 "github.com/carlosframework/platform/internal/hostbox"
15 "github.com/carlosframework/platform/internal/provision"
16 "golang.org/x/sys/unix"
17)
18
19func servingRebootRecordFixture(t *testing.T, phase string) (*servingParkDiskFixture, servingParkRecord) {
20 t.Helper()
21 f := newServingParkDiskFixture(t)
22 a, receipts := f.evidence(1, true, "missing")
23 f.put("", "last database")
24 f.put("-wal", "last WAL")
25 f.put("-shm", "last SHM")
26 if err := f.fs.recordNamespaceParent(t.Context(), a.Successor, a.Directory); err != nil {
27 t.Fatal(err)
28 }
29 root, _, err := f.fs.namespace()
30 if err != nil {
31 t.Fatal(err)
32 }
33 root.Close()
34 var r servingParkRecord
35 if phase == "captured" || phase == "prepared" || phase == "rename-reply" {
36 r, err = f.ledger.capture(t.Context(), a, receipts, nil)
37 if err != nil {
38 t.Fatal(err)
39 }
40 if err = f.ledger.save(r); err != nil {
41 t.Fatal(err)
42 }
43 save := func() error { return f.ledger.save(r) }
44 if phase != "captured" {
45 if err = f.fs.Prepare(&r, save); err != nil {
46 t.Fatal(err)
47 }
48 }
49 if phase == "rename-reply" {
50 f.fs.rename = func(from int, old string, to int, name string, flags uint) error {
51 if err := unix.Renameat2(from, old, to, name, flags); err != nil {
52 return err
53 }
54 return unix.EIO
55 }
56 if err = f.fs.Move(&r, save); !errors.Is(err, unix.EIO) {
57 t.Fatal("missing lost rename reply", err)
58 }
59 f.fs.rename = unix.Renameat2
60 }
61 } else {
62 r, err = f.park(a, receipts)
63 if err != nil {
64 t.Fatal(err)
65 }
66 if phase != "parked" {
67 f.complete(a)
68 }
69 if phase == "complete" {
70 f.put("", "successor database")
71 }
72 if phase == "unlink-reply" || phase == "partial-unlink" || phase == "rmdir-reply" || phase == "closed" {
73 f.now = f.now.Add(8 * 24 * time.Hour)
74 calls := 0
75 f.fs.unlink = func(fd int, name string, flags int) error {
76 calls++
77 if phase == "partial-unlink" && calls == 2 {
78 return unix.EIO
79 }
80 if err := unix.Unlinkat(fd, name, flags); err != nil {
81 return err
82 }
83 if phase == "unlink-reply" || phase == "rmdir-reply" && flags == unix.AT_REMOVEDIR {
84 return unix.EIO
85 }
86 return nil
87 }
88 err = f.ledger.withNamespace(t.Context(), func() error { return f.ledger.reapLocked(t.Context(), a.ID) })
89 if phase == "closed" && err != nil || phase != "closed" && !errors.Is(err, unix.EIO) {
90 t.Fatal(phase, err)
91 }
92 f.fs.unlink = unix.Unlinkat
93 }
94 }
95 r, err = f.ledger.read(a.ID)
96 if err != nil {
97 t.Fatal(err)
98 }
99 f.fs.boot = func() (string, error) { return servingOtherBoot, nil }
100 return f, r
101}
102
103func TestServingParkRebootRecordReadOnly(t *testing.T) {
104 for _, phase := range []string{"captured", "prepared", "rename-reply", "parked", "complete", "unlink-reply", "partial-unlink", "rmdir-reply", "closed"} {
105 t.Run(phase, func(t *testing.T) {
106 f, r := servingRebootRecordFixture(t, phase)
107 before := servingBootSnapshot(t, f.disk.dir)
108 var got servingParkRebootRecord
109 err := f.ledger.withNamespace(t.Context(), func() error {
110 var err error
111 got, err = f.fs.inspectRebootRecord(t.Context(), r.Authorization.ID)
112 return err
113 })
114 if err != nil || !reflect.DeepEqual(got.Record, r) || got.Topology.BootID != servingOtherBoot {
115 t.Fatal("record inspection", got, err)
116 }
117 if r.Complete && got.Source != (servingArtifactDirectory{}) {
118 t.Fatal("completed park depended on live source")
119 }
120 if after := servingBootSnapshot(t, f.disk.dir); !reflect.DeepEqual(before, after) {
121 t.Fatal("inspection wrote authority")
122 }
123 if err := f.fs.CheckParked(&r); err == nil {
124 t.Fatal("observation granted old-boot effects")
125 }
126 })
127 }
128}
129
130func TestServingParkRebootRecordRefusesChangedEvidence(t *testing.T) {
131 for _, fault := range []string{"no-source-proof", "no-file-proof", "no-event-proof", "source-boot", "source-handle", "event-handle", "file-handle", "file-filesystem", "file-mode", "event-mode", "namespace", "source-replaced", "event-replaced", "file-replaced", "file-hardlink", "file-symlink", "unexpected-child", "missing-file", "duplicate-source", "old-format", "source-race", "event-race", "record-race", "cancel-race"} {
132 t.Run(fault, func(t *testing.T) {
133 f, r := servingRebootRecordFixture(t, "parked")
134 event := filepath.Dir(f.path(r.Authorization, ""))
135 source := filepath.Dir(provisionDBPath(f))
136 ctx, cancel := context.WithCancel(t.Context())
137 defer cancel()
138 switch fault {
139 case "no-source-proof":
140 r.Source.Persistent = servingParkPersistentDirectory{}
141 case "no-file-proof":
142 r.Source.Files[0].Persistent = servingParkPersistentFile{}
143 case "no-event-proof":
144 r.EventPersistent = servingParkPersistentDirectory{}
145 case "source-boot":
146 r.Source.BootID = servingOtherBoot
147 case "source-handle":
148 r.Source.Persistent.Handle = "aa"
149 case "event-handle":
150 r.EventPersistent.Handle = "aa"
151 case "file-handle":
152 r.Source.Files[0].Persistent.Handle = "aa"
153 case "file-filesystem":
154 r.Source.Files[0].Persistent.Filesystem[0]++
155 case "file-mode":
156 r.Source.Files[0].Persistent.Mode ^= 0100
157 case "event-mode":
158 r.EventPersistent.Mode ^= 0100
159 case "namespace":
160 r.Namespace.Inode++
161 case "source-replaced", "event-replaced":
162 path := source
163 if fault == "event-replaced" {
164 path = event
165 }
166 if err := os.Rename(path, path+"-old"); err != nil {
167 t.Fatal(err)
168 }
169 if err := os.Mkdir(path, 0700); err != nil {
170 t.Fatal(err)
171 }
172 case "file-replaced", "file-hardlink", "file-symlink", "missing-file":
173 path := f.path(r.Authorization, "")
174 if fault == "file-hardlink" {
175 if err := os.Link(path, path+"-link"); err != nil {
176 t.Fatal(err)
177 }
178 break
179 }
180 if err := os.Rename(path, path+"-old"); err != nil {
181 t.Fatal(err)
182 }
183 if fault == "file-symlink" {
184 if err := os.Symlink(path+"-old", path); err != nil {
185 t.Fatal(err)
186 }
187 }
188 if fault == "file-replaced" {
189 if err := os.WriteFile(path, []byte("replacement"), 0600); err != nil {
190 t.Fatal(err)
191 }
192 }
193 // Put the retained original outside the event so unknown-child checks
194 // cannot hide a missing/replaced-file validation bug.
195 if err := os.Rename(path+"-old", source+"/saved-original"); err != nil {
196 t.Fatal(err)
197 }
198 case "unexpected-child":
199 if err := os.WriteFile(filepath.Join(event, "unknown"), []byte("retain"), 0600); err != nil {
200 t.Fatal(err)
201 }
202 case "duplicate-source":
203 f.put("", "replacement")
204 case "old-format":
205 r.Source.Persistent = servingParkPersistentDirectory{}
206 r.Source.BootID = ""
207 r.EventPersistent = servingParkPersistentDirectory{}
208 for i := range r.Source.Files {
209 r.Source.Files[i].Persistent = servingParkPersistentFile{}
210 }
211 case "source-race", "event-race", "record-race", "cancel-race":
212 calls := 0
213 f.fs.handle = func(d *os.File) (servingParkPersistentDirectory, error) {
214 p, err := servingParkPersistentDir(d)
215 calls++
216 // The first topology inspection takes four handles; the pinned
217 // topology takes two, followed by source and event.
218 if calls == 8 {
219 switch fault {
220 case "cancel-race":
221 cancel()
222 case "record-race":
223 changed := r
224 changed.Receipts = nil
225 if err := f.ledger.write("retirement-"+r.Authorization.ID+".json", changed); err != nil {
226 t.Fatal(err)
227 }
228 default:
229 path := source
230 if fault == "event-race" {
231 path = event
232 }
233 if err := os.Rename(path, path+"-old"); err != nil {
234 t.Fatal(err)
235 }
236 if err := os.Mkdir(path, 0700); err != nil {
237 t.Fatal(err)
238 }
239 }
240 }
241 return p, err
242 }
243 }
244 if err := f.ledger.write("retirement-"+r.Authorization.ID+".json", r); err != nil {
245 t.Fatal(err)
246 }
247 before := servingBootSnapshot(t, f.disk.dir)
248 got, err := f.fs.inspectRebootRecord(ctx, r.Authorization.ID)
249 if err == nil || !reflect.DeepEqual(got, servingParkRebootRecord{}) {
250 t.Fatal("accepted changed record", got, err)
251 }
252 if fault != "record-race" {
253 if after := servingBootSnapshot(t, f.disk.dir); !reflect.DeepEqual(before, after) {
254 t.Fatal("refusal wrote authority")
255 }
256 }
257 })
258 }
259}
260
261func provisionDBPath(f *servingParkDiskFixture) string {
262 return filepath.Join(f.data, "instances", provision.InstanceID(f.base.Host), f.base.Host+".db")
263}
264
265func TestServingParkPersistentEvidenceNeverUpgradesOldIntent(t *testing.T) {
266 for _, phase := range []string{"intent", "consumption"} {
267 t.Run(phase, func(t *testing.T) {
268 f := newServingParkDiskFixture(t)
269 a, receipts := f.evidence(1, true, "missing")
270 f.put("", "legacy")
271 r, err := f.ledger.capture(t.Context(), a, receipts, nil)
272 if err != nil {
273 t.Fatal(err)
274 }
275 r.Source.Persistent = servingParkPersistentDirectory{}
276 r.Source.BootID = ""
277 for i := range r.Source.Files {
278 r.Source.Files[i].Persistent = servingParkPersistentFile{}
279 }
280 if phase == "intent" {
281 err = f.ledger.save(r)
282 } else {
283 err = f.ledger.write(servingLegacyKey("legacy", a.Successor), servingLegacyConsumption{Authorization: a, Source: r.Source, Receipts: receipts})
284 }
285 if err != nil {
286 t.Fatal(err)
287 }
288 parked, err := f.park(a, receipts)
289 if err != nil {
290 t.Fatal("old intent replay", err)
291 }
292 if parked.Source.Persistent != (servingParkPersistentDirectory{}) || parked.Source.BootID != "" || parked.EventPersistent != (servingParkPersistentDirectory{}) {
293 t.Fatal("upgraded old evidence")
294 }
295 f.complete(a)
296 })
297 }
298}
299
300func TestServingParkRebootFileRequiresDeleteIntent(t *testing.T) {
301 f, r := servingRebootRecordFixture(t, "unlink-reply")
302 r.Source.Files[0].DeleteIntent = false
303 if err := f.ledger.write("retirement-"+r.Authorization.ID+".json", r); err != nil {
304 t.Fatal(err)
305 }
306 if _, err := f.fs.inspectRebootRecord(t.Context(), r.Authorization.ID); !errors.Is(err, hostbox.ErrChanged) {
307 t.Fatal("absent artifact without delete intent accepted", err)
308 }
309}
310
311func TestServingParkRebootRecordChangedMount(t *testing.T) {
312 f, r := servingRebootRecordFixture(t, "rename-reply")
313 oldMount := r.Source.Parent.Mount + 123
314 r.Source.Parent.Mount = oldMount
315 r.Source.Directory.Mount = oldMount
316 r.Authorization.Directory.Mount = oldMount
317 r.Event.Mount = oldMount
318 r.Namespace.Mount = oldMount
319 for i := range r.Source.Files {
320 if r.Source.Files[i].Present {
321 r.Source.Files[i].Mount = oldMount
322 }
323 }
324 if err := f.ledger.save(r); err != nil {
325 t.Fatal(err)
326 }
327 parent, err := f.fs.namespaceParent()
328 if err != nil {
329 t.Fatal(err)
330 }
331 parent.Parent.Mount = oldMount
332 if err := f.ledger.write("namespace-parent.json", parent); err != nil {
333 t.Fatal(err)
334 }
335 var n servingParkNamespace
336 b, err := f.disk.Read("namespace.json")
337 if err != nil || servingParkDecode(b, &n) != nil {
338 t.Fatal(err)
339 }
340 n.Parent.Mount = oldMount
341 n.Root.Mount = oldMount
342 if err := f.ledger.write("namespace.json", n); err != nil {
343 t.Fatal(err)
344 }
345 got, err := f.fs.inspectRebootRecord(t.Context(), r.Authorization.ID)
346 if err != nil || got.Source.Mount == oldMount || got.Event.Mount == oldMount || got.Source.Mount != got.Event.Mount || !reflect.DeepEqual(got.Record, r) {
347 t.Fatal("changed-mount inspection", got, err)
348 }
349}
350
351func TestServingParkRebootCompletedDoesNotReadSource(t *testing.T) {
352 f, r := servingRebootRecordFixture(t, "complete")
353 path := filepath.Dir(provisionDBPath(f))
354 if err := os.Rename(path, path+"-removed"); err != nil {
355 t.Fatal(err)
356 }
357 if err := os.Symlink(path+"-removed", path); err != nil {
358 t.Fatal(err)
359 }
360 got, err := f.fs.inspectRebootRecord(t.Context(), r.Authorization.ID)
361 if err != nil || got.Source != (servingArtifactDirectory{}) {
362 t.Fatal("completed park consulted source", got, err)
363 }
364}
365
366func TestServingParkPersistentLegacyConsumption(t *testing.T) {
367 f, r := servingRebootRecordFixture(t, "parked")
368 c, err := f.ledger.legacyConsumption(r.Authorization.Successor)
369 if err != nil || c == nil {
370 t.Fatal(err)
371 }
372 if !sameServingLegacyConsumption(*c, r) || !sameServingLegacySource(r.Source, c.Source) {
373 t.Fatal("fixture consumption mismatch")
374 }
375 old := r.Source
376 old.Persistent = servingParkPersistentDirectory{}
377 old.BootID = ""
378 for i := range old.Files {
379 old.Files[i].Persistent = servingParkPersistentFile{}
380 }
381 partial := old
382 partial.Files[0].Persistent = r.Source.Files[0].Persistent
383 if sameServingLegacySource(old, partial) {
384 t.Fatal("partial consumed proof accepted as old evidence")
385 }
386 for _, field := range []string{"boot", "directory", "file"} {
387 t.Run(field, func(t *testing.T) {
388 changed := *c
389 switch field {
390 case "boot":
391 changed.Source.BootID = servingOtherBoot
392 case "directory":
393 changed.Source.Persistent.Handle = "aa"
394 case "file":
395 changed.Source.Files[0].Persistent.Handle = "aa"
396 }
397 if sameServingLegacyConsumption(changed, r) || sameServingLegacySource(r.Source, changed.Source) {
398 t.Fatal("changed immutable consumption accepted")
399 }
400 })
401 }
402}
403
404func TestServingParkPersistentEventCaptureFailure(t *testing.T) {
405 f := newServingParkDiskFixture(t)
406 a, receipts := f.evidence(1, true, "missing")
407 f.put("", "preserve source")
408 f.fs.handle = func(d *os.File) (servingParkPersistentDirectory, error) {
409 if d.Name() == a.ID {
410 return servingParkPersistentDirectory{}, unix.EIO
411 }
412 return servingParkPersistentDir(d)
413 }
414 if _, err := f.park(a, receipts); !errors.Is(err, unix.EIO) {
415 t.Fatal("event capture failure", err)
416 }
417 r, err := f.ledger.read(a.ID)
418 if err != nil || r.Prepared || r.EventPersistent != (servingParkPersistentDirectory{}) {
419 t.Fatal("failed capture gained event authority", err)
420 }
421 body, err := os.ReadFile(provisionDBPath(f))
422 if err != nil || string(body) != "preserve source" {
423 t.Fatal("capture failure changed source", err)
424 }
425 f.fs.handle = servingParkPersistentDir
426 if _, err := f.park(a, receipts); err != nil {
427 t.Fatal("same-boot event capture recovery", err)
428 }
429}
430