Skip to content

Commit 061ee6b

Browse files
authored
Support for OCI 1.1+ referrers via fallback tag (#1543)
Signed-off-by: Josh Dolitsky <josh@dolit.ski>
1 parent da1008f commit 061ee6b

File tree

11 files changed

+405
-9
lines changed

11 files changed

+405
-9
lines changed

pkg/v1/manifest.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type Manifest struct {
2828
Config Descriptor `json:"config"`
2929
Layers []Descriptor `json:"layers"`
3030
Annotations map[string]string `json:"annotations,omitempty"`
31+
Subject *Descriptor `json:"subject,omitempty"`
3132
}
3233

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

4143
// Descriptor holds a reference from the manifest to one of its constituent elements.

pkg/v1/mutate/image.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type image struct {
3737
configMediaType *types.MediaType
3838
diffIDMap map[v1.Hash]v1.Layer
3939
digestMap map[v1.Hash]v1.Layer
40+
subject *v1.Descriptor
4041
}
4142

4243
var _ v1.Image = (*image)(nil)
@@ -153,6 +154,7 @@ func (i *image) compute() error {
153154
manifest.Annotations[k] = v
154155
}
155156
}
157+
manifest.Subject = i.subject
156158

157159
i.configFile = configFile
158160
i.manifest = manifest

pkg/v1/mutate/index.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ type index struct {
7070
imageMap map[v1.Hash]v1.Image
7171
indexMap map[v1.Hash]v1.ImageIndex
7272
layerMap map[v1.Hash]v1.Layer
73+
subject *v1.Descriptor
7374
}
7475

7576
var _ v1.ImageIndex = (*index)(nil)
@@ -142,6 +143,7 @@ func (i *index) compute() error {
142143
manifest.Annotations[k] = v
143144
}
144145
}
146+
manifest.Subject = i.subject
145147

146148
i.manifest = manifest
147149
i.computed = true

pkg/v1/mutate/mutate.go

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,33 @@ func Config(base v1.Image, cfg v1.Config) (v1.Image, error) {
115115
return ConfigFile(base, cf)
116116
}
117117

118-
// Annotatable represents a manifest that can carry annotations.
119-
type Annotatable interface {
120-
partial.WithRawManifest
118+
// Subject mutates the subject on an image or index manifest.
119+
//
120+
// The input is expected to be a v1.Image or v1.ImageIndex, and
121+
// returns the same type. You can type-assert the result like so:
122+
//
123+
// img := Subject(empty.Image, subj).(v1.Image)
124+
//
125+
// Or for an index:
126+
//
127+
// idx := Subject(empty.Index, subj).(v1.ImageIndex)
128+
//
129+
// If the input is not an Image or ImageIndex, the result will
130+
// attempt to lazily annotate the raw manifest.
131+
func Subject(f partial.WithRawManifest, subject v1.Descriptor) partial.WithRawManifest {
132+
if img, ok := f.(v1.Image); ok {
133+
return &image{
134+
base: img,
135+
subject: &subject,
136+
}
137+
}
138+
if idx, ok := f.(v1.ImageIndex); ok {
139+
return &index{
140+
base: idx,
141+
subject: &subject,
142+
}
143+
}
144+
return arbitraryRawManifest{a: f, subject: &subject}
121145
}
122146

123147
// Annotations mutates the annotations on an annotatable image or index manifest.
@@ -137,7 +161,7 @@ type Annotatable interface {
137161
//
138162
// If the input Annotatable is not an Image or ImageIndex, the result will
139163
// attempt to lazily annotate the raw manifest.
140-
func Annotations(f Annotatable, anns map[string]string) Annotatable {
164+
func Annotations(f partial.WithRawManifest, anns map[string]string) partial.WithRawManifest {
141165
if img, ok := f.(v1.Image); ok {
142166
return &image{
143167
base: img,
@@ -150,12 +174,13 @@ func Annotations(f Annotatable, anns map[string]string) Annotatable {
150174
annotations: anns,
151175
}
152176
}
153-
return arbitraryRawManifest{f, anns}
177+
return arbitraryRawManifest{a: f, anns: anns}
154178
}
155179

156180
type arbitraryRawManifest struct {
157-
a Annotatable
158-
anns map[string]string
181+
a partial.WithRawManifest
182+
anns map[string]string
183+
subject *v1.Descriptor
159184
}
160185

161186
func (a arbitraryRawManifest) RawManifest() ([]byte, error) {
@@ -178,6 +203,9 @@ func (a arbitraryRawManifest) RawManifest() ([]byte, error) {
178203
} else {
179204
m["annotations"] = a.anns
180205
}
206+
if a.subject != nil {
207+
m["subject"] = a.subject
208+
}
181209
return json.Marshal(m)
182210
}
183211

pkg/v1/mutate/mutate_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
"github.com/google/go-containerregistry/pkg/v1/empty"
3333
"github.com/google/go-containerregistry/pkg/v1/match"
3434
"github.com/google/go-containerregistry/pkg/v1/mutate"
35+
"github.com/google/go-containerregistry/pkg/v1/partial"
3536
"github.com/google/go-containerregistry/pkg/v1/random"
3637
"github.com/google/go-containerregistry/pkg/v1/stream"
3738
"github.com/google/go-containerregistry/pkg/v1/tarball"
@@ -279,7 +280,7 @@ func TestAnnotations(t *testing.T) {
279280

280281
for _, c := range []struct {
281282
desc string
282-
in mutate.Annotatable
283+
in partial.WithRawManifest
283284
want string
284285
}{{
285286
desc: "image",

pkg/v1/remote/delete.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,8 @@ func Delete(ref name.Reference, options ...Option) error {
5454
defer resp.Body.Close()
5555

5656
return transport.CheckError(resp, http.StatusOK, http.StatusAccepted)
57+
58+
// TODO(jason): If the manifest had a `subject`, and if the registry
59+
// doesn't support Referrers, update the index pointed to by the
60+
// subject's fallback tag to remove the descriptor for this manifest.
5761
}

pkg/v1/remote/descriptor.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ package remote
1717
import (
1818
"bytes"
1919
"context"
20+
"encoding/json"
21+
"errors"
2022
"fmt"
2123
"io"
2224
"net/http"
@@ -59,7 +61,7 @@ type Descriptor struct {
5961
v1.Descriptor
6062
Manifest []byte
6163

62-
// So we can share this implementation with Image..
64+
// So we can share this implementation with Image.
6365
platform v1.Platform
6466
}
6567

@@ -237,6 +239,45 @@ func (f *fetcher) url(resource, identifier string) url.URL {
237239
}
238240
}
239241

242+
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#referrers-tag-schema
243+
func fallbackTag(d name.Digest) name.Tag {
244+
return d.Context().Tag(strings.Replace(d.DigestStr(), ":", "-", 1))
245+
}
246+
247+
func (f *fetcher) fetchReferrers(ctx context.Context, filter map[string]string, d name.Digest) (*v1.IndexManifest, error) {
248+
// Assume the registry doesn't support the Referrers API endpoint, so we'll use the fallback tag scheme.
249+
b, _, err := f.fetchManifest(fallbackTag(d), []types.MediaType{types.OCIImageIndex})
250+
if err != nil {
251+
return nil, err
252+
}
253+
var terr *transport.Error
254+
if ok := errors.As(err, &terr); ok && terr.StatusCode == http.StatusNotFound {
255+
// Not found just means there are no attachments yet. Start with an empty manifest.
256+
return &v1.IndexManifest{MediaType: types.OCIImageIndex}, nil
257+
}
258+
259+
var im v1.IndexManifest
260+
if err := json.Unmarshal(b, &im); err != nil {
261+
return nil, err
262+
}
263+
264+
// If filter applied, filter out by artifactType and add annotation
265+
// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers
266+
if filter != nil {
267+
if v, ok := filter["artifactType"]; ok {
268+
tmp := []v1.Descriptor{}
269+
for _, desc := range im.Manifests {
270+
if desc.ArtifactType == v {
271+
tmp = append(tmp, desc)
272+
}
273+
}
274+
im.Manifests = tmp
275+
}
276+
}
277+
278+
return &im, nil
279+
}
280+
240281
func (f *fetcher) fetchManifest(ref name.Reference, acceptable []types.MediaType) ([]byte, *v1.Descriptor, error) {
241282
u := f.url("manifests", ref.Identifier())
242283
req, err := http.NewRequest(http.MethodGet, u.String(), nil)

pkg/v1/remote/options.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ type options struct {
4646
pageSize int
4747
retryBackoff Backoff
4848
retryPredicate retry.Predicate
49+
filter map[string]string
4950
}
5051

5152
var defaultPlatform = v1.Platform{
@@ -303,3 +304,14 @@ func WithRetryPredicate(predicate retry.Predicate) Option {
303304
return nil
304305
}
305306
}
307+
308+
// WithFilter sets the filter querystring for HTTP operations.
309+
func WithFilter(key string, value string) Option {
310+
return func(o *options) error {
311+
if o.filter == nil {
312+
o.filter = map[string]string{}
313+
}
314+
o.filter[key] = value
315+
return nil
316+
}
317+
}

pkg/v1/remote/referrers.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright 2023 Google LLC All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package remote
16+
17+
import (
18+
"github.com/google/go-containerregistry/pkg/name"
19+
v1 "github.com/google/go-containerregistry/pkg/v1"
20+
)
21+
22+
// Referrers returns a list of descriptors that refer to the given manifest digest.
23+
//
24+
// The subject manifest doesn't have to exist in the registry for there to be descriptors that refer to it.
25+
func Referrers(d name.Digest, options ...Option) (*v1.IndexManifest, error) {
26+
o, err := makeOptions(d.Context(), options...)
27+
if err != nil {
28+
return nil, err
29+
}
30+
f, err := makeFetcher(d, o)
31+
if err != nil {
32+
return nil, err
33+
}
34+
return f.fetchReferrers(o.context, o.filter, d)
35+
}

0 commit comments

Comments
 (0)