From f201427c8a6e85ab373d701c4260e6c1bbb1c207 Mon Sep 17 00:00:00 2001 From: Juan Bustamante Date: Fri, 20 Jan 2023 13:36:56 -0500 Subject: [PATCH] WIP - implementing the method to export the layout image to a tarball Signed-off-by: Juan Bustamante --- layout/layout.go | 137 ++++++++++++++++++++++++++++++++---------- layout/layout_test.go | 82 +++++++++++++++++++++++++ layout/util.go | 3 +- layout/util_test.go | 10 +-- 4 files changed, 195 insertions(+), 37 deletions(-) diff --git a/layout/layout.go b/layout/layout.go index a0a14cad..c4424ae6 100644 --- a/layout/layout.go +++ b/layout/layout.go @@ -9,6 +9,8 @@ import ( "strings" "time" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/tarball" v1 "github.com/google/go-containerregistry/pkg/v1" @@ -23,17 +25,21 @@ var _ imgutil.Image = (*Image)(nil) type Image struct { v1.Image + createdAt time.Time + fileName string // use for exporting to tarball path string prevLayers []v1.Layer - createdAt time.Time + tag name.Reference // use for exporting to tarball } type imageOptions struct { - platform imgutil.Platform baseImage v1.Image baseImagePath string - prevImagePath string createdAt time.Time + platform imgutil.Platform + prevImagePath string + tarFileName string + tarNameRef name.Reference } type ImageOption func(*imageOptions) error @@ -76,6 +82,20 @@ func WithCreatedAt(createdAt time.Time) ImageOption { } } +// WithTarConfig lets a caller set file tarball file name and tag to be used when the image +// is saved with the method imgutil.SaveFile +// If fileName doesn't contain .tar extension it will be added +func WithTarConfig(fileName string, tag name.Reference) ImageOption { + return func(i *imageOptions) error { + if fileName != "" && filepath.Ext(fileName) != ".tar" { + fileName = fmt.Sprintf("%s.tar", fileName) + } + i.tarFileName = fileName + i.tarNameRef = tag + return nil + } +} + // FromBaseImagePath loads an existing image as the config and layers for the new underlyingImage. // Ignored if underlyingImage is not found. func FromBaseImagePath(path string) ImageOption { @@ -104,8 +124,10 @@ func NewImage(path string, ops ...ImageOption) (*Image, error) { } ri := &Image{ - Image: image, - path: path, + Image: image, + path: path, + fileName: imageOpts.tarFileName, + tag: imageOpts.tarNameRef, } if imageOpts.prevImagePath != "" { @@ -523,33 +545,9 @@ func (i *Image) SaveAs(name string, additionalNames ...string) error { log.Printf("multiple additional names %v are ignored when OCI layout is used", additionalNames) } - err := i.mutateCreatedAt(i.Image, v1.Time{Time: i.createdAt}) - if err != nil { - return errors.Wrap(err, "set creation time") - } - - cfg, err := i.Image.ConfigFile() - if err != nil { - return errors.Wrap(err, "get image config") - } - cfg = cfg.DeepCopy() - - layers, err := i.Image.Layers() + err := i.prepareImage() if err != nil { - return errors.Wrap(err, "get image layers") - } - cfg.History = make([]v1.History, len(layers)) - for j := range cfg.History { - cfg.History[j] = v1.History{ - Created: v1.Time{Time: i.createdAt}, - } - } - - cfg.DockerVersion = "" - cfg.Container = "" - err = i.mutateConfigFile(i.Image, cfg) - if err != nil { - return errors.Wrap(err, "zeroing history") + return err } // initialize image path @@ -572,7 +570,38 @@ func (i *Image) SaveAs(name string, additionalNames ...string) error { } func (i *Image) SaveFile() (string, error) { - return "", errors.New("not yet implemented") + err := i.validateTarInputs() + if err != nil { + return "", err + } + + err = i.prepareImage() + if err != nil { + return "", err + } + + fileName := filepath.Join(i.Name(), i.fileName) + + err = os.MkdirAll(i.Name(), os.ModePerm) + if err != nil { + return "", errors.Wrapf(err, "creating destination folder %s", i.Name()) + } + + f, err := os.Create(fileName) + if err != nil { + return "", errors.Wrapf(err, "creating destination file %s", fileName) + } + defer f.Close() + + var diagnostics []imgutil.SaveDiagnostic + if err := tarball.Write(i.tag, i.Image, f); err != nil { + diagnostics = append(diagnostics, imgutil.SaveDiagnostic{ImageName: fileName, Cause: err}) + } + if len(diagnostics) > 0 { + return "", imgutil.SaveError{Errors: diagnostics} + } + + return fileName, nil } func (i *Image) Delete() error { @@ -739,3 +768,47 @@ func (i *Image) mutateImage(base v1.Image) { Image: base, } } + +// prepareImage prepare the internal images representation before saving +func (i *Image) prepareImage() error { + err := i.mutateCreatedAt(i.Image, v1.Time{Time: i.createdAt}) + if err != nil { + return errors.Wrap(err, "set creation time") + } + + cfg, err := i.Image.ConfigFile() + if err != nil { + return errors.Wrap(err, "get image config") + } + cfg = cfg.DeepCopy() + + layers, err := i.Image.Layers() + if err != nil { + return errors.Wrap(err, "get image layers") + } + cfg.History = make([]v1.History, len(layers)) + for j := range cfg.History { + cfg.History[j] = v1.History{ + Created: v1.Time{Time: i.createdAt}, + } + } + + cfg.DockerVersion = "" + cfg.Container = "" + err = i.mutateConfigFile(i.Image, cfg) + if err != nil { + return errors.Wrap(err, "zeroing history") + } + + return nil +} + +func (i *Image) validateTarInputs() error { + if i.fileName == "" { + return errors.New("file name could not be empty when saving image as a tarball") + } + if i.tag == nil { + return errors.New("a tag must be provided when saving image as a tarball") + } + return nil +} diff --git a/layout/layout_test.go b/layout/layout_test.go index 84d18c25..91cbcdea 100644 --- a/layout/layout_test.go +++ b/layout/layout_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/buildpacks/imgutil" @@ -969,4 +971,84 @@ func testImage(t *testing.T, when spec.G, it spec.S) { }) }) }) + + when("#SaveFile", func() { + var tag name.Reference + var fileName string + + it.After(func() { + os.RemoveAll(imagePath) + }) + + when("#FromBaseImage with full image", func() { + it.Before(func() { + imagePath = filepath.Join(tmpDir, "save-from-base-image") + tag, err = name.NewTag("my-app:latest") + fileName = "my-app" + h.AssertNil(t, err) + }) + + when("file name and tag are provided", func() { + when("file name do not have .tar extension", func() { + it.Before(func() { + fileName = "my-app" + }) + + it("saves the image in a tarball", func() { + image, err := layout.NewImage(imagePath, layout.FromBaseImage(testImage), layout.WithTarConfig(fileName, tag)) + h.AssertNil(t, err) + + // save tarball + output, err := image.SaveFile() + h.AssertNil(t, err) + + h.AssertPathExists(t, output) + h.AssertEq(t, filepath.Base(output), "my-app.tar") + }) + }) + + when("file name has .tar extension", func() { + it.Before(func() { + fileName = "my-app.tar" + }) + + it("saves the image in a tarball", func() { + image, err := layout.NewImage(imagePath, layout.FromBaseImage(testImage), layout.WithTarConfig(fileName, tag)) + h.AssertNil(t, err) + + // save tarball + output, err := image.SaveFile() + h.AssertNil(t, err) + + h.AssertPathExists(t, output) + h.AssertEq(t, filepath.Base(output), "my-app.tar") + }) + }) + }) + + when("file name and tag are not provided", func() { + when("file name is not provided", func() { + it("error is thrown", func() { + image, err := layout.NewImage(imagePath, layout.FromBaseImage(testImage), layout.WithTarConfig("", tag)) + h.AssertNil(t, err) + + // save tarball + _, err = image.SaveFile() + h.AssertError(t, err, "file name could not be empty when saving image as a tarball") + }) + }) + + when("tag is not provided", func() { + it("error is thrown", func() { + image, err := layout.NewImage(imagePath, layout.FromBaseImage(testImage), layout.WithTarConfig(fileName, nil)) + h.AssertNil(t, err) + + // save tarball + _, err = image.SaveFile() + h.AssertError(t, err, "a tag must be provided when saving image as a tarball") + }) + }) + }) + }) + }) } diff --git a/layout/util.go b/layout/util.go index 5fcab01c..8af3052f 100644 --- a/layout/util.go +++ b/layout/util.go @@ -1,9 +1,10 @@ package layout import ( - "github.com/google/go-containerregistry/pkg/name" "path/filepath" "strings" + + "github.com/google/go-containerregistry/pkg/name" ) // ParseRefToPath parse the given image reference to local path directory following the rules: diff --git a/layout/util_test.go b/layout/util_test.go index b72060a1..d0ef3b22 100644 --- a/layout/util_test.go +++ b/layout/util_test.go @@ -2,13 +2,15 @@ package layout_test import ( "fmt" - "github.com/buildpacks/imgutil/layout" - h "github.com/buildpacks/imgutil/testhelpers" + "path/filepath" + "testing" + "github.com/google/go-containerregistry/pkg/name" "github.com/sclevine/spec" "github.com/sclevine/spec/report" - "path/filepath" - "testing" + + "github.com/buildpacks/imgutil/layout" + h "github.com/buildpacks/imgutil/testhelpers" ) const (