Skip to content

Commit f01516b

Browse files
matthewpiwagoodman
authored andcommitted
image: multi-manifest index support
Signed-off-by: Matthew Penner <me@matthewp.io> Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
1 parent cd49355 commit f01516b

9 files changed

Lines changed: 231 additions & 1 deletion

client.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,39 @@ func GetImageFromSource(ctx context.Context, imgStr string, source image.Source,
9999
return img, nil
100100
}
101101

102+
// GetImageIndexFromSource returns an image index from the explicitly provided source.
103+
func GetImageIndexFromSource(ctx context.Context, imgStr string, source image.Source, options ...Option) (*image.Index, error) {
104+
log.Debugf("image index: source=%+v location=%+v", source, imgStr)
105+
106+
var cfg config
107+
for _, option := range options {
108+
if option == nil {
109+
continue
110+
}
111+
if err := option(&cfg); err != nil {
112+
return nil, fmt.Errorf("unable to parse option: %w", err)
113+
}
114+
}
115+
116+
provider, err := selectImageProvider(imgStr, source, cfg)
117+
if err != nil {
118+
return nil, err
119+
}
120+
121+
var indexProvider image.IndexProvider
122+
var ok bool
123+
if indexProvider, ok = provider.(image.IndexProvider); !ok {
124+
return nil, fmt.Errorf("provider doesn't support image indexes")
125+
}
126+
127+
index, err := indexProvider.ProvideIndex(ctx, cfg.AdditionalMetadata...)
128+
if err != nil {
129+
return nil, fmt.Errorf("unable to use %s source: %w", source, err)
130+
}
131+
132+
return index, nil
133+
}
134+
102135
func selectImageProvider(imgStr string, source image.Source, cfg config) (image.Provider, error) {
103136
var provider image.Provider
104137
tempDirGenerator := rootTempDirGenerator.NewGenerator()
@@ -178,6 +211,16 @@ func GetImage(ctx context.Context, userStr string, options ...Option) (*image.Im
178211
return GetImageFromSource(ctx, imgStr, source, options...)
179212
}
180213

214+
// GetImageIndex parses the user provided image string and provides an index object;
215+
// note: the source where the image should be referenced from is automatically inferred.
216+
func GetImageIndex(ctx context.Context, userStr string, options ...Option) (*image.Index, error) {
217+
source, imgStr, err := image.DetectSource(userStr)
218+
if err != nil {
219+
return nil, err
220+
}
221+
return GetImageIndexFromSource(ctx, imgStr, source, options...)
222+
}
223+
181224
func SetLogger(logger logger.Logger) {
182225
log.Log = logger
183226
}

pkg/image/image_index.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package image
2+
3+
import (
4+
v1 "github.com/google/go-containerregistry/pkg/v1"
5+
"github.com/hashicorp/go-multierror"
6+
)
7+
8+
// Index represents a container image index.
9+
type Index struct {
10+
// index is the raw index manifest and content provider from the GCR lib
11+
index v1.ImageIndex
12+
// images is a list of images associated with an index.
13+
images []*Image
14+
}
15+
16+
// NewIndex provides a new image index object.
17+
func NewIndex(index v1.ImageIndex, images []*Image) *Index {
18+
return &Index{
19+
index: index,
20+
images: images,
21+
}
22+
}
23+
24+
// Images returns a list of images associated with an index.
25+
func (i *Index) Images() []*Image {
26+
return i.images
27+
}
28+
29+
// Cleanup removes all temporary files created from parsing the index and associated images.
30+
// Future calls to image will not function correctly after this call.
31+
func (i *Index) Cleanup() error {
32+
if i == nil {
33+
return nil
34+
}
35+
var errs error
36+
for _, img := range i.images {
37+
if err := img.Cleanup(); err != nil {
38+
errs = multierror.Append(errs, err)
39+
}
40+
}
41+
return errs
42+
}

pkg/image/oci/directory_provider.go

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package oci
33
import (
44
"context"
55
"fmt"
6+
"strconv"
67

78
"github.com/google/go-containerregistry/pkg/v1/layout"
89

@@ -52,7 +53,7 @@ func (p *DirectoryImageProvider) Provide(_ context.Context, userMetadata ...imag
5253
return nil, fmt.Errorf("unable to parse OCI directory as an image: %w", err)
5354
}
5455

55-
var metadata = []image.AdditionalMetadata{
56+
metadata := []image.AdditionalMetadata{
5657
image.WithManifestDigest(manifest.Digest.String()),
5758
}
5859

@@ -72,3 +73,63 @@ func (p *DirectoryImageProvider) Provide(_ context.Context, userMetadata ...imag
7273

7374
return image.New(img, p.tmpDirGen, contentTempDir, metadata...), nil
7475
}
76+
77+
// ProvideIndex provides an image index that represents the OCI image as a directory.
78+
func (p *DirectoryImageProvider) ProvideIndex(_ context.Context, userMetadata ...image.AdditionalMetadata) (*image.Index, error) {
79+
pathObj, err := layout.FromPath(p.path)
80+
if err != nil {
81+
return nil, fmt.Errorf("unable to read image from OCI directory path %q: %w", p.path, err)
82+
}
83+
84+
index, err := layout.ImageIndexFromPath(p.path)
85+
if err != nil {
86+
return nil, fmt.Errorf("unable to parse OCI directory index: %w", err)
87+
}
88+
89+
indexManifest, err := index.IndexManifest()
90+
if err != nil {
91+
return nil, fmt.Errorf("unable to parse OCI directory indexManifest: %w", err)
92+
}
93+
94+
if len(indexManifest.Manifests) < 1 {
95+
return nil, fmt.Errorf("expected at least one OCI directory manifests (found %d)", len(indexManifest.Manifests))
96+
}
97+
98+
images := make([]*image.Image, len(indexManifest.Manifests))
99+
for i, manifest := range indexManifest.Manifests {
100+
img, err := pathObj.Image(manifest.Digest)
101+
if err != nil {
102+
return nil, fmt.Errorf("unable to parse OCI directory as an image: %w", err)
103+
}
104+
105+
metadata := []image.AdditionalMetadata{
106+
image.WithManifestDigest(manifest.Digest.String()),
107+
}
108+
if manifest.Platform != nil {
109+
if manifest.Platform.Architecture != "" {
110+
metadata = append(metadata, image.WithArchitecture(manifest.Platform.Architecture, manifest.Platform.Variant))
111+
}
112+
if manifest.Platform.OS != "" {
113+
metadata = append(metadata, image.WithOS(manifest.Platform.OS))
114+
}
115+
}
116+
117+
// make a best-effort attempt at getting the raw indexManifest
118+
rawManifest, err := img.RawManifest()
119+
if err == nil {
120+
metadata = append(metadata, image.WithManifest(rawManifest))
121+
}
122+
123+
// apply user-supplied metadata last to override any default behavior
124+
metadata = append(metadata, userMetadata...)
125+
126+
contentTempDir, err := p.tmpDirGen.NewDirectory("oci-dir-image-" + strconv.Itoa(i))
127+
if err != nil {
128+
return nil, err
129+
}
130+
131+
images[i] = image.New(img, p.tmpDirGen, contentTempDir, metadata...)
132+
}
133+
134+
return image.NewIndex(index, images), nil
135+
}

pkg/image/oci/directory_provider_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ func Test_Directory_Provide(t *testing.T) {
3232
{"reads invalid oci manifest", "test-fixtures/invalid_file", true},
3333
{"reads valid oci manifest with no images", "test-fixtures/no_manifests", true},
3434
{"reads a fully correct manifest", "test-fixtures/valid_manifest", false},
35+
{"fails to read a fully correct manifest index with more than one manifest", "test-fixtures/valid_manifest_index", true},
3536
}
3637

3738
for _, tc := range tests {
@@ -52,3 +53,36 @@ func Test_Directory_Provide(t *testing.T) {
5253
})
5354
}
5455
}
56+
57+
func Test_Directory_ProvideIndex(t *testing.T) {
58+
//GIVEN
59+
tests := []struct {
60+
name string
61+
path string
62+
expectedErr bool
63+
}{
64+
{"fails to read from path", "", true},
65+
{"reads invalid oci manifest", "test-fixtures/invalid_file", true},
66+
{"reads valid oci manifest with no images", "test-fixtures/no_manifests", true},
67+
{"reads a fully correct manifest", "test-fixtures/valid_manifest", false},
68+
{"reads a fully correct manifest index with more than one manifest", "test-fixtures/valid_manifest_index", false},
69+
}
70+
71+
for _, tc := range tests {
72+
provider := NewProviderFromPath(tc.path, file.NewTempDirGenerator("tempDir"))
73+
t.Run(tc.name, func(t *testing.T) {
74+
//WHEN
75+
image, err := provider.ProvideIndex(nil)
76+
77+
//THEN
78+
if tc.expectedErr {
79+
assert.Error(t, err)
80+
assert.Nil(t, image)
81+
} else {
82+
assert.NoError(t, err)
83+
assert.NotNil(t, image)
84+
}
85+
86+
})
87+
}
88+
}

pkg/image/oci/tarball_provider.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,24 @@ func (p *TarballImageProvider) Provide(ctx context.Context, metadata ...image.Ad
4343

4444
return NewProviderFromPath(tempDir, p.tmpDirGen).Provide(ctx, metadata...)
4545
}
46+
47+
// ProvideIndex provides an image index that represents the OCI image index from a tarball.
48+
func (p *TarballImageProvider) ProvideIndex(ctx context.Context, metadata ...image.AdditionalMetadata) (*image.Index, error) {
49+
// note: we are untaring the image and using the existing directory provider, we could probably enhance the google
50+
// container registry lib to do this without needing to untar to a temp dir (https://github.com/google/go-containerregistry/issues/726)
51+
f, err := os.Open(p.path)
52+
if err != nil {
53+
return nil, fmt.Errorf("unable to open OCI tarball: %w", err)
54+
}
55+
56+
tempDir, err := p.tmpDirGen.NewDirectory("oci-tarball-image")
57+
if err != nil {
58+
return nil, err
59+
}
60+
61+
if err = file.UntarToDirectory(f, tempDir); err != nil {
62+
return nil, err
63+
}
64+
65+
return NewProviderFromPath(tempDir, p.tmpDirGen).ProvideIndex(ctx, metadata...)
66+
}

pkg/image/oci/test-fixtures/valid_manifest_index/blobs/sha256/f67dcc5fc786f04f0743abfe0ee5dae9bd8caf8efa6c8144f7f2a43889dc513b

Whitespace-only changes.

pkg/image/oci/test-fixtures/valid_manifest_index/blobs/sha256/f67dcc5fc786f04f0743abfe0ee5dae9bd8caf8efa6c8144f7f2a43889dc513c

Whitespace-only changes.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"schemaVersion": 2,
3+
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
4+
"manifests": [
5+
{
6+
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
7+
"size": 424,
8+
"digest": "sha256:f67dcc5fc786f04f0743abfe0ee5dae9bd8caf8efa6c8144f7f2a43889dc513b",
9+
"platform": {
10+
"architecture": "arm64",
11+
"os": "linux"
12+
}
13+
},
14+
{
15+
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
16+
"size": 424,
17+
"digest": "sha256:f67dcc5fc786f04f0743abfe0ee5dae9bd8caf8efa6c8144f7f2a43889dc513c",
18+
"platform": {
19+
"architecture": "amd64",
20+
"os": "linux"
21+
}
22+
}
23+
]
24+
}

pkg/image/provider.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,8 @@ import "context"
77
type Provider interface {
88
Provide(context.Context, ...AdditionalMetadata) (*Image, error)
99
}
10+
11+
// IndexProvider is an abstraction for any object that provides image indexes.
12+
type IndexProvider interface {
13+
ProvideIndex(context.Context, ...AdditionalMetadata) (*Index, error)
14+
}

0 commit comments

Comments
 (0)