carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostfiles
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "reflect"
9 "strings"
10 "testing"
11
12 "github.com/carlosframework/platform/internal/release"
13)
14
15// Region HA stage B's rule (73bab739, relocationPlatformItem), carried
16// here so both sides share it: a live platform item is the host's; a
17// missing one is "none" only under a pin of a host that never published
18// files (no digest, no retained copy); a missing one under a pin of
19// published files, and a tombstone (which no publisher writes) under
20// either pin, refuse.
21func TestPlatformItemNeedsEvidenceForNone(t *testing.T) {
22 published := PinnedFiles{SHA256: "ab", Retained: "retained/ab"}
23 never := PinnedFiles{}
24 live := Manifest{Platform: map[string]Entry{EnvItem: {Object: "o", SHA256: "cd", Size: 1, Seq: 1}}}
25 missing := Manifest{Platform: map[string]Entry{}}
26 tomb := Manifest{Platform: map[string]Entry{EnvItem: {Deleted: true, Seq: 2}}}
27 for _, tc := range []struct {
28 name string
29 pin PinnedFiles
30 m Manifest
31 want bool
32 err error
33 }{
34 {"a live item", published, live, true, nil},
35 {"a live item, evidence or not", never, live, true, nil},
36 {"missing under published files", published, missing, false, ErrNoEvidence},
37 {"missing under a host that never published", never, missing, false, nil},
38 {"a tombstone under published files", published, tomb, false, ErrTombstone},
39 {"a tombstone under a host that never published", never, tomb, false, ErrTombstone},
40 } {
41 got, err := PlatformItem(tc.pin, tc.m, EnvItem)
42 if got != tc.want || !errors.Is(err, tc.err) || (tc.err == nil) != (err == nil) {
43 t.Errorf("%s: %v %v, want %v %v", tc.name, got, err, tc.want, tc.err)
44 }
45 }
46 // Half-made evidence is not a host that never published files: the
47 // pinner writes both or neither, and ReadPin refuses the rest (H2).
48 for _, half := range []PinnedFiles{{SHA256: "ab"}, {Retained: "retained/ab"}} {
49 if half.NeverPublished() {
50 t.Errorf("%+v reads as never published", half)
51 }
52 }
53}
54
55func prepared(t *testing.T, s release.Store, prefix string) *Prepared {
56 t.Helper()
57 p, err := NewPrepared(s, prefix)
58 if err != nil {
59 t.Fatal(err)
60 }
61 return p
62}
63
64// The prepared manifest (claim rev 6.30 R18-1, moves §7.6): one create-only
65// manifest at sequence 0 over create-only content objects, every object
66// keyed by its digest at sequence 0 under the prepared prefix. A second,
67// different build of the same prefix is refused at its commit (its objects
68// are referenced by no manifest, spec §7.6); the manifest the first build
69// wrote is still there, byte for byte.
70func TestPreparedIsCreateOnlyAtSequenceZero(t *testing.T) {
71 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
72 p := prepared(t, s, pfx)
73 if err := p.Stage(ctx, "a.txt", []byte("A"), 0o644); err != nil {
74 t.Fatal(err)
75 }
76 if err := p.StagePlatform(ctx, EnvItem, []byte("K=1\n")); err != nil {
77 t.Fatal(err)
78 }
79 m, body, version, err := p.Commit(ctx)
80 if err != nil {
81 t.Fatal(err)
82 }
83 if version == "" {
84 t.Fatal("Commit returned no version")
85 }
86 if m.Seq != 0 || m.V != ManifestVersion {
87 t.Fatalf("manifest seq %d v %d", m.Seq, m.V)
88 }
89 for name, e := range map[string]Entry{"a.txt": m.Entries["a.txt"], EnvItem: m.Platform[EnvItem]} {
90 if e.Seq != 0 || !strings.HasPrefix(e.Object, pfx+"objects/0000000000000000-") || e.SHA256 != sum([]byte(map[string]string{"a.txt": "A", EnvItem: "K=1\n"}[name])) {
91 t.Fatalf("%s: %+v", name, e)
92 }
93 if got, _, err := s.Get(ctx, e.Object); err != nil || sum(got) != e.SHA256 {
94 t.Fatalf("%s object: %v %q", name, err, got)
95 }
96 }
97 if err := ValidateManifest(m, []string{pfx}); err != nil {
98 t.Fatal(err)
99 }
100 stored, _, err := s.Get(ctx, ManifestKey(pfx))
101 if err != nil || string(stored) != string(body) {
102 t.Fatalf("stored manifest: %v %s", err, stored)
103 }
104 // A different build of the same prefix is refused and writes nothing.
105 q := prepared(t, s, pfx)
106 if err := q.Stage(ctx, "a.txt", []byte("B"), 0o644); err != nil {
107 t.Fatal(err)
108 }
109 if _, _, _, err := q.Commit(ctx); !errors.Is(err, ErrManifestExists) {
110 t.Fatalf("second build: %v", err)
111 }
112 if again, _, err := s.Get(ctx, ManifestKey(pfx)); err != nil || string(again) != string(body) {
113 t.Fatalf("the manifest was replaced: %s", again)
114 }
115 // Once committed, the builder is spent.
116 if err := p.Stage(ctx, "b.txt", []byte("B"), 0o644); err == nil {
117 t.Fatal("staged after commit")
118 }
119}
120
121// A replay of the same build finds every object by its digest and the
122// manifest by its key (create-only, so the late second write fails and is
123// read back): success, the same bytes, and no new object.
124func TestPreparedReplayReusesObjects(t *testing.T) {
125 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
126 build := func() ([]byte, error) {
127 p := prepared(t, s, pfx)
128 if err := p.Stage(ctx, "a.txt", []byte("A"), 0o644); err != nil {
129 return nil, err
130 }
131 if err := p.Stage(ctx, "big", make([]byte, MaxFileBytes+1), 0o644); err != nil {
132 return nil, err
133 }
134 if err := p.StagePlatform(ctx, WakeItem, []byte(`{"v":1}`)); err != nil {
135 return nil, err
136 }
137 _, body, version, err := p.Commit(ctx)
138 if err != nil {
139 return nil, err
140 }
141 if _, etag, err := s.Get(ctx, ManifestKey(pfx)); err != nil || version != etag {
142 return nil, fmt.Errorf("Commit returned version %q, the manifest is at %q (%v)", version, etag, err)
143 }
144 return body, nil
145 }
146 first, err := build()
147 if err != nil {
148 t.Fatal(err)
149 }
150 keys, err := s.List(ctx, pfx)
151 if err != nil {
152 t.Fatal(err)
153 }
154 second, err := build()
155 if err != nil {
156 t.Fatalf("replay: %v", err)
157 }
158 if string(first) != string(second) {
159 t.Fatalf("replay bytes differ:\n%s\n%s", first, second)
160 }
161 again, err := s.List(ctx, pfx)
162 if err != nil {
163 t.Fatal(err)
164 }
165 if len(again) != len(keys) || len(keys) != 3 { // two objects and the manifest
166 t.Fatalf("objects after replay: %v, before %v", again, keys)
167 }
168 var m Manifest
169 if err := json.Unmarshal(first, &m); err != nil {
170 t.Fatal(err)
171 }
172 if e := m.Entries["big"]; e.Skipped == "" || e.Object != "" {
173 t.Fatalf("the oversize file is not recorded as skipped: %+v", e)
174 }
175}
176
177// StageSkipped records a path the builder did not read (a file over the
178// limit, by its size): an entry with the reason, no object, at sequence 0.
179func TestPreparedStageSkippedNamesNoObject(t *testing.T) {
180 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
181 p := prepared(t, s, pfx)
182 if err := p.StageSkipped("big.bin", "too large"); err != nil {
183 t.Fatal(err)
184 }
185 if err := p.StageSkipped("../out", "x"); err == nil {
186 t.Fatal("staged a path outside the root")
187 }
188 m, _, _, err := p.Commit(ctx)
189 if err != nil {
190 t.Fatal(err)
191 }
192 if e := m.Entries["big.bin"]; e.Skipped != "too large" || e.Object != "" || e.Seq != 0 {
193 t.Fatalf("skipped entry: %+v", e)
194 }
195 keys, err := s.List(ctx, pfx)
196 if err != nil || len(keys) != 1 {
197 t.Fatalf("objects for a skipped entry: %v %v", keys, err)
198 }
199}
200
201// OpenAt publishes over the bytes its caller checked and the token read
202// with them: a replacement at the key after that read fails the commit
203// (ErrSuperseded) and the replacement stands; the same bytes still at the
204// key commit as usual.
205func TestOpenAtPublishesOnlyOverTheBytesRead(t *testing.T) {
206 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
207 src := open(t, s)
208 stage(t, src, "a.txt", "A")
209 commit(t, src)
210 body, etag, err := s.Get(ctx, ManifestKey(pfx))
211 if err != nil {
212 t.Fatal(err)
213 }
214 p, err := OpenAt(s, pfx, body, etag)
215 if err != nil {
216 t.Fatal(err)
217 }
218 if _, err := OpenAt(s, pfx, body, ""); err == nil {
219 t.Fatal("opened without a token")
220 }
221 if _, err := OpenAt(s, pfx, []byte(`{"v":1}`), etag); err == nil {
222 t.Fatal("opened a manifest of another version")
223 }
224 // Another writer replaces the key after the read.
225 other := open(t, s)
226 stage(t, other, "b.txt", "B")
227 commit(t, other)
228 replaced, _, _ := s.Get(ctx, ManifestKey(pfx))
229 stage(t, p, "c.txt", "C")
230 if _, err := p.Commit(ctx); !errors.Is(err, ErrSuperseded) {
231 t.Fatalf("commit over a replacement: %v", err)
232 }
233 if again, _, _ := s.Get(ctx, ManifestKey(pfx)); string(again) != string(replaced) {
234 t.Fatal("the replacement was published over")
235 }
236 // The same bytes still at the key: the commit goes through.
237 body, etag, _ = s.Get(ctx, ManifestKey(pfx))
238 q, err := OpenAt(s, pfx, body, etag)
239 if err != nil {
240 t.Fatal(err)
241 }
242 stage(t, q, "c.txt", "C")
243 if seq := commit(t, q); seq != 3 {
244 t.Fatalf("seq %d", seq)
245 }
246}
247
248// The admitted origin set travels in the manifest (Task 6 fix round 2,
249// finding 3): a prepared manifest carries its own prefix; a copy carries
250// the set its caller validated (SetOrigins, sorted and deduplicated, its
251// own prefix included), and a commit keeps it; an old manifest without
252// the field reads with none, so a reader falls back to the admitted
253// chain alone.
254func TestManifestCarriesItsOriginSet(t *testing.T) {
255 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
256 p := prepared(t, s, pfx)
257 if err := p.Stage(ctx, "a.txt", []byte("A"), 0o644); err != nil {
258 t.Fatal(err)
259 }
260 m, _, _, err := p.Commit(ctx)
261 if err != nil {
262 t.Fatal(err)
263 }
264 if !reflect.DeepEqual(m.Origins, []string{pfx}) {
265 t.Fatalf("prepared origins %v", m.Origins)
266 }
267 to := "instances/shop.test/abc/files/g2/"
268 q, err := NewFromCopy(s, to, m, []string{pfx})
269 if err != nil {
270 t.Fatal(err)
271 }
272 q.SetOrigins([]string{"instances/shop.test/abc/files/g1b/", pfx, to, pfx})
273 if _, err := q.Commit(ctx); err != nil {
274 t.Fatal(err)
275 }
276 got, found, err := Load(ctx, s, to)
277 if err != nil || !found {
278 t.Fatal(err, found)
279 }
280 if !reflect.DeepEqual(got.Origins, []string{pfx, "instances/shop.test/abc/files/g1b/", to}) { // sorted, deduplicated
281 t.Fatalf("copied origins %v", got.Origins)
282 }
283 // A later revision by the ordinary publisher keeps the set.
284 r := open(t, s)
285 stage(t, r, "b.txt", "B")
286 commit(t, r)
287 again, _, _ := Load(ctx, s, pfx)
288 if !reflect.DeepEqual(again.Origins, []string{pfx}) {
289 t.Fatalf("a commit dropped the set: %v", again.Origins)
290 }
291 // SetOrigins with the same set is not a change.
292 same, _ := OpenAt(s, to, mustBytes(t, s, to), mustETag(t, s, to))
293 same.SetOrigins([]string{to, pfx, "instances/shop.test/abc/files/g1b/"})
294 if same.Dirty() {
295 t.Fatal("the same set marked the publisher dirty")
296 }
297 // An old manifest without the field.
298 var old Manifest
299 if err := json.Unmarshal([]byte(`{"v":2,"seq":1,"entries":{},"platform":{}}`), &old); err != nil || old.Origins != nil {
300 t.Fatalf("old manifest: %v %v", old.Origins, err)
301 }
302}
303
304func mustBytes(t *testing.T, s release.Store, prefix string) []byte {
305 t.Helper()
306 b, _, err := s.Get(context.Background(), ManifestKey(prefix))
307 if err != nil {
308 t.Fatal(err)
309 }
310 return b
311}
312
313func mustETag(t *testing.T, s release.Store, prefix string) string {
314 t.Helper()
315 _, etag, err := s.Get(context.Background(), ManifestKey(prefix))
316 if err != nil {
317 t.Fatal(err)
318 }
319 return etag
320}
321
322// NewFromCopy seeds a publisher for a generation that has no manifest yet
323// with a predecessor's validated manifest: every entry keeps its
324// origin-qualified object, the sequence continues from the copy, and the
325// first Commit is create-only. A repeat with the same result reads its own
326// bytes back; another manifest at the target refuses.
327func TestNewFromCopyContinuesTheSequenceCreateOnly(t *testing.T) {
328 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
329 src := open(t, s)
330 stage(t, src, "a.txt", "A")
331 if err := src.StagePlatform(ctx, EnvItem, []byte("E")); err != nil {
332 t.Fatal(err)
333 }
334 if err := src.StagePlatform(ctx, "instance-key-adoption.json", []byte("{}")); err != nil {
335 t.Fatal(err)
336 }
337 if seq := commit(t, src); seq != 3 {
338 t.Fatalf("source seq %d", seq)
339 }
340 base, found, err := Load(ctx, s, pfx)
341 if err != nil || !found {
342 t.Fatal(err, found)
343 }
344 to := "instances/shop.test/abc/files/g2/"
345 establish := func() (int64, error) {
346 p, err := NewFromCopy(s, to, base, []string{pfx})
347 if err != nil {
348 return 0, err
349 }
350 if err := p.StagePlatform(ctx, EnvItem, []byte("E2")); err != nil {
351 return 0, err
352 }
353 if err := p.StagePlatformDelete("instance-key-adoption.json"); err != nil {
354 return 0, err
355 }
356 return p.Commit(ctx)
357 }
358 seq, err := establish()
359 if err != nil {
360 t.Fatal(err)
361 }
362 if seq != 5 {
363 t.Fatalf("seq %d, want 5 (3 copied, the env, the delete)", seq)
364 }
365 m, found, err := Load(ctx, s, to)
366 if err != nil || !found {
367 t.Fatal(err, found)
368 }
369 if m.Entries["a.txt"].Object != base.Entries["a.txt"].Object {
370 t.Fatalf("the copy renamed an object: %+v", m.Entries["a.txt"])
371 }
372 if e := m.Platform[EnvItem]; e.SHA256 != sum([]byte("E2")) || !strings.HasPrefix(e.Object, to+"objects/") {
373 t.Fatalf("env: %+v", e)
374 }
375 if _, ok := m.Platform["instance-key-adoption.json"]; ok {
376 t.Fatal("the adoption item was carried")
377 }
378 if again, err := establish(); err != nil || again != 5 {
379 t.Fatalf("repeat: %d %v", again, err)
380 }
381 // Another manifest at the target: the copy refuses, nothing replaced.
382 other := "instances/shop.test/abc/files/g3/"
383 if _, err := s.Put(ctx, ManifestKey(other), []byte(`{"v":2,"seq":9,"entries":{},"platform":{}}`), ""); err != nil {
384 t.Fatal(err)
385 }
386 p, err := NewFromCopy(s, other, base, []string{pfx})
387 if err != nil {
388 t.Fatal(err)
389 }
390 if _, err := p.Commit(ctx); !errors.Is(err, ErrSuperseded) {
391 t.Fatalf("over another manifest: %v", err)
392 }
393 // An empty base (the claim's own empty bootstrap, or a never-published
394 // host) is allowed; a base that fails validation is not.
395 if _, err := NewFromCopy(s, "instances/shop.test/abc/files/g4/", Manifest{V: ManifestVersion}, nil); err != nil {
396 t.Fatal(err)
397 }
398 bad := base
399 bad.Entries = map[string]Entry{"x": {Object: "elsewhere/objects/0000000000000001-" + sum(nil), SHA256: sum(nil), Seq: 1}}
400 if _, err := NewFromCopy(s, "instances/shop.test/abc/files/g4/", bad, []string{pfx}); !errors.Is(err, ErrOrigin) {
401 t.Fatalf("foreign origin: %v", err)
402 }
403}
404
405// The prepared builder and OpenAt hold the same path cap as Publisher:
406// a prepared manifest cannot stage a path no reader accepts, and OpenAt
407// refuses to continue a manifest that holds one (as Open does).
408func TestPreparedAndOpenAtHoldThePathCap(t *testing.T) {
409 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
410 long := strings.Repeat("d/", MaxPathBytes/2+1) + "f"
411 p := prepared(t, s, pfx)
412 if err := p.Stage(ctx, long, []byte("x"), 0o644); !errors.Is(err, ErrTooDeep) {
413 t.Fatalf("Stage past the cap: %v; want ErrTooDeep", err)
414 }
415 if err := p.StageSkipped(long, "too large"); !errors.Is(err, ErrTooDeep) {
416 t.Fatalf("StageSkipped past the cap: %v; want ErrTooDeep", err)
417 }
418 m := Manifest{V: ManifestVersion, Seq: 1, Entries: map[string]Entry{long: {Seq: 1}}, Platform: map[string]Entry{}}
419 body, err := json.Marshal(m)
420 if err != nil {
421 t.Fatal(err)
422 }
423 if _, err := OpenAt(s, pfx, body, "etag"); !errors.Is(err, ErrTooDeep) {
424 t.Fatalf("OpenAt over a manifest past the cap: %v; want ErrTooDeep", err)
425 }
426}
427