Skip to content

Commit

Permalink
Support for OCI 1.1+ referrers via API (#1546)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
jdolitsky committed Feb 3, 2023
1 parent de35f0f commit b3c23b4
Show file tree
Hide file tree
Showing 6 changed files with 357 additions and 138 deletions.
89 changes: 89 additions & 0 deletions pkg/registry/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ func isCatalog(req *http.Request) bool {
return elems[len(elems)-1] == "_catalog"
}

// Returns whether this url should be handled by the referrers handler
func isReferrers(req *http.Request) bool {
elems := strings.Split(req.URL.Path, "/")
elems = elems[1:]
if len(elems) < 4 {
return false
}
return elems[len(elems)-2] == "referrers"
}

// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pulling-an-image-manifest
// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pushing-an-image
func (m *manifests) handle(resp http.ResponseWriter, req *http.Request) *regError {
Expand Down Expand Up @@ -339,3 +349,82 @@ func (m *manifests) handleCatalog(resp http.ResponseWriter, req *http.Request) *
Message: "We don't understand your method + url",
}
}

// TODO: implement handling of artifactType querystring
func (m *manifests) handleReferrers(resp http.ResponseWriter, req *http.Request) *regError {
// Ensure this is a GET request
if req.Method != "GET" {
return &regError{
Status: http.StatusBadRequest,
Code: "METHOD_UNKNOWN",
Message: "We don't understand your method + url",
}
}

elem := strings.Split(req.URL.Path, "/")
elem = elem[1:]
target := elem[len(elem)-1]
repo := strings.Join(elem[1:len(elem)-2], "/")

// Validate that incoming target is a valid digest
if _, err := v1.NewHash(target); err != nil {
return &regError{
Status: http.StatusBadRequest,
Code: "UNSUPPORTED",
Message: "Target must be a valid digest",
}
}

m.lock.Lock()
defer m.lock.Unlock()

digestToManifestMap, repoExists := m.manifests[repo]
if !repoExists {
return &regError{
Status: http.StatusNotFound,
Code: "NAME_UNKNOWN",
Message: "Unknown name",
}
}

im := v1.IndexManifest{
SchemaVersion: 2,
MediaType: types.OCIImageIndex,
Manifests: []v1.Descriptor{},
}
for digest, manifest := range digestToManifestMap {
h, err := v1.NewHash(digest)
if err != nil {
continue
}
var refPointer struct {
Subject *v1.Descriptor `json:"subject"`
}
json.Unmarshal(manifest.blob, &refPointer)
if refPointer.Subject == nil {
continue
}
referenceDigest := refPointer.Subject.Digest
if referenceDigest.String() != target {
continue
}
// At this point, we know the current digest references the target
var imageAsArtifact struct {
Config struct {
MediaType string `json:"mediaType"`
} `json:"config"`
}
json.Unmarshal(manifest.blob, &imageAsArtifact)
im.Manifests = append(im.Manifests, v1.Descriptor{
MediaType: types.MediaType(manifest.contentType),
Size: int64(len(manifest.blob)),
Digest: h,
ArtifactType: imageAsArtifact.Config.MediaType,
})
}
msg, _ := json.Marshal(&im)
resp.Header().Set("Content-Length", fmt.Sprint(len(msg)))
resp.WriteHeader(http.StatusOK)
io.Copy(resp, bytes.NewReader([]byte(msg)))
return nil
}
17 changes: 14 additions & 3 deletions pkg/registry/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ import (
)

type registry struct {
log *log.Logger
blobs blobs
manifests manifests
log *log.Logger
blobs blobs
manifests manifests
referrersEnabled bool
}

// https://docs.docker.com/registry/spec/api/#api-version-check
Expand All @@ -50,6 +51,9 @@ func (r *registry) v2(resp http.ResponseWriter, req *http.Request) *regError {
if isCatalog(req) {
return r.manifests.handleCatalog(resp, req)
}
if r.referrersEnabled && isReferrers(req) {
return r.manifests.handleReferrers(resp, req)
}
resp.Header().Set("Docker-Distribution-API-Version", "registry/2.0")
if req.URL.Path != "/v2/" && req.URL.Path != "/v2" {
return &regError{
Expand Down Expand Up @@ -104,3 +108,10 @@ func Logger(l *log.Logger) Option {
r.blobs.log = l
}
}

// WithReferrersSupport enables the referrers API endpoint (OCI 1.1+)
func WithReferrersSupport(enabled bool) Option {
return func(r *registry) {
r.referrersEnabled = enabled
}
}
52 changes: 50 additions & 2 deletions pkg/registry/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,17 +437,65 @@ func TestCalls(t *testing.T) {
URL: "/v2/_catalog?n=1000",
Code: http.StatusOK,
},
{
Description: "fetch references",
Method: "GET",
URL: "/v2/foo/referrers/sha256:" + sha256String("foo"),
Code: http.StatusOK,
Manifests: map[string]string{
"foo/manifests/image": "foo",
"foo/manifests/points-to-image": "{\"subject\": {\"digest\": \"sha256:" + sha256String("foo") + "\"}}",
},
},
{
Description: "fetch references, subject pointing elsewhere",
Method: "GET",
URL: "/v2/foo/referrers/sha256:" + sha256String("foo"),
Code: http.StatusOK,
Manifests: map[string]string{
"foo/manifests/image": "foo",
"foo/manifests/points-to-image": "{\"subject\": {\"digest\": \"sha256:" + sha256String("nonexistant") + "\"}}",
},
},
{
Description: "fetch references, no results",
Method: "GET",
URL: "/v2/foo/referrers/sha256:" + sha256String("foo"),
Code: http.StatusOK,
Manifests: map[string]string{
"foo/manifests/image": "foo",
},
},
{
Description: "fetch references, missing repo",
Method: "GET",
URL: "/v2/does-not-exist/referrers/sha256:" + sha256String("foo"),
Code: http.StatusNotFound,
},
{
Description: "fetch references, bad target (tag vs. digest)",
Method: "GET",
URL: "/v2/foo/referrers/latest",
Code: http.StatusBadRequest,
},
{
Description: "fetch references, bad method",
Method: "POST",
URL: "/v2/foo/referrers/sha256:" + sha256String("foo"),
Code: http.StatusBadRequest,
},
}

for _, tc := range tcs {

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

r := registry.New()
opts := []registry.Option{registry.WithReferrersSupport(true)}
if logger != nil {
r = registry.New(registry.Logger(logger))
opts = append(opts, registry.Logger(logger))
}
r := registry.New(opts...)
s := httptest.NewServer(r)
defer s.Close()

Expand Down
62 changes: 46 additions & 16 deletions pkg/v1/remote/descriptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,32 @@ func fallbackTag(d name.Digest) name.Tag {
}

func (f *fetcher) fetchReferrers(ctx context.Context, filter map[string]string, d name.Digest) (*v1.IndexManifest, error) {
// Assume the registry doesn't support the Referrers API endpoint, so we'll use the fallback tag scheme.
// Check the Referrers API endpoint first.
u := f.url("referrers", d.DigestStr())
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", string(types.OCIImageIndex))

resp, err := f.Client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if err := transport.CheckError(resp, http.StatusOK, http.StatusNotFound, http.StatusBadRequest); err != nil {
return nil, err
}
if resp.StatusCode == http.StatusOK {
var im v1.IndexManifest
if err := json.NewDecoder(resp.Body).Decode(&im); err != nil {
return nil, err
}
return filterReferrersResponse(filter, &im), nil
}

// The registry doesn't support the Referrers API endpoint, so we'll use the fallback tag scheme.
b, _, err := f.fetchManifest(fallbackTag(d), []types.MediaType{types.OCIImageIndex})
if err != nil {
return nil, err
Expand All @@ -261,21 +286,7 @@ func (f *fetcher) fetchReferrers(ctx context.Context, filter map[string]string,
return nil, err
}

// If filter applied, filter out by artifactType and add annotation
// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers
if filter != nil {
if v, ok := filter["artifactType"]; ok {
tmp := []v1.Descriptor{}
for _, desc := range im.Manifests {
if desc.ArtifactType == v {
tmp = append(tmp, desc)
}
}
im.Manifests = tmp
}
}

return &im, nil
return filterReferrersResponse(filter, &im), nil
}

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

return resp.StatusCode == http.StatusOK, nil
}

// If filter applied, filter out by artifactType.
// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers
func filterReferrersResponse(filter map[string]string, origIndex *v1.IndexManifest) *v1.IndexManifest {
newIndex := origIndex
if filter == nil {
return newIndex
}
if v, ok := filter["artifactType"]; ok {
tmp := []v1.Descriptor{}
for _, desc := range newIndex.Manifests {
if desc.ArtifactType == v {
tmp = append(tmp, desc)
}
}
newIndex.Manifests = tmp
}
return newIndex
}
Loading

0 comments on commit b3c23b4

Please sign in to comment.