Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for OCI 1.1+ referrers via API #1546

Merged
merged 7 commits into from
Feb 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
}
Comment on lines +112 to +117
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is disabled by default, but we could go the other direction too

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