diff --git a/registry/remote/errcode/errors.go b/registry/remote/errcode/errors.go index 728558b5..cf0018a0 100644 --- a/registry/remote/errcode/errors.go +++ b/registry/remote/errcode/errors.go @@ -23,8 +23,30 @@ import ( "unicode" ) +// References: +// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#error-codes +// - https://docs.docker.com/registry/spec/api/#errors-2 +const ( + ErrorCodeBlobUnknown = "BLOB_UNKNOWN" + ErrorCodeBlobUploadInvalid = "BLOB_UPLOAD_INVALID" + ErrorCodeBlobUploadUnknown = "BLOB_UPLOAD_UNKNOWN" + ErrorCodeDigestInvalid = "DIGEST_INVALID" + ErrorCodeManifestBlobUnknown = "MANIFEST_BLOB_UNKNOWN" + ErrorCodeManifestInvalid = "MANIFEST_INVALID" + ErrorCodeManifestUnknown = "MANIFEST_UNKNOWN" + ErrorCodeNameInvalid = "NAME_INVALID" + ErrorCodeNameUnknown = "NAME_UNKNOWN" + ErrorCodeSizeInvalid = "SIZE_INVALID" + ErrorCodeUnauthorized = "UNAUTHORIZED" + ErrorCodeDenied = "DENIED" + ErrorCodeUnsupported = "UNSUPPORTED" +) + // Error represents a response inner error returned by the remote // registry. +// References: +// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#error-codes +// - https://docs.docker.com/registry/spec/api/#errors-2 type Error struct { Code string `json:"code"` Message string `json:"message"` @@ -48,8 +70,11 @@ func (e Error) Error() string { return fmt.Sprintf("%s: %s: %v", code, e.Message, e.Detail) } -// Errors represents a list of response inner errors returned by -// the remote server. +// Errors represents a list of response inner errors returned by the remote +// server. +// References: +// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#error-codes +// - https://docs.docker.com/registry/spec/api/#errors-2 type Errors []Error // Error returns a error string describing the error. diff --git a/registry/remote/internal/errutil/errutil.go b/registry/remote/internal/errutil/errutil.go index 2444ecdf..52dc3612 100644 --- a/registry/remote/internal/errutil/errutil.go +++ b/registry/remote/internal/errutil/errutil.go @@ -17,6 +17,7 @@ package errutil import ( "encoding/json" + "errors" "io" "net/http" @@ -45,3 +46,9 @@ func ParseErrorResponse(resp *http.Response) error { } return resultErr } + +// IsErrorCode returns true if err is an Error and its Code equals to code. +func IsErrorCode(err error, code string) bool { + var ec errcode.Error + return errors.As(err, &ec) && ec.Code == code +} diff --git a/registry/remote/internal/errutil/errutil_test.go b/registry/remote/internal/errutil/errutil_test.go index fd44b521..7a3870db 100644 --- a/registry/remote/internal/errutil/errutil_test.go +++ b/registry/remote/internal/errutil/errutil_test.go @@ -126,3 +126,114 @@ func Test_ParseErrorResponse_plain(t *testing.T) { t.Errorf("ParseErrorResponse() error = %v, want err message %v", err, want) } } + +func TestIsErrorCode(t *testing.T) { + tests := []struct { + name string + err error + code string + want bool + }{ + { + name: "test errcode.Error, same code", + err: errcode.Error{ + Code: errcode.ErrorCodeNameUnknown, + }, + code: errcode.ErrorCodeNameUnknown, + want: true, + }, + { + name: "test errcode.Error, different code", + err: errcode.Error{ + Code: errcode.ErrorCodeUnauthorized, + }, + code: errcode.ErrorCodeNameUnknown, + want: false, + }, + { + name: "test errcode.Errors containing single error, same code", + err: errcode.Errors{ + { + Code: errcode.ErrorCodeNameUnknown, + }, + }, + code: errcode.ErrorCodeNameUnknown, + want: true, + }, + { + name: "test errcode.Errors containing single error, different code", + err: errcode.Errors{ + { + Code: errcode.ErrorCodeNameUnknown, + }, + }, + code: errcode.ErrorCodeNameUnknown, + want: true, + }, + { + name: "test errcode.Errors containing multiple errors, same code", + err: errcode.Errors{ + { + Code: errcode.ErrorCodeNameUnknown, + }, + { + Code: errcode.ErrorCodeUnauthorized, + }, + }, + code: errcode.ErrorCodeNameUnknown, + want: false, + }, + { + name: "test errcode.ErrorResponse containing single error, same code", + err: &errcode.ErrorResponse{ + Errors: errcode.Errors{ + { + Code: errcode.ErrorCodeNameUnknown, + }, + }, + }, + code: errcode.ErrorCodeNameUnknown, + want: true, + }, + { + name: "test errcode.ErrorResponse containing single error, different code", + err: &errcode.ErrorResponse{ + Errors: errcode.Errors{ + { + Code: errcode.ErrorCodeUnauthorized, + }, + }, + }, + code: errcode.ErrorCodeNameUnknown, + want: false, + }, + { + name: "test errcode.ErrorResponse containing multiple errors, same code", + err: &errcode.ErrorResponse{ + Errors: errcode.Errors{ + { + Code: errcode.ErrorCodeNameUnknown, + }, + { + Code: errcode.ErrorCodeUnauthorized, + }, + }, + }, + code: errcode.ErrorCodeNameUnknown, + want: false, + }, + { + name: "test unstructured error", + err: errors.New(errcode.ErrorCodeNameUnknown), + code: errcode.ErrorCodeNameUnknown, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsErrorCode(tt.err, tt.code); got != tt.want { + t.Errorf("IsErrorCode() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/registry/remote/repository.go b/registry/remote/repository.go index eede12a3..271c77c4 100644 --- a/registry/remote/repository.go +++ b/registry/remote/repository.go @@ -40,14 +40,20 @@ import ( "oras.land/oras-go/v2/internal/registryutil" "oras.land/oras-go/v2/registry" "oras.land/oras-go/v2/registry/remote/auth" + "oras.land/oras-go/v2/registry/remote/errcode" "oras.land/oras-go/v2/registry/remote/internal/errutil" ) -// dockerContentDigestHeader - The Docker-Content-Digest header, if present on -// the response, returns the canonical digest of the uploaded blob. -// See https://docs.docker.com/registry/spec/api/#digest-header -// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pull -const dockerContentDigestHeader = "Docker-Content-Digest" +const ( + // dockerContentDigestHeader - The Docker-Content-Digest header, if present + // on the response, returns the canonical digest of the uploaded blob. + // See https://docs.docker.com/registry/spec/api/#digest-header + // See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pull + dockerContentDigestHeader = "Docker-Content-Digest" + // zeroDigest represents a digest that consists of zeros. zeroDigest is used + // for pinging Referrers API. + zeroDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000" +) // referrersState represents the state of Referrers API. type referrersState = int32 @@ -121,7 +127,11 @@ type Repository struct { referrersState referrersState // referrersTagLocks maps a referrers tag to a lock. - referrersTagLocks sync.Map // map[string]sync.Mutex + referrersTagLocks sync.Map // map[string]*sync.Mutex + + // referrersPingLock locks the pingReferrersAPI() method and allows only + // one go-routine to send the request. + referrersPingLock sync.Mutex } // NewRepository creates a client to the remote repository identified by a @@ -399,13 +409,18 @@ func (r *Repository) Referrers(ctx context.Context, desc ocispec.Descriptor, art // The referrers state is unknown. if err != nil { - if errors.Is(err, errdef.ErrNotFound) { - // A 404 returned by Referrers API indicates that Referrers API is - // not supported. Fallback to referrers tag schema. - r.SetReferrersCapability(false) - return r.referrersByTagSchema(ctx, desc, artifactType, fn) + var errResp *errcode.ErrorResponse + if !errors.As(err, &errResp) || errResp.StatusCode != http.StatusNotFound { + return err } - return err + if errutil.IsErrorCode(errResp, errcode.ErrorCodeNameUnknown) { + // The repository is not found, no fallback. + return err + } + // A 404 returned by Referrers API indicates that Referrers API is + // not supported. Fallback to referrers tag schema. + r.SetReferrersCapability(false) + return r.referrersByTagSchema(ctx, desc, artifactType, fn) } r.SetReferrersCapability(true) @@ -455,9 +470,6 @@ func (r *Repository) referrersPageByAPI(ctx context.Context, artifactType string } defer resp.Body.Close() - if resp.StatusCode == http.StatusNotFound { - return "", fmt.Errorf("%s %q: %w", resp.Request.Method, resp.Request.URL, errdef.ErrNotFound) - } if resp.StatusCode != http.StatusOK { return "", errutil.ParseErrorResponse(resp) } @@ -973,11 +985,11 @@ func (s *manifestStore) indexReferrersForDelete(ctx context.Context, desc ocispe } subject := *manifest.Subject - yes, err := s.repo.isReferrersAPIAvailable(ctx, subject) + ok, err := s.repo.pingReferrers(ctx) if err != nil { return err } - if yes { + if ok { // referrers API is available, no client-side indexing needed return nil } @@ -1238,11 +1250,11 @@ func (s *manifestStore) indexReferrersForPush(ctx context.Context, desc ocispec. return nil } - yes, err := s.repo.isReferrersAPIAvailable(ctx, subject) + ok, err := s.repo.pingReferrers(ctx) if err != nil { return err } - if yes { + if ok { // referrers API is available, no client-side indexing needed return nil } @@ -1288,8 +1300,8 @@ func (s *manifestStore) updateReferrersIndexForPush(ctx context.Context, desc, s return s.repo.delete(ctx, oldIndexDesc, true) } -// isReferrersAPIAvailable returns true if the Referrers API is available for r. -func (r *Repository) isReferrersAPIAvailable(ctx context.Context, desc ocispec.Descriptor) (bool, error) { +// pingReferrers returns true if the Referrers API is available for r. +func (r *Repository) pingReferrers(ctx context.Context) (bool, error) { switch r.loadReferrersState() { case referrersStateSupported: return true, nil @@ -1298,8 +1310,19 @@ func (r *Repository) isReferrersAPIAvailable(ctx context.Context, desc ocispec.D } // referrers state is unknown + // limit the rate of pinging referrers API + r.referrersPingLock.Lock() + defer r.referrersPingLock.Unlock() + + switch r.loadReferrersState() { + case referrersStateSupported: + return true, nil + case referrersStateUnsupported: + return false, nil + } + ref := r.Reference - ref.Reference = desc.Digest.String() + ref.Reference = zeroDigest ctx = registryutil.WithScopeHint(ctx, ref, auth.ActionPull) url := buildReferrersURL(r.PlainHTTP, ref, "") @@ -1318,6 +1341,10 @@ func (r *Repository) isReferrersAPIAvailable(ctx context.Context, desc ocispec.D r.SetReferrersCapability(true) return true, nil case http.StatusNotFound: + if err := errutil.ParseErrorResponse(resp); errutil.IsErrorCode(err, errcode.ErrorCodeNameUnknown) { + // repository not found + return false, err + } r.SetReferrersCapability(false) return false, nil default: diff --git a/registry/remote/repository_test.go b/registry/remote/repository_test.go index 858ca71c..86da2fdc 100644 --- a/registry/remote/repository_test.go +++ b/registry/remote/repository_test.go @@ -30,11 +30,13 @@ import ( "reflect" "strconv" "strings" + "sync/atomic" "testing" "github.com/opencontainers/go-digest" specs "github.com/opencontainers/image-spec/specs-go" ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "golang.org/x/sync/errgroup" "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/errdef" "oras.land/oras-go/v2/internal/interfaces" @@ -1246,8 +1248,8 @@ func TestRepository_Referrers_TagSchemaFallback(t *testing.T) { } if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { return nil - }); !errors.Is(err, errdef.ErrNotFound) { - t.Errorf("Repository.Referrers() error = %v, wantErr %v", err, errdef.ErrNotFound) + }); err == nil { + t.Errorf("Repository.Referrers() error = %v, wantErr %v", err, true) } if state := repo.loadReferrersState(); state != referrersStateSupported { t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) @@ -1432,6 +1434,93 @@ func TestRepository_Referrers_BadRequest(t *testing.T) { } } +func TestRepository_Referrers_RepositoryNotFound(t *testing.T) { + manifest := []byte(`{"layers":[]}`) + manifestDesc := ocispec.Descriptor{ + MediaType: ocispec.MediaTypeImageManifest, + Digest: digest.FromBytes(manifest), + Size: int64(len(manifest)), + } + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + referrersUrl := "/v2/test/referrers/" + manifestDesc.Digest.String() + referrersTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1) + tagSchemaUrl := "/v2/test/manifests/" + referrersTag + if r.Method == http.MethodGet && + (r.URL.Path == referrersUrl || r.URL.Path == tagSchemaUrl) { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(`{ "errors": [ { "code": "NAME_UNKNOWN", "message": "repository name not known to registry" } ] }`)) + return + } + t.Errorf("unexpected access: %s %q", 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() + + // test auto detect + // repository not found, should return error + repo, err := NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + if state := repo.loadReferrersState(); state != referrersStateUnknown { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) + } + if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { + return nil + }); err == nil { + t.Errorf("Repository.Referrers() error = %v, wantErr %v", err, true) + } + if state := repo.loadReferrersState(); state != referrersStateUnknown { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) + } + + // test force attempt Referrers + // repository not found, should return error + repo, err = NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + repo.SetReferrersCapability(true) + if state := repo.loadReferrersState(); state != referrersStateSupported { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) + } + if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { + return nil + }); err == nil { + t.Errorf("Repository.Referrers() error = %v, wantErr %v", err, true) + } + if state := repo.loadReferrersState(); state != referrersStateSupported { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) + } + + // test force attempt tag schema + // repository not found, but should not return error + repo, err = NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + repo.SetReferrersCapability(false) + if state := repo.loadReferrersState(); state != referrersStateUnsupported { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) + } + if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { + return nil + }); err != nil { + t.Errorf("Repository.Referrers() error = %v, wantErr %v", err, nil) + } + if state := repo.loadReferrersState(); state != referrersStateUnsupported { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) + } +} + func TestRepository_Referrers_ServerFiltering(t *testing.T) { manifest := []byte(`{"layers":[]}`) manifestDesc := ocispec.Descriptor{ @@ -2818,7 +2907,7 @@ func Test_ManifestStore_Push_ReferrersAPIAvailable(t *testing.T) { 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/"+subjectDesc.Digest.String(): + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: result := ocispec.Index{ Versioned: specs.Versioned{ SchemaVersion: 2, // historical value. does not pertain to OCI or docker version @@ -2922,7 +3011,7 @@ func Test_ManifestStore_Push_ReferrersAPIUnavailable(t *testing.T) { gotManifest = buf.Bytes() w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String()) w.WriteHeader(http.StatusCreated) - case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+subjectDesc.Digest.String(): + 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.WriteHeader(http.StatusNotFound) @@ -3019,7 +3108,7 @@ func Test_ManifestStore_Push_ReferrersAPIUnavailable(t *testing.T) { 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/"+subjectDesc.Digest.String(): + 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) @@ -3091,7 +3180,7 @@ func Test_ManifestStore_Push_ReferrersAPIUnavailable(t *testing.T) { 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/"+subjectDesc.Digest.String(): + 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_2) @@ -3313,16 +3402,13 @@ func Test_ManifestStore_Delete_ReferrersAPIAvailable(t *testing.T) { 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(): + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 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, - }, + Manifests: []ocispec.Descriptor{}, } if err := json.NewEncoder(w).Encode(result); err != nil { t.Errorf("failed to write response: %v", err) @@ -3456,7 +3542,7 @@ func Test_ManifestStore_Delete_ReferrersAPIUnavailable(t *testing.T) { 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(): + 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) @@ -3535,7 +3621,7 @@ func Test_ManifestStore_Delete_ReferrersAPIUnavailable(t *testing.T) { 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(): + 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_2) @@ -3612,7 +3698,7 @@ func Test_ManifestStore_Delete_ReferrersAPIUnavailable_InconsistentIndex(t *test 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(): + 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.WriteHeader(http.StatusNotFound) @@ -3666,7 +3752,7 @@ func Test_ManifestStore_Delete_ReferrersAPIUnavailable_InconsistentIndex(t *test 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(): + 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: result := ocispec.Index{ @@ -4131,7 +4217,7 @@ func Test_ManifestStore_PushReference_ReferrersAPIAvailable(t *testing.T) { 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/"+subjectDesc.Digest.String(): + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: result := ocispec.Index{ Versioned: specs.Versioned{ SchemaVersion: 2, // historical value. does not pertain to OCI or docker version @@ -4236,7 +4322,7 @@ func Test_ManifestStore_PushReference_ReferrersAPIUnavailable(t *testing.T) { gotManifest = buf.Bytes() w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String()) w.WriteHeader(http.StatusCreated) - case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+subjectDesc.Digest.String(): + 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.WriteHeader(http.StatusNotFound) @@ -4335,7 +4421,7 @@ func Test_ManifestStore_PushReference_ReferrersAPIUnavailable(t *testing.T) { 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/"+subjectDesc.Digest.String(): + 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) @@ -4407,7 +4493,7 @@ func Test_ManifestStore_PushReference_ReferrersAPIUnavailable(t *testing.T) { 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/"+subjectDesc.Digest.String(): + 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_2) @@ -5179,6 +5265,13 @@ func Test_getReferrersTag(t *testing.T) { desc ocispec.Descriptor want string }{ + { + name: "zero digest", + desc: ocispec.Descriptor{ + Digest: "sha256:0000000000000000000000000000000000000000000000000000000000000000", + }, + want: "sha256-0000000000000000000000000000000000000000000000000000000000000000", + }, { name: "sha256", desc: ocispec.Descriptor{ @@ -5297,19 +5390,12 @@ func Test_generateIndex(t *testing.T) { } } -func TestRepository_isReferrersAPIAvailable(t *testing.T) { - manifest := []byte(`{"layers":[]}`) - manifestDesc := ocispec.Descriptor{ - MediaType: ocispec.MediaTypeImageManifest, - Digest: digest.FromBytes(manifest), - Size: int64(len(manifest)), - } - +func TestRepository_pingReferrers(t *testing.T) { // referrers available count := 0 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { - case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+manifestDesc.Digest.String(): + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: count++ w.WriteHeader(http.StatusOK) default: @@ -5335,43 +5421,43 @@ func TestRepository_isReferrersAPIAvailable(t *testing.T) { if state := repo.loadReferrersState(); state != referrersStateUnknown { t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) } - got, err := repo.isReferrersAPIAvailable(ctx, manifestDesc) + got, err := repo.pingReferrers(ctx) if err != nil { - t.Errorf("Repository.isReferrersAPIAvailable() error = %v, wantErr %v", err, nil) + t.Errorf("Repository.pingReferrers() error = %v, wantErr %v", err, nil) } if got != true { - t.Errorf("Repository.isReferrersAPIAvailable() = %v, want %v", got, true) + t.Errorf("Repository.pingReferrers() = %v, want %v", got, true) } if state := repo.loadReferrersState(); state != referrersStateSupported { t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) } if count != 1 { - t.Errorf("count(Repository.isReferrersAPIAvailable()) = %v, want %v", count, 1) + t.Errorf("count(Repository.pingReferrers()) = %v, want %v", count, 1) } // 2nd call if state := repo.loadReferrersState(); state != referrersStateSupported { t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) } - got, err = repo.isReferrersAPIAvailable(ctx, manifestDesc) + got, err = repo.pingReferrers(ctx) if err != nil { - t.Errorf("Repository.isReferrersAPIAvailable() error = %v, wantErr %v", err, nil) + t.Errorf("Repository.pingReferrers() error = %v, wantErr %v", err, nil) } if got != true { - t.Errorf("Repository.isReferrersAPIAvailable() = %v, want %v", got, true) + t.Errorf("Repository.pingReferrers() = %v, want %v", got, true) } if state := repo.loadReferrersState(); state != referrersStateSupported { t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) } if count != 1 { - t.Errorf("count(Repository.isReferrersAPIAvailable()) = %v, want %v", count, 1) + t.Errorf("count(Repository.pingReferrers()) = %v, want %v", count, 1) } // referrers unavailable count = 0 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { - case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+manifestDesc.Digest.String(): + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: count++ w.WriteHeader(http.StatusNotFound) default: @@ -5397,35 +5483,170 @@ func TestRepository_isReferrersAPIAvailable(t *testing.T) { if state := repo.loadReferrersState(); state != referrersStateUnknown { t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) } - got, err = repo.isReferrersAPIAvailable(ctx, manifestDesc) + got, err = repo.pingReferrers(ctx) if err != nil { - t.Errorf("Repository.isReferrersAPIAvailable() error = %v, wantErr %v", err, nil) + t.Errorf("Repository.pingReferrers() error = %v, wantErr %v", err, nil) } if got != false { - t.Errorf("Repository.isReferrersAPIAvailable() = %v, want %v", got, false) + t.Errorf("Repository.pingReferrers() = %v, want %v", got, false) } if state := repo.loadReferrersState(); state != referrersStateUnsupported { t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) } if count != 1 { - t.Errorf("count(Repository.isReferrersAPIAvailable()) = %v, want %v", count, 1) + t.Errorf("count(Repository.pingReferrers()) = %v, want %v", count, 1) } // 2nd call if state := repo.loadReferrersState(); state != referrersStateUnsupported { t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) } - got, err = repo.isReferrersAPIAvailable(ctx, manifestDesc) + got, err = repo.pingReferrers(ctx) if err != nil { - t.Errorf("Repository.isReferrersAPIAvailable() error = %v, wantErr %v", err, nil) + t.Errorf("Repository.pingReferrers() error = %v, wantErr %v", err, nil) } if got != false { - t.Errorf("Repository.isReferrersAPIAvailable() = %v, want %v", got, false) + t.Errorf("Repository.pingReferrers() = %v, want %v", got, false) } if state := repo.loadReferrersState(); state != referrersStateUnsupported { t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) } if count != 1 { - t.Errorf("count(Repository.isReferrersAPIAvailable()) = %v, want %v", count, 1) + t.Errorf("count(Repository.pingReferrers()) = %v, want %v", count, 1) + } +} + +func TestRepository_pingReferrers_RepositoryNotFound(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(`{ "errors": [ { "code": "NAME_UNKNOWN", "message": "repository name not known to registry" } ] }`)) + return + } + t.Errorf("unexpected access: %s %q", 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() + + // test referrers state unknown + repo, err := NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + if state := repo.loadReferrersState(); state != referrersStateUnknown { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) + } + if _, err = repo.pingReferrers(ctx); err == nil { + t.Fatalf("Repository.pingReferrers() error = %v, wantErr %v", err, true) + } + if state := repo.loadReferrersState(); state != referrersStateUnknown { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) + } + + // test referrers state supported + repo, err = NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + repo.SetReferrersCapability(true) + if state := repo.loadReferrersState(); state != referrersStateSupported { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) + } + got, err := repo.pingReferrers(ctx) + if err != nil { + t.Errorf("Repository.pingReferrers() error = %v, wantErr %v", err, nil) + } + if got != true { + t.Errorf("Repository.pingReferrers() = %v, want %v", got, true) + } + if state := repo.loadReferrersState(); state != referrersStateSupported { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) + } + + // test referrers state unsupported + repo, err = NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + repo.SetReferrersCapability(false) + if state := repo.loadReferrersState(); state != referrersStateUnsupported { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) + } + got, err = repo.pingReferrers(ctx) + if err != nil { + t.Errorf("Repository.pingReferrers() error = %v, wantErr %v", err, nil) + } + if got != false { + t.Errorf("Repository.pingReferrers() = %v, want %v", got, false) + } + if state := repo.loadReferrersState(); state != referrersStateUnsupported { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) + } +} + +func TestRepository_pingReferrers_Concurrent(t *testing.T) { + // referrers available + var count int32 + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: + atomic.AddInt32(&count, 1) + w.WriteHeader(http.StatusOK) + 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 + + concurrency := 64 + eg, egCtx := errgroup.WithContext(ctx) + + if state := repo.loadReferrersState(); state != referrersStateUnknown { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) + } + for i := 0; i < concurrency; i++ { + eg.Go(func() func() error { + return func() error { + got, err := repo.pingReferrers(egCtx) + if err != nil { + t.Fatalf("Repository.pingReferrers() error = %v, wantErr %v", err, nil) + } + if got != true { + t.Errorf("Repository.pingReferrers() = %v, want %v", got, true) + } + return nil + } + }()) + } + if err := eg.Wait(); err != nil { + t.Fatal(err) + } + + if got := atomic.LoadInt32(&count); got != 1 { + t.Errorf("count(Repository.pingReferrers()) = %v, want %v", count, 1) + } + if state := repo.loadReferrersState(); state != referrersStateSupported { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) } }