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

feat: update Referrers API for distribution-spec v1.1.0-rc3 #553

Merged
merged 10 commits into from
Jul 25, 2023
Merged
Show file tree
Hide file tree
Changes from 9 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
7 changes: 2 additions & 5 deletions registry/remote/referrers.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import (
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/internal/descriptor"
"oras.land/oras-go/v2/internal/spec"
)

// zeroDigest represents a digest that consists of zeros. zeroDigest is used
Expand Down Expand Up @@ -110,10 +109,8 @@ func buildReferrersTag(desc ocispec.Descriptor) string {
return alg + "-" + encoded
}

// isReferrersFilterApplied checks annotations to see if requested is in the
// applied filter list.
func isReferrersFilterApplied(annotations map[string]string, requested string) bool {
applied := annotations[spec.AnnotationReferrersFiltersApplied]
// isReferrersFilterApplied checks if requsted is in the applied filter list.
func isReferrersFilterApplied(applied, requested string) bool {
if applied == "" || requested == "" {
return false
}
Expand Down
72 changes: 33 additions & 39 deletions registry/remote/referrers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,63 +63,57 @@ func Test_buildReferrersTag(t *testing.T) {

func Test_isReferrersFilterApplied(t *testing.T) {
tests := []struct {
name string
annotations map[string]string
requested string
want bool
name string
applied string
requested string
want bool
}{
{
name: "single filter applied, specified filter matches",
annotations: map[string]string{spec.AnnotationReferrersFiltersApplied: "artifactType"},
requested: "artifactType",
want: true,
name: "single filter applied, specified filter matches",
applied: "artifactType",
requested: "artifactType",
want: true,
},
{
name: "single filter applied, specified filter does not match",
annotations: map[string]string{spec.AnnotationReferrersFiltersApplied: "foo"},
requested: "artifactType",
want: false,
name: "single filter applied, specified filter does not match",
applied: "foo",
requested: "artifactType",
want: false,
},
{
name: "multiple filters applied, specified filter matches",
annotations: map[string]string{spec.AnnotationReferrersFiltersApplied: "foo,artifactType"},
requested: "artifactType",
want: true,
name: "multiple filters applied, specified filter matches",
applied: "foo,artifactType",
requested: "artifactType",
want: true,
},
{
name: "multiple filters applied, specified filter does not match",
annotations: map[string]string{spec.AnnotationReferrersFiltersApplied: "foo,bar"},
requested: "artifactType",
want: false,
name: "multiple filters applied, specified filter does not match",
applied: "foo,bar",
requested: "artifactType",
want: false,
},
{
name: "single filter applied, specified filter empty",
annotations: map[string]string{spec.AnnotationReferrersFiltersApplied: "foo"},
requested: "",
want: false,
name: "single filter applied, no specified filter",
applied: "foo",
requested: "",
want: false,
},
{
name: "no filter applied",
annotations: map[string]string{},
requested: "artifactType",
want: false,
name: "no filter applied, specified filter does not match",
applied: "",
requested: "artifactType",
want: false,
},
{
name: "empty filter applied",
annotations: map[string]string{spec.AnnotationReferrersFiltersApplied: ""},
requested: "artifactType",
want: false,
},
{
name: "no filter applied, specified filter empty",
annotations: map[string]string{},
requested: "",
want: false,
name: "no filter applied, no specified filter",
applied: "",
requested: "",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isReferrersFilterApplied(tt.annotations, tt.requested); got != tt.want {
if got := isReferrersFilterApplied(tt.applied, tt.requested); got != tt.want {
t.Errorf("isReferrersFilterApplied() = %v, want %v", got, tt.want)
}
})
Expand Down
52 changes: 37 additions & 15 deletions registry/remote/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,24 @@ import (
"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/v1.1.0-rc1/spec.md#pull
const dockerContentDigestHeader = "Docker-Content-Digest"
const (
// headerDockerContentDigest is the "Docker-Content-Digest" header.
// If present on the response, it contains the canonical digest of the
// uploaded blob.
//
// References:
// - https://docs.docker.com/registry/spec/api/#digest-header
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#pull
Wwwsylvia marked this conversation as resolved.
Show resolved Hide resolved
headerDockerContentDigest = "Docker-Content-Digest"

// headerOCIFiltersApplied is the "OCI-Filters-Applied" header.
// If present on the response, it contains a comma-separated list of the
// applied filters.
//
// Reference:
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc3/spec.md#listing-referrers
headerOCIFiltersApplied = "OCI-Filters-Applied"
)

// Client is an interface for a HTTP client.
type Client interface {
Expand Down Expand Up @@ -497,10 +510,19 @@ func (r *Repository) referrersPageByAPI(ctx context.Context, artifactType string
if err := json.NewDecoder(lr).Decode(&index); err != nil {
return "", fmt.Errorf("%s %q: failed to decode response: %w", resp.Request.Method, resp.Request.URL, err)
}

referrers := index.Manifests
if artifactType != "" && !isReferrersFilterApplied(index.Annotations, "artifactType") {
// perform client side filtering if the filter is not applied on the server side
referrers = filterReferrers(referrers, artifactType)
if artifactType != "" {
requested := "artifactType"
Wwwsylvia marked this conversation as resolved.
Show resolved Hide resolved
// check both filters header and filters annotations for compatibility
// reference for filters header: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc3/spec.md#listing-referrers
// reference for filters annotations: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#listing-referrers
filtersHeader := resp.Header.Get(headerOCIFiltersApplied)
filtersAnnotation := index.Annotations[spec.AnnotationReferrersFiltersApplied]
if !isReferrersFilterApplied(filtersHeader, requested) && !isReferrersFilterApplied(filtersAnnotation, requested) {
// perform client side filtering if the filter is not applied on the server side
referrers = filterReferrers(referrers, artifactType)
}
}
if len(referrers) > 0 {
if err := fn(referrers); err != nil {
Expand Down Expand Up @@ -1420,13 +1442,13 @@ func (s *manifestStore) generateDescriptor(resp *http.Response, ref registry.Ref

// 4. Validate Server Digest (if present)
var serverHeaderDigest digest.Digest
if serverHeaderDigestStr := resp.Header.Get(dockerContentDigestHeader); serverHeaderDigestStr != "" {
if serverHeaderDigestStr := resp.Header.Get(headerDockerContentDigest); serverHeaderDigestStr != "" {
if serverHeaderDigest, err = digest.Parse(serverHeaderDigestStr); err != nil {
return ocispec.Descriptor{}, fmt.Errorf(
"%s %q: invalid response header value: `%s: %s`; %w",
resp.Request.Method,
resp.Request.URL,
dockerContentDigestHeader,
headerDockerContentDigest,
serverHeaderDigestStr,
err,
)
Expand All @@ -1443,7 +1465,7 @@ func (s *manifestStore) generateDescriptor(resp *http.Response, ref registry.Ref
// immediate fail
return ocispec.Descriptor{}, fmt.Errorf(
"HTTP %s request missing required header %q",
httpMethod, dockerContentDigestHeader,
httpMethod, headerDockerContentDigest,
)
}
// Otherwise, just trust the client-supplied digest
Expand All @@ -1465,7 +1487,7 @@ func (s *manifestStore) generateDescriptor(resp *http.Response, ref registry.Ref
return ocispec.Descriptor{}, fmt.Errorf(
"%s %q: invalid response; digest mismatch in %s: received %q when expecting %q",
resp.Request.Method, resp.Request.URL,
dockerContentDigestHeader, contentDigest,
headerDockerContentDigest, contentDigest,
refDigest,
)
}
Expand Down Expand Up @@ -1497,7 +1519,7 @@ func calculateDigestFromResponse(resp *http.Response, maxMetadataBytes int64) (d
// OCI distribution-spec states the Docker-Content-Digest header is optional.
// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.0.1/spec.md#legacy-docker-support-http-headers
func verifyContentDigest(resp *http.Response, expected digest.Digest) error {
digestStr := resp.Header.Get(dockerContentDigestHeader)
digestStr := resp.Header.Get(headerDockerContentDigest)

if len(digestStr) == 0 {
return nil
Expand All @@ -1508,15 +1530,15 @@ func verifyContentDigest(resp *http.Response, expected digest.Digest) error {
return fmt.Errorf(
"%s %q: invalid response header: `%s: %s`",
resp.Request.Method, resp.Request.URL,
dockerContentDigestHeader, digestStr,
headerDockerContentDigest, digestStr,
)
}

if contentDigest != expected {
return fmt.Errorf(
"%s %q: invalid response; digest mismatch in %s: received %q when expecting %q",
resp.Request.Method, resp.Request.URL,
dockerContentDigestHeader, contentDigest,
headerDockerContentDigest, contentDigest,
expected,
)
}
Expand Down
Loading