| 1 | package release |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "context" |
| 6 | "os" |
| 7 | "testing" |
| 8 | ) |
| 9 | |
| 10 | func TestPlan14GenerationSiblingWrites(t *testing.T) { |
| 11 | for _, mode := range []string{"repair", "sync", "backfill", "bound-backfill"} { |
| 12 | type outcome struct { |
| 13 | sibling, live, version, previous, output string |
| 14 | restarts int |
| 15 | } |
| 16 | outcomes := map[string]outcome{} |
| 17 | for _, authority := range []string{"pinned", "unknown", "unreserved", "legacy"} { |
| 18 | t.Run(mode+"/"+authority, func(t *testing.T) { |
| 19 | f := newReuseFixture(t) |
| 20 | version := "v2" |
| 21 | if mode == "backfill" || mode == "bound-backfill" { |
| 22 | version = "v1" |
| 23 | if err := Promote(t.Context(), f.s, f.priv, "bab", "myapp", "canary/x", version); err != nil { |
| 24 | t.Fatal(err) |
| 25 | } |
| 26 | } else if err := StageArtifact(f.opt, "bab", version, "myapp", []byte("new v2")); err != nil { |
| 27 | t.Fatal(err) |
| 28 | } |
| 29 | _, raw, err := GetManifestRaw(t.Context(), f.s, "bab", "myapp", version) |
| 30 | if err != nil { |
| 31 | t.Fatal(err) |
| 32 | } |
| 33 | ptr, err := Channel(t.Context(), f.s, f.pub, "bab", "myapp", "canary/x") |
| 34 | if err != nil { |
| 35 | t.Fatal(err) |
| 36 | } |
| 37 | ptr.Digest, ptr.HistoryDigests = "", nil |
| 38 | if mode == "sync" || mode == "bound-backfill" { |
| 39 | ptr.Digest = ManifestDigest(raw) |
| 40 | } |
| 41 | signed, err := signPointer(RootSigner(f.priv), ptr) |
| 42 | if err != nil { |
| 43 | t.Fatal(err) |
| 44 | } |
| 45 | _, etag, err := GetQualified(t.Context(), f.s, ChannelKey("bab", "myapp", "canary/x")) |
| 46 | if err != nil { |
| 47 | t.Fatal(err) |
| 48 | } |
| 49 | if _, err := f.s.Put(t.Context(), ChannelKey("bab", "myapp", "canary/x"), signed, etag); err != nil { |
| 50 | t.Fatal(err) |
| 51 | } |
| 52 | path := StagedManifestPath(f.opt, "bab", version, "myapp") |
| 53 | original := []byte("{\"stale\":true}") |
| 54 | if mode == "sync" { |
| 55 | if err := StageManifest(f.opt, "bab", version, "myapp", original); err != nil { |
| 56 | t.Fatal(err) |
| 57 | } |
| 58 | } else if err := os.Remove(path); err != nil && !os.IsNotExist(err) { |
| 59 | t.Fatal(err) |
| 60 | } |
| 61 | var before os.FileInfo |
| 62 | if mode == "sync" { |
| 63 | before, err = os.Stat(path) |
| 64 | if err != nil { |
| 65 | t.Fatal(err) |
| 66 | } |
| 67 | } |
| 68 | g, _ := generationPinFixture(t, f.opt, "bab", "myapp", version) |
| 69 | var source PinSource = RootGenerationPins{Ledger: g, Verify: func(context.Context, []GenerationReservation) error { return nil }} |
| 70 | if authority == "unknown" { |
| 71 | source = RootGenerationPins{Ledger: g} |
| 72 | } |
| 73 | if authority == "unreserved" { |
| 74 | source = PinSourceFunc(func(context.Context, string) (StagedPaths, error) { return StagedPaths{}, nil }) |
| 75 | } |
| 76 | var opts []PassOption |
| 77 | if authority != "legacy" { |
| 78 | opts = append(opts, WithRootGenerationPins(source)) |
| 79 | } |
| 80 | restarts := 0 |
| 81 | var output bytes.Buffer |
| 82 | withCapturedStdout(t, &output, func() { |
| 83 | if err := Pass(t.Context(), f.reg, f.s, f.pub, f.opt, func(string) error { restarts++; return nil }, opts...); err != nil { |
| 84 | t.Fatal(err) |
| 85 | } |
| 86 | }) |
| 87 | got, readErr := os.ReadFile(path) |
| 88 | live, err := LiveVersion(f.opt, "h1.example.com") |
| 89 | if err != nil { |
| 90 | t.Fatal(err) |
| 91 | } |
| 92 | row, err := f.reg.Get("h1.example.com") |
| 93 | if err != nil || row == nil { |
| 94 | t.Fatal(err) |
| 95 | } |
| 96 | outcomes[authority] = outcome{string(got), live, row.Version, row.PrevVersion, output.String(), restarts} |
| 97 | if authority == "pinned" || authority == "unknown" { |
| 98 | if mode == "sync" { |
| 99 | after, err := os.Stat(path) |
| 100 | if readErr != nil || err != nil || !bytes.Equal(got, original) || !os.SameFile(before, after) { |
| 101 | t.Fatalf("reserved sibling replaced: %q %v %v", got, readErr, err) |
| 102 | } |
| 103 | } else if !os.IsNotExist(readErr) { |
| 104 | t.Fatalf("reserved tree gained sibling: %q %v", got, readErr) |
| 105 | } |
| 106 | } else if readErr != nil || !bytes.Equal(got, raw) { |
| 107 | t.Fatalf("permitted sibling not written: %q %v", got, readErr) |
| 108 | } |
| 109 | }) |
| 110 | } |
| 111 | if outcomes["unreserved"] != outcomes["legacy"] { |
| 112 | t.Fatalf("%s changed legacy effects: branch=%+v main=%+v", mode, outcomes["unreserved"], outcomes["legacy"]) |
| 113 | } |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | func TestPlan14GenerationVerifiedSiblingWrites(t *testing.T) { |
| 118 | f := newReuseFixture(t) |
| 119 | if err := StageArtifact(f.opt, "bab", "v2", "myapp", []byte("new v2")); err != nil { |
| 120 | t.Fatal(err) |
| 121 | } |
| 122 | m, raw, err := GetManifestRaw(t.Context(), f.s, "bab", "myapp", "v2") |
| 123 | if err != nil { |
| 124 | t.Fatal(err) |
| 125 | } |
| 126 | path := VersionPath(f.opt, "bab", "v2", "myapp") |
| 127 | calls := 0 |
| 128 | chk := stageCheck{verified: map[string]string{path: m.Artifacts[adoptPlatform].SHA256}, beforeWrite: func(string) error { calls++; return os.ErrPermission }} |
| 129 | if _, err := stageFrom(t.Context(), f.s, f.opt, "bab", "myapp", "v2", "h1.example.com", chk, true, m, raw, true); err != nil { |
| 130 | t.Fatal(err) |
| 131 | } |
| 132 | if _, err := os.Stat(StagedManifestPath(f.opt, "bab", "v2", "myapp")); !os.IsNotExist(err) || calls != 1 { |
| 133 | t.Fatalf("verified cache bypassed sibling check: %v calls=%d", err, calls) |
| 134 | } |
| 135 | } |
| 136 | |