Skip to content

Commit

Permalink
image: multi-manifest index support
Browse files Browse the repository at this point in the history
Signed-off-by: Matthew Penner <[email protected]>
  • Loading branch information
matthewpi committed Jun 14, 2023
1 parent 5b5049b commit cf4dfa0
Show file tree
Hide file tree
Showing 9 changed files with 231 additions and 1 deletion.
43 changes: 43 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,39 @@ func GetImageFromSource(ctx context.Context, imgStr string, source image.Source,
return img, nil
}

// GetImageIndexFromSource returns an image index from the explicitly provided source.
func GetImageIndexFromSource(ctx context.Context, imgStr string, source image.Source, options ...Option) (*image.Index, error) {
log.Debugf("image index: source=%+v location=%+v", source, imgStr)

var cfg config
for _, option := range options {
if option == nil {
continue
}
if err := option(&cfg); err != nil {
return nil, fmt.Errorf("unable to parse option: %w", err)
}
}

provider, err := selectImageProvider(imgStr, source, cfg)
if err != nil {
return nil, err
}

var indexProvider image.IndexProvider
var ok bool
if indexProvider, ok = provider.(image.IndexProvider); !ok {
return nil, fmt.Errorf("provider doesn't support image indexes")
}

index, err := indexProvider.ProvideIndex(ctx, cfg.AdditionalMetadata...)
if err != nil {
return nil, fmt.Errorf("unable to use %s source: %w", source, err)
}

return index, nil
}

func selectImageProvider(imgStr string, source image.Source, cfg config) (image.Provider, error) {
var provider image.Provider
tempDirGenerator := rootTempDirGenerator.NewGenerator()
Expand Down Expand Up @@ -178,6 +211,16 @@ func GetImage(ctx context.Context, userStr string, options ...Option) (*image.Im
return GetImageFromSource(ctx, imgStr, source, options...)
}

// GetImageIndex parses the user provided image string and provides an index object;
// note: the source where the image should be referenced from is automatically inferred.
func GetImageIndex(ctx context.Context, userStr string, options ...Option) (*image.Index, error) {
source, imgStr, err := image.DetectSource(userStr)
if err != nil {
return nil, err
}
return GetImageIndexFromSource(ctx, imgStr, source, options...)
}

func SetLogger(logger logger.Logger) {
log.Log = logger
}
Expand Down
42 changes: 42 additions & 0 deletions pkg/image/image_index.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package image

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

// Index represents a container image index.
type Index struct {
// index is the raw index manifest and content provider from the GCR lib
index v1.ImageIndex
// images is a list of images associated with an index.
images []*Image
}

// NewIndex provides a new image index object.
func NewIndex(index v1.ImageIndex, images []*Image) *Index {
return &Index{
index: index,
images: images,
}
}

// Images returns a list of images associated with an index.
func (i *Index) Images() []*Image {
return i.images
}

// Cleanup removes all temporary files created from parsing the index and associated images.
// Future calls to image will not function correctly after this call.
func (i *Index) Cleanup() error {
if i == nil {
return nil
}
var errs error
for _, img := range i.images {
if err := img.Cleanup(); err != nil {
errs = multierror.Append(errs, err)
}
}
return errs
}
63 changes: 62 additions & 1 deletion pkg/image/oci/directory_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package oci
import (
"context"
"fmt"
"strconv"

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

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

var metadata = []image.AdditionalMetadata{
metadata := []image.AdditionalMetadata{
image.WithManifestDigest(manifest.Digest.String()),
}

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

return image.New(img, p.tmpDirGen, contentTempDir, metadata...), nil
}

// ProvideIndex provides an image index that represents the OCI image as a directory.
func (p *DirectoryImageProvider) ProvideIndex(_ context.Context, userMetadata ...image.AdditionalMetadata) (*image.Index, error) {
pathObj, err := layout.FromPath(p.path)
if err != nil {
return nil, fmt.Errorf("unable to read image from OCI directory path %q: %w", p.path, err)
}

index, err := layout.ImageIndexFromPath(p.path)
if err != nil {
return nil, fmt.Errorf("unable to parse OCI directory index: %w", err)
}

indexManifest, err := index.IndexManifest()
if err != nil {
return nil, fmt.Errorf("unable to parse OCI directory indexManifest: %w", err)
}

if len(indexManifest.Manifests) < 1 {
return nil, fmt.Errorf("expected at least one OCI directory manifests (found %d)", len(indexManifest.Manifests))
}

images := make([]*image.Image, len(indexManifest.Manifests))
for i, manifest := range indexManifest.Manifests {
img, err := pathObj.Image(manifest.Digest)
if err != nil {
return nil, fmt.Errorf("unable to parse OCI directory as an image: %w", err)
}

metadata := []image.AdditionalMetadata{
image.WithManifestDigest(manifest.Digest.String()),
}
if manifest.Platform != nil {
if manifest.Platform.Architecture != "" {
metadata = append(metadata, image.WithArchitecture(manifest.Platform.Architecture, manifest.Platform.Variant))
}
if manifest.Platform.OS != "" {
metadata = append(metadata, image.WithOS(manifest.Platform.OS))
}
}

// make a best-effort attempt at getting the raw indexManifest
rawManifest, err := img.RawManifest()
if err == nil {
metadata = append(metadata, image.WithManifest(rawManifest))
}

// apply user-supplied metadata last to override any default behavior
metadata = append(metadata, userMetadata...)

contentTempDir, err := p.tmpDirGen.NewDirectory("oci-dir-image-" + strconv.Itoa(i))
if err != nil {
return nil, err
}

images[i] = image.New(img, p.tmpDirGen, contentTempDir, metadata...)
}

return image.NewIndex(index, images), nil
}
34 changes: 34 additions & 0 deletions pkg/image/oci/directory_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func Test_Directory_Provide(t *testing.T) {
{"reads invalid oci manifest", "test-fixtures/invalid_file", true},
{"reads valid oci manifest with no images", "test-fixtures/no_manifests", true},
{"reads a fully correct manifest", "test-fixtures/valid_manifest", false},
{"fails to read a fully correct manifest index with more than one manifest", "test-fixtures/valid_manifest_index", true},
}

for _, tc := range tests {
Expand All @@ -52,3 +53,36 @@ func Test_Directory_Provide(t *testing.T) {
})
}
}

func Test_Directory_ProvideIndex(t *testing.T) {
//GIVEN
tests := []struct {
name string
path string
expectedErr bool
}{
{"fails to read from path", "", true},
{"reads invalid oci manifest", "test-fixtures/invalid_file", true},
{"reads valid oci manifest with no images", "test-fixtures/no_manifests", true},
{"reads a fully correct manifest", "test-fixtures/valid_manifest", false},
{"reads a fully correct manifest index with more than one manifest", "test-fixtures/valid_manifest_index", false},
}

for _, tc := range tests {
provider := NewProviderFromPath(tc.path, file.NewTempDirGenerator("tempDir"))
t.Run(tc.name, func(t *testing.T) {
//WHEN
image, err := provider.ProvideIndex(nil)

//THEN
if tc.expectedErr {
assert.Error(t, err)
assert.Nil(t, image)
} else {
assert.NoError(t, err)
assert.NotNil(t, image)
}

})
}
}
21 changes: 21 additions & 0 deletions pkg/image/oci/tarball_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,24 @@ func (p *TarballImageProvider) Provide(ctx context.Context, metadata ...image.Ad

return NewProviderFromPath(tempDir, p.tmpDirGen).Provide(ctx, metadata...)
}

// ProvideIndex provides an image index that represents the OCI image index from a tarball.
func (p *TarballImageProvider) ProvideIndex(ctx context.Context, metadata ...image.AdditionalMetadata) (*image.Index, error) {
// note: we are untaring the image and using the existing directory provider, we could probably enhance the google
// container registry lib to do this without needing to untar to a temp dir (https://github.com/google/go-containerregistry/issues/726)
f, err := os.Open(p.path)
if err != nil {
return nil, fmt.Errorf("unable to open OCI tarball: %w", err)
}

tempDir, err := p.tmpDirGen.NewDirectory("oci-tarball-image")
if err != nil {
return nil, err
}

if err = file.UntarToDirectory(f, tempDir); err != nil {
return nil, err
}

return NewProviderFromPath(tempDir, p.tmpDirGen).ProvideIndex(ctx, metadata...)
}
24 changes: 24 additions & 0 deletions pkg/image/oci/test-fixtures/valid_manifest_index/index.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
"manifests": [
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 424,
"digest": "sha256:f67dcc5fc786f04f0743abfe0ee5dae9bd8caf8efa6c8144f7f2a43889dc513b",
"platform": {
"architecture": "arm64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 424,
"digest": "sha256:f67dcc5fc786f04f0743abfe0ee5dae9bd8caf8efa6c8144f7f2a43889dc513c",
"platform": {
"architecture": "amd64",
"os": "linux"
}
}
]
}
5 changes: 5 additions & 0 deletions pkg/image/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@ import "context"
type Provider interface {
Provide(context.Context, ...AdditionalMetadata) (*Image, error)
}

// IndexProvider is an abstraction for any object that provides image indexes.
type IndexProvider interface {
ProvideIndex(context.Context, ...AdditionalMetadata) (*Index, error)
}

0 comments on commit cf4dfa0

Please sign in to comment.