Skip to content

Commit

Permalink
Support for OCI 1.1+ referrers via fallback tag (#1543)
Browse files Browse the repository at this point in the history
Signed-off-by: Josh Dolitsky <josh@dolit.ski>
  • Loading branch information
jdolitsky committed Jan 31, 2023
1 parent da1008f commit 061ee6b
Show file tree
Hide file tree
Showing 11 changed files with 405 additions and 9 deletions.
2 changes: 2 additions & 0 deletions pkg/v1/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type Manifest struct {
Config Descriptor `json:"config"`
Layers []Descriptor `json:"layers"`
Annotations map[string]string `json:"annotations,omitempty"`
Subject *Descriptor `json:"subject,omitempty"`
}

// IndexManifest represents an OCI image index in a structured way.
Expand All @@ -36,6 +37,7 @@ type IndexManifest struct {
MediaType types.MediaType `json:"mediaType,omitempty"`
Manifests []Descriptor `json:"manifests"`
Annotations map[string]string `json:"annotations,omitempty"`
Subject *Descriptor `json:"subject,omitempty"`
}

// Descriptor holds a reference from the manifest to one of its constituent elements.
Expand Down
2 changes: 2 additions & 0 deletions pkg/v1/mutate/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type image struct {
configMediaType *types.MediaType
diffIDMap map[v1.Hash]v1.Layer
digestMap map[v1.Hash]v1.Layer
subject *v1.Descriptor
}

var _ v1.Image = (*image)(nil)
Expand Down Expand Up @@ -153,6 +154,7 @@ func (i *image) compute() error {
manifest.Annotations[k] = v
}
}
manifest.Subject = i.subject

i.configFile = configFile
i.manifest = manifest
Expand Down
2 changes: 2 additions & 0 deletions pkg/v1/mutate/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ type index struct {
imageMap map[v1.Hash]v1.Image
indexMap map[v1.Hash]v1.ImageIndex
layerMap map[v1.Hash]v1.Layer
subject *v1.Descriptor
}

var _ v1.ImageIndex = (*index)(nil)
Expand Down Expand Up @@ -142,6 +143,7 @@ func (i *index) compute() error {
manifest.Annotations[k] = v
}
}
manifest.Subject = i.subject

i.manifest = manifest
i.computed = true
Expand Down
42 changes: 35 additions & 7 deletions pkg/v1/mutate/mutate.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,33 @@ func Config(base v1.Image, cfg v1.Config) (v1.Image, error) {
return ConfigFile(base, cf)
}

// Annotatable represents a manifest that can carry annotations.
type Annotatable interface {
partial.WithRawManifest
// Subject mutates the subject on an image or index manifest.
//
// The input is expected to be a v1.Image or v1.ImageIndex, and
// returns the same type. You can type-assert the result like so:
//
// img := Subject(empty.Image, subj).(v1.Image)
//
// Or for an index:
//
// idx := Subject(empty.Index, subj).(v1.ImageIndex)
//
// If the input is not an Image or ImageIndex, the result will
// attempt to lazily annotate the raw manifest.
func Subject(f partial.WithRawManifest, subject v1.Descriptor) partial.WithRawManifest {
if img, ok := f.(v1.Image); ok {
return &image{
base: img,
subject: &subject,
}
}
if idx, ok := f.(v1.ImageIndex); ok {
return &index{
base: idx,
subject: &subject,
}
}
return arbitraryRawManifest{a: f, subject: &subject}
}

// Annotations mutates the annotations on an annotatable image or index manifest.
Expand All @@ -137,7 +161,7 @@ type Annotatable interface {
//
// If the input Annotatable is not an Image or ImageIndex, the result will
// attempt to lazily annotate the raw manifest.
func Annotations(f Annotatable, anns map[string]string) Annotatable {
func Annotations(f partial.WithRawManifest, anns map[string]string) partial.WithRawManifest {
if img, ok := f.(v1.Image); ok {
return &image{
base: img,
Expand All @@ -150,12 +174,13 @@ func Annotations(f Annotatable, anns map[string]string) Annotatable {
annotations: anns,
}
}
return arbitraryRawManifest{f, anns}
return arbitraryRawManifest{a: f, anns: anns}
}

type arbitraryRawManifest struct {
a Annotatable
anns map[string]string
a partial.WithRawManifest
anns map[string]string
subject *v1.Descriptor
}

func (a arbitraryRawManifest) RawManifest() ([]byte, error) {
Expand All @@ -178,6 +203,9 @@ func (a arbitraryRawManifest) RawManifest() ([]byte, error) {
} else {
m["annotations"] = a.anns
}
if a.subject != nil {
m["subject"] = a.subject
}
return json.Marshal(m)
}

Expand Down
3 changes: 2 additions & 1 deletion pkg/v1/mutate/mutate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/match"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/partial"
"github.com/google/go-containerregistry/pkg/v1/random"
"github.com/google/go-containerregistry/pkg/v1/stream"
"github.com/google/go-containerregistry/pkg/v1/tarball"
Expand Down Expand Up @@ -279,7 +280,7 @@ func TestAnnotations(t *testing.T) {

for _, c := range []struct {
desc string
in mutate.Annotatable
in partial.WithRawManifest
want string
}{{
desc: "image",
Expand Down
4 changes: 4 additions & 0 deletions pkg/v1/remote/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,8 @@ func Delete(ref name.Reference, options ...Option) error {
defer resp.Body.Close()

return transport.CheckError(resp, http.StatusOK, http.StatusAccepted)

// TODO(jason): If the manifest had a `subject`, and if the registry
// doesn't support Referrers, update the index pointed to by the
// subject's fallback tag to remove the descriptor for this manifest.
}
43 changes: 42 additions & 1 deletion pkg/v1/remote/descriptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ package remote
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
Expand Down Expand Up @@ -59,7 +61,7 @@ type Descriptor struct {
v1.Descriptor
Manifest []byte

// So we can share this implementation with Image..
// So we can share this implementation with Image.
platform v1.Platform
}

Expand Down Expand Up @@ -237,6 +239,45 @@ func (f *fetcher) url(resource, identifier string) url.URL {
}
}

// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#referrers-tag-schema
func fallbackTag(d name.Digest) name.Tag {
return d.Context().Tag(strings.Replace(d.DigestStr(), ":", "-", 1))
}

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.
b, _, err := f.fetchManifest(fallbackTag(d), []types.MediaType{types.OCIImageIndex})
if err != nil {
return nil, err
}
var terr *transport.Error
if ok := errors.As(err, &terr); ok && terr.StatusCode == http.StatusNotFound {
// Not found just means there are no attachments yet. Start with an empty manifest.
return &v1.IndexManifest{MediaType: types.OCIImageIndex}, nil
}

var im v1.IndexManifest
if err := json.Unmarshal(b, &im); err != nil {
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
}

func (f *fetcher) fetchManifest(ref name.Reference, acceptable []types.MediaType) ([]byte, *v1.Descriptor, error) {
u := f.url("manifests", ref.Identifier())
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
Expand Down
12 changes: 12 additions & 0 deletions pkg/v1/remote/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type options struct {
pageSize int
retryBackoff Backoff
retryPredicate retry.Predicate
filter map[string]string
}

var defaultPlatform = v1.Platform{
Expand Down Expand Up @@ -303,3 +304,14 @@ func WithRetryPredicate(predicate retry.Predicate) Option {
return nil
}
}

// WithFilter sets the filter querystring for HTTP operations.
func WithFilter(key string, value string) Option {
return func(o *options) error {
if o.filter == nil {
o.filter = map[string]string{}
}
o.filter[key] = value
return nil
}
}
35 changes: 35 additions & 0 deletions pkg/v1/remote/referrers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright 2023 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package remote

import (
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
)

// Referrers returns a list of descriptors that refer to the given manifest digest.
//
// The subject manifest doesn't have to exist in the registry for there to be descriptors that refer to it.
func Referrers(d name.Digest, options ...Option) (*v1.IndexManifest, error) {
o, err := makeOptions(d.Context(), options...)
if err != nil {
return nil, err
}
f, err := makeFetcher(d, o)
if err != nil {
return nil, err
}
return f.fetchReferrers(o.context, o.filter, d)
}
Loading

0 comments on commit 061ee6b

Please sign in to comment.