Adds ability to squash image after build

Allow built images to be squash to scratch.
Squashing does not destroy any images or layers, and preserves the
build cache.

Introduce a new CLI argument --squash to docker build
Introduce a new param to the build API endpoint `squash`

Once the build is complete, docker creates a new image loading the diffs
from each layer into a single new layer and references all the parent's
layers.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
This commit is contained in:
Brian Goff 2016-04-21 12:08:37 -04:00
parent 2fc760496b
commit 362369b4bb
21 changed files with 330 additions and 23 deletions

View File

@ -54,6 +54,7 @@ func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBui
options.NetworkMode = r.FormValue("networkmode")
options.Tags = r.Form["t"]
options.SecurityOpt = r.Form["securityopt"]
options.Squash = httputils.BoolValue(r, "squash")
if r.Form.Get("shmsize") != "" {
shmSize, err := strconv.ParseInt(r.Form.Get("shmsize"), 10, 64)

View File

@ -135,9 +135,15 @@ type Backend interface {
// TODO: make an Extract method instead of passing `decompress`
// TODO: do not pass a FileInfo, instead refactor the archive package to export a Walk function that can be used
// with Context.Walk
//ContainerCopy(name string, res string) (io.ReadCloser, error)
// ContainerCopy(name string, res string) (io.ReadCloser, error)
// TODO: use copyBackend api
CopyOnBuild(containerID string, destPath string, src FileInfo, decompress bool) error
// HasExperimental checks if the backend supports experimental features
HasExperimental() bool
// SquashImage squashes the fs layers from the provided image down to the specified `to` image
SquashImage(from string, to string) (string, error)
}
// Image represents a Docker image used by the builder.

View File

@ -10,6 +10,7 @@ import (
"strings"
"github.com/Sirupsen/logrus"
apierrors "github.com/docker/docker/api/errors"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/backend"
"github.com/docker/docker/api/types/container"
@ -18,6 +19,7 @@ import (
"github.com/docker/docker/image"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/reference"
perrors "github.com/pkg/errors"
"golang.org/x/net/context"
)
@ -77,6 +79,7 @@ type Builder struct {
id string
imageCache builder.ImageCache
from builder.Image
}
// BuildManager implements builder.Backend and is shared across all Builder objects.
@ -91,6 +94,9 @@ func NewBuildManager(b builder.Backend) (bm *BuildManager) {
// BuildFromContext builds a new image from a given context.
func (bm *BuildManager) BuildFromContext(ctx context.Context, src io.ReadCloser, remote string, buildOptions *types.ImageBuildOptions, pg backend.ProgressWriter) (string, error) {
if buildOptions.Squash && !bm.backend.HasExperimental() {
return "", apierrors.NewBadRequestError(errors.New("squash is only supported with experimental mode"))
}
buildContext, dockerfileName, err := builder.DetectContextFromRemoteURL(src, remote, pg.ProgressReaderFunc)
if err != nil {
return "", err
@ -100,6 +106,7 @@ func (bm *BuildManager) BuildFromContext(ctx context.Context, src io.ReadCloser,
logrus.Debugf("[BUILDER] failed to remove temporary context: %v", err)
}
}()
if len(dockerfileName) > 0 {
buildOptions.Dockerfile = dockerfileName
}
@ -286,6 +293,17 @@ func (b *Builder) build(stdout io.Writer, stderr io.Writer, out io.Writer) (stri
return "", fmt.Errorf("No image was generated. Is your Dockerfile empty?")
}
if b.options.Squash {
var fromID string
if b.from != nil {
fromID = b.from.ImageID()
}
b.image, err = b.docker.SquashImage(b.image, fromID)
if err != nil {
return "", perrors.Wrap(err, "error squashing image")
}
}
imageID := image.ID(b.image)
for _, rt := range repoAndTags {
if err := b.docker.TagImageWithReference(imageID, rt); err != nil {

View File

@ -221,6 +221,7 @@ func from(b *Builder, args []string, attributes map[string]bool, original string
}
}
}
b.from = image
return b.processImageFrom(image)
}

View File

@ -59,6 +59,7 @@ type buildOptions struct {
compress bool
securityOpt []string
networkMode string
squash bool
}
// NewBuildCommand creates a new `docker build` command
@ -110,6 +111,10 @@ func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command {
command.AddTrustedFlags(flags, true)
if dockerCli.HasExperimental() {
flags.BoolVar(&options.squash, "squash", false, "Squash newly built layers into a single new layer")
}
return cmd
}
@ -305,6 +310,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
CacheFrom: options.cacheFrom,
SecurityOpt: options.securityOpt,
NetworkMode: options.networkMode,
Squash: options.squash,
}
response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions)

View File

@ -74,6 +74,7 @@ type Driver struct {
ctr *graphdriver.RefCounter
pathCacheLock sync.Mutex
pathCache map[string]string
naiveDiff graphdriver.DiffDriver
}
// Init returns a new AUFS driver.
@ -137,6 +138,8 @@ func Init(root string, options []string, uidMaps, gidMaps []idtools.IDMap) (grap
return nil, err
}
}
a.naiveDiff = graphdriver.NewNaiveDiffDriver(a, uidMaps, gidMaps)
return a, nil
}
@ -225,7 +228,7 @@ func (a *Driver) Create(id, parent, mountLabel string, storageOpt map[string]str
defer f.Close()
if parent != "" {
ids, err := getParentIds(a.rootPath(), parent)
ids, err := getParentIDs(a.rootPath(), parent)
if err != nil {
return err
}
@ -427,9 +430,22 @@ func (a *Driver) Put(id string) error {
return err
}
// isParent returns if the passed in parent is the direct parent of the passed in layer
func (a *Driver) isParent(id, parent string) bool {
parents, _ := getParentIDs(a.rootPath(), id)
if parent == "" && len(parents) > 0 {
return false
}
return !(len(parents) > 0 && parent != parents[0])
}
// Diff produces an archive of the changes between the specified
// layer and its parent layer which may be "".
func (a *Driver) Diff(id, parent string) (io.ReadCloser, error) {
if !a.isParent(id, parent) {
return a.naiveDiff.Diff(id, parent)
}
// AUFS doesn't need the parent layer to produce a diff.
return archive.TarWithOptions(path.Join(a.rootPath(), "diff", id), &archive.TarOptions{
Compression: archive.Uncompressed,
@ -465,6 +481,9 @@ func (a *Driver) applyDiff(id string, diff io.Reader) error {
// and its parent and returns the size in bytes of the changes
// relative to its base filesystem directory.
func (a *Driver) DiffSize(id, parent string) (size int64, err error) {
if !a.isParent(id, parent) {
return a.naiveDiff.DiffSize(id, parent)
}
// AUFS doesn't need the parent layer to calculate the diff size.
return directory.Size(path.Join(a.rootPath(), "diff", id))
}
@ -473,7 +492,11 @@ func (a *Driver) DiffSize(id, parent string) (size int64, err error) {
// layer with the specified id and parent, returning the size of the
// new layer in bytes.
func (a *Driver) ApplyDiff(id, parent string, diff io.Reader) (size int64, err error) {
// AUFS doesn't need the parent id to apply the diff.
if !a.isParent(id, parent) {
return a.naiveDiff.ApplyDiff(id, parent, diff)
}
// AUFS doesn't need the parent id to apply the diff if it is the direct parent.
if err = a.applyDiff(id, diff); err != nil {
return
}
@ -484,6 +507,10 @@ func (a *Driver) ApplyDiff(id, parent string, diff io.Reader) (size int64, err e
// Changes produces a list of changes between the specified layer
// and its parent layer. If parent is "", then all changes will be ADD changes.
func (a *Driver) Changes(id, parent string) ([]archive.Change, error) {
if !a.isParent(id, parent) {
return a.naiveDiff.Changes(id, parent)
}
// AUFS doesn't have snapshots, so we need to get changes from all parent
// layers.
layers, err := a.getParentLayerPaths(id)
@ -494,7 +521,7 @@ func (a *Driver) Changes(id, parent string) ([]archive.Change, error) {
}
func (a *Driver) getParentLayerPaths(id string) ([]string, error) {
parentIds, err := getParentIds(a.rootPath(), id)
parentIds, err := getParentIDs(a.rootPath(), id)
if err != nil {
return nil, err
}

View File

@ -424,7 +424,7 @@ func TestChanges(t *testing.T) {
t.Fatal(err)
}
changes, err = d.Changes("3", "")
changes, err = d.Changes("3", "2")
if err != nil {
t.Fatal(err)
}
@ -530,7 +530,7 @@ func TestChildDiffSize(t *testing.T) {
t.Fatal(err)
}
diffSize, err = d.DiffSize("2", "")
diffSize, err = d.DiffSize("2", "1")
if err != nil {
t.Fatal(err)
}

View File

@ -29,7 +29,7 @@ func loadIds(root string) ([]string, error) {
//
// If there are no lines in the file then the id has no parent
// and an empty slice is returned.
func getParentIds(root, id string) ([]string, error) {
func getParentIDs(root, id string) ([]string, error) {
f, err := os.Open(path.Join(root, "layers", id))
if err != nil {
return nil, err

View File

@ -78,9 +78,8 @@ type ProtoDriver interface {
Cleanup() error
}
// Driver is the interface for layered/snapshot file system drivers.
type Driver interface {
ProtoDriver
// DiffDriver is the interface to use to implement graph diffs
type DiffDriver interface {
// Diff produces an archive of the changes between the specified
// layer and its parent layer which may be "".
Diff(id, parent string) (io.ReadCloser, error)
@ -98,6 +97,12 @@ type Driver interface {
DiffSize(id, parent string) (size int64, err error)
}
// Driver is the interface for layered/snapshot file system drivers.
type Driver interface {
ProtoDriver
DiffDriver
}
// DiffGetterDriver is the interface for layered file system drivers that
// provide a specialized function for getting file contents for tar-split.
type DiffGetterDriver interface {

View File

@ -11,6 +11,7 @@ import (
"os"
"os/exec"
"path"
"path/filepath"
"strconv"
"strings"
"syscall"
@ -44,7 +45,7 @@ var (
// Each container/image has at least a "diff" directory and "link" file.
// If there is also a "lower" file when there are diff layers
// below as well as "merged" and "work" directories. The "diff" directory
// below as well as "merged" and "work" directories. The "diff" directory
// has the upper layer of the overlay and is used to capture any
// changes to the layer. The "lower" file contains all the lower layer
// mounts separated by ":" and ordered from uppermost to lowermost
@ -86,12 +87,13 @@ type overlayOptions struct {
// Driver contains information about the home directory and the list of active mounts that are created using this driver.
type Driver struct {
home string
uidMaps []idtools.IDMap
gidMaps []idtools.IDMap
ctr *graphdriver.RefCounter
quotaCtl *quota.Control
options overlayOptions
home string
uidMaps []idtools.IDMap
gidMaps []idtools.IDMap
ctr *graphdriver.RefCounter
quotaCtl *quota.Control
options overlayOptions
naiveDiff graphdriver.DiffDriver
}
var (
@ -163,6 +165,8 @@ func Init(home string, options []string, uidMaps, gidMaps []idtools.IDMap) (grap
ctr: graphdriver.NewRefCounter(graphdriver.NewFsChecker(graphdriver.FsMagicOverlay)),
}
d.naiveDiff = graphdriver.NewNaiveDiffDriver(d, uidMaps, gidMaps)
if backingFs == "xfs" {
// Try to enable project quota support over xfs.
if d.quotaCtl, err = quota.NewControl(home); err == nil {
@ -525,7 +529,7 @@ func (d *Driver) Put(id string) error {
return nil
}
if err := syscall.Unmount(mountpoint, 0); err != nil {
logrus.Debugf("Failed to unmount %s overlay: %v", id, err)
logrus.Debugf("Failed to unmount %s overlay: %s - %v", id, mountpoint, err)
}
return nil
}
@ -536,8 +540,33 @@ func (d *Driver) Exists(id string) bool {
return err == nil
}
// isParent returns if the passed in parent is the direct parent of the passed in layer
func (d *Driver) isParent(id, parent string) bool {
lowers, err := d.getLowerDirs(id)
if err != nil {
return false
}
if parent == "" && len(lowers) > 0 {
return false
}
parentDir := d.dir(parent)
var ld string
if len(lowers) > 0 {
ld = filepath.Dir(lowers[0])
}
if ld == "" && parent == "" {
return true
}
return ld == parentDir
}
// ApplyDiff applies the new layer into a root
func (d *Driver) ApplyDiff(id string, parent string, diff io.Reader) (size int64, err error) {
if !d.isParent(id, parent) {
return d.naiveDiff.ApplyDiff(id, parent, diff)
}
applyDir := d.getDiffPath(id)
logrus.Debugf("Applying tar in %s", applyDir)
@ -563,12 +592,19 @@ func (d *Driver) getDiffPath(id string) string {
// and its parent and returns the size in bytes of the changes
// relative to its base filesystem directory.
func (d *Driver) DiffSize(id, parent string) (size int64, err error) {
if !d.isParent(id, parent) {
return d.naiveDiff.DiffSize(id, parent)
}
return directory.Size(d.getDiffPath(id))
}
// Diff produces an archive of the changes between the specified
// layer and its parent layer which may be "".
func (d *Driver) Diff(id, parent string) (io.ReadCloser, error) {
if !d.isParent(id, parent) {
return d.naiveDiff.Diff(id, parent)
}
diffPath := d.getDiffPath(id)
logrus.Debugf("Tar with options on %s", diffPath)
return archive.TarWithOptions(diffPath, &archive.TarOptions{
@ -582,6 +618,9 @@ func (d *Driver) Diff(id, parent string) (io.ReadCloser, error) {
// Changes produces a list of changes between the specified layer
// and its parent layer. If parent is "", then all changes will be ADD changes.
func (d *Driver) Changes(id, parent string) ([]archive.Change, error) {
if !d.isParent(id, parent) {
return d.naiveDiff.Changes(id, parent)
}
// Overlay doesn't have snapshots, so we need to get changes from all parent
// layers.
diffPath := d.getDiffPath(id)

View File

@ -1,9 +1,13 @@
package daemon
import (
"encoding/json"
"fmt"
"path"
"sort"
"time"
"github.com/pkg/errors"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
@ -241,6 +245,89 @@ func (daemon *Daemon) Images(filterArgs, filter string, all bool, withExtraAttrs
return images, nil
}
// SquashImage creates a new image with the diff of the specified image and the specified parent.
// This new image contains only the layers from it's parent + 1 extra layer which contains the diff of all the layers in between.
// The existing image(s) is not destroyed.
// If no parent is specified, a new image with the diff of all the specified image's layers merged into a new layer that has no parents.
func (daemon *Daemon) SquashImage(id, parent string) (string, error) {
img, err := daemon.imageStore.Get(image.ID(id))
if err != nil {
return "", err
}
var parentImg *image.Image
var parentChainID layer.ChainID
if len(parent) != 0 {
parentImg, err = daemon.imageStore.Get(image.ID(parent))
if err != nil {
return "", errors.Wrap(err, "error getting specified parent layer")
}
parentChainID = parentImg.RootFS.ChainID()
} else {
rootFS := image.NewRootFS()
parentImg = &image.Image{RootFS: rootFS}
}
l, err := daemon.layerStore.Get(img.RootFS.ChainID())
if err != nil {
return "", errors.Wrap(err, "error getting image layer")
}
defer daemon.layerStore.Release(l)
ts, err := l.TarStreamFrom(parentChainID)
if err != nil {
return "", errors.Wrapf(err, "error getting tar stream to parent")
}
defer ts.Close()
newL, err := daemon.layerStore.Register(ts, parentChainID)
if err != nil {
return "", errors.Wrap(err, "error registering layer")
}
defer daemon.layerStore.Release(newL)
var newImage image.Image
newImage = *img
newImage.RootFS = nil
var rootFS image.RootFS
rootFS = *parentImg.RootFS
rootFS.DiffIDs = append(rootFS.DiffIDs, newL.DiffID())
newImage.RootFS = &rootFS
for i, hi := range newImage.History {
if i >= len(parentImg.History) {
hi.EmptyLayer = true
}
newImage.History[i] = hi
}
now := time.Now()
var historyComment string
if len(parent) > 0 {
historyComment = fmt.Sprintf("merge %s to %s", id, parent)
} else {
historyComment = fmt.Sprintf("create new from %s", id)
}
newImage.History = append(newImage.History, image.History{
Created: now,
Comment: historyComment,
})
newImage.Created = now
b, err := json.Marshal(&newImage)
if err != nil {
return "", errors.Wrap(err, "error marshalling image config")
}
newImgID, err := daemon.imageStore.Create(b)
if err != nil {
return "", errors.Wrap(err, "error creating new image after squash")
}
return string(newImgID), nil
}
func newImage(image *image.Image, virtualSize int64) *types.ImageSummary {
newImage := new(types.ImageSummary)
newImage.ParentID = image.Parent.String()

View File

@ -3,6 +3,7 @@ package xfer
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"runtime"
@ -31,6 +32,10 @@ func (ml *mockLayer) TarStream() (io.ReadCloser, error) {
return ioutil.NopCloser(bytes.NewBuffer(ml.layerData.Bytes())), nil
}
func (ml *mockLayer) TarStreamFrom(layer.ChainID) (io.ReadCloser, error) {
return nil, fmt.Errorf("not implemented")
}
func (ml *mockLayer) ChainID() layer.ChainID {
return ml.chainID
}

View File

@ -1800,6 +1800,7 @@ or being killed.
variable expansion in other Dockerfile instructions. This is not meant for
passing secret values. [Read more about the buildargs instruction](../../reference/builder.md#arg)
- **shmsize** - Size of `/dev/shm` in bytes. The size must be greater than 0. If omitted the system uses 64MB.
- **squash** - squash the resulting images layers into a single layer (boolean) **Experimental Only**
- **labels** JSON map of string pairs for labels to set on the image.
- **networkmode** - Sets the networking mode for the run commands during
build. Supported standard values are: `bridge`, `host`, `none`, and

View File

@ -54,6 +54,7 @@ Options:
The format is `<number><unit>`. `number` must be greater than `0`.
Unit is optional and can be `b` (bytes), `k` (kilobytes), `m` (megabytes),
or `g` (gigabytes). If you omit the unit, the system uses bytes.
--squash Squash newly built layers into a single new layer (**Experimental Only**)
-t, --tag value Name and optionally a tag in the 'name:tag' format (default [])
--ulimit value Ulimit options (default [])
```
@ -432,3 +433,20 @@ Linux namespaces. On Microsoft Windows, you can specify these values:
| `hyperv` | Hyper-V hypervisor partition-based isolation. |
Specifying the `--isolation` flag without a value is the same as setting `--isolation="default"`.
### Squash an image's layers (--squash) **Experimental Only**
Once the image is built, squash the new layers into a new image with a single
new layer. Squashing does not destroy any existing image, rather it creates a new
image with the content of the squshed layers. This effectively makes it look
like all `Dockerfile` commands were created with a single layer. The build
cache is preserved with this method.
**Note**: using this option means the new image will not be able to take
advantage of layer sharing with other images and may use significantly more
space.
**Note**: using this option you may see significantly more space used due to
storing two copies of the image, one for the build cache with all the cache
layers in tact, and one for the squashed version.

View File

@ -7195,3 +7195,44 @@ RUN ["cat", "/foo/file"]
c.Fatal(err)
}
}
func (s *DockerSuite) TestBuildSquashParent(c *check.C) {
testRequires(c, ExperimentalDaemon)
dockerFile := `
FROM busybox
RUN echo hello > /hello
RUN echo world >> /hello
RUN echo hello > /remove_me
ENV HELLO world
RUN rm /remove_me
`
// build and get the ID that we can use later for history comparison
origID, err := buildImage("test", dockerFile, false)
c.Assert(err, checker.IsNil)
// build with squash
id, err := buildImage("test", dockerFile, true, "--squash")
c.Assert(err, checker.IsNil)
out, _ := dockerCmd(c, "run", "--rm", id, "/bin/sh", "-c", "cat /hello")
c.Assert(strings.TrimSpace(out), checker.Equals, "hello\nworld")
dockerCmd(c, "run", "--rm", id, "/bin/sh", "-c", "[ ! -f /remove_me ]")
dockerCmd(c, "run", "--rm", id, "/bin/sh", "-c", `[ "$(echo $HELLO)" == "world" ]`)
// make sure the ID produced is the ID of the tag we specified
inspectID, err := inspectImage("test", ".ID")
c.Assert(err, checker.IsNil)
c.Assert(inspectID, checker.Equals, id)
origHistory, _ := dockerCmd(c, "history", origID)
testHistory, _ := dockerCmd(c, "history", "test")
splitOrigHistory := strings.Split(strings.TrimSpace(origHistory), "\n")
splitTestHistory := strings.Split(strings.TrimSpace(testHistory), "\n")
c.Assert(len(splitTestHistory), checker.Equals, len(splitOrigHistory)+1)
out, err = inspectImage(id, "len .RootFS.Layers")
c.Assert(err, checker.IsNil)
c.Assert(strings.TrimSpace(out), checker.Equals, "3")
}

View File

@ -3,6 +3,7 @@ package layer
import (
"archive/tar"
"bytes"
"fmt"
"io"
"io/ioutil"
)
@ -23,6 +24,10 @@ func (el *emptyLayer) TarStream() (io.ReadCloser, error) {
return ioutil.NopCloser(buf), nil
}
func (el *emptyLayer) TarStreamFrom(ChainID) (io.ReadCloser, error) {
return nil, fmt.Errorf("can't get parent tar stream of an empty layer")
}
func (el *emptyLayer) ChainID() ChainID {
return ChainID(DigestSHA256EmptyTar)
}

View File

@ -78,6 +78,9 @@ type TarStreamer interface {
// TarStream returns a tar archive stream
// for the contents of a layer.
TarStream() (io.ReadCloser, error)
// TarStreamFrom returns a tar archive stream for all the layer chain with
// arbitrary depth.
TarStreamFrom(ChainID) (io.ReadCloser, error)
}
// Layer represents a read-only layer

View File

@ -1,6 +1,7 @@
package layer
import (
"fmt"
"io"
"github.com/docker/docker/pkg/archive"
@ -28,11 +29,14 @@ func (ml *mountedLayer) cacheParent() string {
}
func (ml *mountedLayer) TarStream() (io.ReadCloser, error) {
archiver, err := ml.layerStore.driver.Diff(ml.mountID, ml.cacheParent())
if err != nil {
return nil, err
}
return archiver, nil
return ml.layerStore.driver.Diff(ml.mountID, ml.cacheParent())
}
func (ml *mountedLayer) TarStreamFrom(parent ChainID) (io.ReadCloser, error) {
// Not supported since this will include the init layer as well
// This can already be acheived with mount + tar.
// Should probably never reach this point, but error out here.
return nil, fmt.Errorf("getting a layer diff from an arbitrary parent is not supported on mounted layer")
}
func (ml *mountedLayer) Name() string {

View File

@ -21,6 +21,8 @@ type roLayer struct {
references map[Layer]struct{}
}
// TarStream for roLayer guarentees that the data that is produced is the exact
// data that the layer was registered with.
func (rl *roLayer) TarStream() (io.ReadCloser, error) {
r, err := rl.layerStore.store.TarSplitReader(rl.chainID)
if err != nil {
@ -43,6 +45,24 @@ func (rl *roLayer) TarStream() (io.ReadCloser, error) {
return rc, nil
}
// TarStreamFrom does not make any guarentees to the correctness of the produced
// data. As such it should not be used when the layer content must be verified
// to be an exact match to the registered layer.
func (rl *roLayer) TarStreamFrom(parent ChainID) (io.ReadCloser, error) {
var parentCacheID string
for pl := rl.parent; pl != nil; pl = pl.parent {
if pl.chainID == parent {
parentCacheID = pl.cacheID
break
}
}
if parent != ChainID("") && parentCacheID == "" {
return nil, fmt.Errorf("layer ID '%s' is not a parent of the specified layer: cannot provide diff to non-parent", parent)
}
return rl.layerStore.driver.Diff(rl.cacheID, parentCacheID)
}
func (rl *roLayer) ChainID() ChainID {
return rl.chainID
}

View File

@ -11,6 +11,7 @@ docker-build - Build a new image from the source code at PATH
[**--cgroup-parent**[=*CGROUP-PARENT*]]
[**--help**]
[**-f**|**--file**[=*PATH/Dockerfile*]]
[**-squash**] *Experimental*
[**--force-rm**]
[**--isolation**[=*default*]]
[**--label**[=*[]*]]
@ -57,6 +58,22 @@ set as the **URL**, the repository is cloned locally and then sent as the contex
the remote context. In all cases, the file must be within the build context.
The default is *Dockerfile*.
**--squash**=*true*|*false*
**Experimental Only**
Once the image is built, squash the new layers into a new image with a single
new layer. Squashing does not destroy any existing image, rather it creates a new
image with the content of the squshed layers. This effectively makes it look
like all `Dockerfile` commands were created with a single layer. The build
cache is preserved with this method.
**Note**: using this option means the new image will not be able to take
advantage of layer sharing with other images and may use significantly more
space.
**Note**: using this option you may see significantly more space used due to
storing two copies of the image, one for the build cache with all the cache
layers in tact, and one for the squashed version.
**--build-arg**=*variable*
name and value of a **buildarg**.

View File

@ -406,6 +406,9 @@ type mockLayer struct {
func (l *mockLayer) TarStream() (io.ReadCloser, error) {
return nil, nil
}
func (l *mockLayer) TarStreamFrom(layer.ChainID) (io.ReadCloser, error) {
return nil, nil
}
func (l *mockLayer) ChainID() layer.ChainID {
return layer.CreateChainID(l.diffIDs)