diff --git a/registry/remote/repository.go b/registry/remote/repository.go index 1f8bf730..eede12a3 100644 --- a/registry/remote/repository.go +++ b/registry/remote/repository.go @@ -928,11 +928,103 @@ func (s *manifestStore) Exists(ctx context.Context, target ocispec.Descriptor) ( return false, err } -// Delete removes the content identified by the descriptor. +// Delete removes the manifest content identified by the descriptor. func (s *manifestStore) Delete(ctx context.Context, target ocispec.Descriptor) error { + return s.deleteWithIndexing(ctx, target) +} + +// deleteWithIndexing removes the manifest content identified by the descriptor, +// and indexes referrers for the manifest when needed. +func (s *manifestStore) deleteWithIndexing(ctx context.Context, target ocispec.Descriptor) error { + if target.MediaType == ocispec.MediaTypeArtifactManifest || target.MediaType == ocispec.MediaTypeImageManifest { + if state := s.repo.loadReferrersState(); state == referrersStateSupported { + // referrers API is available, no client-side indexing needed + return s.repo.delete(ctx, target, true) + } + + if err := limitSize(target, s.repo.MaxMetadataBytes); err != nil { + return err + } + manifestJSON, err := content.FetchAll(ctx, s, target) + if err != nil { + return err + } + if err := s.indexReferrersForDelete(ctx, target, manifestJSON); err != nil { + return err + } + } + return s.repo.delete(ctx, target, true) } +// indexReferrersForDelete indexes referrers for image or artifact manifest with +// the subject field on manifest delete. +// Reference: https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-manifests +func (s *manifestStore) indexReferrersForDelete(ctx context.Context, desc ocispec.Descriptor, manifestJSON []byte) error { + var manifest struct { + Subject *ocispec.Descriptor `json:"subject"` + } + if err := json.Unmarshal(manifestJSON, &manifest); err != nil { + return fmt.Errorf("failed to decode manifest: %s: %s: %w", desc.Digest, desc.MediaType, err) + } + if manifest.Subject == nil { + // no subject, no indexing needed + return nil + } + + subject := *manifest.Subject + yes, err := s.repo.isReferrersAPIAvailable(ctx, subject) + if err != nil { + return err + } + if yes { + // referrers API is available, no client-side indexing needed + return nil + } + return s.updateReferrersIndexForDelete(ctx, desc, subject) +} + +// updateReferrersIndexForDelete updates the referrers index for desc referencing +// subject on manifest delete. +func (s *manifestStore) updateReferrersIndexForDelete(ctx context.Context, desc, subject ocispec.Descriptor) error { + // there can be multiple go-routines updating the referrers tag concurrently + referrersTag := buildReferrersTag(subject) + s.repo.lockReferrersTag(referrersTag) + defer s.repo.unlockReferrersTag(referrersTag) + + oldIndexDesc, referrers, err := s.repo.referrersFromIndex(ctx, referrersTag) + if err != nil { + if errors.Is(err, errdef.ErrNotFound) { + // inconsistent indexing state: no old index found, skip update + return nil + } + return err + } + if len(referrers) == 0 { + // inconsistent indexing state: no referrers to the subject, skip update + return nil + } + + var updatedReferrers []ocispec.Descriptor + for _, r := range referrers { + // remove the current entry + if !content.Equal(r, desc) { + updatedReferrers = append(updatedReferrers, r) + } + } + if len(updatedReferrers) > 0 { + newIndexDesc, newIndex, err := generateIndex(updatedReferrers) + if err != nil { + return fmt.Errorf("failed to generate referrers index for referrers tag %s: %w", referrersTag, err) + } + if err := s.push(ctx, newIndexDesc, bytes.NewReader(newIndex), referrersTag); err != nil { + return fmt.Errorf("failed to push referrers index tagged by %s: %w", referrersTag, err) + } + } + + return s.repo.delete(ctx, oldIndexDesc, true) +} + // Resolve resolves a reference to a descriptor. // See also `ManifestMediaTypes`. func (s *manifestStore) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) { @@ -1106,16 +1198,16 @@ func (s *manifestStore) pushWithIndexing(ctx context.Context, expected ocispec.D if err := s.push(ctx, expected, bytes.NewReader(manifestJSON), reference); err != nil { return err } - return s.indexReferrers(ctx, expected, manifestJSON) + return s.indexReferrersForPush(ctx, expected, manifestJSON) default: return s.push(ctx, expected, r, reference) } } -// indexReferrers indexes referrers for image or artifact manifest with -// the subject field. +// indexReferrersForPush indexes referrers for image or artifact manifest with +// the subject field on manifest push. // Reference: https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests-with-subject -func (s *manifestStore) indexReferrers(ctx context.Context, desc ocispec.Descriptor, manifestJSON []byte) error { +func (s *manifestStore) indexReferrersForPush(ctx context.Context, desc ocispec.Descriptor, manifestJSON []byte) error { var subject ocispec.Descriptor switch desc.MediaType { case ocispec.MediaTypeArtifactManifest: @@ -1154,11 +1246,12 @@ func (s *manifestStore) indexReferrers(ctx context.Context, desc ocispec.Descrip // referrers API is available, no client-side indexing needed return nil } - return s.updateReferrersIndex(ctx, desc, subject) + return s.updateReferrersIndexForPush(ctx, desc, subject) } -// updateReferrersIndex updates the referrers index for desc referencing subject. -func (s *manifestStore) updateReferrersIndex(ctx context.Context, desc, subject ocispec.Descriptor) error { +// updateReferrersIndexForPush updates the referrers index for desc referencing +// subject on manifest push. +func (s *manifestStore) updateReferrersIndexForPush(ctx context.Context, desc, subject ocispec.Descriptor) error { // there can be multiple go-routines updating the referrers tag concurrently referrersTag := buildReferrersTag(subject) s.repo.lockReferrersTag(referrersTag) diff --git a/registry/remote/repository_test.go b/registry/remote/repository_test.go index a3ded7d9..858ca71c 100644 --- a/registry/remote/repository_test.go +++ b/registry/remote/repository_test.go @@ -3204,16 +3204,26 @@ func Test_ManifestStore_Delete(t *testing.T) { } manifestDeleted := false ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodDelete { + if r.Method != http.MethodDelete && r.Method != http.MethodGet { t.Errorf("unexpected access: %s %s", r.Method, r.URL) w.WriteHeader(http.StatusMethodNotAllowed) - return } - switch r.URL.Path { - case "/v2/test/manifests/" + manifestDesc.Digest.String(): + switch { + case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): manifestDeleted = true // no "Docker-Content-Digest" header for manifest deletion w.WriteHeader(http.StatusAccepted) + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): + if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) { + t.Errorf("manifest not convertable: %s", accept) + w.WriteHeader(http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", manifestDesc.MediaType) + w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) + if _, err := w.Write(manifest); err != nil { + t.Errorf("failed to write %q: %v", r.URL, err) + } default: w.WriteHeader(http.StatusNotFound) } @@ -3232,6 +3242,7 @@ func Test_ManifestStore_Delete(t *testing.T) { store := repo.Manifests() ctx := context.Background() + // test delete manifest without subject err = store.Delete(ctx, manifestDesc) if err != nil { t.Fatalf("Manifests.Delete() error = %v", err) @@ -3240,6 +3251,7 @@ func Test_ManifestStore_Delete(t *testing.T) { t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) } + // test delete content that does not exist content := []byte(`{"manifests":[]}`) contentDesc := ocispec.Descriptor{ MediaType: ocispec.MediaTypeImageIndex, @@ -3252,6 +3264,453 @@ func Test_ManifestStore_Delete(t *testing.T) { } } +func Test_ManifestStore_Delete_ReferrersAPIAvailable(t *testing.T) { + // generate test content + subject := []byte(`{"layers":[]}`) + subjectDesc := content.NewDescriptorFromBytes(ocispec.MediaTypeArtifactManifest, subject) + artifact := ocispec.Artifact{ + MediaType: ocispec.MediaTypeArtifactManifest, + Subject: &subjectDesc, + } + artifactJSON, err := json.Marshal(artifact) + if err != nil { + t.Errorf("failed to marshal manifest: %v", err) + } + artifactDesc := content.NewDescriptorFromBytes(artifact.MediaType, artifactJSON) + manifest := ocispec.Manifest{ + MediaType: ocispec.MediaTypeImageManifest, + Subject: &subjectDesc, + } + manifestJSON, err := json.Marshal(manifest) + if err != nil { + t.Errorf("failed to marshal manifest: %v", err) + } + manifestDesc := content.NewDescriptorFromBytes(manifest.MediaType, manifestJSON) + manifestDeleted := false + artifactDeleted := false + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodDelete && r.Method != http.MethodGet { + t.Errorf("unexpected access: %s %s", r.Method, r.URL) + w.WriteHeader(http.StatusMethodNotAllowed) + } + switch { + case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): + artifactDeleted = true + // no "Docker-Content-Digest" header for manifest deletion + w.WriteHeader(http.StatusAccepted) + case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): + manifestDeleted = true + // no "Docker-Content-Digest" header for manifest deletion + w.WriteHeader(http.StatusAccepted) + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): + if accept := r.Header.Get("Accept"); !strings.Contains(accept, artifactDesc.MediaType) { + t.Errorf("manifest not convertable: %s", accept) + w.WriteHeader(http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", artifactDesc.MediaType) + w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String()) + if _, err := w.Write(artifactJSON); err != nil { + t.Errorf("failed to write %q: %v", r.URL, err) + } + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+subjectDesc.Digest.String(): + result := ocispec.Index{ + Versioned: specs.Versioned{ + SchemaVersion: 2, // historical value. does not pertain to OCI or docker version + }, + MediaType: ocispec.MediaTypeImageIndex, + Manifests: []ocispec.Descriptor{ + manifestDesc, + artifactDesc, + }, + } + if err := json.NewEncoder(w).Encode(result); err != nil { + t.Errorf("failed to write response: %v", err) + } + default: + w.WriteHeader(http.StatusNotFound) + } + })) + defer ts.Close() + uri, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("invalid test http server: %v", err) + } + repo, err := NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + store := repo.Manifests() + ctx := context.Background() + // test delete artifact with subject + if state := repo.loadReferrersState(); state != referrersStateUnknown { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) + } + err = store.Delete(ctx, artifactDesc) + if err != nil { + t.Fatalf("Manifests.Delete() error = %v", err) + } + if !artifactDeleted { + t.Errorf("Manifests.Delete() = %v, want %v", artifactDeleted, true) + } + + // test delete manifest with subject + if state := repo.loadReferrersState(); state != referrersStateSupported { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) + } + err = store.Delete(ctx, manifestDesc) + if err != nil { + t.Fatalf("Manifests.Delete() error = %v", err) + } + if !manifestDeleted { + t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) + } + + // test delete content that does not exist + content := []byte("whatever") + contentDesc := ocispec.Descriptor{ + MediaType: ocispec.MediaTypeImageManifest, + Digest: digest.FromBytes(content), + Size: int64(len(content)), + } + ctx = context.Background() + err = store.Delete(ctx, contentDesc) + if !errors.Is(err, errdef.ErrNotFound) { + t.Errorf("Manifests.Delete() error = %v, wantErr %v", err, errdef.ErrNotFound) + } +} + +func Test_ManifestStore_Delete_ReferrersAPIUnavailable(t *testing.T) { + // generate test content + subject := []byte(`{"layers":[]}`) + subjectDesc := content.NewDescriptorFromBytes(ocispec.MediaTypeArtifactManifest, subject) + referrersTag := strings.Replace(subjectDesc.Digest.String(), ":", "-", 1) + artifact := ocispec.Artifact{ + MediaType: ocispec.MediaTypeArtifactManifest, + Subject: &subjectDesc, + } + artifactJSON, err := json.Marshal(artifact) + if err != nil { + t.Errorf("failed to marshal manifest: %v", err) + } + artifactDesc := content.NewDescriptorFromBytes(artifact.MediaType, artifactJSON) + manifest := ocispec.Manifest{ + MediaType: ocispec.MediaTypeImageManifest, + Subject: &subjectDesc, + } + manifestJSON, err := json.Marshal(manifest) + if err != nil { + t.Errorf("failed to marshal manifest: %v", err) + } + manifestDesc := content.NewDescriptorFromBytes(manifest.MediaType, manifestJSON) + + // test delete artifact with subject + index_1 := ocispec.Index{ + Versioned: specs.Versioned{ + SchemaVersion: 2, // historical value. does not pertain to OCI or docker version + }, + MediaType: ocispec.MediaTypeImageIndex, + Manifests: []ocispec.Descriptor{ + artifactDesc, + manifestDesc, + }, + } + indexJSON_1, err := json.Marshal(index_1) + if err != nil { + t.Errorf("failed to marshal manifest: %v", err) + } + indexDesc_1 := content.NewDescriptorFromBytes(index_1.MediaType, indexJSON_1) + index_2 := ocispec.Index{ + Versioned: specs.Versioned{ + SchemaVersion: 2, // historical value. does not pertain to OCI or docker version + }, + MediaType: ocispec.MediaTypeImageIndex, + Manifests: []ocispec.Descriptor{ + manifestDesc, + }, + } + indexJSON_2, err := json.Marshal(index_2) + if err != nil { + t.Errorf("failed to marshal manifest: %v", err) + } + indexDesc_2 := content.NewDescriptorFromBytes(index_2.MediaType, indexJSON_2) + + manifestDeleted := false + indexDeleted := false + var gotReferrerIndex []byte + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): + manifestDeleted = true + // no "Docker-Content-Digest" header for manifest deletion + w.WriteHeader(http.StatusAccepted) + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): + if accept := r.Header.Get("Accept"); !strings.Contains(accept, artifactDesc.MediaType) { + t.Errorf("manifest not convertable: %s", accept) + w.WriteHeader(http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", artifactDesc.MediaType) + w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String()) + if _, err := w.Write(artifactJSON); err != nil { + t.Errorf("failed to write %q: %v", r.URL, err) + } + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+subjectDesc.Digest.String(): + 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(): + indexDeleted = 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) + } + repo, err := NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + store := repo.Manifests() + ctx := context.Background() + + // test delete artifact with subject + if state := repo.loadReferrersState(); state != referrersStateUnknown { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) + } + err = store.Delete(ctx, artifactDesc) + if err != nil { + t.Fatalf("Manifests.Delete() error = %v", err) + } + if !manifestDeleted { + t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) + } + if !bytes.Equal(gotReferrerIndex, indexJSON_2) { + t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_2)) + } + if !indexDeleted { + t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) + } + if state := repo.loadReferrersState(); state != referrersStateUnsupported { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) + } + + // test delete manifest with subject + manifestDeleted = false + indexDeleted = false + ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): + manifestDeleted = true + // no "Docker-Content-Digest" header for manifest deletion + w.WriteHeader(http.StatusAccepted) + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): + if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) { + t.Errorf("manifest not convertable: %s", accept) + w.WriteHeader(http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", manifestDesc.MediaType) + w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) + if _, err := w.Write(manifestJSON); err != nil { + t.Errorf("failed to write %q: %v", r.URL, err) + } + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+subjectDesc.Digest.String(): + w.WriteHeader(http.StatusNotFound) + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: + w.Write(indexJSON_2) + case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexDesc_2.Digest.String(): + indexDeleted = 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) + } + repo, err = NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + store = repo.Manifests() + ctx = context.Background() + if state := repo.loadReferrersState(); state != referrersStateUnknown { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) + } + err = store.Delete(ctx, manifestDesc) + if err != nil { + t.Fatalf("Manifests.Delete() error = %v", err) + } + if !manifestDeleted { + t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) + } + if !indexDeleted { + t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) + } + if state := repo.loadReferrersState(); state != referrersStateUnsupported { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) + } +} + +func Test_ManifestStore_Delete_ReferrersAPIUnavailable_InconsistentIndex(t *testing.T) { + // generate test content + subject := []byte(`{"layers":[]}`) + subjectDesc := content.NewDescriptorFromBytes(ocispec.MediaTypeArtifactManifest, subject) + referrersTag := strings.Replace(subjectDesc.Digest.String(), ":", "-", 1) + artifact := ocispec.Artifact{ + MediaType: ocispec.MediaTypeArtifactManifest, + Subject: &subjectDesc, + } + artifactJSON, err := json.Marshal(artifact) + if err != nil { + t.Errorf("failed to marshal manifest: %v", err) + } + artifactDesc := content.NewDescriptorFromBytes(artifact.MediaType, artifactJSON) + + // test inconsistent state: index not found + manifestDeleted := true + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): + manifestDeleted = true + // no "Docker-Content-Digest" header for manifest deletion + w.WriteHeader(http.StatusAccepted) + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): + if accept := r.Header.Get("Accept"); !strings.Contains(accept, artifactDesc.MediaType) { + t.Errorf("manifest not convertable: %s", accept) + w.WriteHeader(http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", artifactDesc.MediaType) + w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String()) + if _, err := w.Write(artifactJSON); err != nil { + t.Errorf("failed to write %q: %v", r.URL, err) + } + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+subjectDesc.Digest.String(): + w.WriteHeader(http.StatusNotFound) + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: + w.WriteHeader(http.StatusNotFound) + 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) + } + repo, err := NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + store := repo.Manifests() + ctx := context.Background() + if state := repo.loadReferrersState(); state != referrersStateUnknown { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) + } + err = store.Delete(ctx, artifactDesc) + if err != nil { + t.Fatalf("Manifests.Delete() error = %v", err) + } + if !manifestDeleted { + t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) + } + if state := repo.loadReferrersState(); state != referrersStateUnsupported { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) + } + + // test inconsistent state: empty referrers list + manifestDeleted = true + ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): + manifestDeleted = true + // no "Docker-Content-Digest" header for manifest deletion + w.WriteHeader(http.StatusAccepted) + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): + if accept := r.Header.Get("Accept"); !strings.Contains(accept, artifactDesc.MediaType) { + t.Errorf("manifest not convertable: %s", accept) + w.WriteHeader(http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", artifactDesc.MediaType) + w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String()) + if _, err := w.Write(artifactJSON); err != nil { + t.Errorf("failed to write %q: %v", r.URL, err) + } + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+subjectDesc.Digest.String(): + w.WriteHeader(http.StatusNotFound) + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: + result := ocispec.Index{ + Versioned: specs.Versioned{ + SchemaVersion: 2, // historical value. does not pertain to OCI or docker version + }, + MediaType: ocispec.MediaTypeImageIndex, + Manifests: []ocispec.Descriptor{}, + } + if err := json.NewEncoder(w).Encode(result); err != nil { + t.Errorf("failed to write response: %v", err) + } + 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) + } + repo, err = NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + store = repo.Manifests() + ctx = context.Background() + if state := repo.loadReferrersState(); state != referrersStateUnknown { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) + } + err = store.Delete(ctx, artifactDesc) + if err != nil { + t.Fatalf("Manifests.Delete() error = %v", err) + } + if !manifestDeleted { + t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) + } + if state := repo.loadReferrersState(); state != referrersStateUnsupported { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) + } +} + func Test_ManifestStore_Resolve(t *testing.T) { manifest := []byte(`{"layers":[]}`) manifestDesc := ocispec.Descriptor{