CAT-AS1: distribution removes an index's platforms with it, but those a kept index names

The sweep asks remove now instead of deleting over the store's door, so
what its door did for an index is the holder's: a removal for retention
removes the platform manifests no tagged or held index of the repository
names, and refuses a manifest such an index names.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-04 16:22:48 +01:00
parent 37a16a4e98
commit b88c6dac5b
4 changed files with 189 additions and 3 deletions
@@ -89,6 +89,7 @@ type fakeCatalog struct {
tags map[string]string // name:version -> digest
removed []string
mirrorErr error
indexes map[string][]string // name@digest -> the platform manifests it names
}
func (c *fakeCatalog) Versions(name string) ([]string, error) {
@@ -112,6 +113,9 @@ func (c *fakeCatalog) RemoveDigest(name, digest string) error {
c.removed = append(c.removed, name+"@"+digest)
return nil
}
func (c *fakeCatalog) Platforms(name, digest string) ([]string, error) {
return c.indexes[name+"@"+digest], nil
}
func (c *fakeCatalog) Mirror(source, digest string) (string, error) {
if c.mirrorErr != nil {
return "", c.mirrorErr
@@ -359,6 +363,69 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
t.Errorf("the builder's removal of a tagged version: %v", err)
}
})
// CAT-AS1: what the sweep did over the store's door is the holder's: a removal for retention of an index removes
// its platform manifests first, but those a tagged or held index of the repository names; a manifest such an index
// names is refused; OCI.Platforms reads an index's from the store.
t.Run("an index's platforms go with it, but those a kept index names", func(t *testing.T) {
d := func(s string) string { return "sha256:" + strings.Repeat(s, 64) }
sweep := "novox.artifact-store-tools"
name := "upstream/docker.io/library/alpine"
for _, c := range []struct {
case_, remove string
tagged, held string // the kept index, by a tag or a hold; none when empty
removed []string
refused string
}{
{case_: "no kept index", remove: d("1"), removed: []string{d("a"), d("b"), d("1")}},
{case_: "a tagged index shares a platform", remove: d("1"), tagged: d("2"), removed: []string{d("a"), d("1")}},
{case_: "a held index shares a platform", remove: d("1"), held: d("2"), removed: []string{d("a"), d("1")}},
{case_: "a platform of a held index", remove: d("b"), held: d("2"), refused: "is kept by the index " + d("2")},
{case_: "an image that is no index", remove: d("c"), removed: []string{d("c")}},
} {
b := newBed(true)
b.cat.indexes = map[string][]string{name + "@" + d("1"): {d("a"), d("b")}, name + "@" + d("2"): {d("b")}}
if c.tagged != "" {
b.cat.tags[name+":3.20"] = c.tagged
}
if c.held != "" {
if err := b.v.HoldOne(ImageSeat, sweep, HoldRequest{Name: name, Digest: c.held}); err != nil {
t.Fatal(err)
}
}
err := b.v.Remove(ImageSeat, sweep, RemoveRequest{Name: name, Digest: c.remove, Reason: ReasonRetention})
var removed []string
for _, r := range b.cat.removed {
removed = append(removed, strings.TrimPrefix(r, name+"@"))
}
if c.refused != "" {
if err == nil || !strings.Contains(err.Error(), c.refused) || len(removed) != 0 {
t.Errorf("%s: %v, removed %v; want refused saying %q and nothing removed", c.case_, err, removed, c.refused)
}
continue
}
if err != nil || !reflect.DeepEqual(removed, c.removed) {
t.Errorf("%s: %v, removed %v, want %v", c.case_, err, removed, c.removed)
}
}
store := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/v2/" + name + "/manifests/" + d("1"):
_, _ = w.Write([]byte(`{"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[{"digest":"` + d("a") + `"},{"digest":"` + d("b") + `"}]}`))
case "/v2/" + name + "/manifests/" + d("c"):
_, _ = w.Write([]byte(`{"mediaType":"application/vnd.oci.image.manifest.v1+json","layers":[]}`))
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer store.Close()
u, _ := url.Parse(store.URL)
o := &OCI{Base: u}
for digest, want := range map[string][]string{d("1"): {d("a"), d("b")}, d("c"): nil, d("9"): nil} {
if got, err := o.Platforms(name, digest); err != nil || !reflect.DeepEqual(got, want) {
t.Errorf("Platforms(%s) = %v, %v, want %v", digest[:10], got, err, want)
}
}
})
t.Run("the store's port: reads open, writes from distribution alone, and the direct-write window", func(t *testing.T) {
store := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) }))
defer store.Close()