| new file mode 100644 |
| index 000000000..2ba4bff63 |
| --- /dev/null |
| +++ b/internal/release/writers_test.go |
| @@ -0,0 +1,360 @@ |
| +package release |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + "fmt" |
| + "path" |
| + "strings" |
| + "sync/atomic" |
| + "testing" |
| + "time" |
| +) |
| + |
| +func manifestDigestOf(t *testing.T, s Store, acct, app, ver string) string { |
| + t.Helper() |
| + raw, _, err := s.Get(context.Background(), ManifestKey(acct, app, ver)) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + return ManifestDigest(raw) |
| +} |
| + |
| +func readPtr(t *testing.T, s Store, pub []byte, ch string) *ChannelPointer { |
| + t.Helper() |
| + p, err := Channel(context.Background(), s, pub, "bab", "km", ch) |
| + if err != nil { |
| + t.Fatalf("Channel: %v", err) |
| + } |
| + return p |
| +} |
| + |
| +func TestWritersEmitNothingWithoutTheMarker(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one")) |
| + shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two")) |
| + for _, v := range []string{"v1", "v2"} { |
| + if err := Promote(ctx, s, priv, "bab", "km", "stable", v); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if p := readPtr(t, s, pub, "stable"); p.Digest != "" || p.HistoryDigests != nil { |
| + t.Fatalf("no marker, yet digests: %+v", p) |
| + } |
| + raw, _, _ := s.Get(ctx, ChannelKey("bab", "km", "stable")) |
| + if strings.Contains(string(raw), "digest") { |
| + t.Fatal("the wrapper mentions digests with the marker off") |
| + } |
| +} |
| + |
| +func TestPromoteSignsTheShippedDigestAndInheritsNothing(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one")) |
| + shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two")) |
| + shipRelease(t, s, "bab", "km", "v3", adoptPlatform, []byte("three")) |
| + if err := Promote(ctx, s, priv, "bab", "km", "stable", "v1"); err != nil { // a legacy pointer |
| + t.Fatal(err) |
| + } |
| + markerOn(t, s, priv, "bab") |
| + if err := Promote(ctx, s, priv, "bab", "km", "stable", "v2"); err != nil { |
| + t.Fatal(err) |
| + } |
| + p := readPtr(t, s, pub, "stable") |
| + if p.Digest != manifestDigestOf(t, s, "bab", "km", "v2") { |
| + t.Fatalf("head digest %q is not v2's shipped manifest", p.Digest) |
| + } |
| + // Spec §8: promoting over a legacy pointer leaves inherited entries |
| + // without digests, never computed. |
| + if len(p.HistoryDigests) != 1 || p.HistoryDigests[0] != "" { |
| + t.Fatalf("v1 was inherited from a legacy pointer, yet history_digests = %q", p.HistoryDigests) |
| + } |
| + if err := Promote(ctx, s, priv, "bab", "km", "stable", "v3"); err != nil { |
| + t.Fatal(err) |
| + } |
| + p = readPtr(t, s, pub, "stable") |
| + want := []string{manifestDigestOf(t, s, "bab", "km", "v2"), ""} |
| + if p.History[0] != "v2" || p.HistoryDigests[0] != want[0] || p.HistoryDigests[1] != "" { |
| + t.Fatalf("history %q / %q, want v2's signed digest copied and v1 still empty", p.History, p.HistoryDigests) |
| + } |
| +} |
| + |
| +func TestHistoryCapTrimsDigestsWithVersions(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + markerOn(t, s, priv, "bab") |
| + for i := 0; i < historyMax+3; i++ { |
| + v := "v" + string(rune('a'+i)) |
| + shipRelease(t, s, "bab", "km", v, adoptPlatform, []byte(v)) |
| + if err := Promote(ctx, s, priv, "bab", "km", "canary/x", v); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + p := readPtr(t, s, pub, "canary/x") |
| + if len(p.History) != historyMax || len(p.HistoryDigests) != historyMax { |
| + t.Fatalf("history %d, digests %d; want %d each", len(p.History), len(p.HistoryDigests), historyMax) |
| + } |
| + for i, v := range p.History { |
| + if p.HistoryDigests[i] != manifestDigestOf(t, s, "bab", "km", v) { |
| + t.Fatalf("history[%d] %s carries another build's digest", i, v) |
| + } |
| + } |
| +} |
| + |
| +// Review Focus 2. |
| +func TestPromoteRefusesAnUnverifiableMarker(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one")) |
| + other, _ := testKeys(t) |
| + markerOn(t, s, other, "bab") |
| + err := Promote(ctx, s, priv, "bab", "km", "stable", "v1") |
| + if err == nil || !strings.Contains(err.Error(), "marker") { |
| + t.Fatalf("Promote with a foreign marker = %v; want a refusal naming the marker", err) |
| + } |
| + if _, err := Channel(ctx, s, pub, "bab", "km", "stable"); err == nil { |
| + t.Fatal("the refused promotion still wrote a pointer") |
| + } |
| +} |
| + |
| +func TestRetryCopiesTheHeadDigest(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one")) |
| + if err := Promote(ctx, s, priv, "bab", "km", "stable", "v1"); err != nil { // legacy head |
| + t.Fatal(err) |
| + } |
| + markerOn(t, s, priv, "bab") |
| + time.Sleep(1100 * time.Millisecond) |
| + if _, err := PromoteOnLadder(ctx, s, priv, "bab", "km", "stable", "v1", PromoteOptions{Retry: true, Hotfix: true}); err != nil { |
| + t.Fatal(err) |
| + } |
| + if p := readPtr(t, s, pub, "stable"); p.Digest != "" { |
| + t.Fatalf("a retry computed a digest for the head it renewed: %q", p.Digest) |
| + } |
| +} |
| + |
| +func TestScopedPromoteCarriesDigests(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + root, pub := testKeys(t) |
| + markerOn(t, s, root, "bab") |
| + seed, grant, err := MintKeyGrant(root, "ci", []string{"canary"}, time.Hour) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + key, _ := ParseSigningKey(seed, "test") |
| + sg, err := ScopedSigner(key, grant, pub) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one")) |
| + if err := sg.Promote(ctx, s, "bab", "km", "canary/x", "v1"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if p := readPtr(t, s, pub, "canary/x"); p.Digest != manifestDigestOf(t, s, "bab", "km", "v1") { |
| + t.Fatalf("a scoped promotion signed digest %q", p.Digest) |
| + } |
| +} |
| + |
| +func TestRollbackSwapsDigestsWithVersions(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + markerOn(t, s, priv, "bab") |
| + shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one")) |
| + shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two")) |
| + for _, v := range []string{"v1", "v2"} { |
| + if err := Promote(ctx, s, priv, "bab", "km", "stable", v); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if err := Rollback(ctx, s, priv, "bab", "km", "stable"); err != nil { |
| + t.Fatal(err) |
| + } |
| + p := readPtr(t, s, pub, "stable") |
| + if p.Version != "v1" || p.Digest != manifestDigestOf(t, s, "bab", "km", "v1") || |
| + p.History[0] != "v2" || p.HistoryDigests[0] != manifestDigestOf(t, s, "bab", "km", "v2") { |
| + t.Fatalf("after rollback: %+v", p) |
| + } |
| +} |
| + |
| +func TestRollbackOntoAnEntryWithoutADigestIsRefused(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one")) |
| + shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two")) |
| + if err := Promote(ctx, s, priv, "bab", "km", "stable", "v1"); err != nil { |
| + t.Fatal(err) |
| + } |
| + markerOn(t, s, priv, "bab") |
| + if err := Promote(ctx, s, priv, "bab", "km", "stable", "v2"); err != nil { |
| + t.Fatal(err) |
| + } |
| + err := Rollback(ctx, s, priv, "bab", "km", "stable") |
| + var rn *ResignNeededError |
| + if !errors.As(err, &rn) || rn.Version != "v1" || err.Error() != fmt.Sprintf(TextRollbackResign, "km", "stable", "v1", "km", "stable") { |
| + t.Fatalf("rollback onto a digest-less v1 = %v; want ResignNeededError naming v1", err) |
| + } |
| + if p := readPtr(t, s, pub, "stable"); p.Version != "v2" { |
| + t.Fatalf("the refused rollback moved the channel to %s", p.Version) |
| + } |
| +} |
| + |
| +// manifestCountingStore counts manifest reads, so a test can prove a writer never |
| +// fetched one. |
| +type manifestCountingStore struct { |
| + Store |
| + manifests atomic.Int32 |
| +} |
| + |
| +func (c *manifestCountingStore) Get(ctx context.Context, key string) ([]byte, string, error) { |
| + if strings.HasSuffix(key, "/manifest.json") { |
| + c.manifests.Add(1) |
| + } |
| + return c.Store.Get(ctx, key) |
| +} |
| + |
| +func TestFastTrackKeepsTheSignedDigestsVerbatim(t *testing.T) { |
| + base := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + shipRelease(t, base, "bab", "km", "v1", adoptPlatform, []byte("one")) |
| + shipRelease(t, base, "bab", "km", "v2", adoptPlatform, []byte("two")) |
| + if err := Promote(ctx, base, priv, "bab", "km", "stable", "v1"); err != nil { |
| + t.Fatal(err) |
| + } |
| + markerOn(t, base, priv, "bab") |
| + if err := Promote(ctx, base, priv, "bab", "km", "stable", "v2"); err != nil { |
| + t.Fatal(err) |
| + } |
| + before := readPtr(t, base, pub, "stable") |
| + s := &manifestCountingStore{Store: base} |
| + if _, err := FastTrack(ctx, s, priv, "bab", "km", "stable", "ops@example.com"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if n := s.manifests.Load(); n != 0 { |
| + t.Fatalf("FastTrack read %d manifests; it must copy digests, never fetch them", n) |
| + } |
| + after := readPtr(t, base, pub, "stable") |
| + if !after.FastTrack || after.Digest != before.Digest || len(after.HistoryDigests) != 1 || after.HistoryDigests[0] != before.HistoryDigests[0] { |
| + t.Fatalf("FastTrack changed the digests: before %+v, after %+v", before, after) |
| + } |
| +} |
| + |
| +func TestRebindKeepsDigests(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + markerOn(t, s, priv, "bab") |
| + shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one")) |
| + if err := Promote(ctx, s, priv, "bab", "km", "stable", "v1"); err != nil { |
| + t.Fatal(err) |
| + } |
| + raw, _, _ := s.Get(ctx, ChannelKey("bab", "km", "stable")) |
| + out, err := RebindPointer(priv, raw, "bab", "cdf", "km", "stable") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + p, err := verifyPointer(pub, out, "cdf", "km", "stable") |
| + if err != nil || p.Digest != manifestDigestOf(t, s, "bab", "km", "v1") { |
| + t.Fatalf("rebound pointer = %+v, %v; want the digest carried verbatim", p, err) |
| + } |
| +} |
| + |
| +// reship deletes every object of a shipped version and ships the name again |
| +// with other bytes: the reused-name replay of #253. |
| +func reship(t *testing.T, s Store, acct, app, ver string, body []byte) { |
| + t.Helper() |
| + ctx := context.Background() |
| + keys, err := s.List(ctx, path.Dir(ManifestKey(acct, app, ver))+"/") |
| + if err != nil || len(keys) == 0 { |
| + t.Fatalf("List %s: %v %v", ver, keys, err) |
| + } |
| + for _, k := range keys { |
| + if err := s.Delete(ctx, k); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + shipRelease(t, s, acct, app, ver, adoptPlatform, body) |
| +} |
| + |
| +// Ruling P7: a promote onto a version in the predecessor's history is bound |
| +// to that entry's signed digest. |
| +func TestPromoteOfAHistoryEntryWithReshippedBytesIsRefused(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + markerOn(t, s, priv, "bab") |
| + shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one")) |
| + shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two")) |
| + for _, v := range []string{"v1", "v2"} { |
| + if err := Promote(ctx, s, priv, "bab", "km", "stable", v); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + before := readPtr(t, s, pub, "stable") |
| + reship(t, s, "bab", "km", "v1", []byte("not what was promoted")) |
| + err := Promote(ctx, s, priv, "bab", "km", "stable", "v1") |
| + if !errors.Is(err, ErrManifestDigest) { |
| + t.Fatalf("promote of a re-shipped history entry = %v; want ErrManifestDigest", err) |
| + } |
| + after := readPtr(t, s, pub, "stable") |
| + if after.Version != before.Version || after.Digest != before.Digest || !after.PromotedAt.Equal(before.PromotedAt) { |
| + t.Fatalf("the refused promote changed the pointer: %+v -> %+v", before, after) |
| + } |
| +} |
| + |
| +func TestPromoteOfADigestlessHistoryEntryIsRefused(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one")) |
| + shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two")) |
| + if err := Promote(ctx, s, priv, "bab", "km", "stable", "v1"); err != nil { |
| + t.Fatal(err) |
| + } |
| + markerOn(t, s, priv, "bab") |
| + if err := Promote(ctx, s, priv, "bab", "km", "stable", "v2"); err != nil { |
| + t.Fatal(err) |
| + } |
| + before := readPtr(t, s, pub, "stable") |
| + err := Promote(ctx, s, priv, "bab", "km", "stable", "v1") |
| + var rn *ResignNeededError |
| + if !errors.As(err, &rn) || rn.Version != "v1" || rn.App != "km" || rn.Channel != "stable" { |
| + t.Fatalf("promote of a digest-less history entry = %v; want ResignNeededError naming v1", err) |
| + } |
| + after := readPtr(t, s, pub, "stable") |
| + if after.Version != before.Version || !after.PromotedAt.Equal(before.PromotedAt) { |
| + t.Fatalf("the refused promote changed the pointer: %+v -> %+v", before, after) |
| + } |
| +} |
| + |
| +func TestPromoteOfAnUnchangedHistoryEntrySignsItsDigest(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + ctx := context.Background() |
| + priv, pub := testKeys(t) |
| + markerOn(t, s, priv, "bab") |
| + shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one")) |
| + shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("two")) |
| + for _, v := range []string{"v1", "v2"} { |
| + if err := Promote(ctx, s, priv, "bab", "km", "stable", v); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + signed := readPtr(t, s, pub, "stable").HistoryDigests[0] |
| + if err := Promote(ctx, s, priv, "bab", "km", "stable", "v1"); err != nil { |
| + t.Fatal(err) |
| + } |
| + p := readPtr(t, s, pub, "stable") |
| + if p.Version != "v1" || p.Digest != signed || p.Digest != manifestDigestOf(t, s, "bab", "km", "v1") { |
| + t.Fatalf("promote back to v1: %+v; want the entry's signed digest %s", p, signed) |
| + } |
| +} |