Skip to content

Commit

Permalink
feat: update Referrers API for distribution-spec v1.1.0-rc3 (#553)
Browse files Browse the repository at this point in the history
Resolves: #443
Resolves: #533
Signed-off-by: Lixia (Sylvia) Lei <lixlei@microsoft.com>
  • Loading branch information
Wwwsylvia authored Jul 25, 2023
1 parent 9b52269 commit 2c23ef6
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 62 deletions.
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
60 changes: 45 additions & 15 deletions registry/remote/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,32 @@ 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
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"
)

// filterTypeArtifactType is the "artifactType" filter applied on the list of
// referrers.
//
// References:
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc3/spec.md#listing-referrers
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#listing-referrers
const filterTypeArtifactType = "artifactType"

// Client is an interface for a HTTP client.
type Client interface {
Expand Down Expand Up @@ -497,10 +518,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 != "" {
// 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, filterTypeArtifactType) &&
!isReferrersFilterApplied(filtersAnnotation, filterTypeArtifactType) {
// 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 +1450,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 +1473,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 +1495,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 +1527,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 +1538,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

0 comments on commit 2c23ef6

Please sign in to comment.