diff --git a/cnb_image.go b/cnb_image.go index af8c78a8..880c45b7 100644 --- a/cnb_image.go +++ b/cnb_image.go @@ -17,7 +17,7 @@ import ( // Specific implementations may choose to override certain methods, and will need to supply the methods that are omitted, // such as Identifier() and Found(). // The working image could be any v1.Image, -// but in practice will start off as a pointer to a locallayout.v1ImageFacade (or similar). +// but in practice will start off as a pointer to a local.v1ImageFacade (or similar). type CNBImageCore struct { // required v1.Image // the working image @@ -305,17 +305,22 @@ func (i *CNBImageCore) AddLayerWithDiffID(path, _ string) error { } func (i *CNBImageCore) AddLayerWithDiffIDAndHistory(path, _ string, history v1.History) error { + layer, err := tarball.LayerFromFile(path) + if err != nil { + return err + } + return i.AddLayerWithHistory(layer, history) +} + +func (i *CNBImageCore) AddLayerWithHistory(layer v1.Layer, history v1.History) error { + var err error // ensure existing history - if err := i.MutateConfigFile(func(c *v1.ConfigFile) { + if err = i.MutateConfigFile(func(c *v1.ConfigFile) { c.History = NormalizedHistory(c.History, len(c.RootFS.DiffIDs)) }); err != nil { return err } - layer, err := tarball.LayerFromFile(path) - if err != nil { - return err - } if !i.preserveHistory { history = emptyHistory } diff --git a/image.go b/image.go index 87bd82c3..f856b95c 100644 --- a/image.go +++ b/image.go @@ -25,7 +25,7 @@ type Image interface { History() ([]v1.History, error) Identifier() (Identifier, error) // Kind exposes the type of image that backs the imgutil.Image implementation. - // It could be `local`, `locallayout`, `remote`, or `layout`. + // It could be `local`, `remote`, or `layout`. Kind() string Label(string) (string, error) Labels() (map[string]string, error) diff --git a/layout/new.go b/layout/new.go index 91cc6dde..8f3fdc25 100644 --- a/layout/new.go +++ b/layout/new.go @@ -14,7 +14,7 @@ func NewImage(path string, ops ...ImageOption) (*Image, error) { op(options) } - options.Platform = processDefaultPlatformOption(options.Platform) + options.Platform = processPlatformOption(options.Platform) var err error @@ -58,7 +58,7 @@ func NewImage(path string, ops ...ImageOption) (*Image, error) { }, nil } -func processDefaultPlatformOption(requestedPlatform imgutil.Platform) imgutil.Platform { +func processPlatformOption(requestedPlatform imgutil.Platform) imgutil.Platform { var emptyPlatform imgutil.Platform if requestedPlatform != emptyPlatform { return requestedPlatform diff --git a/layout/options.go b/layout/options.go index 1204943f..6853a3cc 100644 --- a/layout/options.go +++ b/layout/options.go @@ -26,18 +26,18 @@ func FromBaseImagePath(name string) func(*imgutil.ImageOptions) { } } -// WithCreatedAt lets a caller set the "created at" timestamp for the working image when saved. -// If not provided, the default is imgutil.NormalizedDateTime. -func WithCreatedAt(t time.Time) func(*imgutil.ImageOptions) { +// WithConfig lets a caller provided a `config` object for the working image. +func WithConfig(c *v1.Config) func(*imgutil.ImageOptions) { return func(o *imgutil.ImageOptions) { - o.CreatedAt = t + o.Config = c } } -// WithConfig lets a caller provided a `config` object for the working image. -func WithConfig(c *v1.Config) func(*imgutil.ImageOptions) { +// WithCreatedAt lets a caller set the "created at" timestamp for the working image when saved. +// If not provided, the default is imgutil.NormalizedDateTime. +func WithCreatedAt(t time.Time) func(*imgutil.ImageOptions) { return func(o *imgutil.ImageOptions) { - o.Config = c + o.CreatedAt = t } } diff --git a/local/local.go b/local/local.go index 1a5edcec..5c407f0f 100644 --- a/local/local.go +++ b/local/local.go @@ -1,365 +1,200 @@ package local import ( - "context" "crypto/sha256" "encoding/hex" + "errors" "fmt" "io" "os" "path/filepath" "strings" - "sync" - "time" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/image" v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/pkg/errors" "github.com/buildpacks/imgutil" ) +// Image wraps an imgutil.CNBImageCore and implements the methods needed to complete the imgutil.Image interface. type Image struct { - docker DockerClient - repoName string - inspect types.ImageInspect - history []v1.History - layerPaths []string - prevImage *Image // reused layers will be fetched from prevImage - downloadBaseOnce *sync.Once - createdAt time.Time - withHistory bool + *imgutil.CNBImageCore + repoName string + store *Store + lastIdentifier string + daemonOS string } -// DockerClient is subset of client.CommonAPIClient required by this package -type DockerClient interface { - ImageHistory(ctx context.Context, image string) ([]image.HistoryResponseItem, error) - ImageInspectWithRaw(ctx context.Context, image string) (types.ImageInspect, []byte, error) - ImageLoad(ctx context.Context, input io.Reader, quiet bool) (types.ImageLoadResponse, error) - ImageRemove(ctx context.Context, image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) - ImageSave(ctx context.Context, images []string) (io.ReadCloser, error) - ImageTag(ctx context.Context, image, ref string) error - Info(ctx context.Context) (types.Info, error) - ServerVersion(ctx context.Context) (types.Version, error) -} - -// getters - -func (i *Image) Architecture() (string, error) { - return i.inspect.Architecture, nil -} - -func (i *Image) CreatedAt() (time.Time, error) { - createdAtTime := i.inspect.Created - createdTime, err := time.Parse(time.RFC3339Nano, createdAtTime) - - if err != nil { - return time.Time{}, err - } - return createdTime, nil +func (i *Image) Kind() string { + return "local" } -func (i *Image) Entrypoint() ([]string, error) { - return i.inspect.Config.Entrypoint, nil +func (i *Image) Name() string { + return i.repoName } -func (i *Image) Env(key string) (string, error) { - for _, envVar := range i.inspect.Config.Env { - parts := strings.Split(envVar, "=") - if parts[0] == key { - return parts[1], nil - } - } - return "", nil +func (i *Image) Rename(name string) { + i.repoName = name } func (i *Image) Found() bool { - return i.inspect.ID != "" -} - -func (i *Image) Valid() bool { - return i.Found() -} - -func (i *Image) GetAnnotateRefName() (string, error) { - return "", nil -} - -func (i *Image) GetLayer(diffID string) (io.ReadCloser, error) { - for l := range i.inspect.RootFS.Layers { - if i.inspect.RootFS.Layers[l] != diffID { - continue - } - if i.layerPaths[l] == "" { - if err := i.downloadBaseLayersOnce(); err != nil { - return nil, err - } - if i.layerPaths[l] == "" { - return nil, fmt.Errorf("fetching layer %q from daemon", diffID) - } - } - return os.Open(i.layerPaths[l]) - } - - return nil, fmt.Errorf("image %q does not contain layer with diff ID %q", i.repoName, diffID) -} - -func (i *Image) History() ([]v1.History, error) { - return i.history, nil + return i.lastIdentifier != "" } func (i *Image) Identifier() (imgutil.Identifier, error) { return IDIdentifier{ - ImageID: strings.TrimPrefix(i.inspect.ID, "sha256:"), + ImageID: strings.TrimPrefix(i.lastIdentifier, "sha256:"), }, nil } -func (i *Image) Kind() string { - return `local` -} - -func (i *Image) Label(key string) (string, error) { - labels := i.inspect.Config.Labels - return labels[key], nil -} - -func (i *Image) Labels() (map[string]string, error) { - copiedLabels := make(map[string]string) - for i, l := range i.inspect.Config.Labels { - copiedLabels[i] = l +// GetLayer returns an io.ReadCloser with uncompressed layer data. +// The layer will always have data, even if that means downloading ALL the image layers from the daemon. +func (i *Image) GetLayer(diffID string) (io.ReadCloser, error) { + layerHash, err := v1.NewHash(diffID) + if err != nil { + return nil, err + } + layer, err := i.LayerByDiffID(layerHash) + if err == nil { + // this avoids downloading ALL the image layers from the daemon + // if the layer is available locally + // (e.g., it was added using AddLayer). + if size, err := layer.Size(); err != nil && size != -1 { + return layer.Uncompressed() + } } - return copiedLabels, nil -} - -func (i *Image) ManifestSize() (int64, error) { - return 0, nil -} - -func (i *Image) Name() string { - return i.repoName -} - -func (i *Image) OS() (string, error) { - return i.inspect.Os, nil -} - -func (i *Image) OSVersion() (string, error) { - return i.inspect.OsVersion, nil -} - -func (i *Image) TopLayer() (string, error) { - all := i.inspect.RootFS.Layers - - if len(all) == 0 { - return "", fmt.Errorf("image %q has no layers", i.repoName) + configFile, err := i.ConfigFile() + if err != nil { + return nil, err } - - topLayer := all[len(all)-1] - return topLayer, nil -} - -func (i *Image) UnderlyingImage() v1.Image { - return nil -} - -func (i *Image) Variant() (string, error) { - return i.inspect.Variant, nil -} - -func (i *Image) WorkingDir() (string, error) { - return i.inspect.Config.WorkingDir, nil -} - -// setters - -func (i *Image) AnnotateRefName(refName string) error { - return nil -} - -func (i *Image) Rename(name string) { - i.repoName = name -} - -func (i *Image) SetArchitecture(architecture string) error { - i.inspect.Architecture = architecture - return nil -} - -func (i *Image) SetCmd(cmd ...string) error { - i.inspect.Config.Cmd = cmd - return nil -} - -func (i *Image) SetEntrypoint(ep ...string) error { - i.inspect.Config.Entrypoint = ep - return nil + if !contains(configFile.RootFS.DiffIDs, layerHash) { + return nil, fmt.Errorf("image %q does not contain layer with diff ID %q", i.Name(), layerHash.String()) + } + if err = i.ensureLayers(); err != nil { + return nil, err + } + layer, err = i.LayerByDiffID(layerHash) + if err != nil { + return nil, err + } + return layer.Uncompressed() } -func (i *Image) SetEnv(key, val string) error { - ignoreCase := i.inspect.Os == "windows" - for idx, kv := range i.inspect.Config.Env { - parts := strings.SplitN(kv, "=", 2) - foundKey := parts[0] - searchKey := key - if ignoreCase { - foundKey = strings.ToUpper(foundKey) - searchKey = strings.ToUpper(searchKey) - } - if foundKey == searchKey { - i.inspect.Config.Env[idx] = fmt.Sprintf("%s=%s", key, val) - return nil +func contains(diffIDs []v1.Hash, hash v1.Hash) bool { + for _, diffID := range diffIDs { + if diffID.String() == hash.String() { + return true } } - i.inspect.Config.Env = append(i.inspect.Config.Env, fmt.Sprintf("%s=%s", key, val)) - return nil + return false } -func (i *Image) SetHistory(history []v1.History) error { - i.history = history - return nil -} - -func (i *Image) SetLabel(key, val string) error { - if i.inspect.Config.Labels == nil { - i.inspect.Config.Labels = map[string]string{} +func (i *Image) ensureLayers() error { + if err := i.store.downloadLayersFor(i.lastIdentifier); err != nil { + return fmt.Errorf("failed to fetch base layers: %w", err) } - - i.inspect.Config.Labels[key] = val return nil } func (i *Image) SetOS(osVal string) error { - if osVal != i.inspect.Os { - return fmt.Errorf("invalid os: must match the daemon: %q", i.inspect.Os) + if osVal != i.daemonOS { + return errors.New("invalid os: must match the daemon") } - return nil -} - -func (i *Image) SetOSVersion(osVersion string) error { - i.inspect.OsVersion = osVersion - return nil + return i.CNBImageCore.SetOS(osVal) } -func (i *Image) SetVariant(v string) error { - i.inspect.Variant = v - return nil -} +var emptyHistory = v1.History{Created: v1.Time{Time: imgutil.NormalizedDateTime}} -func (i *Image) SetWorkingDir(dir string) error { - i.inspect.Config.WorkingDir = dir - return nil +func (i *Image) AddLayer(path string) error { + diffID, err := calculateChecksum(path) + if err != nil { + return err + } + layer, err := i.addLayerToStore(path, diffID) + if err != nil { + return err + } + return i.AddLayerWithHistory(layer, emptyHistory) } -// modifiers - -func (i *Image) AddLayer(path string) error { +func calculateChecksum(path string) (string, error) { f, err := os.Open(filepath.Clean(path)) if err != nil { - return errors.Wrapf(err, "AddLayer: open layer: %s", path) + return "", fmt.Errorf("failed to open layer at path %s: %w", path, err) } defer f.Close() hasher := sha256.New() if _, err := io.Copy(hasher, f); err != nil { - return errors.Wrapf(err, "AddLayer: calculate checksum: %s", path) + return "", fmt.Errorf("failed to calculate checksum for layer at path %s: %w", path, err) } - diffID := "sha256:" + hex.EncodeToString(hasher.Sum(make([]byte, 0, hasher.Size()))) - return i.AddLayerWithDiffIDAndHistory(path, diffID, v1.History{}) + return "sha256:" + hex.EncodeToString(hasher.Sum(make([]byte, 0, hasher.Size()))), nil } func (i *Image) AddLayerWithDiffID(path, diffID string) error { - return i.AddLayerWithDiffIDAndHistory(path, diffID, v1.History{}) + layer, err := i.addLayerToStore(path, diffID) + if err != nil { + return err + } + return i.AddLayerWithHistory(layer, emptyHistory) } func (i *Image) AddLayerWithDiffIDAndHistory(path, diffID string, history v1.History) error { - i.layerPaths = append(i.layerPaths, path) - i.inspect.RootFS.Layers = append(i.inspect.RootFS.Layers, diffID) - i.history = append(i.history, history) - return nil -} - -func (i *Image) Delete() error { - if !i.Found() { - return nil - } - options := types.ImageRemoveOptions{ - Force: true, - PruneChildren: true, + layer, err := i.addLayerToStore(path, diffID) + if err != nil { + return err } - _, err := i.docker.ImageRemove(context.Background(), i.inspect.ID, options) - return err + return i.AddLayerWithHistory(layer, history) } -func (i *Image) Rebase(baseTopLayer string, newBase imgutil.Image) error { - ctx := context.Background() - - // FIND TOP LAYER - var keepLayersIdx int - for idx, diffID := range i.inspect.RootFS.Layers { - if diffID == baseTopLayer { - keepLayersIdx = idx + 1 - break - } - } - if keepLayersIdx == 0 { - return fmt.Errorf("%q not found in %q during rebase", baseTopLayer, i.repoName) +func (i *Image) addLayerToStore(fromPath, withDiffID string) (v1.Layer, error) { + var ( + layer v1.Layer + err error + ) + diffID, err := v1.NewHash(withDiffID) + if err != nil { + return nil, err } - - // DOWNLOAD IMAGE - if err := i.downloadBaseLayersOnce(); err != nil { - return err + layer = newPopulatedLayer(diffID, fromPath, 1) + if err != nil { + return nil, err } - - // SWITCH BASE LAYERS - newBaseInspect, _, err := i.docker.ImageInspectWithRaw(ctx, newBase.Name()) + fi, err := os.Stat(fromPath) if err != nil { - return errors.Wrapf(err, "read config for new base image %q", newBase) + return nil, err } - i.inspect.ID = newBaseInspect.ID - i.downloadBaseOnce = &sync.Once{} - i.inspect.RootFS.Layers = append(newBaseInspect.RootFS.Layers, i.inspect.RootFS.Layers[keepLayersIdx:]...) - i.layerPaths = append(make([]string, len(newBaseInspect.RootFS.Layers)), i.layerPaths[keepLayersIdx:]...) - return nil + i.store.AddLayer(layer, diffID, fi.Size()) + return layer, nil } -func (i *Image) RemoveLabel(key string) error { - delete(i.inspect.Config.Labels, key) - return nil -} - -func (i *Image) ReuseLayer(diffID string) error { +func (i *Image) Rebase(baseTopLayerDiffID string, withNewBase imgutil.Image) error { if err := i.ensureLayers(); err != nil { return err } - for idx := range i.prevImage.inspect.RootFS.Layers { - if i.prevImage.inspect.RootFS.Layers[idx] == diffID { - return i.AddLayerWithDiffIDAndHistory(i.prevImage.layerPaths[idx], diffID, i.prevImage.history[idx]) - } - } - return fmt.Errorf("SHA %s was not found in %s", diffID, i.prevImage.Name()) + return i.CNBImageCore.Rebase(baseTopLayerDiffID, withNewBase) } -func (i *Image) ReuseLayerWithHistory(diffID string, history v1.History) error { - if err := i.ensureLayers(); err != nil { +func (i *Image) Save(additionalNames ...string) error { + err := i.SetCreatedAtAndHistory() + if err != nil { return err } - for idx := range i.prevImage.inspect.RootFS.Layers { - if i.prevImage.inspect.RootFS.Layers[idx] == diffID { - return i.AddLayerWithDiffIDAndHistory(i.prevImage.layerPaths[idx], diffID, history) - } - } - return fmt.Errorf("SHA %s was not found in %s", diffID, i.prevImage.Name()) + i.lastIdentifier, err = i.store.Save(i, i.Name(), additionalNames...) + return err } -func (i *Image) ensureLayers() error { - if i.prevImage == nil { - return errors.New("failed to reuse layer because no previous image was provided") - } - if !i.prevImage.Found() { - return fmt.Errorf("failed to reuse layer because previous image %q was not found in daemon", i.prevImage.repoName) - } - if err := i.prevImage.downloadBaseLayersOnce(); err != nil { +func (i *Image) SaveAs(name string, additionalNames ...string) error { + err := i.SetCreatedAtAndHistory() + if err != nil { return err } - return nil + i.lastIdentifier, err = i.store.Save(i, name, additionalNames...) + return err +} + +func (i *Image) SaveFile() (string, error) { + return i.store.SaveFile(i, i.Name()) +} + +func (i *Image) Delete() error { + return i.store.Delete(i.lastIdentifier) } diff --git a/local/local_test.go b/local/local_test.go index e8d4905c..dd54dd54 100644 --- a/local/local_test.go +++ b/local/local_test.go @@ -11,7 +11,6 @@ import ( "time" "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" "github.com/google/go-containerregistry/pkg/authn" v1 "github.com/google/go-containerregistry/pkg/v1" @@ -19,14 +18,15 @@ import ( "github.com/sclevine/spec/report" "github.com/buildpacks/imgutil" - "github.com/buildpacks/imgutil/local" + local "github.com/buildpacks/imgutil/local" "github.com/buildpacks/imgutil/remote" h "github.com/buildpacks/imgutil/testhelpers" ) +const someSHA = "sha256:aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f" + var localTestRegistry *h.DockerRegistry -// FIXME: relevant tests in this file should be moved into new_test.go and save_test.go to mirror the implementation func TestLocal(t *testing.T) { localTestRegistry = h.NewDockerRegistry() localTestRegistry.Start(t) @@ -51,13 +51,12 @@ func testImage(t *testing.T, when spec.G, it spec.S) { var err error dockerClient = h.DockerCli(t) - versionInfo, err := dockerClient.ServerVersion(context.TODO()) + daemonInfo, err := dockerClient.ServerVersion(context.TODO()) h.AssertNil(t, err) - daemonOS = versionInfo.Os - daemonArchitecture = versionInfo.Arch + daemonOS = daemonInfo.Os + daemonArchitecture = daemonInfo.Arch runnableBaseImageName = h.RunnableBaseImage(daemonOS) - h.PullIfMissing(t, dockerClient, runnableBaseImageName) }) @@ -81,11 +80,11 @@ func testImage(t *testing.T, when spec.G, it spec.S) { inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), img.Name()) h.AssertNil(t, err) - versionInfo, err := dockerClient.ServerVersion(context.TODO()) + daemonInfo, err := dockerClient.ServerVersion(context.TODO()) h.AssertNil(t, err) - h.AssertEq(t, inspect.Os, versionInfo.Os) - h.AssertEq(t, inspect.Architecture, versionInfo.Arch) + h.AssertEq(t, inspect.Os, daemonInfo.Os) + h.AssertEq(t, inspect.Architecture, daemonInfo.Arch) h.AssertEq(t, inspect.RootFS.Type, "layers") }) }) @@ -396,7 +395,7 @@ func testImage(t *testing.T, when spec.G, it spec.S) { }) when("#WithConfig", func() { - var config = &container.Config{Entrypoint: []string{"some-entrypoint"}} + var config = &v1.Config{Entrypoint: []string{"some-entrypoint"}} it("sets the image config", func() { localImage, err := local.NewImage(newTestImageName(), dockerClient, local.WithConfig(config)) @@ -785,6 +784,14 @@ func testImage(t *testing.T, when spec.G, it spec.S) { }) }) + when("#Kind", func() { + it("returns local", func() { + img, err := local.NewImage(newTestImageName(), dockerClient) + h.AssertNil(t, err) + h.AssertEq(t, img.Kind(), "local") + }) + }) + when("#SetLabel", func() { var ( img imgutil.Image @@ -1576,11 +1583,11 @@ func testImage(t *testing.T, when spec.G, it spec.S) { img, err := local.NewImage(repoName, dockerClient, local.FromBaseImage(repoName)) h.AssertNil(t, err) h.AssertNil(t, err) - _, err = img.GetLayer("not-exist") + _, err = img.GetLayer(someSHA) h.AssertError( t, err, - fmt.Sprintf(`image %q does not contain layer with diff ID "not-exist"`, repoName), + fmt.Sprintf(`image %q does not contain layer with diff ID "%s"`, repoName, someSHA), ) }) }) @@ -1591,9 +1598,9 @@ func testImage(t *testing.T, when spec.G, it spec.S) { image, err := local.NewImage("not-exist", dockerClient) h.AssertNil(t, err) - readCloser, err := image.GetLayer("some-layer") + readCloser, err := image.GetLayer(someSHA) h.AssertNil(t, readCloser) - h.AssertError(t, err, `image "not-exist" does not contain layer with diff ID "some-layer"`) + h.AssertError(t, err, fmt.Sprintf("image %q does not contain layer with diff ID %q", "not-exist", someSHA)) }) }) }) @@ -1674,6 +1681,8 @@ func testImage(t *testing.T, when spec.G, it spec.S) { }) it("does not download the old image if layers are directly above (performance)", func() { + // FIXME: npa: not sure this test validates what is claimed in the `it`; it looks like even the `local` package + // always downloads the previous image layers whenever `ReuseLayer` is called. img, err := local.NewImage( repoName, dockerClient, @@ -2048,6 +2057,7 @@ func testImage(t *testing.T, when spec.G, it spec.S) { _, err = dockerClient.ImageLoad(context.TODO(), f, true) h.AssertNil(t, err) + f.Close() defer h.DockerRmi(dockerClient, img.Name()) inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), img.Name()) @@ -2058,6 +2068,19 @@ func testImage(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, err) rc.Close() } + + f, err = os.Open(path) + h.AssertNil(t, err) + defer f.Close() + tr := tar.NewReader(f) + for { + hdr, err := tr.Next() + if err == io.EOF { + break + } + h.AssertNil(t, err) + h.AssertNotEq(t, strings.Contains(hdr.Name, "blank_"), true) + } } when("no previous image or base image is configured", func() { diff --git a/local/new.go b/local/new.go index 29e79880..437b00da 100644 --- a/local/new.go +++ b/local/new.go @@ -2,237 +2,134 @@ package local import ( "context" - "crypto/sha256" - "encoding/hex" "fmt" - "io" - "os" - "sync" - "time" "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/image" "github.com/docker/docker/client" v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/pkg/errors" "github.com/buildpacks/imgutil" - "github.com/buildpacks/imgutil/layer" ) -// NewImage returns a new Image that can be modified and saved to a registry. -func NewImage(repoName string, dockerClient DockerClient, ops ...ImageOption) (*Image, error) { - imageOpts := &options{} +// NewImage returns a new image that can be modified and saved to a docker daemon +// via a tarball in legacy format. +func NewImage(repoName string, dockerClient DockerClient, ops ...func(*imgutil.ImageOptions)) (*Image, error) { + options := &imgutil.ImageOptions{} for _, op := range ops { - if err := op(imageOpts); err != nil { - return nil, err - } + op(options) } - platform, err := defaultPlatform(dockerClient) + var err error + options.Platform, err = processPlatformOption(options.Platform, dockerClient) if err != nil { return nil, err } - if (imageOpts.platform != imgutil.Platform{}) { - if err := validatePlatformOption(platform, imageOpts.platform); err != nil { - return nil, err - } - platform = imageOpts.platform - } - - inspect := defaultInspect(platform) - - image := &Image{ - docker: dockerClient, - repoName: repoName, - inspect: inspect, - history: make([]v1.History, len(inspect.RootFS.Layers)), - layerPaths: make([]string, len(inspect.RootFS.Layers)), - downloadBaseOnce: &sync.Once{}, - withHistory: imageOpts.withHistory, + previousImage, err := processImageOption(options.PreviousImageRepoName, dockerClient, true) + if err != nil { + return nil, err } - - if imageOpts.prevImageRepoName != "" { - if err := processPreviousImageOption(image, imageOpts.prevImageRepoName, platform, dockerClient); err != nil { - return nil, err - } + if previousImage.image != nil { + options.PreviousImage = previousImage.image } - if imageOpts.baseImageRepoName != "" { - if err := processBaseImageOption(image, imageOpts.baseImageRepoName, platform, dockerClient); err != nil { - return nil, err - } - } - - if image.inspect.Os == "windows" { - if err := prepareNewWindowsImage(image); err != nil { - return nil, err - } + var ( + baseIdentifier string + store *Store + ) + baseImage, err := processImageOption(options.BaseImageRepoName, dockerClient, false) + if err != nil { + return nil, err } - - if imageOpts.createdAt.IsZero() { - image.createdAt = imgutil.NormalizedDateTime + if baseImage.image != nil { + options.BaseImage = baseImage.image + baseIdentifier = baseImage.identifier + store = baseImage.layerStore } else { - image.createdAt = imageOpts.createdAt + store = NewStore(dockerClient) } - if imageOpts.config != nil { - image.inspect.Config = imageOpts.config + cnbImage, err := imgutil.NewCNBImage(*options) + if err != nil { + return nil, err } - return image, nil + return &Image{ + CNBImageCore: cnbImage, + repoName: repoName, + store: store, + lastIdentifier: baseIdentifier, + daemonOS: options.Platform.OS, + }, nil } func defaultPlatform(dockerClient DockerClient) (imgutil.Platform, error) { - versionInfo, err := dockerClient.ServerVersion(context.Background()) + daemonInfo, err := dockerClient.ServerVersion(context.Background()) if err != nil { return imgutil.Platform{}, err } - return imgutil.Platform{ - OS: versionInfo.Os, - Architecture: versionInfo.Arch, + OS: daemonInfo.Os, + Architecture: daemonInfo.Arch, }, nil } -func validatePlatformOption(defaultPlatform imgutil.Platform, optionPlatform imgutil.Platform) error { - if optionPlatform.OS != "" && optionPlatform.OS != defaultPlatform.OS { - return fmt.Errorf("invalid os: platform os %q must match the daemon os %q", optionPlatform.OS, defaultPlatform.OS) - } - - return nil -} - -func defaultInspect(platform imgutil.Platform) types.ImageInspect { - return types.ImageInspect{ - Os: platform.OS, - Architecture: platform.Architecture, - OsVersion: platform.OSVersion, - Config: &container.Config{}, - } -} - -func processPreviousImageOption(image *Image, prevImageRepoName string, platform imgutil.Platform, dockerClient DockerClient) error { - inspect, err := inspectOptionalImage(dockerClient, prevImageRepoName, platform) +func processPlatformOption(requestedPlatform imgutil.Platform, dockerClient DockerClient) (imgutil.Platform, error) { + dockerPlatform, err := defaultPlatform(dockerClient) if err != nil { - return err + return imgutil.Platform{}, err } - - history, err := historyOptionalImage(dockerClient, prevImageRepoName) - if err != nil { - return err + if (requestedPlatform == imgutil.Platform{}) { + return dockerPlatform, nil } - - v1History := toV1History(history) - if len(history) != len(inspect.RootFS.Layers) { - v1History = make([]v1.History, len(inspect.RootFS.Layers)) + if requestedPlatform.OS != "" && requestedPlatform.OS != dockerPlatform.OS { + return imgutil.Platform{}, + fmt.Errorf("invalid os: platform os %q must match the daemon os %q", requestedPlatform.OS, dockerPlatform.OS) } - - prevImage, err := NewImage(prevImageRepoName, dockerClient, FromBaseImage(prevImageRepoName)) - if err != nil { - return errors.Wrapf(err, "getting previous image %q", prevImageRepoName) - } - - image.prevImage = prevImage - image.prevImage.history = v1History - - return nil + return requestedPlatform, nil } -func inspectOptionalImage(docker DockerClient, imageName string, platform imgutil.Platform) (types.ImageInspect, error) { - var ( - err error - inspect types.ImageInspect - ) - if inspect, _, err = docker.ImageInspectWithRaw(context.Background(), imageName); err != nil { - if client.IsErrNotFound(err) { - return defaultInspect(platform), nil - } - - return types.ImageInspect{}, errors.Wrapf(err, "verifying image %q", imageName) - } - return inspect, nil +type imageResult struct { + image v1.Image + identifier string + layerStore *Store } -func historyOptionalImage(docker DockerClient, imageName string) ([]image.HistoryResponseItem, error) { - var ( - history []image.HistoryResponseItem - err error - ) - if history, err = docker.ImageHistory(context.Background(), imageName); err != nil { - if client.IsErrNotFound(err) { - return nil, nil - } - return nil, fmt.Errorf("getting history for image: %w", err) +func processImageOption(repoName string, dockerClient DockerClient, downloadLayersOnAccess bool) (imageResult, error) { + if repoName == "" { + return imageResult{}, nil } - return history, nil -} - -func processBaseImageOption(image *Image, baseImageRepoName string, platform imgutil.Platform, dockerClient DockerClient) error { - inspect, err := inspectOptionalImage(dockerClient, baseImageRepoName, platform) + inspect, history, err := getInspectAndHistory(repoName, dockerClient) if err != nil { - return err + return imageResult{}, err } - - history, err := historyOptionalImage(dockerClient, baseImageRepoName) - if err != nil { - return err + if inspect == nil { + return imageResult{}, nil } - - v1History := imgutil.NormalizedHistory(toV1History(history), len(inspect.RootFS.Layers)) - - image.inspect = inspect - image.history = v1History - image.layerPaths = make([]string, len(image.inspect.RootFS.Layers)) - - return nil -} - -func toV1History(history []image.HistoryResponseItem) []v1.History { - v1History := make([]v1.History, len(history)) - for offset, h := range history { - // the daemon reports history in reverse order, so build up the array backwards - v1History[len(v1History)-offset-1] = v1.History{ - Created: v1.Time{Time: time.Unix(h.Created, 0)}, - CreatedBy: h.CreatedBy, - Comment: h.Comment, - } + layerStore := NewStore(dockerClient) + image, err := newV1ImageFacadeFromInspect(*inspect, history, layerStore, downloadLayersOnAccess) + if err != nil { + return imageResult{}, err } - return v1History + return imageResult{ + image: image, + identifier: inspect.ID, + layerStore: layerStore, + }, nil } -func prepareNewWindowsImage(image *Image) error { - // only append base layer to empty image - if len(image.inspect.RootFS.Layers) > 0 { - return nil - } - - layerReader, err := layer.WindowsBaseLayer() +func getInspectAndHistory(repoName string, dockerClient DockerClient) (*types.ImageInspect, []image.HistoryResponseItem, error) { + inspect, _, err := dockerClient.ImageInspectWithRaw(context.Background(), repoName) if err != nil { - return err + if client.IsErrNotFound(err) { + return nil, nil, nil + } + return nil, nil, fmt.Errorf("inspecting image %q: %w", repoName, err) } - - layerFile, err := os.CreateTemp("", "imgutil.local.image.windowsbaselayer") + history, err := dockerClient.ImageHistory(context.Background(), repoName) if err != nil { - return errors.Wrap(err, "creating temp file") - } - defer layerFile.Close() - - hasher := sha256.New() - - multiWriter := io.MultiWriter(layerFile, hasher) - - if _, err := io.Copy(multiWriter, layerReader); err != nil { - return errors.Wrap(err, "copying base layer") + return nil, nil, fmt.Errorf("get history for image %q: %w", repoName, err) } - - diffID := "sha256:" + hex.EncodeToString(hasher.Sum(nil)) - - if err := image.AddLayerWithDiffIDAndHistory(layerFile.Name(), diffID, v1.History{}); err != nil { - return errors.Wrap(err, "adding base layer to image") - } - - return nil + return &inspect, history, nil } diff --git a/local/options.go b/local/options.go index c8563d45..5dc52f14 100644 --- a/local/options.go +++ b/local/options.go @@ -3,71 +3,63 @@ package local import ( "time" - "github.com/docker/docker/api/types/container" + v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/buildpacks/imgutil" ) -type ImageOption func(*options) error - -type options struct { - platform imgutil.Platform - baseImageRepoName string - prevImageRepoName string - withHistory bool - createdAt time.Time - config *container.Config -} - -// FromBaseImage loads an existing image as the config and layers for the new image. -// Ignored if image is not found. -func FromBaseImage(imageName string) ImageOption { - return func(i *options) error { - i.baseImageRepoName = imageName - return nil +// FromBaseImage loads the provided image as the manifest, config, and layers for the working image. +// If the image is not found, it does nothing. +func FromBaseImage(name string) func(*imgutil.ImageOptions) { + return func(o *imgutil.ImageOptions) { + o.BaseImageRepoName = name } } -// WithCreatedAt lets a caller set the created at timestamp for the image. -// Defaults for a new image is imgutil.NormalizedDateTime -func WithCreatedAt(createdAt time.Time) ImageOption { - return func(opts *options) error { - opts.createdAt = createdAt - return nil +// WithConfig lets a caller provided a `config` object for the working image. +func WithConfig(c *v1.Config) func(*imgutil.ImageOptions) { + return func(o *imgutil.ImageOptions) { + o.Config = c } } -func WithConfig(config *container.Config) ImageOption { - return func(opts *options) error { - opts.config = config - return nil +// WithCreatedAt lets a caller set the "created at" timestamp for the working image when saved. +// If not provided, the default is imgutil.NormalizedDateTime. +func WithCreatedAt(t time.Time) func(*imgutil.ImageOptions) { + return func(o *imgutil.ImageOptions) { + o.CreatedAt = t } } -// WithDefaultPlatform provides Architecture/OS/OSVersion defaults for the new image. -// Defaults for a new image are ignored when FromBaseImage returns an image. -func WithDefaultPlatform(platform imgutil.Platform) ImageOption { - return func(i *options) error { - i.platform = platform - return nil +// WithDefaultPlatform provides the default Architecture/OS/OSVersion if no base image is provided, +// or if the provided image inputs (base and previous) are manifest lists. +func WithDefaultPlatform(p imgutil.Platform) func(*imgutil.ImageOptions) { + return func(o *imgutil.ImageOptions) { + o.Platform = p } } // WithHistory if provided will configure the image to preserve history when saved // (including any history from the base image if valid). -func WithHistory() ImageOption { - return func(opts *options) error { - opts.withHistory = true - return nil +func WithHistory() func(*imgutil.ImageOptions) { + return func(o *imgutil.ImageOptions) { + o.PreserveHistory = true + } +} + +// WithMediaTypes lets a caller set the desired media types for the manifest and config (including layers referenced in the manifest) +// to be either OCI media types or Docker media types. +func WithMediaTypes(m imgutil.MediaTypes) func(*imgutil.ImageOptions) { + return func(o *imgutil.ImageOptions) { + o.MediaTypes = m } } -// WithPreviousImage loads an existing image as a source for reusable layers. +// WithPreviousImage loads an existing image as the source for reusable layers. // Use with ReuseLayer(). -// Ignored if image is not found. -func WithPreviousImage(imageName string) ImageOption { - return func(i *options) error { - i.prevImageRepoName = imageName - return nil +// If the image is not found, it does nothing. +func WithPreviousImage(name string) func(*imgutil.ImageOptions) { + return func(o *imgutil.ImageOptions) { + o.PreviousImageRepoName = name } } diff --git a/local/save.go b/local/save.go deleted file mode 100644 index 0c6aca52..00000000 --- a/local/save.go +++ /dev/null @@ -1,298 +0,0 @@ -package local - -import ( - "archive/tar" - "context" - "crypto/sha256" - "encoding/json" - "fmt" - "io" - "os" - "path/filepath" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/client" - "github.com/docker/docker/pkg/jsonmessage" - registryName "github.com/google/go-containerregistry/pkg/name" - "github.com/pkg/errors" - - "github.com/buildpacks/imgutil" -) - -func (i *Image) Save(additionalNames ...string) error { - return i.SaveAs(i.Name(), additionalNames...) -} - -func (i *Image) SaveAs(name string, additionalNames ...string) error { - var ( - inspect types.ImageInspect - err error - ) - canOmitBaseLayers := !usesContainerdStorage(i.docker) - if canOmitBaseLayers { - // During the first save attempt some layers may be excluded. - // The docker daemon allows this if the given set of layers already exists in the daemon in the given order. - inspect, err = i.doSaveAs(name) - } - if !canOmitBaseLayers || err != nil { - // populate all layer paths and try again without the above performance optimization. - if err := i.downloadBaseLayersOnce(); err != nil { - return err - } - - inspect, err = i.doSaveAs(name) - if err != nil { - saveErr := imgutil.SaveError{} - for _, n := range append([]string{name}, additionalNames...) { - saveErr.Errors = append(saveErr.Errors, imgutil.SaveDiagnostic{ImageName: n, Cause: err}) - } - return saveErr - } - } - i.inspect = inspect - - var errs []imgutil.SaveDiagnostic - for _, n := range append([]string{name}, additionalNames...) { - if err := i.docker.ImageTag(context.Background(), i.inspect.ID, n); err != nil { - errs = append(errs, imgutil.SaveDiagnostic{ImageName: n, Cause: err}) - } - } - - if len(errs) > 0 { - return imgutil.SaveError{Errors: errs} - } - - return nil -} - -func usesContainerdStorage(docker DockerClient) bool { - info, err := docker.Info(context.Background()) - if err != nil { - return false - } - - for _, driverStatus := range info.DriverStatus { - if driverStatus[0] == "driver-type" && driverStatus[1] == "io.containerd.snapshotter.v1" { - return true - } - } - - return false -} - -func (i *Image) doSaveAs(name string) (types.ImageInspect, error) { - ctx := context.Background() - done := make(chan error) - - t, err := registryName.NewTag(name, registryName.WeakValidation) - if err != nil { - return types.ImageInspect{}, err - } - - // returns valid 'name:tag' appending 'latest', if missing tag - repoName := t.Name() - - pr, pw := io.Pipe() - defer pw.Close() - go func() { - res, err := i.docker.ImageLoad(ctx, pr, true) - if err != nil { - done <- err - return - } - - // only return response error after response is drained and closed - responseErr := checkResponseError(res.Body) - drainCloseErr := ensureReaderClosed(res.Body) - if responseErr != nil { - done <- responseErr - return - } - if drainCloseErr != nil { - done <- drainCloseErr - } - - done <- nil - }() - - tw := tar.NewWriter(pw) - _, err = i.addImageToTar(tw, repoName) - if err != nil { - return types.ImageInspect{}, err - } - defer tw.Close() - - tw.Close() - pw.Close() - err = <-done - if err != nil { - return types.ImageInspect{}, errors.Wrapf(err, "loading image %q. first error", i.repoName) - } - - inspect, _, err := i.docker.ImageInspectWithRaw(context.Background(), i.repoName) - if err != nil { - if client.IsErrNotFound(err) { - return types.ImageInspect{}, errors.Wrapf(err, "saving image %q", i.repoName) - } - return types.ImageInspect{}, err - } - - return inspect, nil -} - -// downloadBaseLayersOnce exports the base image from the daemon and populates layerPaths the first time it is called. -// subsequent calls do nothing. -func (i *Image) downloadBaseLayersOnce() error { - var err error - if !i.Found() { - return nil - } - i.downloadBaseOnce.Do(func() { - err = i.downloadBaseLayers() - }) - if err != nil { - return errors.Wrap(err, "fetching base layers") - } - return err -} - -func (i *Image) downloadBaseLayers() error { - ctx := context.Background() - - imageReader, err := i.docker.ImageSave(ctx, []string{i.inspect.ID}) - if err != nil { - return errors.Wrapf(err, "saving base image with ID %q from the docker daemon", i.inspect.ID) - } - defer ensureReaderClosed(imageReader) - - tmpDir, err := os.MkdirTemp("", "imgutil.local.image.") - if err != nil { - return errors.Wrap(err, "failed to create temp dir") - } - - err = untar(imageReader, tmpDir) - if err != nil { - return err - } - - mf, err := os.Open(filepath.Clean(filepath.Join(tmpDir, "manifest.json"))) - if err != nil { - return err - } - defer mf.Close() - - var manifest []struct { - Config string - Layers []string - } - if err := json.NewDecoder(mf).Decode(&manifest); err != nil { - return err - } - - if len(manifest) != 1 { - return fmt.Errorf("manifest.json had unexpected number of entries: %d", len(manifest)) - } - - df, err := os.Open(filepath.Clean(filepath.Join(tmpDir, manifest[0].Config))) - if err != nil { - return err - } - defer df.Close() - - var details struct { - RootFS struct { - DiffIDs []string `json:"diff_ids"` - } `json:"rootfs"` - } - - if err = json.NewDecoder(df).Decode(&details); err != nil { - return err - } - - for l := range details.RootFS.DiffIDs { - i.layerPaths[l] = filepath.Join(tmpDir, manifest[0].Layers[l]) - } - - for l := range i.layerPaths { - if i.layerPaths[l] == "" { - return errors.New("failed to download all base layers from daemon") - } - } - - return nil -} - -func (i *Image) addImageToTar(tw *tar.Writer, repoName string) (string, error) { - configFile, err := i.newConfigFile() - if err != nil { - return "", errors.Wrap(err, "generating config file") - } - - configHash := fmt.Sprintf("%x", sha256.Sum256(configFile)) - if err := addTextToTar(tw, configHash+".json", configFile); err != nil { - return "", err - } - - var blankIdx int - var layerPaths []string - for _, path := range i.layerPaths { - if path == "" { - layerName := fmt.Sprintf("blank_%d", blankIdx) - blankIdx++ - hdr := &tar.Header{Name: layerName, Mode: 0644, Size: 0} - if err := tw.WriteHeader(hdr); err != nil { - return "", err - } - layerPaths = append(layerPaths, layerName) - } else { - layerName := fmt.Sprintf("/%x.tar", sha256.Sum256([]byte(path))) - f, err := os.Open(filepath.Clean(path)) - if err != nil { - return "", err - } - defer f.Close() - if err := addFileToTar(tw, layerName, f); err != nil { - return "", err - } - f.Close() - layerPaths = append(layerPaths, layerName) - } - } - - manifest, err := json.Marshal([]map[string]interface{}{ - { - "Config": configHash + ".json", - "RepoTags": []string{repoName}, - "Layers": layerPaths, - }, - }) - if err != nil { - return "", err - } - - return configHash, addTextToTar(tw, "manifest.json", manifest) -} - -// helpers - -func checkResponseError(r io.Reader) error { - decoder := json.NewDecoder(r) - var jsonMessage jsonmessage.JSONMessage - if err := decoder.Decode(&jsonMessage); err != nil { - return errors.Wrapf(err, "parsing daemon response") - } - - if jsonMessage.Error != nil { - return errors.Wrap(jsonMessage.Error, "embedded daemon response") - } - return nil -} - -// ensureReaderClosed drains and closes and reader, returning the first error -func ensureReaderClosed(r io.ReadCloser) error { - _, err := io.Copy(io.Discard, r) - if closeErr := r.Close(); closeErr != nil && err == nil { - err = closeErr - } - return err -} diff --git a/local/save_file.go b/local/save_file.go deleted file mode 100644 index 1a238d1b..00000000 --- a/local/save_file.go +++ /dev/null @@ -1,249 +0,0 @@ -package local - -import ( - "archive/tar" - "context" - "encoding/json" - "fmt" - "io" - "os" - "path/filepath" - "strings" - "time" - - "github.com/docker/docker/api/types" - registryName "github.com/google/go-containerregistry/pkg/name" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/pkg/errors" - "golang.org/x/sync/errgroup" -) - -func (i *Image) SaveFile() (string, error) { - f, err := os.CreateTemp("", "imgutil.local.image.export.*.tar") - if err != nil { - return "", errors.Wrap(err, "failed to create temporary file") - } - defer func() { - f.Close() - if err != nil { - os.Remove(f.Name()) - } - }() - - // All layers need to be present here. Missing layers are either due to utilization of - // (1) WithPreviousImage(), or (2) FromBaseImage(). - // The former is only relevant if ReuseLayers() has been called which takes care of resolving them. - // The latter case needs to be handled explicitly. - if err := i.downloadBaseLayersOnce(); err != nil { - return "", errors.Wrap(err, "failed to fetch base layers") - } - - errs, _ := errgroup.WithContext(context.Background()) - pr, pw := io.Pipe() - - // File writer - errs.Go(func() error { - defer pr.Close() - _, err = f.ReadFrom(pr) - return err - }) - - // Tar producer - errs.Go(func() error { - defer pw.Close() - - tw := tar.NewWriter(pw) - defer tw.Close() - - t, err := registryName.NewTag(i.repoName, registryName.WeakValidation) - if err != nil { - return errors.Wrap(err, "failed to create tag") - } - - // returns valid 'name:tag' appending 'latest', if missing tag - repoName := t.Name() - - _, err = i.addImageToTar(tw, repoName) - return err - }) - - err = errs.Wait() - if err != nil { - return "", err - } - - return f.Name(), nil -} - -func (i *Image) newConfigFile() ([]byte, error) { - if !i.withHistory { - // zero history - i.history = make([]v1.History, len(i.inspect.RootFS.Layers)) - } - cfg, err := v1Config(i.inspect, i.createdAt, i.history) - if err != nil { - return nil, err - } - return json.Marshal(cfg) -} - -// helpers - -func addFileToTar(tw *tar.Writer, name string, contents *os.File) error { - fi, err := contents.Stat() - if err != nil { - return err - } - hdr := &tar.Header{Name: name, Mode: 0644, Size: fi.Size()} - if err := tw.WriteHeader(hdr); err != nil { - return err - } - _, err = io.Copy(tw, contents) - return err -} - -func addTextToTar(tw *tar.Writer, name string, contents []byte) error { - hdr := &tar.Header{Name: name, Mode: 0644, Size: int64(len(contents))} - if err := tw.WriteHeader(hdr); err != nil { - return err - } - _, err := tw.Write(contents) - return err -} - -func cleanPath(dest, header string) (string, error) { - joined := filepath.Join(dest, header) - if strings.HasPrefix(joined, filepath.Clean(dest)) { - return joined, nil - } - return "", fmt.Errorf("bad filepath: %s", header) -} - -func untar(r io.Reader, dest string) error { - tr := tar.NewReader(r) - for { - hdr, err := tr.Next() - if err == io.EOF { - // end of tar archive - return nil - } - if err != nil { - return err - } - - path, err := cleanPath(dest, hdr.Name) - if err != nil { - return err - } - - switch hdr.Typeflag { - case tar.TypeDir: - if err := os.MkdirAll(path, hdr.FileInfo().Mode()); err != nil { - return err - } - case tar.TypeReg: - _, err := os.Stat(filepath.Dir(path)) - if os.IsNotExist(err) { - if err := os.MkdirAll(filepath.Dir(path), 0750); err != nil { - return err - } - } - - fh, err := os.OpenFile(filepath.Clean(path), os.O_CREATE|os.O_WRONLY, hdr.FileInfo().Mode()) - if err != nil { - return err - } - if _, err := io.Copy(fh, tr); err != nil { - fh.Close() - return err - } // #nosec G110 - fh.Close() - case tar.TypeSymlink: - _, err := os.Stat(filepath.Dir(path)) - if os.IsNotExist(err) { - if err := os.MkdirAll(filepath.Dir(path), 0750); err != nil { - return err - } - } - - if err := os.Symlink(hdr.Linkname, path); err != nil { - return err - } - default: - return fmt.Errorf("unknown file type in tar %d", hdr.Typeflag) - } - } -} - -func v1Config(inspect types.ImageInspect, createdAt time.Time, history []v1.History) (v1.ConfigFile, error) { - if len(history) != len(inspect.RootFS.Layers) { - history = make([]v1.History, len(inspect.RootFS.Layers)) - } - for i := range history { - // zero history - history[i].Created = v1.Time{Time: createdAt} - } - diffIDs := make([]v1.Hash, len(inspect.RootFS.Layers)) - for i, layer := range inspect.RootFS.Layers { - hash, err := v1.NewHash(layer) - if err != nil { - return v1.ConfigFile{}, err - } - diffIDs[i] = hash - } - exposedPorts := make(map[string]struct{}, len(inspect.Config.ExposedPorts)) - for key, val := range inspect.Config.ExposedPorts { - exposedPorts[string(key)] = val - } - var config v1.Config - if inspect.Config != nil { - var healthcheck *v1.HealthConfig - if inspect.Config.Healthcheck != nil { - healthcheck = &v1.HealthConfig{ - Test: inspect.Config.Healthcheck.Test, - Interval: inspect.Config.Healthcheck.Interval, - Timeout: inspect.Config.Healthcheck.Timeout, - StartPeriod: inspect.Config.Healthcheck.StartPeriod, - Retries: inspect.Config.Healthcheck.Retries, - } - } - config = v1.Config{ - AttachStderr: inspect.Config.AttachStderr, - AttachStdin: inspect.Config.AttachStdin, - AttachStdout: inspect.Config.AttachStdout, - Cmd: inspect.Config.Cmd, - Healthcheck: healthcheck, - Domainname: inspect.Config.Domainname, - Entrypoint: inspect.Config.Entrypoint, - Env: inspect.Config.Env, - Hostname: inspect.Config.Hostname, - Image: inspect.Config.Image, - Labels: inspect.Config.Labels, - OnBuild: inspect.Config.OnBuild, - OpenStdin: inspect.Config.OpenStdin, - StdinOnce: inspect.Config.StdinOnce, - Tty: inspect.Config.Tty, - User: inspect.Config.User, - Volumes: inspect.Config.Volumes, - WorkingDir: inspect.Config.WorkingDir, - ExposedPorts: exposedPorts, - ArgsEscaped: inspect.Config.ArgsEscaped, - NetworkDisabled: inspect.Config.NetworkDisabled, - MacAddress: inspect.Config.MacAddress, - StopSignal: inspect.Config.StopSignal, - Shell: inspect.Config.Shell, - } - } - return v1.ConfigFile{ - Architecture: inspect.Architecture, - Created: v1.Time{Time: createdAt}, - History: history, - OS: inspect.Os, - OSVersion: inspect.OsVersion, - RootFS: v1.RootFS{ - Type: "layers", - DiffIDs: diffIDs, - }, - Config: config, - }, nil -} diff --git a/locallayout/store.go b/local/store.go similarity index 82% rename from locallayout/store.go rename to local/store.go index 08e98ed0..934988b3 100644 --- a/locallayout/store.go +++ b/local/store.go @@ -1,4 +1,4 @@ -package locallayout +package local import ( "archive/tar" @@ -18,7 +18,6 @@ import ( "github.com/docker/docker/pkg/jsonmessage" registryName "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/tarball" "golang.org/x/sync/errgroup" "github.com/buildpacks/imgutil" @@ -31,8 +30,8 @@ type Store struct { // required dockerClient DockerClient // optional - downloadOnce *sync.Once - onDiskLayers []v1.Layer + downloadOnce *sync.Once + onDiskLayersByDiffID map[v1.Hash]annotatedLayer } // DockerClient is subset of client.CommonAPIClient required by this package. @@ -47,6 +46,19 @@ type DockerClient interface { ServerVersion(ctx context.Context) (types.Version, error) } +type annotatedLayer struct { + layer v1.Layer + uncompressedSize int64 +} + +func NewStore(dockerClient DockerClient) *Store { + return &Store{ + dockerClient: dockerClient, + downloadOnce: &sync.Once{}, + onDiskLayersByDiffID: make(map[v1.Hash]annotatedLayer), + } +} + // images func (s *Store) Contains(identifier string) bool { @@ -68,10 +80,19 @@ func (s *Store) Delete(identifier string) error { func (s *Store) Save(image *Image, withName string, withAdditionalNames ...string) (string, error) { withName = tryNormalizing(withName) + var ( + inspect types.ImageInspect + err error + ) // save - inspect, err := s.doSave(image, withName) - if err != nil { + canOmitBaseLayers := !usesContainerdStorage(s.dockerClient) + if canOmitBaseLayers { + // During the first save attempt some layers may be excluded. + // The docker daemon allows this if the given set of layers already exists in the daemon in the given order. + inspect, err = s.doSave(image, withName) + } + if !canOmitBaseLayers || err != nil { if err = image.ensureLayers(); err != nil { return "", err } @@ -108,6 +129,21 @@ func tryNormalizing(name string) string { return t.Name() // returns valid 'name:tag' appending 'latest', if missing tag } +func usesContainerdStorage(docker DockerClient) bool { + info, err := docker.Info(context.Background()) + if err != nil { + return false + } + + for _, driverStatus := range info.DriverStatus { + if driverStatus[0] == "driver-type" && driverStatus[1] == "io.containerd.snapshotter.v1" { + return true + } + } + + return false +} + func (s *Store) doSave(image v1.Image, withName string) (types.ImageInspect, error) { ctx := context.Background() done := make(chan error) @@ -220,7 +256,7 @@ func (s *Store) addLayerToTar(tw *tar.Writer, layer v1.Layer) (string, error) { } withName := fmt.Sprintf("/%s.tar", layerDiffID.String()) - uncompressedSize, err := getLayerSize(layer) // FIXME: this degrades performance compared to `local` package + uncompressedSize, err := s.getLayerSize(layer) if err != nil { return "", err } @@ -241,9 +277,21 @@ func (s *Store) addLayerToTar(tw *tar.Writer, layer v1.Layer) (string, error) { return withName, nil } -// FIXME: this is a hack because the daemon expects uncompressed layer size and a v1.Layer reports compressed layer size; -// when we send OCI layout tars we should be able to remove this method and get improved performance -func getLayerSize(layer v1.Layer) (int64, error) { +// getLayerSize returns the uncompressed layer size. +// This is needed because the daemon expects uncompressed layer size and a v1.Layer reports compressed layer size; +// in a future where we send OCI layout tars to the daemon we should be able to remove this method +// and the need to track layers individually. +func (s *Store) getLayerSize(layer v1.Layer) (int64, error) { + diffID, err := layer.DiffID() + if err != nil { + return 0, err + } + knownLayer, layerFound := s.onDiskLayersByDiffID[diffID] + if layerFound { + return knownLayer.uncompressedSize, nil + } + // If layer was not seen previously, we need to read it to get the uncompressed size + // In practice, we should not get here layerReader, err := layer.Uncompressed() if err != nil { return 0, err @@ -401,12 +449,18 @@ func (s *Store) doDownloadLayersFor(identifier string) error { return err } - for idx := range configFile.RootFS.DiffIDs { - layer, err := tarball.LayerFromFile(filepath.Join(tmpDir, manifest[0].Layers[idx])) + for idx, diffID := range configFile.RootFS.DiffIDs { + layerPath := filepath.Join(tmpDir, manifest[0].Layers[idx]) + hash, err := v1.NewHash(diffID) + if err != nil { + return err + } + layer := newPopulatedLayer(hash, layerPath, 1) + fi, err := os.Stat(layerPath) if err != nil { return err } - s.onDiskLayers = append(s.onDiskLayers, layer) + s.AddLayer(layer, hash, fi.Size()) } return nil } @@ -476,22 +530,24 @@ func cleanPath(dest, header string) (string, error) { } func (s *Store) LayerByDiffID(h v1.Hash) (v1.Layer, error) { - layer := findLayer(h, s.onDiskLayers) + layer := s.findLayer(h) if layer == nil { return nil, fmt.Errorf("failed to find layer with diff ID %q", h.String()) } return layer, nil } -func findLayer(withHash v1.Hash, inLayers []v1.Layer) v1.Layer { - for _, layer := range inLayers { - layerHash, err := layer.DiffID() - if err != nil { - continue - } - if layerHash.String() == withHash.String() { - return layer - } +func (s *Store) findLayer(withHash v1.Hash) v1.Layer { + aLayer, layerFound := s.onDiskLayersByDiffID[withHash] + if !layerFound { + return nil + } + return aLayer.layer +} + +func (s *Store) AddLayer(layer v1.Layer, withDiffID v1.Hash, withSize int64) { + s.onDiskLayersByDiffID[withDiffID] = annotatedLayer{ + layer: layer, + uncompressedSize: withSize, } - return nil } diff --git a/locallayout/v1_facade.go b/local/v1_facade.go similarity index 71% rename from locallayout/v1_facade.go rename to local/v1_facade.go index 35418e29..7574f23e 100644 --- a/locallayout/v1_facade.go +++ b/local/v1_facade.go @@ -1,9 +1,10 @@ -package locallayout +package local import ( "bytes" "fmt" "io" + "os" "time" "github.com/docker/docker/api/types" @@ -40,7 +41,7 @@ func newV1ImageFacadeFromInspect(dockerInspect types.ImageInspect, history []ima OSVersion: dockerInspect.OsVersion, Variant: dockerInspect.Variant, } - layersToSet := newEmptyLayerListFrom(configFile, dockerInspect.ID, withStore, downloadLayersOnAccess) + layersToSet := newEmptyLayerListFrom(configFile, downloadLayersOnAccess, withStore, dockerInspect.ID) return imageFrom(layersToSet, configFile, imgutil.DockerTypes) } @@ -182,79 +183,114 @@ func toV1Config(dockerCfg *container.Config) v1.Config { var _ v1.Layer = &v1LayerFacade{} type v1LayerFacade struct { - diffID v1.Hash - store *Store - // for downloading layers from the daemon as needed - downloadOnAccess bool - imageIdentifier string + diffID v1.Hash + uncompressed func() (io.ReadCloser, error) + size func() (int64, error) } -func newEmptyLayerListFrom(configFile *v1.ConfigFile, withImageIdentifier string, withStore *Store, downloadOnAccess bool) []v1.Layer { +func newEmptyLayer(diffID v1.Hash, store *Store, imageID string) *v1LayerFacade { + return &v1LayerFacade{ + diffID: diffID, + uncompressed: func() (io.ReadCloser, error) { + layer, err := store.LayerByDiffID(diffID) + if err == nil { + return layer.Uncompressed() + } + return io.NopCloser(bytes.NewReader([]byte{})), nil + }, + size: func() (int64, error) { + layer, err := store.LayerByDiffID(diffID) + if err == nil { + return layer.Size() + } + return -1, nil + }, + } +} + +func newDownloadableEmptyLayer(diffID v1.Hash, store *Store, imageID string) *v1LayerFacade { + return &v1LayerFacade{ + diffID: diffID, + uncompressed: func() (io.ReadCloser, error) { + layer, err := store.LayerByDiffID(diffID) + if err == nil { + return layer.Uncompressed() + } + if err = store.downloadLayersFor(imageID); err != nil { + return nil, err + } + layer, err = store.LayerByDiffID(diffID) + if err == nil { + return layer.Uncompressed() + } + return nil, err + }, + size: func() (int64, error) { + layer, err := store.LayerByDiffID(diffID) + if err == nil { + return layer.Size() + } + if err = store.downloadLayersFor(imageID); err != nil { + return -1, err + } + layer, err = store.LayerByDiffID(diffID) + if err == nil { + return layer.Size() + } + return -1, err + }, + } +} + +func newPopulatedLayer(diffID v1.Hash, fromPath string, sentinelSize int64) *v1LayerFacade { + return &v1LayerFacade{ + diffID: diffID, + uncompressed: func() (io.ReadCloser, error) { + f, err := os.Open(fromPath) + if err != nil { + return nil, err + } + return f, nil + }, + size: func() (int64, error) { + return sentinelSize, nil + }, + } +} + +func newEmptyLayerListFrom(configFile *v1.ConfigFile, downloadOnAccess bool, withStore *Store, withImageIdentifier string) []v1.Layer { layers := make([]v1.Layer, len(configFile.RootFS.DiffIDs)) for idx, diffID := range configFile.RootFS.DiffIDs { - layers[idx] = &v1LayerFacade{ - diffID: diffID, - store: withStore, - downloadOnAccess: downloadOnAccess, - imageIdentifier: withImageIdentifier, + if downloadOnAccess { + layers[idx] = newDownloadableEmptyLayer(diffID, withStore, withImageIdentifier) + } else { + layers[idx] = newEmptyLayer(diffID, withStore, withImageIdentifier) } } return layers } -func (l v1LayerFacade) Compressed() (io.ReadCloser, error) { +func (l *v1LayerFacade) Compressed() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader([]byte{})), nil } -func (l v1LayerFacade) DiffID() (v1.Hash, error) { +func (l *v1LayerFacade) DiffID() (v1.Hash, error) { return l.diffID, nil } -func (l v1LayerFacade) Digest() (v1.Hash, error) { +func (l *v1LayerFacade) Digest() (v1.Hash, error) { return v1.Hash{}, nil } -func (l v1LayerFacade) Uncompressed() (io.ReadCloser, error) { - layer, err := l.store.LayerByDiffID(l.diffID) - if err == nil { - return layer.Uncompressed() - } - if !l.downloadOnAccess { - return io.NopCloser(bytes.NewReader([]byte{})), nil - } - if err = l.store.downloadLayersFor(l.imageIdentifier); err != nil { - return nil, err - } - layer, err = l.store.LayerByDiffID(l.diffID) - if err != nil { - return io.NopCloser(bytes.NewReader([]byte{})), nil - } - return layer.Uncompressed() +func (l *v1LayerFacade) Uncompressed() (io.ReadCloser, error) { + return l.uncompressed() } // Size returns a sentinel value indicating if the layer has data. -func (l v1LayerFacade) Size() (int64, error) { - layer, err := l.store.LayerByDiffID(l.diffID) - if err == nil { - return layer.Size() - } - if !l.downloadOnAccess { - return -1, nil - } - if err = l.store.downloadLayersFor(l.imageIdentifier); err != nil { - return -1, err - } - layer, err = l.store.LayerByDiffID(l.diffID) - if err != nil { - return -1, nil - } - return layer.Size() +func (l *v1LayerFacade) Size() (int64, error) { + return l.size() } -func (l v1LayerFacade) MediaType() (v1types.MediaType, error) { - layer, err := l.store.LayerByDiffID(l.diffID) - if err != nil { - return v1types.OCILayer, nil - } - return layer.MediaType() +func (l *v1LayerFacade) MediaType() (v1types.MediaType, error) { + return v1types.DockerLayer, nil } diff --git a/locallayout/image.go b/locallayout/image.go deleted file mode 100644 index f48c434b..00000000 --- a/locallayout/image.go +++ /dev/null @@ -1,124 +0,0 @@ -package locallayout - -import ( - "errors" - "fmt" - "io" - "strings" - - v1 "github.com/google/go-containerregistry/pkg/v1" - - "github.com/buildpacks/imgutil" -) - -// Image wraps an imgutil.CNBImageCore and implements the methods needed to complete the imgutil.Image interface. -type Image struct { - *imgutil.CNBImageCore - repoName string - store *Store - lastIdentifier string - daemonOS string -} - -func (i *Image) Kind() string { - return "locallayout" -} - -func (i *Image) Name() string { - return i.repoName -} - -func (i *Image) Rename(name string) { - i.repoName = name -} - -func (i *Image) Found() bool { - return i.lastIdentifier != "" -} - -func (i *Image) Identifier() (imgutil.Identifier, error) { - return idStringer{ - id: strings.TrimPrefix(i.lastIdentifier, "sha256:"), - }, nil -} - -type idStringer struct { - id string -} - -func (i idStringer) String() string { - return i.id -} - -// GetLayer returns an io.ReadCloser with uncompressed layer data. -// The layer will always have data, even if that means downloading ALL the image layers from the daemon. -func (i *Image) GetLayer(diffID string) (io.ReadCloser, error) { - layerHash, err := v1.NewHash(diffID) - if err != nil { - return nil, err - } - layer, err := i.LayerByDiffID(layerHash) - if err == nil { - // this avoids downloading ALL the image layers from the daemon - // if the layer is available locally - // (e.g., it was added using AddLayer). - if size, err := layer.Size(); err != nil && size != -1 { - return layer.Uncompressed() - } - } - if err = i.ensureLayers(); err != nil { - return nil, err - } - layer, err = i.LayerByDiffID(layerHash) - if err != nil { - return nil, fmt.Errorf("image %q does not contain layer with diff ID %q", i.Name(), layerHash.String()) - } - return layer.Uncompressed() -} - -func (i *Image) ensureLayers() error { - if err := i.store.downloadLayersFor(i.lastIdentifier); err != nil { - return fmt.Errorf("fetching base layers: %w", err) - } - return nil -} - -func (i *Image) SetOS(osVal string) error { - if osVal != i.daemonOS { - return errors.New("invalid os: must match the daemon") - } - return i.CNBImageCore.SetOS(osVal) -} - -func (i *Image) Rebase(baseTopLayerDiffID string, withNewBase imgutil.Image) error { - if err := i.ensureLayers(); err != nil { - return err - } - return i.CNBImageCore.Rebase(baseTopLayerDiffID, withNewBase) -} - -func (i *Image) Save(additionalNames ...string) error { - err := i.SetCreatedAtAndHistory() - if err != nil { - return err - } - i.lastIdentifier, err = i.store.Save(i, i.Name(), additionalNames...) - return err -} - -func (i *Image) SaveAs(name string, additionalNames ...string) error { - err := i.SetCreatedAtAndHistory() - if err != nil { - return err - } - i.lastIdentifier, err = i.store.Save(i, name, additionalNames...) - return err -} - -func (i *Image) SaveFile() (string, error) { - return i.store.SaveFile(i, i.Name()) -} - -func (i *Image) Delete() error { - return i.store.Delete(i.lastIdentifier) -} diff --git a/locallayout/image_test.go b/locallayout/image_test.go deleted file mode 100644 index 61f0d848..00000000 --- a/locallayout/image_test.go +++ /dev/null @@ -1,2247 +0,0 @@ -package locallayout_test - -import ( - "archive/tar" - "context" - "fmt" - "io" - "os" - "strings" - "testing" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/client" - "github.com/google/go-containerregistry/pkg/authn" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/sclevine/spec" - "github.com/sclevine/spec/report" - - "github.com/buildpacks/imgutil" - local "github.com/buildpacks/imgutil/locallayout" - "github.com/buildpacks/imgutil/remote" - h "github.com/buildpacks/imgutil/testhelpers" -) - -const someSHA = "sha256:aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f" - -var localTestRegistry *h.DockerRegistry - -func TestLocalLayout(t *testing.T) { - localTestRegistry = h.NewDockerRegistry() - localTestRegistry.Start(t) - defer localTestRegistry.Stop(t) - - spec.Run(t, "Image", testImage, spec.Sequential(), spec.Report(report.Terminal{})) -} - -func newTestImageName() string { - return localTestRegistry.RepoName("pack-image-test-" + h.RandString(10)) -} - -func testImage(t *testing.T, when spec.G, it spec.S) { - var ( - dockerClient client.CommonAPIClient - daemonOS string - runnableBaseImageName string - ) - - it.Before(func() { - var err error - dockerClient = h.DockerCli(t) - - daemonInfo, err := dockerClient.Info(context.TODO()) - h.AssertNil(t, err) - - daemonOS = daemonInfo.OSType - runnableBaseImageName = h.RunnableBaseImage(daemonOS) - h.PullIfMissing(t, dockerClient, runnableBaseImageName) - }) - - when("#NewImage", func() { - when("no base image or platform is given", func() { - it("returns an empty image", func() { - _, err := local.NewImage(newTestImageName(), dockerClient) - h.AssertNil(t, err) - }) - - it("sets sensible defaults from daemon for all required fields", func() { - // os, architecture, and rootfs are required per https://github.com/opencontainers/image-spec/blob/master/config.md - img, err := local.NewImage(newTestImageName(), dockerClient) - h.AssertNil(t, err) - h.AssertNil(t, img.Save()) - - defer func() { - err = h.DockerRmi(dockerClient, img.Name()) - h.AssertNil(t, err) - }() - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), img.Name()) - h.AssertNil(t, err) - - daemonInfo, err := dockerClient.ServerVersion(context.TODO()) - h.AssertNil(t, err) - - h.AssertEq(t, inspect.Os, daemonInfo.Os) - h.AssertEq(t, inspect.Architecture, daemonInfo.Arch) - h.AssertEq(t, inspect.RootFS.Type, "layers") - }) - }) - - when("#WithDefaultPlatform", func() { - it("sets all available platform fields", func() { - expectedArmArch := "arm64" - expectedOSVersion := "" - if daemonOS == "windows" { - // windows/arm nanoserver image - expectedArmArch = "arm" - expectedOSVersion = "10.0.17763.1040" - } - - img, err := local.NewImage( - newTestImageName(), - dockerClient, - local.WithDefaultPlatform(imgutil.Platform{ - Architecture: expectedArmArch, - OS: daemonOS, - OSVersion: expectedOSVersion, - }), - ) - h.AssertNil(t, err) - h.AssertNil(t, img.Save()) - - defer func() { - err = h.DockerRmi(dockerClient, img.Name()) - h.AssertNil(t, err) - }() - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), img.Name()) - h.AssertNil(t, err) - - daemonInfo, err := dockerClient.Info(context.TODO()) - h.AssertNil(t, err) - - // image os must match daemon - h.AssertEq(t, inspect.Os, daemonInfo.OSType) - h.AssertEq(t, inspect.Architecture, expectedArmArch) - h.AssertEq(t, inspect.OsVersion, expectedOSVersion) - h.AssertEq(t, inspect.RootFS.Type, "layers") - - // base layer is added for windows - if daemonOS == "windows" { - h.AssertEq(t, len(inspect.RootFS.Layers), 1) - } else { - h.AssertEq(t, len(inspect.RootFS.Layers), 0) - } - }) - }) - - when("#FromBaseImage", func() { - when("no platform is specified", func() { - when("base image exists", func() { - var baseImageName = newTestImageName() - var repoName = newTestImageName() - - it.After(func() { - h.DockerRmi(dockerClient, baseImageName) - }) - - it("returns the local image", func() { - baseImage, err := local.NewImage(baseImageName, dockerClient) - h.AssertNil(t, err) - - h.AssertNil(t, baseImage.SetEnv("MY_VAR", "my_val")) - h.AssertNil(t, baseImage.SetLabel("some.label", "some.value")) - h.AssertNil(t, baseImage.Save()) - - localImage, err := local.NewImage( - repoName, - dockerClient, - local.FromBaseImage(baseImageName), - ) - h.AssertNil(t, err) - - labelValue, err := localImage.Label("some.label") - h.AssertNil(t, err) - h.AssertEq(t, labelValue, "some.value") - }) - }) - - when("base image does not exist", func() { - it("returns an empty image", func() { - img, err := local.NewImage( - newTestImageName(), - dockerClient, - local.FromBaseImage("some-bad-repo-name"), - ) - - h.AssertNil(t, err) - - // base layer is added for windows - if daemonOS == "windows" { - topLayerDiffID, err := img.TopLayer() - h.AssertNil(t, err) - - h.AssertNotEq(t, topLayerDiffID, "") - } else { - _, err = img.TopLayer() - h.AssertError(t, err, "has no layers") - } - }) - }) - - when("base image and daemon os/architecture match", func() { - it("uses the base image architecture/OS", func() { - img, err := local.NewImage( - newTestImageName(), - dockerClient, - local.FromBaseImage(runnableBaseImageName), - ) - h.AssertNil(t, err) - h.AssertNil(t, img.Save()) - defer func() { - err = h.DockerRmi(dockerClient, img.Name()) - h.AssertNil(t, err) - }() - - imgOS, err := img.OS() - h.AssertNil(t, err) - h.AssertEq(t, imgOS, daemonOS) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), img.Name()) - h.AssertNil(t, err) - h.AssertEq(t, inspect.Os, daemonOS) - h.AssertEq(t, inspect.Architecture, "amd64") - h.AssertEq(t, inspect.RootFS.Type, "layers") - - h.AssertEq(t, img.Found(), true) - }) - }) - - when("base image and daemon architecture do not match", func() { - it("uses the base image architecture/OS", func() { - armBaseImageName := "busybox@sha256:50edf1d080946c6a76989d1c3b0e753b62f7d9b5f5e66e88bef23ebbd1e9709c" - expectedArmArch := "arm64" - expectedOSVersion := "" - if daemonOS == "windows" { - // windows/arm nanoserver image - armBaseImageName = "mcr.microsoft.com/windows/nanoserver@sha256:29e2270953589a12de7a77a7e77d39e3b3e9cdfd243c922b3b8a63e2d8a71026" - expectedArmArch = "arm" - expectedOSVersion = "10.0.17763.1040" - } - - h.PullIfMissing(t, dockerClient, armBaseImageName) - - img, err := local.NewImage( - newTestImageName(), - dockerClient, - local.FromBaseImage(armBaseImageName), - ) - h.AssertNil(t, err) - h.AssertNil(t, img.Save()) - defer h.DockerRmi(dockerClient, img.Name()) - - imgArch, err := img.Architecture() - h.AssertNil(t, err) - h.AssertEq(t, imgArch, expectedArmArch) - - imgOSVersion, err := img.OSVersion() - h.AssertNil(t, err) - h.AssertEq(t, imgOSVersion, expectedOSVersion) - }) - }) - }) - - when("#WithDefaultPlatform", func() { - when("base image and platform architecture/OS do not match", func() { - it("uses the base image architecture/OS, ignoring platform", func() { - // linux/arm64 busybox image - armBaseImageName := "busybox@sha256:50edf1d080946c6a76989d1c3b0e753b62f7d9b5f5e66e88bef23ebbd1e9709c" - expectedArchitecture := "arm64" - expectedOSVersion := "" - if daemonOS == "windows" { - // windows/arm nanoserver image - armBaseImageName = "mcr.microsoft.com/windows/nanoserver@sha256:29e2270953589a12de7a77a7e77d39e3b3e9cdfd243c922b3b8a63e2d8a71026" - expectedArchitecture = "arm" - expectedOSVersion = "10.0.17763.1040" - } - - h.PullIfMissing(t, dockerClient, armBaseImageName) - - img, err := local.NewImage( - newTestImageName(), - dockerClient, - local.FromBaseImage(armBaseImageName), - local.WithDefaultPlatform(imgutil.Platform{ - Architecture: "not-an-arch", - OSVersion: "10.0.99999.9999", - }), - ) - h.AssertNil(t, err) - h.AssertNil(t, img.Save()) - defer h.DockerRmi(dockerClient, img.Name()) - - imgArch, err := img.Architecture() - h.AssertNil(t, err) - h.AssertEq(t, imgArch, expectedArchitecture) - - imgOSVersion, err := img.OSVersion() - h.AssertNil(t, err) - h.AssertEq(t, imgOSVersion, expectedOSVersion) - }) - }) - - when("base image does not exist", func() { - it("returns an empty image based on platform fields", func() { - img, err := local.NewImage( - newTestImageName(), - dockerClient, - local.FromBaseImage("some-bad-repo-name"), - local.WithDefaultPlatform(imgutil.Platform{ - Architecture: "arm64", - OS: daemonOS, - OSVersion: "10.0.99999.9999", - }), - ) - - h.AssertNil(t, err) - h.AssertNil(t, img.Save()) - defer h.DockerRmi(dockerClient, img.Name()) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), img.Name()) - h.AssertNil(t, err) - - daemonInfo, err := dockerClient.Info(context.TODO()) - h.AssertNil(t, err) - - // image os must match daemon - h.AssertEq(t, inspect.Os, daemonInfo.OSType) - h.AssertEq(t, inspect.Architecture, "arm64") - h.AssertEq(t, inspect.OsVersion, "10.0.99999.9999") - - // base layer is added for windows - if daemonOS == "windows" { - h.AssertEq(t, len(inspect.RootFS.Layers), 1) - } else { - h.AssertEq(t, len(inspect.RootFS.Layers), 0) - } - }) - }) - }) - }) - - when("#WithPreviousImage", func() { - when("previous image is exists", func() { - var armBaseImageName string - var existingLayerSHA string - - it.Before(func() { - // linux/arm64 busybox image - armBaseImageName = "busybox@sha256:50edf1d080946c6a76989d1c3b0e753b62f7d9b5f5e66e88bef23ebbd1e9709c" - if daemonOS == "windows" { - // windows/arm nanoserver image - armBaseImageName = "mcr.microsoft.com/windows/nanoserver@sha256:29e2270953589a12de7a77a7e77d39e3b3e9cdfd243c922b3b8a63e2d8a71026" - } - h.PullIfMissing(t, dockerClient, armBaseImageName) - - refImage, err := local.NewImage( - newTestImageName(), - dockerClient, - local.FromBaseImage(armBaseImageName), - ) - h.AssertNil(t, err) - - existingLayerSHA, err = refImage.TopLayer() - h.AssertNil(t, err) - }) - - it("provides reusable layers", func() { - img, err := local.NewImage( - newTestImageName(), - dockerClient, - local.WithPreviousImage(armBaseImageName), - ) - h.AssertNil(t, err) - - h.AssertNil(t, img.ReuseLayer(existingLayerSHA)) - }) - - it("provides reusable layers, ignoring WithDefaultPlatform", func() { - img, err := local.NewImage( - newTestImageName(), - dockerClient, - local.WithPreviousImage(armBaseImageName), - local.WithDefaultPlatform(imgutil.Platform{ - Architecture: "some-fake-os", - }), - ) - h.AssertNil(t, err) - - h.AssertNil(t, img.ReuseLayer(existingLayerSHA)) - }) - }) - - when("previous image does not exist", func() { - it("does not error", func() { - _, err := local.NewImage( - newTestImageName(), - dockerClient, - local.WithPreviousImage("some-bad-repo-name"), - ) - - h.AssertNil(t, err) - }) - }) - }) - - when("#WithConfig", func() { - var config = &v1.Config{Entrypoint: []string{"some-entrypoint"}} - - it("sets the image config", func() { - localImage, err := local.NewImage(newTestImageName(), dockerClient, local.WithConfig(config)) - h.AssertNil(t, err) - - entrypoint, err := localImage.Entrypoint() - h.AssertNil(t, err) - h.AssertEq(t, entrypoint, []string{"some-entrypoint"}) - }) - - when("#FromBaseImage", func() { - var baseImageName = newTestImageName() - - it("overrides the base image config", func() { - baseImage, err := local.NewImage(baseImageName, dockerClient) - h.AssertNil(t, err) - h.AssertNil(t, baseImage.Save()) - - localImage, err := local.NewImage( - newTestImageName(), - dockerClient, - local.WithConfig(config), - local.FromBaseImage(baseImageName), - ) - h.AssertNil(t, err) - - entrypoint, err := localImage.Entrypoint() - h.AssertNil(t, err) - h.AssertEq(t, entrypoint, []string{"some-entrypoint"}) - }) - - it.After(func() { - h.AssertNil(t, h.DockerRmi(dockerClient, baseImageName)) - }) - }) - }) - }) - - when("#Labels", func() { - when("image exists with labels", func() { - var repoName = newTestImageName() - - it.Before(func() { - existingImage, err := local.NewImage(repoName, dockerClient) - h.AssertNil(t, err) - - h.AssertNil(t, existingImage.SetLabel("mykey", "myvalue")) - h.AssertNil(t, existingImage.SetLabel("other", "data")) - h.AssertNil(t, existingImage.Save()) - }) - - it.After(func() { - h.AssertNil(t, h.DockerRmi(dockerClient, repoName)) - }) - - it("returns all the labels", func() { - img, err := local.NewImage(repoName, dockerClient, local.FromBaseImage(repoName)) - h.AssertNil(t, err) - - labels, err := img.Labels() - h.AssertNil(t, err) - h.AssertEq(t, labels["mykey"], "myvalue") - h.AssertEq(t, labels["other"], "data") - }) - }) - - when("image exists with no labels", func() { - var repoName = newTestImageName() - - it.Before(func() { - existingImage, err := local.NewImage(repoName, dockerClient) - h.AssertNil(t, err) - h.AssertNil(t, existingImage.Save()) - }) - - it.After(func() { - h.AssertNil(t, h.DockerRmi(dockerClient, repoName)) - }) - - it("returns nil", func() { - img, err := local.NewImage(repoName, dockerClient, local.FromBaseImage(repoName)) - h.AssertNil(t, err) - - labels, err := img.Labels() - h.AssertNil(t, err) - h.AssertEq(t, 0, len(labels)) - }) - }) - - when("image NOT exists", func() { - it("returns an empty map", func() { - img, err := local.NewImage(newTestImageName(), dockerClient) - h.AssertNil(t, err) - - labels, err := img.Labels() - h.AssertNil(t, err) - h.AssertEq(t, 0, len(labels)) - }) - }) - }) - - when("#Label", func() { - when("image exists", func() { - var repoName = newTestImageName() - - it.Before(func() { - existingImage, err := local.NewImage(repoName, dockerClient) - h.AssertNil(t, err) - - h.AssertNil(t, existingImage.SetLabel("mykey", "myvalue")) - h.AssertNil(t, existingImage.SetLabel("other", "data")) - h.AssertNil(t, existingImage.Save()) - }) - - it.After(func() { - h.AssertNil(t, h.DockerRmi(dockerClient, repoName)) - }) - - it("returns the label value", func() { - img, err := local.NewImage(repoName, dockerClient, local.FromBaseImage(repoName)) - h.AssertNil(t, err) - - label, err := img.Label("mykey") - h.AssertNil(t, err) - h.AssertEq(t, label, "myvalue") - }) - - it("returns an empty string for a missing label", func() { - img, err := local.NewImage(repoName, dockerClient) - h.AssertNil(t, err) - - label, err := img.Label("missing-label") - h.AssertNil(t, err) - h.AssertEq(t, label, "") - }) - }) - - when("image NOT exists", func() { - it("returns an empty string", func() { - img, err := local.NewImage(newTestImageName(), dockerClient) - h.AssertNil(t, err) - - label, err := img.Label("some-label") - h.AssertNil(t, err) - h.AssertEq(t, label, "") - }) - }) - }) - - when("#Env", func() { - when("image exists", func() { - var repoName = newTestImageName() - - it.Before(func() { - existingImage, err := local.NewImage(repoName, dockerClient) - h.AssertNil(t, err) - - h.AssertNil(t, existingImage.SetEnv("MY_VAR", "my_val")) - h.AssertNil(t, existingImage.Save()) - }) - - it.After(func() { - h.AssertNil(t, h.DockerRmi(dockerClient, repoName)) - }) - - it("returns the label value", func() { - img, err := local.NewImage(repoName, dockerClient, local.FromBaseImage(repoName)) - h.AssertNil(t, err) - - val, err := img.Env("MY_VAR") - h.AssertNil(t, err) - h.AssertEq(t, val, "my_val") - }) - - it("returns an empty string for a missing label", func() { - img, err := local.NewImage(repoName, dockerClient, local.FromBaseImage(repoName)) - h.AssertNil(t, err) - - val, err := img.Env("MISSING_VAR") - h.AssertNil(t, err) - h.AssertEq(t, val, "") - }) - }) - - when("image NOT exists", func() { - it("returns an empty string", func() { - img, err := local.NewImage(newTestImageName(), dockerClient) - h.AssertNil(t, err) - - val, err := img.Env("SOME_VAR") - h.AssertNil(t, err) - h.AssertEq(t, val, "") - }) - }) - }) - - when("#WorkingDir", func() { - when("image exists", func() { - var repoName = newTestImageName() - - it.Before(func() { - existingImage, err := local.NewImage(repoName, dockerClient) - h.AssertNil(t, err) - - h.AssertNil(t, existingImage.SetWorkingDir("/testWorkingDir")) - h.AssertNil(t, existingImage.Save()) - }) - - it.After(func() { - h.AssertNil(t, h.DockerRmi(dockerClient, repoName)) - }) - - it("returns the WorkingDir value", func() { - img, err := local.NewImage(repoName, dockerClient, local.FromBaseImage(repoName)) - h.AssertNil(t, err) - - val, err := img.WorkingDir() - h.AssertNil(t, err) - - h.AssertEq(t, val, "/testWorkingDir") - }) - - it("returns empty string for missing WorkingDir", func() { - existingImage, err := local.NewImage(repoName, dockerClient) - h.AssertNil(t, err) - h.AssertNil(t, existingImage.Save()) - - img, err := local.NewImage(repoName, dockerClient, local.FromBaseImage(repoName)) - h.AssertNil(t, err) - - val, err := img.WorkingDir() - h.AssertNil(t, err) - var expected string - h.AssertEq(t, val, expected) - }) - }) - - when("image NOT exists", func() { - it("returns empty string", func() { - img, err := local.NewImage(newTestImageName(), dockerClient) - h.AssertNil(t, err) - - val, err := img.WorkingDir() - h.AssertNil(t, err) - var expected string - h.AssertEq(t, val, expected) - }) - }) - }) - - when("#Entrypoint", func() { - when("image exists", func() { - var repoName = newTestImageName() - - it.Before(func() { - existingImage, err := local.NewImage(repoName, dockerClient) - h.AssertNil(t, err) - - h.AssertNil(t, existingImage.SetEntrypoint("entrypoint1", "entrypoint2")) - h.AssertNil(t, existingImage.Save()) - }) - - it.After(func() { - h.AssertNil(t, h.DockerRmi(dockerClient, repoName)) - }) - - it("returns the entrypoint value", func() { - img, err := local.NewImage(repoName, dockerClient, local.FromBaseImage(repoName)) - h.AssertNil(t, err) - - val, err := img.Entrypoint() - h.AssertNil(t, err) - - h.AssertEq(t, val, []string{"entrypoint1", "entrypoint2"}) - }) - - it("returns nil slice for a missing entrypoint", func() { - existingImage, err := local.NewImage(repoName, dockerClient) - h.AssertNil(t, err) - h.AssertNil(t, existingImage.Save()) - - img, err := local.NewImage(repoName, dockerClient, local.FromBaseImage(repoName)) - h.AssertNil(t, err) - - val, err := img.Entrypoint() - h.AssertNil(t, err) - var expected []string - h.AssertEq(t, val, expected) - }) - }) - - when("image NOT exists", func() { - it("returns nil slice", func() { - img, err := local.NewImage(newTestImageName(), dockerClient) - h.AssertNil(t, err) - - val, err := img.Entrypoint() - h.AssertNil(t, err) - var expected []string - h.AssertEq(t, val, expected) - }) - }) - }) - - when("#Name", func() { - it("always returns the original name", func() { - var repoName = newTestImageName() - - img, err := local.NewImage(repoName, dockerClient) - h.AssertNil(t, err) - - h.AssertEq(t, img.Name(), repoName) - }) - }) - - when("#CreatedAt", func() { - it("returns the containers created at time", func() { - img, err := local.NewImage(newTestImageName(), dockerClient, local.FromBaseImage(runnableBaseImageName)) - h.AssertNil(t, err) - - // based on static base image refs - expectedTime := time.Date(2018, 10, 2, 17, 19, 34, 239926273, time.UTC) - if daemonOS == "windows" { - expectedTime = time.Date(2020, 03, 04, 13, 28, 48, 673000000, time.UTC) - } - - createdTime, err := img.CreatedAt() - - h.AssertNil(t, err) - h.AssertEq(t, createdTime, expectedTime) - }) - }) - - when("#Identifier", func() { - var repoName = newTestImageName() - var baseImageName = newTestImageName() - - it.Before(func() { - baseImage, err := local.NewImage(baseImageName, dockerClient) - h.AssertNil(t, err) - - h.AssertNil(t, baseImage.SetLabel("existingLabel", "existingValue")) - h.AssertNil(t, baseImage.Save()) - }) - - it.After(func() { - h.AssertNil(t, h.DockerRmi(dockerClient, baseImageName)) - }) - - it("returns an Docker Image ID type identifier", func() { - img, err := local.NewImage(repoName, dockerClient, local.FromBaseImage(baseImageName)) - h.AssertNil(t, err) - - id, err := img.Identifier() - h.AssertNil(t, err) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), id.String()) - h.AssertNil(t, err) - labelValue := inspect.Config.Labels["existingLabel"] - h.AssertEq(t, labelValue, "existingValue") - }) - - when("the image has been modified and saved", func() { - it.After(func() { - h.AssertNil(t, h.DockerRmi(dockerClient, repoName)) - }) - - it("returns the new image ID", func() { - img, err := local.NewImage(repoName, dockerClient, local.FromBaseImage(baseImageName)) - h.AssertNil(t, err) - - h.AssertNil(t, img.SetLabel("new", "label")) - - h.AssertNil(t, img.Save()) - h.AssertNil(t, err) - - id, err := img.Identifier() - h.AssertNil(t, err) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), id.String()) - h.AssertNil(t, err) - - label := inspect.Config.Labels["new"] - h.AssertEq(t, strings.TrimSpace(label), "label") - }) - }) - }) - - when("#Kind", func() { - it("returns locallayout", func() { - img, err := local.NewImage(newTestImageName(), dockerClient) - h.AssertNil(t, err) - h.AssertEq(t, img.Kind(), "locallayout") - }) - }) - - when("#SetLabel", func() { - var ( - img imgutil.Image - repoName = newTestImageName() - baseImageName = newTestImageName() - ) - - it.After(func() { - h.AssertNil(t, h.DockerRmi(dockerClient, repoName, baseImageName)) - }) - - when("base image has labels", func() { - it("sets label and saves label to docker daemon", func() { - var err error - - baseImage, err := local.NewImage(baseImageName, dockerClient) - h.AssertNil(t, err) - - h.AssertNil(t, baseImage.SetLabel("some-key", "some-value")) - h.AssertNil(t, baseImage.Save()) - - img, err = local.NewImage(repoName, dockerClient, local.FromBaseImage(baseImageName)) - h.AssertNil(t, err) - - h.AssertNil(t, img.SetLabel("somekey", "new-val")) - - label, err := img.Label("somekey") - h.AssertNil(t, err) - h.AssertEq(t, label, "new-val") - - h.AssertNil(t, img.Save()) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) - h.AssertNil(t, err) - label = inspect.Config.Labels["somekey"] - h.AssertEq(t, strings.TrimSpace(label), "new-val") - }) - }) - - when("no labels exists", func() { - it("sets label and saves label to docker daemon", func() { - var err error - baseImage, err := local.NewImage(baseImageName, dockerClient) - h.AssertNil(t, err) - - h.AssertNil(t, baseImage.SetCmd("/usr/bin/run")) - h.AssertNil(t, baseImage.Save()) - - img, err = local.NewImage(repoName, dockerClient, local.FromBaseImage(baseImageName)) - h.AssertNil(t, err) - - h.AssertNil(t, img.SetLabel("somekey", "new-val")) - - label, err := img.Label("somekey") - h.AssertNil(t, err) - h.AssertEq(t, label, "new-val") - - h.AssertNil(t, img.Save()) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) - h.AssertNil(t, err) - - label = inspect.Config.Labels["somekey"] - h.AssertEq(t, strings.TrimSpace(label), "new-val") - }) - }) - }) - - when("#RemoveLabel", func() { - var ( - img imgutil.Image - repoName = newTestImageName() - baseImageName = newTestImageName() - ) - - it.After(func() { - h.AssertNil(t, h.DockerRmi(dockerClient, repoName, baseImageName)) - }) - - when("image exists", func() { - it("removes matching label on img object", func() { - var err error - - baseImage, err := local.NewImage(baseImageName, dockerClient) - h.AssertNil(t, err) - h.AssertNil(t, baseImage.SetLabel("my.custom.label", "old-value")) - h.AssertNil(t, baseImage.Save()) - - img, err = local.NewImage(repoName, dockerClient, local.FromBaseImage(baseImageName)) - h.AssertNil(t, err) - - h.AssertNil(t, img.RemoveLabel("my.custom.label")) - h.AssertNil(t, img.Save()) - - labels, err := img.Labels() - h.AssertNil(t, err) - _, exists := labels["my.custom.label"] - h.AssertEq(t, exists, false) - }) - - it("saves removal of the label", func() { - var err error - - baseImage, err := local.NewImage(baseImageName, dockerClient) - h.AssertNil(t, err) - h.AssertNil(t, baseImage.SetLabel("my.custom.label", "old-value")) - h.AssertNil(t, baseImage.Save()) - - img, err = local.NewImage(repoName, dockerClient, local.FromBaseImage(baseImageName)) - h.AssertNil(t, err) - - h.AssertNil(t, img.RemoveLabel("my.custom.label")) - h.AssertNil(t, img.Save()) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) - h.AssertNil(t, err) - _, exists := inspect.Config.Labels["my.custom.label"] - h.AssertEq(t, exists, false) - }) - }) - }) - - when("#SetEnv", func() { - var repoName = newTestImageName() - var skipCleanup bool - - it.After(func() { - if !skipCleanup { - h.AssertNil(t, h.DockerRmi(dockerClient, repoName)) - } - }) - - it("sets the environment", func() { - img, err := local.NewImage(repoName, dockerClient) - h.AssertNil(t, err) - - err = img.SetEnv("ENV_KEY", "ENV_VAL") - h.AssertNil(t, err) - - h.AssertNil(t, img.Save()) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) - h.AssertNil(t, err) - - h.AssertContains(t, inspect.Config.Env, "ENV_KEY=ENV_VAL") - }) - - when("the key already exists", func() { - it("overrides the existing key", func() { - img, err := local.NewImage(repoName, dockerClient) - h.AssertNil(t, err) - - err = img.SetEnv("ENV_KEY", "SOME_VAL") - h.AssertNil(t, err) - - err = img.SetEnv("ENV_KEY", "SOME_OTHER_VAL") - h.AssertNil(t, err) - - h.AssertNil(t, img.Save()) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) - h.AssertNil(t, err) - - h.AssertContains(t, inspect.Config.Env, "ENV_KEY=SOME_OTHER_VAL") - h.AssertDoesNotContain(t, inspect.Config.Env, "ENV_KEY=SOME_VAL") - }) - - when("windows", func() { - it("ignores case", func() { - if daemonOS != "windows" { - skipCleanup = true - t.Skip("windows test") - } - - img, err := local.NewImage(repoName, dockerClient) - h.AssertNil(t, err) - - err = img.SetEnv("ENV_KEY", "SOME_VAL") - h.AssertNil(t, err) - - err = img.SetEnv("env_key", "SOME_OTHER_VAL") - h.AssertNil(t, err) - - err = img.SetEnv("env_key2", "SOME_VAL") - h.AssertNil(t, err) - - err = img.SetEnv("ENV_KEY2", "SOME_OTHER_VAL") - h.AssertNil(t, err) - - h.AssertNil(t, img.Save()) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) - h.AssertNil(t, err) - - h.AssertContains(t, inspect.Config.Env, "env_key=SOME_OTHER_VAL") - h.AssertDoesNotContain(t, inspect.Config.Env, "ENV_KEY=SOME_VAL") - h.AssertDoesNotContain(t, inspect.Config.Env, "ENV_KEY=SOME_OTHER_VAL") - - h.AssertContains(t, inspect.Config.Env, "ENV_KEY2=SOME_OTHER_VAL") - h.AssertDoesNotContain(t, inspect.Config.Env, "env_key2=SOME_OTHER_VAL") - h.AssertDoesNotContain(t, inspect.Config.Env, "env_key2=SOME_VAL") - }) - }) - }) - }) - - when("#SetWorkingDir", func() { - var repoName = newTestImageName() - - it.After(func() { - h.AssertNil(t, h.DockerRmi(dockerClient, repoName)) - }) - - it("sets the environment", func() { - img, err := local.NewImage(repoName, dockerClient) - h.AssertNil(t, err) - - err = img.SetWorkingDir("/some/work/dir") - h.AssertNil(t, err) - - h.AssertNil(t, img.Save()) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) - h.AssertNil(t, err) - - h.AssertEq(t, inspect.Config.WorkingDir, "/some/work/dir") - }) - }) - - when("#SetEntrypoint", func() { - var repoName = newTestImageName() - - it.After(func() { - h.AssertNil(t, h.DockerRmi(dockerClient, repoName)) - }) - - it("sets the entrypoint", func() { - img, err := local.NewImage(repoName, dockerClient) - h.AssertNil(t, err) - - err = img.SetEntrypoint("some", "entrypoint") - h.AssertNil(t, err) - - h.AssertNil(t, img.Save()) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) - h.AssertNil(t, err) - - h.AssertEq(t, []string(inspect.Config.Entrypoint), []string{"some", "entrypoint"}) - }) - }) - - when("#SetCmd", func() { - var repoName = newTestImageName() - - it.After(func() { - h.AssertNil(t, h.DockerRmi(dockerClient, repoName)) - }) - - it("sets the cmd", func() { - img, err := local.NewImage(repoName, dockerClient) - h.AssertNil(t, err) - - err = img.SetCmd("some", "cmd") - h.AssertNil(t, err) - - h.AssertNil(t, img.Save()) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) - h.AssertNil(t, err) - - h.AssertEq(t, []string(inspect.Config.Cmd), []string{"some", "cmd"}) - }) - }) - - when("#SetOS", func() { - var repoName = newTestImageName() - - it.After(func() { - h.AssertNil(t, h.DockerRmi(dockerClient, repoName)) - }) - - it("allows noop sets for values that match the daemon", func() { - img, err := local.NewImage(repoName, dockerClient) - h.AssertNil(t, err) - - err = img.SetOS("fakeos") - h.AssertError(t, err, "invalid os: must match the daemon") - - err = img.SetOS(daemonOS) - h.AssertNil(t, err) - - h.AssertNil(t, img.Save()) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) - h.AssertNil(t, err) - - h.AssertEq(t, inspect.Os, daemonOS) - }) - }) - - when("#SetOSVersion #SetArchitecture", func() { - var repoName = newTestImageName() - - it.After(func() { - h.AssertNil(t, h.DockerRmi(dockerClient, repoName)) - }) - - it("sets the os.version/arch", func() { - img, err := local.NewImage(repoName, dockerClient) - h.AssertNil(t, err) - - err = img.SetOSVersion("1.2.3.4") - h.AssertNil(t, err) - - err = img.SetArchitecture("arm64") - h.AssertNil(t, err) - - h.AssertNil(t, img.Save()) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) - h.AssertNil(t, err) - - h.AssertEq(t, inspect.OsVersion, "1.2.3.4") - h.AssertEq(t, inspect.Architecture, "arm64") - }) - }) - - when("#Rebase", func() { - when("image exists", func() { - var ( - repoName = newTestImageName() - oldBase, oldTopLayer, newBase string - oldBaseLayer1DiffID string - oldBaseLayer2DiffID string - newBaseLayer1DiffID string - newBaseLayer2DiffID string - imgLayer1DiffID string - imgLayer2DiffID string - origNumLayers int - ) - - it.Before(func() { - // new base image - newBase = "pack-newbase-test-" + h.RandString(10) - newBaseImage, err := local.NewImage(newBase, dockerClient, local.FromBaseImage(runnableBaseImageName)) - h.AssertNil(t, err) - - newBaseLayer1Path, err := h.CreateSingleFileLayerTar("/new-base.txt", "new-base", daemonOS) - h.AssertNil(t, err) - defer os.Remove(newBaseLayer1Path) - - newBaseLayer1DiffID = h.FileDiffID(t, newBaseLayer1Path) - - newBaseLayer2Path, err := h.CreateSingleFileLayerTar("/otherfile.txt", "text-new-base", daemonOS) - h.AssertNil(t, err) - defer os.Remove(newBaseLayer2Path) - - newBaseLayer2DiffID = h.FileDiffID(t, newBaseLayer2Path) - - h.AssertNil(t, newBaseImage.AddLayer(newBaseLayer1Path)) - h.AssertNil(t, newBaseImage.AddLayer(newBaseLayer2Path)) - - h.AssertNil(t, newBaseImage.Save()) - - // old base image - oldBase = "pack-oldbase-test-" + h.RandString(10) - oldBaseImage, err := local.NewImage(oldBase, dockerClient, local.FromBaseImage(runnableBaseImageName)) - h.AssertNil(t, err) - - oldBaseLayer1Path, err := h.CreateSingleFileLayerTar("/old-base.txt", "old-base", daemonOS) - h.AssertNil(t, err) - defer os.Remove(oldBaseLayer1Path) - - oldBaseLayer1DiffID = h.FileDiffID(t, oldBaseLayer1Path) - - oldBaseLayer2Path, err := h.CreateSingleFileLayerTar("/otherfile.txt", "text-old-base", daemonOS) - h.AssertNil(t, err) - defer os.Remove(oldBaseLayer2Path) - - oldBaseLayer2DiffID = h.FileDiffID(t, oldBaseLayer2Path) - - h.AssertNil(t, oldBaseImage.AddLayer(oldBaseLayer1Path)) - h.AssertNil(t, oldBaseImage.AddLayer(oldBaseLayer2Path)) - - h.AssertNil(t, oldBaseImage.Save()) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), oldBase) - h.AssertNil(t, err) - oldTopLayer = h.StringElementAt(inspect.RootFS.Layers, -1) - - // original image - origImage, err := local.NewImage(repoName, dockerClient, local.FromBaseImage(oldBase)) - h.AssertNil(t, err) - - imgLayer1Path, err := h.CreateSingleFileLayerTar("/myimage.txt", "text-from-image", daemonOS) - h.AssertNil(t, err) - defer os.Remove(imgLayer1Path) - - imgLayer1DiffID = h.FileDiffID(t, imgLayer1Path) - - imgLayer2Path, err := h.CreateSingleFileLayerTar("/myimage2.txt", "text-from-image", daemonOS) - h.AssertNil(t, err) - defer os.Remove(imgLayer2Path) - - imgLayer2DiffID = h.FileDiffID(t, imgLayer2Path) - - h.AssertNil(t, origImage.AddLayer(imgLayer1Path)) - h.AssertNil(t, origImage.AddLayer(imgLayer2Path)) - - h.AssertNil(t, origImage.Save()) - - inspect, _, err = dockerClient.ImageInspectWithRaw(context.TODO(), repoName) - h.AssertNil(t, err) - origNumLayers = len(inspect.RootFS.Layers) - }) - - it.After(func() { - h.AssertNil(t, h.DockerRmi(dockerClient, repoName, oldBase, newBase)) - }) - - it("switches the base", func() { - // Before - beforeInspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) - h.AssertNil(t, err) - - beforeOldBaseLayer1DiffID := h.StringElementAt(beforeInspect.RootFS.Layers, -4) - h.AssertEq(t, oldBaseLayer1DiffID, beforeOldBaseLayer1DiffID) - - beforeOldBaseLayer2DiffID := h.StringElementAt(beforeInspect.RootFS.Layers, -3) - h.AssertEq(t, oldBaseLayer2DiffID, beforeOldBaseLayer2DiffID) - - beforeLayer3DiffID := h.StringElementAt(beforeInspect.RootFS.Layers, -2) - h.AssertEq(t, imgLayer1DiffID, beforeLayer3DiffID) - - beforeLayer4DiffID := h.StringElementAt(beforeInspect.RootFS.Layers, -1) - h.AssertEq(t, imgLayer2DiffID, beforeLayer4DiffID) - - // Run rebase - img, err := local.NewImage(repoName, dockerClient, local.FromBaseImage(repoName)) - h.AssertNil(t, err) - newBaseImg, err := local.NewImage(newBase, dockerClient, local.FromBaseImage(newBase)) - h.AssertNil(t, err) - err = img.Rebase(oldTopLayer, newBaseImg) - h.AssertNil(t, err) - - h.AssertNil(t, img.Save()) - - // After - afterInspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) - h.AssertNil(t, err) - - numLayers := len(afterInspect.RootFS.Layers) - h.AssertEq(t, numLayers, origNumLayers) - - afterLayer1DiffID := h.StringElementAt(afterInspect.RootFS.Layers, -4) - h.AssertEq(t, newBaseLayer1DiffID, afterLayer1DiffID) - - afterLayer2DiffID := h.StringElementAt(afterInspect.RootFS.Layers, -3) - h.AssertEq(t, newBaseLayer2DiffID, afterLayer2DiffID) - - afterLayer3DiffID := h.StringElementAt(afterInspect.RootFS.Layers, -2) - h.AssertEq(t, imgLayer1DiffID, afterLayer3DiffID) - - afterLayer4DiffID := h.StringElementAt(afterInspect.RootFS.Layers, -1) - h.AssertEq(t, imgLayer2DiffID, afterLayer4DiffID) - - h.AssertEq(t, afterInspect.Os, beforeInspect.Os) - h.AssertEq(t, afterInspect.OsVersion, beforeInspect.OsVersion) - h.AssertEq(t, afterInspect.Architecture, beforeInspect.Architecture) - }) - }) - }) - - when("#TopLayer", func() { - when("image exists", func() { - var ( - expectedTopLayer string - repoName = newTestImageName() - ) - it.Before(func() { - existingImage, err := local.NewImage( - repoName, - dockerClient, - local.FromBaseImage(runnableBaseImageName), - ) - h.AssertNil(t, err) - - layer1Path, err := h.CreateSingleFileLayerTar("/newfile.txt", "old-base", daemonOS) - h.AssertNil(t, err) - layer2Path, err := h.CreateSingleFileLayerTar("/otherfile.txt", "text-old-base", daemonOS) - h.AssertNil(t, err) - - h.AssertNil(t, existingImage.AddLayer(layer1Path)) - h.AssertNil(t, existingImage.AddLayer(layer2Path)) - - h.AssertNil(t, existingImage.Save()) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) - h.AssertNil(t, err) - expectedTopLayer = h.StringElementAt(inspect.RootFS.Layers, -1) - }) - - it.After(func() { - h.AssertNil(t, h.DockerRmi(dockerClient, repoName)) - }) - - it("returns the digest for the top layer (useful for rebasing)", func() { - img, err := local.NewImage(repoName, dockerClient, local.FromBaseImage(repoName)) - h.AssertNil(t, err) - - actualTopLayer, err := img.TopLayer() - h.AssertNil(t, err) - - h.AssertEq(t, actualTopLayer, expectedTopLayer) - }) - }) - - when("image has no layers", func() { - it("returns error", func() { - img, err := local.NewImage(newTestImageName(), dockerClient) - h.AssertNil(t, err) - - if daemonOS == "windows" { - layer, err := img.TopLayer() - h.AssertNil(t, err) - h.AssertNotEq(t, layer, "") - } else { - _, err = img.TopLayer() - h.AssertError(t, err, "has no layers") - } - }) - }) - }) - - when("#AddLayer", func() { - when("empty image", func() { - var repoName = newTestImageName() - - it("appends a layer", func() { - img, err := local.NewImage(repoName, dockerClient) - h.AssertNil(t, err) - - newLayerPath, err := h.CreateSingleFileLayerTar("/new-layer.txt", "new-layer", daemonOS) - h.AssertNil(t, err) - defer os.Remove(newLayerPath) - - newLayerDiffID := h.FileDiffID(t, newLayerPath) - - h.AssertNil(t, img.AddLayer(newLayerPath)) - - h.AssertNil(t, img.Save()) - defer h.DockerRmi(dockerClient, repoName) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) - h.AssertNil(t, err) - - h.AssertEq(t, newLayerDiffID, h.StringElementAt(inspect.RootFS.Layers, -1)) - }) - }) - - when("base image exists", func() { - var ( - repoName = newTestImageName() - baseImageName = newTestImageName() - ) - - it("appends a layer", func() { - baseImage, err := local.NewImage( - baseImageName, - dockerClient, - local.FromBaseImage(runnableBaseImageName), - ) - h.AssertNil(t, err) - - oldLayerPath, err := h.CreateSingleFileLayerTar("/old-layer.txt", "old-layer", daemonOS) - h.AssertNil(t, err) - defer os.Remove(oldLayerPath) - - oldLayerDiffID := h.FileDiffID(t, oldLayerPath) - - h.AssertNil(t, baseImage.AddLayer(oldLayerPath)) - - h.AssertNil(t, baseImage.Save()) - defer h.DockerRmi(dockerClient, baseImageName) - - img, err := local.NewImage( - repoName, - dockerClient, - local.FromBaseImage(baseImageName), - ) - h.AssertNil(t, err) - - newLayerPath, err := h.CreateSingleFileLayerTar("/new-layer.txt", "new-layer", daemonOS) - h.AssertNil(t, err) - defer os.Remove(newLayerPath) - - newLayerDiffID := h.FileDiffID(t, newLayerPath) - - h.AssertNil(t, img.AddLayer(newLayerPath)) - - h.AssertNil(t, img.Save()) - defer h.DockerRmi(dockerClient, repoName) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) - h.AssertNil(t, err) - - h.AssertEq(t, oldLayerDiffID, h.StringElementAt(inspect.RootFS.Layers, -2)) - h.AssertEq(t, newLayerDiffID, h.StringElementAt(inspect.RootFS.Layers, -1)) - }) - }) - }) - - when("#AddLayerWithDiffID", func() { - it("appends a layer", func() { - repoName := newTestImageName() - - existingImage, err := local.NewImage(repoName, dockerClient) - h.AssertNil(t, err) - - oldLayerPath, err := h.CreateSingleFileLayerTar("/old-layer.txt", "old-layer", daemonOS) - h.AssertNil(t, err) - defer os.Remove(oldLayerPath) - - oldLayerDiffID := h.FileDiffID(t, oldLayerPath) - - h.AssertNil(t, existingImage.AddLayer(oldLayerPath)) - - h.AssertNil(t, existingImage.Save()) - - id, err := existingImage.Identifier() - h.AssertNil(t, err) - - existingImageID := id.String() - defer h.DockerRmi(dockerClient, existingImageID) - - img, err := local.NewImage( - repoName, - dockerClient, - local.FromBaseImage(repoName), - ) - h.AssertNil(t, err) - - newLayerPath, err := h.CreateSingleFileLayerTar("/new-layer.txt", "new-layer", daemonOS) - h.AssertNil(t, err) - defer os.Remove(newLayerPath) - - newLayerDiffID := h.FileDiffID(t, newLayerPath) - - h.AssertNil(t, img.AddLayerWithDiffID(newLayerPath, newLayerDiffID)) - h.AssertNil(t, img.Save()) - defer h.DockerRmi(dockerClient, repoName) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) - h.AssertNil(t, err) - - h.AssertEq(t, oldLayerDiffID, h.StringElementAt(inspect.RootFS.Layers, -2)) - h.AssertEq(t, newLayerDiffID, h.StringElementAt(inspect.RootFS.Layers, -1)) - }) - }) - - when("#AddLayerWithDiffIDAndHistory", func() { - it("appends a layer", func() { - repoName := newTestImageName() - - existingImage, err := local.NewImage(repoName, dockerClient) - h.AssertNil(t, err) - - oldLayerPath, err := h.CreateSingleFileLayerTar("/old-layer.txt", "old-layer", daemonOS) - h.AssertNil(t, err) - defer os.Remove(oldLayerPath) - - oldLayerDiffID := h.FileDiffID(t, oldLayerPath) - - h.AssertNil(t, existingImage.AddLayer(oldLayerPath)) - - h.AssertNil(t, existingImage.Save()) - - id, err := existingImage.Identifier() - h.AssertNil(t, err) - - existingImageID := id.String() - defer h.DockerRmi(dockerClient, existingImageID) - - img, err := local.NewImage( - repoName, - dockerClient, - local.FromBaseImage(repoName), - local.WithHistory(), - ) - h.AssertNil(t, err) - - newLayerPath, err := h.CreateSingleFileLayerTar("/new-layer.txt", "new-layer", daemonOS) - h.AssertNil(t, err) - defer os.Remove(newLayerPath) - - newLayerDiffID := h.FileDiffID(t, newLayerPath) - - oldHistory, err := img.History() - h.AssertNil(t, err) - addedHistory := v1.History{ - Author: "some-author", - Created: v1.Time{Time: imgutil.NormalizedDateTime}, - CreatedBy: "some-history", - Comment: "some-comment", - EmptyLayer: false, - } - err = img.AddLayerWithDiffIDAndHistory(newLayerPath, newLayerDiffID, addedHistory) - h.AssertNil(t, err) - - h.AssertNil(t, img.Save()) - - // check history - imageReportsHistory, err := img.History() - h.AssertNil(t, err) - h.AssertEq(t, len(imageReportsHistory), len(oldHistory)+1) - h.AssertEq(t, imageReportsHistory[len(imageReportsHistory)-1], addedHistory) - - daemonReportsHistory, err := dockerClient.ImageHistory(context.TODO(), repoName) - h.AssertNil(t, err) - h.AssertEq(t, len(imageReportsHistory), len(daemonReportsHistory)) - lastHistory := daemonReportsHistory[0] // the daemon reports history in reverse order - h.AssertEq(t, lastHistory.CreatedBy, "some-history") - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) - h.AssertNil(t, err) - - h.AssertEq(t, oldLayerDiffID, h.StringElementAt(inspect.RootFS.Layers, -2)) - h.AssertEq(t, newLayerDiffID, h.StringElementAt(inspect.RootFS.Layers, -1)) - }) - }) - - when("#GetLayer", func() { - when("the layer exists", func() { - var repoName = newTestImageName() - - it.Before(func() { - var err error - - existingImage, err := local.NewImage(repoName, dockerClient, local.FromBaseImage(runnableBaseImageName)) - h.AssertNil(t, err) - - layerPath, err := h.CreateSingleFileLayerTar("/file.txt", "file-contents", daemonOS) - h.AssertNil(t, err) - defer os.Remove(layerPath) - - h.AssertNil(t, existingImage.AddLayer(layerPath)) - - h.AssertNil(t, existingImage.Save()) - }) - - it.After(func() { - h.AssertNil(t, h.DockerRmi(dockerClient, repoName)) - }) - - when("the layer exists", func() { - it("returns a layer tar", func() { - img, err := local.NewImage(repoName, dockerClient, local.FromBaseImage(repoName)) - h.AssertNil(t, err) - - topLayer, err := img.TopLayer() - h.AssertNil(t, err) - - r, err := img.GetLayer(topLayer) - h.AssertNil(t, err) - tr := tar.NewReader(r) - - // continue until reader is at matching file - for { - header, err := tr.Next() - h.AssertNil(t, err) - - if strings.HasSuffix(header.Name, "/file.txt") { - break - } - } - - contents := make([]byte, len("file-contents")) - _, err = tr.Read(contents) - if err != io.EOF { - t.Fatalf("expected end of file: %x", err) - } - h.AssertEq(t, string(contents), "file-contents") - }) - }) - - when("the layer does not exist", func() { - it("returns an error", func() { - img, err := local.NewImage(repoName, dockerClient, local.FromBaseImage(repoName)) - h.AssertNil(t, err) - h.AssertNil(t, err) - _, err = img.GetLayer(someSHA) - h.AssertError( - t, - err, - fmt.Sprintf(`image %q does not contain layer with diff ID "%s"`, repoName, someSHA), - ) - }) - }) - }) - - when("image does NOT exist", func() { - it("returns error", func() { - image, err := local.NewImage("not-exist", dockerClient) - h.AssertNil(t, err) - - readCloser, err := image.GetLayer(someSHA) - h.AssertNil(t, readCloser) - h.AssertError(t, err, fmt.Sprintf("image %q does not contain layer with diff ID %q", "not-exist", someSHA)) - }) - }) - }) - - when("#ReuseLayer", func() { - var ( - prevImage imgutil.Image - prevImageName = newTestImageName() - repoName = newTestImageName() - prevLayer1SHA string - prevLayer2SHA string - layer1Path string - layer2Path string - ) - - it.Before(func() { - var err error - prevImage, err = local.NewImage( - prevImageName, - dockerClient, - local.FromBaseImage(runnableBaseImageName), - local.WithHistory(), - ) - h.AssertNil(t, err) - - layer1Path, err = h.CreateSingleFileLayerTar("/layer-1.txt", "old-layer-1", daemonOS) - h.AssertNil(t, err) - - layer2Path, err = h.CreateSingleFileLayerTar("/layer-2.txt", "old-layer-2", daemonOS) - h.AssertNil(t, err) - - h.AssertNil(t, prevImage.AddLayer(layer1Path)) - h.AssertNil(t, prevImage.AddLayer(layer2Path)) - - h.AssertNil(t, prevImage.Save()) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), prevImageName) - h.AssertNil(t, err) - - prevLayer1SHA = h.StringElementAt(inspect.RootFS.Layers, -2) - prevLayer2SHA = h.StringElementAt(inspect.RootFS.Layers, -1) - }) - - it.After(func() { - h.AssertNil(t, h.DockerRmi(dockerClient, repoName, prevImageName)) - h.AssertNil(t, os.RemoveAll(layer1Path)) - h.AssertNil(t, os.RemoveAll(layer2Path)) - }) - - it("reuses a layer", func() { - img, err := local.NewImage( - repoName, - dockerClient, - local.WithPreviousImage(prevImageName), - local.FromBaseImage(runnableBaseImageName), - ) - h.AssertNil(t, err) - - newLayer1Path, err := h.CreateSingleFileLayerTar("/new-base.txt", "base-content", daemonOS) - h.AssertNil(t, err) - defer os.Remove(newLayer1Path) - - h.AssertNil(t, img.AddLayer(newLayer1Path)) - - err = img.ReuseLayer(prevLayer2SHA) - h.AssertNil(t, err) - - h.AssertNil(t, img.Save()) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) - h.AssertNil(t, err) - - newLayer1SHA := h.StringElementAt(inspect.RootFS.Layers, -2) - reusedLayer2SHA := h.StringElementAt(inspect.RootFS.Layers, -1) - - h.AssertNotEq(t, prevLayer1SHA, newLayer1SHA) - h.AssertEq(t, prevLayer2SHA, reusedLayer2SHA) - }) - - it("does not download the old image if layers are directly above (performance)", func() { - // FIXME: npa: not sure this test validates what is claimed in the `it`; it looks like even the `local` package - // always downloads the previous image layers whenever `ReuseLayer` is called. - img, err := local.NewImage( - repoName, - dockerClient, - local.WithPreviousImage(prevImageName), - ) - h.AssertNil(t, err) - - err = img.ReuseLayer(prevLayer1SHA) - h.AssertNil(t, err) - - h.AssertNil(t, img.Save()) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) - h.AssertNil(t, err) - - if daemonOS == "windows" { - h.AssertEq(t, len(inspect.RootFS.Layers), 2) - } else { - h.AssertEq(t, len(inspect.RootFS.Layers), 1) - } - - newLayer1SHA := h.StringElementAt(inspect.RootFS.Layers, -1) - - h.AssertEq(t, prevLayer1SHA, newLayer1SHA) - }) - - when("there is history", func() { - var prevHistory []v1.History - - it.Before(func() { - // get the number of layers - baseImage, err := remote.NewImage( - repoName, - authn.DefaultKeychain, - remote.FromBaseImage(runnableBaseImageName), - ) - h.AssertNil(t, err) - layers, err := baseImage.UnderlyingImage().Layers() - h.AssertNil(t, err) - nLayers := len(layers) + 2 // added two layers in the test setup - // add history - prevHistory = make([]v1.History, nLayers) - for idx := range prevHistory { - prevHistory[idx].CreatedBy = fmt.Sprintf("some-history-%d", idx) - } - h.AssertNil(t, prevImage.SetHistory(prevHistory)) - h.AssertNil(t, prevImage.Save()) - }) - - it("reuses a layer with history", func() { - img, err := local.NewImage( - repoName, - dockerClient, - local.WithPreviousImage(prevImageName), - local.FromBaseImage(runnableBaseImageName), - local.WithHistory(), - ) - h.AssertNil(t, err) - - newBaseLayerPath, err := h.CreateSingleFileLayerTar("/new-base.txt", "base-content", daemonOS) - h.AssertNil(t, err) - defer os.Remove(newBaseLayerPath) - - h.AssertNil(t, img.AddLayer(newBaseLayerPath)) - - err = img.ReuseLayer(prevLayer2SHA) - h.AssertNil(t, err) - - h.AssertNil(t, img.Save()) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) - h.AssertNil(t, err) - - newLayer1SHA := h.StringElementAt(inspect.RootFS.Layers, -2) - reusedLayer2SHA := h.StringElementAt(inspect.RootFS.Layers, -1) - - h.AssertNotEq(t, prevLayer1SHA, newLayer1SHA) - h.AssertEq(t, prevLayer2SHA, reusedLayer2SHA) - - history, err := img.History() - h.AssertNil(t, err) - reusedLayer2History := history[len(history)-1] - newLayer1History := history[len(history)-2] - h.AssertEq(t, strings.Contains(reusedLayer2History.CreatedBy, "some-history-"), true) - h.AssertEq(t, newLayer1History, v1.History{Created: v1.Time{Time: imgutil.NormalizedDateTime}}) - }) - }) - }) - - when("#ReuseLayerWithHistory", func() { - var ( - prevImage imgutil.Image - prevImageName = newTestImageName() - repoName = newTestImageName() - prevLayer1SHA string - prevLayer2SHA string - ) - - it.Before(func() { - var err error - prevImage, err = local.NewImage( - prevImageName, - dockerClient, - local.FromBaseImage(runnableBaseImageName), - local.WithHistory(), - ) - h.AssertNil(t, err) - - layer1Path, err := h.CreateSingleFileLayerTar("/layer-1.txt", "old-layer-1", daemonOS) - h.AssertNil(t, err) - defer os.Remove(layer1Path) - - layer2Path, err := h.CreateSingleFileLayerTar("/layer-2.txt", "old-layer-2", daemonOS) - h.AssertNil(t, err) - defer os.Remove(layer2Path) - - h.AssertNil(t, prevImage.AddLayer(layer1Path)) - h.AssertNil(t, prevImage.AddLayer(layer2Path)) - - h.AssertNil(t, prevImage.Save()) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), prevImageName) - h.AssertNil(t, err) - - prevLayer1SHA = h.StringElementAt(inspect.RootFS.Layers, -2) - prevLayer2SHA = h.StringElementAt(inspect.RootFS.Layers, -1) - }) - - it.After(func() { - h.AssertNil(t, h.DockerRmi(dockerClient, repoName, prevImageName)) - }) - - it("reuses a layer with history", func() { - img, err := local.NewImage( - repoName, - dockerClient, - local.WithPreviousImage(prevImageName), - local.FromBaseImage(runnableBaseImageName), - local.WithHistory(), - ) - h.AssertNil(t, err) - - newBaseLayerPath, err := h.CreateSingleFileLayerTar("/new-base.txt", "base-content", daemonOS) - h.AssertNil(t, err) - defer os.Remove(newBaseLayerPath) - - h.AssertNil(t, img.AddLayer(newBaseLayerPath)) - - err = img.ReuseLayerWithHistory(prevLayer2SHA, v1.History{CreatedBy: "some-new-history"}) - h.AssertNil(t, err) - - h.AssertNil(t, img.Save()) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) - h.AssertNil(t, err) - - newLayer1SHA := h.StringElementAt(inspect.RootFS.Layers, -2) - reusedLayer2SHA := h.StringElementAt(inspect.RootFS.Layers, -1) - - h.AssertNotEq(t, prevLayer1SHA, newLayer1SHA) - h.AssertEq(t, prevLayer2SHA, reusedLayer2SHA) - - history, err := img.History() - h.AssertNil(t, err) - reusedLayer2History := history[len(history)-1] - newLayer1History := history[len(history)-2] - h.AssertEq(t, strings.Contains(reusedLayer2History.CreatedBy, "some-new-history"), true) - h.AssertEq(t, newLayer1History, v1.History{Created: v1.Time{Time: imgutil.NormalizedDateTime}}) - }) - }) - - when("#Save", func() { - when("image is valid", func() { - var ( - img imgutil.Image - origID string - tarPath string - repoName = newTestImageName() - ) - - it.Before(func() { - oldImage, err := local.NewImage(repoName, dockerClient) - h.AssertNil(t, err) - - h.AssertNil(t, oldImage.SetLabel("mykey", "oldValue")) - h.AssertNil(t, oldImage.Save()) - - origID = h.ImageID(t, repoName) - - img, err = local.NewImage(repoName, dockerClient, local.FromBaseImage(runnableBaseImageName)) - h.AssertNil(t, err) - - tarPath, err = h.CreateSingleFileLayerTar("/new-layer.txt", "new-layer", daemonOS) - h.AssertNil(t, err) - }) - - it.After(func() { - h.AssertNil(t, os.Remove(tarPath)) - h.AssertNil(t, h.DockerRmi(dockerClient, repoName)) - }) - - it("saved image overrides image with new ID", func() { - err := img.SetLabel("mykey", "newValue") - h.AssertNil(t, err) - - err = img.AddLayer(tarPath) - h.AssertNil(t, err) - - h.AssertNil(t, img.Save()) - - identifier, err := img.Identifier() - h.AssertNil(t, err) - - h.AssertEq(t, origID != identifier.String(), true) - - newImageID := h.ImageID(t, repoName) - h.AssertNotEq(t, origID, newImageID) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), identifier.String()) - h.AssertNil(t, err) - label := inspect.Config.Labels["mykey"] - h.AssertEq(t, strings.TrimSpace(label), "newValue") - }) - - it("zeroes times and client specific fields", func() { - err := img.SetLabel("mykey", "newValue") - h.AssertNil(t, err) - - err = img.AddLayer(tarPath) - h.AssertNil(t, err) - - h.AssertNil(t, img.Save()) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) - h.AssertNil(t, err) - - h.AssertEq(t, inspect.Created, imgutil.NormalizedDateTime.Format(time.RFC3339)) - h.AssertEq(t, inspect.Container, "") - - history, err := dockerClient.ImageHistory(context.TODO(), repoName) - h.AssertNil(t, err) - h.AssertEq(t, len(history), len(inspect.RootFS.Layers)) - for i := range inspect.RootFS.Layers { - h.AssertEq(t, history[i].Created, imgutil.NormalizedDateTime.Unix()) - } - }) - - when("the WithCreatedAt option is used", func() { - it("uses the value for all times and client specific fields", func() { - expectedTime := time.Date(2022, 1, 5, 5, 5, 5, 0, time.UTC) - img, err := local.NewImage(repoName, dockerClient, local.FromBaseImage(runnableBaseImageName), - local.WithCreatedAt(expectedTime), - ) - h.AssertNil(t, err) - - err = img.SetLabel("mykey", "newValue") - h.AssertNil(t, err) - - err = img.AddLayer(tarPath) - h.AssertNil(t, err) - - h.AssertNil(t, img.Save()) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) - h.AssertNil(t, err) - - h.AssertEq(t, inspect.Created, expectedTime.Format(time.RFC3339)) - h.AssertEq(t, inspect.Container, "") - - history, err := dockerClient.ImageHistory(context.TODO(), repoName) - h.AssertNil(t, err) - h.AssertEq(t, len(history), len(inspect.RootFS.Layers)) - for i := range inspect.RootFS.Layers { - h.AssertEq(t, history[i].Created, expectedTime.Unix()) - } - }) - }) - - when("additional names are provided", func() { - var ( - additionalRepoNames = []string{ - repoName + ":" + h.RandString(5), - newTestImageName(), - newTestImageName(), - } - successfulRepoNames = append([]string{repoName}, additionalRepoNames...) - ) - - it.After(func() { - h.AssertNil(t, h.DockerRmi(dockerClient, additionalRepoNames...)) - }) - - it("saves to multiple names", func() { - h.AssertNil(t, img.Save(additionalRepoNames...)) - - for _, n := range successfulRepoNames { - _, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), n) - h.AssertNil(t, err) - } - }) - - when("a single image name fails", func() { - it("returns results with errors for those that failed", func() { - failingName := newTestImageName() + ":🧨" - - err := img.Save(append([]string{failingName}, additionalRepoNames...)...) - h.AssertError(t, err, fmt.Sprintf("failed to write image to the following tags: [%s:", failingName)) - - saveErr, ok := err.(imgutil.SaveError) - h.AssertEq(t, ok, true) - h.AssertEq(t, len(saveErr.Errors), 1) - h.AssertEq(t, saveErr.Errors[0].ImageName, failingName) - h.AssertError(t, saveErr.Errors[0].Cause, "invalid reference format") - - for _, n := range successfulRepoNames { - _, _, err = dockerClient.ImageInspectWithRaw(context.TODO(), n) - h.AssertNil(t, err) - } - }) - }) - }) - }) - - when("invalid image content for daemon", func() { - it("returns errors from daemon", func() { - repoName := newTestImageName() - - invalidLayerTarFile, err := os.CreateTemp("", "daemon-error-test") - h.AssertNil(t, err) - defer func() { invalidLayerTarFile.Close(); os.Remove(invalidLayerTarFile.Name()) }() - - invalidLayerTarFile.Write([]byte("NOT A TAR")) - invalidLayerPath := invalidLayerTarFile.Name() - - img, err := local.NewImage(repoName, dockerClient) - h.AssertNil(t, err) - - err = img.AddLayer(invalidLayerPath) - h.AssertNil(t, err) - - err = img.Save() - h.AssertError(t, err, fmt.Sprintf("failed to write image to the following tags: [%s:", repoName)) - h.AssertError(t, err, "daemon response") - }) - }) - }) - - when("#SaveFile", func() { - var ( - img imgutil.Image - tarPath1 string - tarPath2 string - repoName = newTestImageName() - ) - - it.After(func() { - os.Remove(tarPath1) - os.Remove(tarPath2) - }) - - saveFileTest := func() { - h.AssertNil(t, img.AddLayer(tarPath1)) - h.AssertNil(t, img.AddLayer(tarPath2)) - - path, err := img.SaveFile() - h.AssertNil(t, err) - defer os.Remove(path) - - f, err := os.Open(path) - h.AssertNil(t, err) - defer f.Close() - - _, err = dockerClient.ImageLoad(context.TODO(), f, true) - h.AssertNil(t, err) - f.Close() - defer h.DockerRmi(dockerClient, img.Name()) - - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), img.Name()) - h.AssertNil(t, err) - - for _, diffID := range inspect.RootFS.Layers { - rc, err := img.GetLayer(diffID) - h.AssertNil(t, err) - rc.Close() - } - - f, err = os.Open(path) - h.AssertNil(t, err) - defer f.Close() - tr := tar.NewReader(f) - for { - hdr, err := tr.Next() - if err == io.EOF { - break - } - h.AssertNil(t, err) - h.AssertNotEq(t, strings.Contains(hdr.Name, "blank_"), true) - } - } - - when("no previous image or base image is configured", func() { - it.Before(func() { - var err error - - img, err = local.NewImage(repoName, dockerClient) - h.AssertNil(t, err) - - tarPath1, err = h.CreateSingleFileLayerTar("/foo", "foo", daemonOS) - h.AssertNil(t, err) - - tarPath2, err = h.CreateSingleFileLayerTar("/bar", "bar", daemonOS) - h.AssertNil(t, err) - }) - - it("creates an archive that can be imported and has correct diffIDs", saveFileTest) - }) - - when("previous image is configured and layers are reused", func() { - it.Before(func() { - var err error - - prevImg, err := local.NewImage(newTestImageName(), dockerClient) - h.AssertNil(t, err) - - prevImgBase, err := h.CreateSingleFileLayerTar("/root", "root", daemonOS) - h.AssertNil(t, err) - - h.AssertNil(t, prevImg.AddLayer(prevImgBase)) - h.AssertNil(t, prevImg.Save()) - defer h.DockerRmi(dockerClient, prevImg.Name()) - - img, err = local.NewImage(repoName, dockerClient, local.WithPreviousImage(prevImg.Name())) - h.AssertNil(t, err) - - prevImgTopLayer, err := prevImg.TopLayer() - h.AssertNil(t, err) - - err = img.ReuseLayer(prevImgTopLayer) - h.AssertNil(t, err) - - tarPath1, err = h.CreateSingleFileLayerTar("/foo", "foo", daemonOS) - h.AssertNil(t, err) - - tarPath2, err = h.CreateSingleFileLayerTar("/bar", "bar", daemonOS) - h.AssertNil(t, err) - }) - - it("creates an archive that can be imported and has correct diffIDs", saveFileTest) - }) - - when("base image is configured", func() { - it.Before(func() { - var err error - - img, err = local.NewImage(repoName, dockerClient, local.FromBaseImage(runnableBaseImageName)) - h.AssertNil(t, err) - - tarPath1, err = h.CreateSingleFileLayerTar("/foo", "foo", daemonOS) - h.AssertNil(t, err) - - tarPath2, err = h.CreateSingleFileLayerTar("/bar", "bar", daemonOS) - h.AssertNil(t, err) - }) - - it("creates an archive that can be imported and has correct diffIDs", saveFileTest) - }) - }) - - when("#Found", func() { - when("it exists", func() { - var repoName = newTestImageName() - - it.Before(func() { - existingImage, err := local.NewImage(repoName, dockerClient) - h.AssertNil(t, err) - h.AssertNil(t, existingImage.Save()) - }) - - it.After(func() { - h.DockerRmi(dockerClient, repoName) - }) - - it("returns true, nil", func() { - image, err := local.NewImage(repoName, dockerClient, local.FromBaseImage(repoName)) - h.AssertNil(t, err) - - h.AssertEq(t, image.Found(), true) - }) - }) - - when("it does not exist", func() { - it("returns false, nil", func() { - image, err := local.NewImage(newTestImageName(), dockerClient) - h.AssertNil(t, err) - - h.AssertEq(t, image.Found(), false) - }) - }) - }) - - when("#Delete", func() { - when("the image does not exist", func() { - it("should not error", func() { - img, err := local.NewImage("image-does-not-exist", dockerClient) - h.AssertNil(t, err) - - h.AssertNil(t, img.Delete()) - }) - }) - - when("the image does exist", func() { - var ( - origImg imgutil.Image - origID string - repoName = newTestImageName() - ) - - it.Before(func() { - existingImage, err := local.NewImage(repoName, dockerClient) - h.AssertNil(t, err) - h.AssertNil(t, existingImage.SetLabel("some", "label")) - h.AssertNil(t, existingImage.Save()) - - origImg, err = local.NewImage(repoName, dockerClient, local.FromBaseImage(repoName)) - h.AssertNil(t, err) - - origID = h.ImageID(t, repoName) - }) - - it("should delete the image", func() { - h.AssertEq(t, origImg.Found(), true) - - h.AssertNil(t, origImg.Delete()) - - img, err := local.NewImage(origID, dockerClient) - h.AssertNil(t, err) - - h.AssertEq(t, img.Found(), false) - }) - - when("the image has been re-tagged", func() { - const newTag = "different-tag" - - it.Before(func() { - h.AssertNil(t, dockerClient.ImageTag(context.TODO(), origImg.Name(), newTag)) - - _, err := dockerClient.ImageRemove(context.TODO(), origImg.Name(), types.ImageRemoveOptions{}) - h.AssertNil(t, err) - }) - - it("should delete the image", func() { - h.AssertEq(t, origImg.Found(), true) - - h.AssertNil(t, origImg.Delete()) - - origImg, err := local.NewImage(newTag, dockerClient) - h.AssertNil(t, err) - - h.AssertEq(t, origImg.Found(), false) - }) - }) - }) - }) -} diff --git a/locallayout/new.go b/locallayout/new.go deleted file mode 100644 index 1ca41033..00000000 --- a/locallayout/new.go +++ /dev/null @@ -1,136 +0,0 @@ -package locallayout - -import ( - "context" - "fmt" - "sync" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/client" - v1 "github.com/google/go-containerregistry/pkg/v1" - - "github.com/buildpacks/imgutil" -) - -// NewImage returns a new image that can be modified and saved to a docker daemon -// via a tarball in legacy format. -func NewImage(repoName string, dockerClient DockerClient, ops ...func(*imgutil.ImageOptions)) (imgutil.Image, error) { - options := &imgutil.ImageOptions{} - for _, op := range ops { - op(options) - } - - var err error - options.Platform, err = processDefaultPlatformOption(options.Platform, dockerClient) - if err != nil { - return nil, err - } - - processPrevious, err := processImageOption(options.PreviousImageRepoName, dockerClient, true) - if err != nil { - return nil, err - } - if processPrevious.image != nil { - options.PreviousImage = processPrevious.image - } - - var ( - baseIdentifier string - store *Store - ) - processBase, err := processImageOption(options.BaseImageRepoName, dockerClient, false) - if err != nil { - return nil, err - } - if processBase.image != nil { - options.BaseImage = processBase.image - baseIdentifier = processBase.identifier - store = processBase.layerStore - } else { - store = &Store{dockerClient: dockerClient, downloadOnce: &sync.Once{}} - } - - cnbImage, err := imgutil.NewCNBImage(*options) - if err != nil { - return nil, err - } - - return &Image{ - CNBImageCore: cnbImage, - repoName: repoName, - store: store, - lastIdentifier: baseIdentifier, - daemonOS: options.Platform.OS, - }, nil -} - -func processDefaultPlatformOption(requestedPlatform imgutil.Platform, dockerClient DockerClient) (imgutil.Platform, error) { - dockerPlatform, err := defaultPlatform(dockerClient) - if err != nil { - return imgutil.Platform{}, err - } - if (requestedPlatform == imgutil.Platform{}) { - return dockerPlatform, nil - } - if requestedPlatform.OS != "" && requestedPlatform.OS != dockerPlatform.OS { - return imgutil.Platform{}, - fmt.Errorf("invalid os: platform os %q must match the daemon os %q", requestedPlatform.OS, dockerPlatform.OS) - } - return requestedPlatform, nil -} - -func defaultPlatform(dockerClient DockerClient) (imgutil.Platform, error) { - daemonInfo, err := dockerClient.ServerVersion(context.Background()) - if err != nil { - return imgutil.Platform{}, err - } - return imgutil.Platform{ - OS: daemonInfo.Os, - Architecture: daemonInfo.Arch, - }, nil -} - -type imageResult struct { - image v1.Image - identifier string - layerStore *Store -} - -func processImageOption(repoName string, dockerClient DockerClient, downloadLayersOnAccess bool) (imageResult, error) { - if repoName == "" { - return imageResult{}, nil - } - inspect, history, err := getInspectAndHistory(repoName, dockerClient) - if err != nil { - return imageResult{}, err - } - if inspect == nil { - return imageResult{}, nil - } - layerStore := &Store{dockerClient: dockerClient, downloadOnce: &sync.Once{}} - image, err := newV1ImageFacadeFromInspect(*inspect, history, layerStore, downloadLayersOnAccess) - if err != nil { - return imageResult{}, err - } - return imageResult{ - image: image, - identifier: inspect.ID, - layerStore: layerStore, - }, nil -} - -func getInspectAndHistory(repoName string, dockerClient DockerClient) (*types.ImageInspect, []image.HistoryResponseItem, error) { - inspect, _, err := dockerClient.ImageInspectWithRaw(context.Background(), repoName) - if err != nil { - if client.IsErrNotFound(err) { - return nil, nil, nil - } - return nil, nil, fmt.Errorf("inspecting image %q: %w", repoName, err) - } - history, err := dockerClient.ImageHistory(context.Background(), repoName) - if err != nil { - return nil, nil, fmt.Errorf("get history for image %q: %w", repoName, err) - } - return &inspect, history, nil -} diff --git a/locallayout/options.go b/locallayout/options.go deleted file mode 100644 index 5a54dfc7..00000000 --- a/locallayout/options.go +++ /dev/null @@ -1,51 +0,0 @@ -package locallayout - -import ( - "time" - - v1 "github.com/google/go-containerregistry/pkg/v1" - - "github.com/buildpacks/imgutil" -) - -func FromBaseImage(name string) func(*imgutil.ImageOptions) { - return func(o *imgutil.ImageOptions) { - o.BaseImageRepoName = name - } -} - -func WithConfig(c *v1.Config) func(*imgutil.ImageOptions) { - return func(o *imgutil.ImageOptions) { - o.Config = c - } -} - -func WithCreatedAt(t time.Time) func(*imgutil.ImageOptions) { - return func(o *imgutil.ImageOptions) { - o.CreatedAt = t - } -} - -func WithDefaultPlatform(p imgutil.Platform) func(*imgutil.ImageOptions) { - return func(o *imgutil.ImageOptions) { - o.Platform = p - } -} - -func WithHistory() func(*imgutil.ImageOptions) { - return func(o *imgutil.ImageOptions) { - o.PreserveHistory = true - } -} - -func WithPreviousImage(name string) func(*imgutil.ImageOptions) { - return func(o *imgutil.ImageOptions) { - o.PreviousImageRepoName = name - } -} - -func WithMediaTypes(m imgutil.MediaTypes) func(*imgutil.ImageOptions) { - return func(o *imgutil.ImageOptions) { - o.MediaTypes = m - } -}