Skip to content

Commit f701e60

Browse files
authored
refactor: refactor functions related to querying referrers (#353)
1. Use zero digest (`sha256:0000000000000000000000000000000000000000000000000000000000000000`) for pinging referrers API 2. Check `NAME_UNKNOWN` error code Signed-off-by: Lixia (Sylvia) Lei <lixlei@microsoft.com>
1 parent 7ab862c commit f701e60

File tree

5 files changed

+460
-69
lines changed

5 files changed

+460
-69
lines changed

registry/remote/errcode/errors.go

+27-2
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,30 @@ import (
2323
"unicode"
2424
)
2525

26+
// References:
27+
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#error-codes
28+
// - https://docs.docker.com/registry/spec/api/#errors-2
29+
const (
30+
ErrorCodeBlobUnknown = "BLOB_UNKNOWN"
31+
ErrorCodeBlobUploadInvalid = "BLOB_UPLOAD_INVALID"
32+
ErrorCodeBlobUploadUnknown = "BLOB_UPLOAD_UNKNOWN"
33+
ErrorCodeDigestInvalid = "DIGEST_INVALID"
34+
ErrorCodeManifestBlobUnknown = "MANIFEST_BLOB_UNKNOWN"
35+
ErrorCodeManifestInvalid = "MANIFEST_INVALID"
36+
ErrorCodeManifestUnknown = "MANIFEST_UNKNOWN"
37+
ErrorCodeNameInvalid = "NAME_INVALID"
38+
ErrorCodeNameUnknown = "NAME_UNKNOWN"
39+
ErrorCodeSizeInvalid = "SIZE_INVALID"
40+
ErrorCodeUnauthorized = "UNAUTHORIZED"
41+
ErrorCodeDenied = "DENIED"
42+
ErrorCodeUnsupported = "UNSUPPORTED"
43+
)
44+
2645
// Error represents a response inner error returned by the remote
2746
// registry.
47+
// References:
48+
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#error-codes
49+
// - https://docs.docker.com/registry/spec/api/#errors-2
2850
type Error struct {
2951
Code string `json:"code"`
3052
Message string `json:"message"`
@@ -48,8 +70,11 @@ func (e Error) Error() string {
4870
return fmt.Sprintf("%s: %s: %v", code, e.Message, e.Detail)
4971
}
5072

51-
// Errors represents a list of response inner errors returned by
52-
// the remote server.
73+
// Errors represents a list of response inner errors returned by the remote
74+
// server.
75+
// References:
76+
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#error-codes
77+
// - https://docs.docker.com/registry/spec/api/#errors-2
5378
type Errors []Error
5479

5580
// Error returns a error string describing the error.

registry/remote/internal/errutil/errutil.go

+7
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package errutil
1717

1818
import (
1919
"encoding/json"
20+
"errors"
2021
"io"
2122
"net/http"
2223

@@ -45,3 +46,9 @@ func ParseErrorResponse(resp *http.Response) error {
4546
}
4647
return resultErr
4748
}
49+
50+
// IsErrorCode returns true if err is an Error and its Code equals to code.
51+
func IsErrorCode(err error, code string) bool {
52+
var ec errcode.Error
53+
return errors.As(err, &ec) && ec.Code == code
54+
}

registry/remote/internal/errutil/errutil_test.go

+111
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,114 @@ func Test_ParseErrorResponse_plain(t *testing.T) {
126126
t.Errorf("ParseErrorResponse() error = %v, want err message %v", err, want)
127127
}
128128
}
129+
130+
func TestIsErrorCode(t *testing.T) {
131+
tests := []struct {
132+
name string
133+
err error
134+
code string
135+
want bool
136+
}{
137+
{
138+
name: "test errcode.Error, same code",
139+
err: errcode.Error{
140+
Code: errcode.ErrorCodeNameUnknown,
141+
},
142+
code: errcode.ErrorCodeNameUnknown,
143+
want: true,
144+
},
145+
{
146+
name: "test errcode.Error, different code",
147+
err: errcode.Error{
148+
Code: errcode.ErrorCodeUnauthorized,
149+
},
150+
code: errcode.ErrorCodeNameUnknown,
151+
want: false,
152+
},
153+
{
154+
name: "test errcode.Errors containing single error, same code",
155+
err: errcode.Errors{
156+
{
157+
Code: errcode.ErrorCodeNameUnknown,
158+
},
159+
},
160+
code: errcode.ErrorCodeNameUnknown,
161+
want: true,
162+
},
163+
{
164+
name: "test errcode.Errors containing single error, different code",
165+
err: errcode.Errors{
166+
{
167+
Code: errcode.ErrorCodeNameUnknown,
168+
},
169+
},
170+
code: errcode.ErrorCodeNameUnknown,
171+
want: true,
172+
},
173+
{
174+
name: "test errcode.Errors containing multiple errors, same code",
175+
err: errcode.Errors{
176+
{
177+
Code: errcode.ErrorCodeNameUnknown,
178+
},
179+
{
180+
Code: errcode.ErrorCodeUnauthorized,
181+
},
182+
},
183+
code: errcode.ErrorCodeNameUnknown,
184+
want: false,
185+
},
186+
{
187+
name: "test errcode.ErrorResponse containing single error, same code",
188+
err: &errcode.ErrorResponse{
189+
Errors: errcode.Errors{
190+
{
191+
Code: errcode.ErrorCodeNameUnknown,
192+
},
193+
},
194+
},
195+
code: errcode.ErrorCodeNameUnknown,
196+
want: true,
197+
},
198+
{
199+
name: "test errcode.ErrorResponse containing single error, different code",
200+
err: &errcode.ErrorResponse{
201+
Errors: errcode.Errors{
202+
{
203+
Code: errcode.ErrorCodeUnauthorized,
204+
},
205+
},
206+
},
207+
code: errcode.ErrorCodeNameUnknown,
208+
want: false,
209+
},
210+
{
211+
name: "test errcode.ErrorResponse containing multiple errors, same code",
212+
err: &errcode.ErrorResponse{
213+
Errors: errcode.Errors{
214+
{
215+
Code: errcode.ErrorCodeNameUnknown,
216+
},
217+
{
218+
Code: errcode.ErrorCodeUnauthorized,
219+
},
220+
},
221+
},
222+
code: errcode.ErrorCodeNameUnknown,
223+
want: false,
224+
},
225+
{
226+
name: "test unstructured error",
227+
err: errors.New(errcode.ErrorCodeNameUnknown),
228+
code: errcode.ErrorCodeNameUnknown,
229+
want: false,
230+
},
231+
}
232+
for _, tt := range tests {
233+
t.Run(tt.name, func(t *testing.T) {
234+
if got := IsErrorCode(tt.err, tt.code); got != tt.want {
235+
t.Errorf("IsErrorCode() = %v, want %v", got, tt.want)
236+
}
237+
})
238+
}
239+
}

registry/remote/repository.go

+49-22
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,20 @@ import (
4040
"oras.land/oras-go/v2/internal/registryutil"
4141
"oras.land/oras-go/v2/registry"
4242
"oras.land/oras-go/v2/registry/remote/auth"
43+
"oras.land/oras-go/v2/registry/remote/errcode"
4344
"oras.land/oras-go/v2/registry/remote/internal/errutil"
4445
)
4546

46-
// dockerContentDigestHeader - The Docker-Content-Digest header, if present on
47-
// the response, returns the canonical digest of the uploaded blob.
48-
// See https://docs.docker.com/registry/spec/api/#digest-header
49-
// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pull
50-
const dockerContentDigestHeader = "Docker-Content-Digest"
47+
const (
48+
// dockerContentDigestHeader - The Docker-Content-Digest header, if present
49+
// on the response, returns the canonical digest of the uploaded blob.
50+
// See https://docs.docker.com/registry/spec/api/#digest-header
51+
// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pull
52+
dockerContentDigestHeader = "Docker-Content-Digest"
53+
// zeroDigest represents a digest that consists of zeros. zeroDigest is used
54+
// for pinging Referrers API.
55+
zeroDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
56+
)
5157

5258
// referrersState represents the state of Referrers API.
5359
type referrersState = int32
@@ -121,7 +127,11 @@ type Repository struct {
121127
referrersState referrersState
122128

123129
// referrersTagLocks maps a referrers tag to a lock.
124-
referrersTagLocks sync.Map // map[string]sync.Mutex
130+
referrersTagLocks sync.Map // map[string]*sync.Mutex
131+
132+
// referrersPingLock locks the pingReferrersAPI() method and allows only
133+
// one go-routine to send the request.
134+
referrersPingLock sync.Mutex
125135
}
126136

127137
// 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
399409

400410
// The referrers state is unknown.
401411
if err != nil {
402-
if errors.Is(err, errdef.ErrNotFound) {
403-
// A 404 returned by Referrers API indicates that Referrers API is
404-
// not supported. Fallback to referrers tag schema.
405-
r.SetReferrersCapability(false)
406-
return r.referrersByTagSchema(ctx, desc, artifactType, fn)
412+
var errResp *errcode.ErrorResponse
413+
if !errors.As(err, &errResp) || errResp.StatusCode != http.StatusNotFound {
414+
return err
407415
}
408-
return err
416+
if errutil.IsErrorCode(errResp, errcode.ErrorCodeNameUnknown) {
417+
// The repository is not found, no fallback.
418+
return err
419+
}
420+
// A 404 returned by Referrers API indicates that Referrers API is
421+
// not supported. Fallback to referrers tag schema.
422+
r.SetReferrersCapability(false)
423+
return r.referrersByTagSchema(ctx, desc, artifactType, fn)
409424
}
410425

411426
r.SetReferrersCapability(true)
@@ -455,9 +470,6 @@ func (r *Repository) referrersPageByAPI(ctx context.Context, artifactType string
455470
}
456471
defer resp.Body.Close()
457472

458-
if resp.StatusCode == http.StatusNotFound {
459-
return "", fmt.Errorf("%s %q: %w", resp.Request.Method, resp.Request.URL, errdef.ErrNotFound)
460-
}
461473
if resp.StatusCode != http.StatusOK {
462474
return "", errutil.ParseErrorResponse(resp)
463475
}
@@ -973,11 +985,11 @@ func (s *manifestStore) indexReferrersForDelete(ctx context.Context, desc ocispe
973985
}
974986

975987
subject := *manifest.Subject
976-
yes, err := s.repo.isReferrersAPIAvailable(ctx, subject)
988+
ok, err := s.repo.pingReferrers(ctx)
977989
if err != nil {
978990
return err
979991
}
980-
if yes {
992+
if ok {
981993
// referrers API is available, no client-side indexing needed
982994
return nil
983995
}
@@ -1238,11 +1250,11 @@ func (s *manifestStore) indexReferrersForPush(ctx context.Context, desc ocispec.
12381250
return nil
12391251
}
12401252

1241-
yes, err := s.repo.isReferrersAPIAvailable(ctx, subject)
1253+
ok, err := s.repo.pingReferrers(ctx)
12421254
if err != nil {
12431255
return err
12441256
}
1245-
if yes {
1257+
if ok {
12461258
// referrers API is available, no client-side indexing needed
12471259
return nil
12481260
}
@@ -1288,8 +1300,8 @@ func (s *manifestStore) updateReferrersIndexForPush(ctx context.Context, desc, s
12881300
return s.repo.delete(ctx, oldIndexDesc, true)
12891301
}
12901302

1291-
// isReferrersAPIAvailable returns true if the Referrers API is available for r.
1292-
func (r *Repository) isReferrersAPIAvailable(ctx context.Context, desc ocispec.Descriptor) (bool, error) {
1303+
// pingReferrers returns true if the Referrers API is available for r.
1304+
func (r *Repository) pingReferrers(ctx context.Context) (bool, error) {
12931305
switch r.loadReferrersState() {
12941306
case referrersStateSupported:
12951307
return true, nil
@@ -1298,8 +1310,19 @@ func (r *Repository) isReferrersAPIAvailable(ctx context.Context, desc ocispec.D
12981310
}
12991311

13001312
// referrers state is unknown
1313+
// limit the rate of pinging referrers API
1314+
r.referrersPingLock.Lock()
1315+
defer r.referrersPingLock.Unlock()
1316+
1317+
switch r.loadReferrersState() {
1318+
case referrersStateSupported:
1319+
return true, nil
1320+
case referrersStateUnsupported:
1321+
return false, nil
1322+
}
1323+
13011324
ref := r.Reference
1302-
ref.Reference = desc.Digest.String()
1325+
ref.Reference = zeroDigest
13031326
ctx = registryutil.WithScopeHint(ctx, ref, auth.ActionPull)
13041327

13051328
url := buildReferrersURL(r.PlainHTTP, ref, "")
@@ -1318,6 +1341,10 @@ func (r *Repository) isReferrersAPIAvailable(ctx context.Context, desc ocispec.D
13181341
r.SetReferrersCapability(true)
13191342
return true, nil
13201343
case http.StatusNotFound:
1344+
if err := errutil.ParseErrorResponse(resp); errutil.IsErrorCode(err, errcode.ErrorCodeNameUnknown) {
1345+
// repository not found
1346+
return false, err
1347+
}
13211348
r.SetReferrersCapability(false)
13221349
return false, nil
13231350
default:

0 commit comments

Comments
 (0)