Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Save a layout image as a tarball #171

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 105 additions & 32 deletions layout/layout.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 != "" {
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
82 changes: 82 additions & 0 deletions layout/layout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")
})
})
})
})
})
}
3 changes: 2 additions & 1 deletion layout/util.go
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
10 changes: 6 additions & 4 deletions layout/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down