| 1 | //go:build linux |
| 2 | |
| 3 | package hostfiles |
| 4 | |
| 5 | import ( |
| 6 | "bytes" |
| 7 | "context" |
| 8 | "encoding/json" |
| 9 | "errors" |
| 10 | "os" |
| 11 | "strings" |
| 12 | "testing" |
| 13 | "time" |
| 14 | |
| 15 | "github.com/carlosframework/platform/internal/release" |
| 16 | ) |
| 17 | |
| 18 | // inheritOverCap writes, under pfx, the manifest a writer from before the |
| 19 | // path cap could have left: a healthy published file beside an entry whose |
| 20 | // path is past MaxPathBytes. It returns the manifest's bytes. |
| 21 | func inheritOverCap(t *testing.T, s release.Store) []byte { |
| 22 | t.Helper() |
| 23 | ctx := context.Background() |
| 24 | p := open(t, s) |
| 25 | stage(t, p, "healthy.txt", "fine") |
| 26 | commit(t, p) |
| 27 | m, _, err := Load(ctx, s, pfx) |
| 28 | if err != nil { |
| 29 | t.Fatal(err) |
| 30 | } |
| 31 | m.Seq++ |
| 32 | m.Entries[pathOfBytes(MaxPathBytes+1)] = Entry{Seq: m.Seq, Skipped: "too-large"} |
| 33 | body, err := json.Marshal(m) |
| 34 | if err != nil { |
| 35 | t.Fatal(err) |
| 36 | } |
| 37 | _, etag, err := s.Get(ctx, ManifestKey(pfx)) |
| 38 | if err != nil { |
| 39 | t.Fatal(err) |
| 40 | } |
| 41 | if _, err := s.Put(ctx, ManifestKey(pfx), body, etag); err != nil { |
| 42 | t.Fatal(err) |
| 43 | } |
| 44 | return body |
| 45 | } |
| 46 | |
| 47 | // Fix 5 (Astra confirming P2): a manifest inherited with a path past the |
| 48 | // cap is refused by Open, terminally, and by every reader, and nothing is |
| 49 | // committed over it. |
| 50 | func TestAnInheritedManifestPastTheCapIsRefusedByOpenAndTheReaders(t *testing.T) { |
| 51 | ctx := context.Background() |
| 52 | s := release.NewDirStore(t.TempDir()) |
| 53 | before := inheritOverCap(t, s) |
| 54 | if p, err := Open(ctx, s, pfx); !errors.Is(err, ErrTooDeep) || p != nil { |
| 55 | t.Fatalf("Open of an inherited over-cap manifest: %v", err) |
| 56 | } |
| 57 | if _, _, err := Restore(ctx, s, pfx, t.TempDir(), nil); !errors.Is(err, ErrTooDeep) { |
| 58 | t.Fatalf("Restore: %v", err) |
| 59 | } |
| 60 | m, _, err := Load(ctx, s, pfx) |
| 61 | if err != nil { |
| 62 | t.Fatal(err) |
| 63 | } |
| 64 | if err := ValidateManifest(m, []string{pfx}); !errors.Is(err, ErrTooDeep) { |
| 65 | t.Fatalf("ValidateManifest: %v", err) |
| 66 | } |
| 67 | const to = "instances/shop.test/abc/files/g9/" |
| 68 | if _, err := CopyManifest(ctx, s, pfx, to, 0, nil); !errors.Is(err, ErrTooDeep) { |
| 69 | t.Fatalf("CopyManifest: %v", err) |
| 70 | } |
| 71 | if _, _, err := s.Get(ctx, ManifestKey(to)); !errors.Is(err, os.ErrNotExist) { |
| 72 | t.Fatalf("CopyManifest wrote its target: %v", err) |
| 73 | } |
| 74 | after, _, err := s.Get(ctx, ManifestKey(pfx)) |
| 75 | if err != nil || !bytes.Equal(before, after) { |
| 76 | t.Fatalf("the inherited manifest was rewritten: %v", err) |
| 77 | } |
| 78 | } |
| 79 | |
| 80 | // Run over a publisher holding such an entry (one continued before Open |
| 81 | // refused them) stops on its first commit with one line, never retrying |
| 82 | // at every tick, and commits nothing. |
| 83 | func TestRunStopsOnAManifestItCannotCommit(t *testing.T) { |
| 84 | logs := captureLog(t) |
| 85 | s, dir := release.NewDirStore(t.TempDir()), t.TempDir() |
| 86 | p := open(t, s) |
| 87 | stage(t, p, "healthy.txt", "fine") |
| 88 | commit(t, p) |
| 89 | before, _, err := s.Get(context.Background(), ManifestKey(pfx)) |
| 90 | if err != nil { |
| 91 | t.Fatal(err) |
| 92 | } |
| 93 | p.mu.Lock() |
| 94 | p.next++ |
| 95 | p.man.Entries[pathOfBytes(MaxPathBytes+1)] = Entry{Seq: p.next, Skipped: "too-large"} |
| 96 | p.mu.Unlock() |
| 97 | write(t, dir, "healthy.txt", "changed") // the first scan stages it, and Commit is attempted |
| 98 | ctx, cancel := context.WithCancel(context.Background()) |
| 99 | defer cancel() |
| 100 | done := make(chan error, 1) |
| 101 | go func() { done <- run(ctx, p, dir, polled, nil) }() |
| 102 | select { |
| 103 | case err := <-done: |
| 104 | if !errors.Is(err, ErrTooDeep) { |
| 105 | t.Fatalf("run ended with %v, want ErrTooDeep", err) |
| 106 | } |
| 107 | case <-time.After(3 * time.Second): |
| 108 | t.Fatalf("run kept going on a manifest no commit can write (%d commit lines)", logs.count("commit")) |
| 109 | } |
| 110 | if got := logs.count("cannot commit"); got != 1 { |
| 111 | t.Fatalf("logged %d times, want once", got) |
| 112 | } |
| 113 | after, _, err := s.Get(context.Background(), ManifestKey(pfx)) |
| 114 | if err != nil || !bytes.Equal(before, after) { |
| 115 | t.Fatalf("something was committed: %v", err) |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | // RestorableWithinCap is the readers' rule, exported for the checks made |
| 120 | // before work that needs a manifest restored (#236): an entry of any kind |
| 121 | // past the cap, live, tombstoned or skipped, is ErrTooDeep, and every |
| 122 | // reader refuses the same manifest. |
| 123 | func TestRestorableWithinCapIsTheReadersRule(t *testing.T) { |
| 124 | sha := strings.Repeat("a", 64) |
| 125 | for name, e := range map[string]Entry{ |
| 126 | "live": {Object: pfx + objectKey(1, sha), SHA256: sha, Seq: 1, Size: 1}, |
| 127 | "tombstone": {Seq: 1, Deleted: true}, |
| 128 | "skipped": {Seq: 1, Skipped: "too-large"}, |
| 129 | } { |
| 130 | past := Manifest{V: ManifestVersion, Seq: 1, Entries: map[string]Entry{pathOfBytes(MaxPathBytes + 1): e}, Platform: map[string]Entry{}} |
| 131 | if err := RestorableWithinCap(past); !errors.Is(err, ErrTooDeep) { |
| 132 | t.Fatalf("%s past the cap: %v", name, err) |
| 133 | } |
| 134 | if err := ValidateManifest(past, []string{pfx}); !errors.Is(err, ErrTooDeep) { |
| 135 | t.Fatalf("%s past the cap, ValidateManifest: %v", name, err) |
| 136 | } |
| 137 | at := Manifest{V: ManifestVersion, Seq: 1, Entries: map[string]Entry{pathOfBytes(MaxPathBytes): e}, Platform: map[string]Entry{}} |
| 138 | if err := RestorableWithinCap(at); err != nil { |
| 139 | t.Fatalf("%s at the cap: %v", name, err) |
| 140 | } |
| 141 | } |
| 142 | } |
| 143 | |