Skip to content

Commit b3c23b4

Browse files
authored
Support for OCI 1.1+ referrers via API (#1546)
* Support for OCI 1.1+ referrers via API Signed-off-by: Josh Dolitsky <josh@dolit.ski> * modify signature of referrers filter method Signed-off-by: Josh Dolitsky <josh@dolit.ski> * add referrers endpoint support to registry package Signed-off-by: Josh Dolitsky <josh@dolit.ski> * add test coverage Signed-off-by: Josh Dolitsky <josh@dolit.ski> * additional test for passing tag to referrers api Signed-off-by: Josh Dolitsky <josh@dolit.ski> * additional test for missing repo Signed-off-by: Josh Dolitsky <josh@dolit.ski> --------- Signed-off-by: Josh Dolitsky <josh@dolit.ski>
1 parent de35f0f commit b3c23b4

File tree

6 files changed

+357
-138
lines changed

6 files changed

+357
-138
lines changed

pkg/registry/manifest.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,16 @@ func isCatalog(req *http.Request) bool {
7979
return elems[len(elems)-1] == "_catalog"
8080
}
8181

82+
// Returns whether this url should be handled by the referrers handler
83+
func isReferrers(req *http.Request) bool {
84+
elems := strings.Split(req.URL.Path, "/")
85+
elems = elems[1:]
86+
if len(elems) < 4 {
87+
return false
88+
}
89+
return elems[len(elems)-2] == "referrers"
90+
}
91+
8292
// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pulling-an-image-manifest
8393
// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pushing-an-image
8494
func (m *manifests) handle(resp http.ResponseWriter, req *http.Request) *regError {
@@ -339,3 +349,82 @@ func (m *manifests) handleCatalog(resp http.ResponseWriter, req *http.Request) *
339349
Message: "We don't understand your method + url",
340350
}
341351
}
352+
353+
// TODO: implement handling of artifactType querystring
354+
func (m *manifests) handleReferrers(resp http.ResponseWriter, req *http.Request) *regError {
355+
// Ensure this is a GET request
356+
if req.Method != "GET" {
357+
return &regError{
358+
Status: http.StatusBadRequest,
359+
Code: "METHOD_UNKNOWN",
360+
Message: "We don't understand your method + url",
361+
}
362+
}
363+
364+
elem := strings.Split(req.URL.Path, "/")
365+
elem = elem[1:]
366+
target := elem[len(elem)-1]
367+
repo := strings.Join(elem[1:len(elem)-2], "/")
368+
369+
// Validate that incoming target is a valid digest
370+
if _, err := v1.NewHash(target); err != nil {
371+
return &regError{
372+
Status: http.StatusBadRequest,
373+
Code: "UNSUPPORTED",
374+
Message: "Target must be a valid digest",
375+
}
376+
}
377+
378+
m.lock.Lock()
379+
defer m.lock.Unlock()
380+
381+
digestToManifestMap, repoExists := m.manifests[repo]
382+
if !repoExists {
383+
return &regError{
384+
Status: http.StatusNotFound,
385+
Code: "NAME_UNKNOWN",
386+
Message: "Unknown name",
387+
}
388+
}
389+
390+
im := v1.IndexManifest{
391+
SchemaVersion: 2,
392+
MediaType: types.OCIImageIndex,
393+
Manifests: []v1.Descriptor{},
394+
}
395+
for digest, manifest := range digestToManifestMap {
396+
h, err := v1.NewHash(digest)
397+
if err != nil {
398+
continue
399+
}
400+
var refPointer struct {
401+
Subject *v1.Descriptor `json:"subject"`
402+
}
403+
json.Unmarshal(manifest.blob, &refPointer)
404+
if refPointer.Subject == nil {
405+
continue
406+
}
407+
referenceDigest := refPointer.Subject.Digest
408+
if referenceDigest.String() != target {
409+
continue
410+
}
411+
// At this point, we know the current digest references the target
412+
var imageAsArtifact struct {
413+
Config struct {
414+
MediaType string `json:"mediaType"`
415+
} `json:"config"`
416+
}
417+
json.Unmarshal(manifest.blob, &imageAsArtifact)
418+
im.Manifests = append(im.Manifests, v1.Descriptor{
419+
MediaType: types.MediaType(manifest.contentType),
420+
Size: int64(len(manifest.blob)),
421+
Digest: h,
422+
ArtifactType: imageAsArtifact.Config.MediaType,
423+
})
424+
}
425+
msg, _ := json.Marshal(&im)
426+
resp.Header().Set("Content-Length", fmt.Sprint(len(msg)))
427+
resp.WriteHeader(http.StatusOK)
428+
io.Copy(resp, bytes.NewReader([]byte(msg)))
429+
return nil
430+
}

pkg/registry/registry.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@ import (
3030
)
3131

3232
type registry struct {
33-
log *log.Logger
34-
blobs blobs
35-
manifests manifests
33+
log *log.Logger
34+
blobs blobs
35+
manifests manifests
36+
referrersEnabled bool
3637
}
3738

3839
// https://docs.docker.com/registry/spec/api/#api-version-check
@@ -50,6 +51,9 @@ func (r *registry) v2(resp http.ResponseWriter, req *http.Request) *regError {
5051
if isCatalog(req) {
5152
return r.manifests.handleCatalog(resp, req)
5253
}
54+
if r.referrersEnabled && isReferrers(req) {
55+
return r.manifests.handleReferrers(resp, req)
56+
}
5357
resp.Header().Set("Docker-Distribution-API-Version", "registry/2.0")
5458
if req.URL.Path != "/v2/" && req.URL.Path != "/v2" {
5559
return &regError{
@@ -104,3 +108,10 @@ func Logger(l *log.Logger) Option {
104108
r.blobs.log = l
105109
}
106110
}
111+
112+
// WithReferrersSupport enables the referrers API endpoint (OCI 1.1+)
113+
func WithReferrersSupport(enabled bool) Option {
114+
return func(r *registry) {
115+
r.referrersEnabled = enabled
116+
}
117+
}

pkg/registry/registry_test.go

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -437,17 +437,65 @@ func TestCalls(t *testing.T) {
437437
URL: "/v2/_catalog?n=1000",
438438
Code: http.StatusOK,
439439
},
440+
{
441+
Description: "fetch references",
442+
Method: "GET",
443+
URL: "/v2/foo/referrers/sha256:" + sha256String("foo"),
444+
Code: http.StatusOK,
445+
Manifests: map[string]string{
446+
"foo/manifests/image": "foo",
447+
"foo/manifests/points-to-image": "{\"subject\": {\"digest\": \"sha256:" + sha256String("foo") + "\"}}",
448+
},
449+
},
450+
{
451+
Description: "fetch references, subject pointing elsewhere",
452+
Method: "GET",
453+
URL: "/v2/foo/referrers/sha256:" + sha256String("foo"),
454+
Code: http.StatusOK,
455+
Manifests: map[string]string{
456+
"foo/manifests/image": "foo",
457+
"foo/manifests/points-to-image": "{\"subject\": {\"digest\": \"sha256:" + sha256String("nonexistant") + "\"}}",
458+
},
459+
},
460+
{
461+
Description: "fetch references, no results",
462+
Method: "GET",
463+
URL: "/v2/foo/referrers/sha256:" + sha256String("foo"),
464+
Code: http.StatusOK,
465+
Manifests: map[string]string{
466+
"foo/manifests/image": "foo",
467+
},
468+
},
469+
{
470+
Description: "fetch references, missing repo",
471+
Method: "GET",
472+
URL: "/v2/does-not-exist/referrers/sha256:" + sha256String("foo"),
473+
Code: http.StatusNotFound,
474+
},
475+
{
476+
Description: "fetch references, bad target (tag vs. digest)",
477+
Method: "GET",
478+
URL: "/v2/foo/referrers/latest",
479+
Code: http.StatusBadRequest,
480+
},
481+
{
482+
Description: "fetch references, bad method",
483+
Method: "POST",
484+
URL: "/v2/foo/referrers/sha256:" + sha256String("foo"),
485+
Code: http.StatusBadRequest,
486+
},
440487
}
441488

442489
for _, tc := range tcs {
443490

444491
var logger *log.Logger
445492
testf := func(t *testing.T) {
446493

447-
r := registry.New()
494+
opts := []registry.Option{registry.WithReferrersSupport(true)}
448495
if logger != nil {
449-
r = registry.New(registry.Logger(logger))
496+
opts = append(opts, registry.Logger(logger))
450497
}
498+
r := registry.New(opts...)
451499
s := httptest.NewServer(r)
452500
defer s.Close()
453501

pkg/v1/remote/descriptor.go

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,32 @@ func fallbackTag(d name.Digest) name.Tag {
245245
}
246246

247247
func (f *fetcher) fetchReferrers(ctx context.Context, filter map[string]string, d name.Digest) (*v1.IndexManifest, error) {
248-
// Assume the registry doesn't support the Referrers API endpoint, so we'll use the fallback tag scheme.
248+
// Check the Referrers API endpoint first.
249+
u := f.url("referrers", d.DigestStr())
250+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
251+
if err != nil {
252+
return nil, err
253+
}
254+
req.Header.Set("Accept", string(types.OCIImageIndex))
255+
256+
resp, err := f.Client.Do(req)
257+
if err != nil {
258+
return nil, err
259+
}
260+
defer resp.Body.Close()
261+
262+
if err := transport.CheckError(resp, http.StatusOK, http.StatusNotFound, http.StatusBadRequest); err != nil {
263+
return nil, err
264+
}
265+
if resp.StatusCode == http.StatusOK {
266+
var im v1.IndexManifest
267+
if err := json.NewDecoder(resp.Body).Decode(&im); err != nil {
268+
return nil, err
269+
}
270+
return filterReferrersResponse(filter, &im), nil
271+
}
272+
273+
// The registry doesn't support the Referrers API endpoint, so we'll use the fallback tag scheme.
249274
b, _, err := f.fetchManifest(fallbackTag(d), []types.MediaType{types.OCIImageIndex})
250275
if err != nil {
251276
return nil, err
@@ -261,21 +286,7 @@ func (f *fetcher) fetchReferrers(ctx context.Context, filter map[string]string,
261286
return nil, err
262287
}
263288

264-
// If filter applied, filter out by artifactType and add annotation
265-
// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers
266-
if filter != nil {
267-
if v, ok := filter["artifactType"]; ok {
268-
tmp := []v1.Descriptor{}
269-
for _, desc := range im.Manifests {
270-
if desc.ArtifactType == v {
271-
tmp = append(tmp, desc)
272-
}
273-
}
274-
im.Manifests = tmp
275-
}
276-
}
277-
278-
return &im, nil
289+
return filterReferrersResponse(filter, &im), nil
279290
}
280291

281292
func (f *fetcher) fetchManifest(ref name.Reference, acceptable []types.MediaType) ([]byte, *v1.Descriptor, error) {
@@ -479,3 +490,22 @@ func (f *fetcher) blobExists(h v1.Hash) (bool, error) {
479490

480491
return resp.StatusCode == http.StatusOK, nil
481492
}
493+
494+
// If filter applied, filter out by artifactType.
495+
// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers
496+
func filterReferrersResponse(filter map[string]string, origIndex *v1.IndexManifest) *v1.IndexManifest {
497+
newIndex := origIndex
498+
if filter == nil {
499+
return newIndex
500+
}
501+
if v, ok := filter["artifactType"]; ok {
502+
tmp := []v1.Descriptor{}
503+
for _, desc := range newIndex.Manifests {
504+
if desc.ArtifactType == v {
505+
tmp = append(tmp, desc)
506+
}
507+
}
508+
newIndex.Manifests = tmp
509+
}
510+
return newIndex
511+
}

0 commit comments

Comments
 (0)