diff --git a/registry/remote/registry_test.go b/registry/remote/registry_test.go index 0cbfbacb..994e8db3 100644 --- a/registry/remote/registry_test.go +++ b/registry/remote/registry_test.go @@ -178,6 +178,7 @@ func TestRegistry_Repository(t *testing.T) { t.Fatalf("NewRegistry() error = %v", err) } reg.PlainHTTP = true + reg.SkipReferrersGC = true reg.RepositoryListPageSize = 50 reg.TagListPageSize = 100 reg.ReferrerListPageSize = 10 @@ -265,7 +266,7 @@ func TestRegistry_Repositories_WithLastParam(t *testing.T) { } } -//indexOf returns the index of an element within a slice +// indexOf returns the index of an element within a slice func indexOf(element string, data []string) int { for ind, val := range data { if element == val { diff --git a/registry/remote/repository.go b/registry/remote/repository.go index 32ac347d..c8c45baa 100644 --- a/registry/remote/repository.go +++ b/registry/remote/repository.go @@ -102,6 +102,17 @@ type Repository struct { // If less than or equal to zero, a default (currently 4MiB) is used. MaxMetadataBytes int64 + // SkipReferrersGC specifies whether to delete the dangling referrers + // index when referrers tag schema is utilized. + // - If false, the old referrers index will be deleted after the new one + // is successfully uploaded. + // - If true, the old referrers index is kept. + // By default, it is disabled (set to false). See also: + // - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#referrers-tag-schema + // - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#pushing-manifests-with-subject + // - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#deleting-manifests + SkipReferrersGC bool + // NOTE: Must keep fields in sync with newRepositoryWithOptions function. // referrersState represents that if the repository supports Referrers API. @@ -145,6 +156,7 @@ func newRepositoryWithOptions(ref registry.Reference, opts *RepositoryOptions) ( Client: opts.Client, Reference: ref, PlainHTTP: opts.PlainHTTP, + SkipReferrersGC: opts.SkipReferrersGC, ManifestMediaTypes: slices.Clone(opts.ManifestMediaTypes), TagListPageSize: opts.TagListPageSize, ReferrerListPageSize: opts.ReferrerListPageSize, @@ -1316,7 +1328,7 @@ func (s *manifestStore) indexReferrersForPush(ctx context.Context, desc ocispec. func (s *manifestStore) updateReferrersIndex(ctx context.Context, subject ocispec.Descriptor, change referrerChange) (err error) { referrersTag := buildReferrersTag(subject) - var skipDelete bool + skipDelete := s.repo.SkipReferrersGC var oldIndexDesc ocispec.Descriptor var referrers []ocispec.Descriptor prepare := func() error { diff --git a/registry/remote/repository_test.go b/registry/remote/repository_test.go index f062509d..deb894cb 100644 --- a/registry/remote/repository_test.go +++ b/registry/remote/repository_test.go @@ -3424,6 +3424,80 @@ func Test_ManifestStore_Push_ReferrersAPIUnavailable(t *testing.T) { t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) } + // test push image manifest with subject without cleaning dangling referrers + ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): + if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType { + w.WriteHeader(http.StatusBadRequest) + break + } + buf := bytes.NewBuffer(nil) + if _, err := buf.ReadFrom(r.Body); err != nil { + t.Errorf("fail to read: %v", err) + } + gotManifest = buf.Bytes() + w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) + w.WriteHeader(http.StatusCreated) + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: + w.WriteHeader(http.StatusNotFound) + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: + w.Write(indexJSON_1) + case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag: + if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex { + w.WriteHeader(http.StatusBadRequest) + break + } + buf := bytes.NewBuffer(nil) + if _, err := buf.ReadFrom(r.Body); err != nil { + t.Errorf("fail to read: %v", err) + } + gotReferrerIndex = buf.Bytes() + w.Header().Set("Docker-Content-Digest", indexDesc_2.Digest.String()) + w.WriteHeader(http.StatusCreated) + case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexDesc_1.Digest.String(): + manifestDeleted = true + // no "Docker-Content-Digest" header for manifest deletion + w.WriteHeader(http.StatusAccepted) + default: + t.Errorf("unexpected access: %s %s", r.Method, r.URL) + w.WriteHeader(http.StatusNotFound) + } + })) + defer ts.Close() + uri, err = url.Parse(ts.URL) + if err != nil { + t.Fatalf("invalid test http server: %v", err) + } + + ctx = context.Background() + repo, err = NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + repo.SkipReferrersGC = true + if state := repo.loadReferrersState(); state != referrersStateUnknown { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) + } + manifestDeleted = false + err = repo.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON)) + if err != nil { + t.Fatalf("Manifests.Push() error = %v", err) + } + if !bytes.Equal(gotManifest, manifestJSON) { + t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(manifestJSON)) + } + if !bytes.Equal(gotReferrerIndex, indexJSON_2) { + t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_2)) + } + if manifestDeleted { + t.Errorf("manifestDeleted = %v, want %v", manifestDeleted, false) + } + if state := repo.loadReferrersState(); state != referrersStateUnsupported { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) + } + // test push image manifest with subject again, referrers list should not be changed ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch {