From bd5f92d2631df7c932b93e72e45b39cba19f2f3b Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Sun, 14 May 2017 14:18:48 -0400 Subject: [PATCH] Remove CopyOnBuild from the daemon. Add CreateImage() to the daemon Refactor daemon.Comit() and expose a Image.NewChild() Update copy to use IDMappings. Signed-off-by: Daniel Nephin --- builder/builder.go | 14 ++-- builder/dockerfile/builder.go | 4 + builder/dockerfile/copy.go | 57 ++++++++++++++ builder/dockerfile/copy_unix.go | 64 +++++++++++++++ builder/dockerfile/copy_windows.go | 8 ++ builder/dockerfile/dispatchers.go | 11 ++- builder/dockerfile/imagecontext.go | 32 ++++---- builder/dockerfile/internals.go | 43 ++++++++-- builder/dockerfile/mockbackend_test.go | 25 ++++++ daemon/archive.go | 104 +------------------------ daemon/archive_unix.go | 35 --------- daemon/archive_windows.go | 5 -- daemon/build.go | 28 +++++++ daemon/commit.go | 66 +++++----------- image/image.go | 58 ++++++++++++++ layer/empty.go | 5 ++ 16 files changed, 334 insertions(+), 225 deletions(-) create mode 100644 builder/dockerfile/copy_unix.go create mode 100644 builder/dockerfile/copy_windows.go diff --git a/builder/builder.go b/builder/builder.go index 1aea99ea90ce8..07a5500adb970 100644 --- a/builder/builder.go +++ b/builder/builder.go @@ -11,6 +11,9 @@ import ( "github.com/docker/docker/api/types/backend" "github.com/docker/docker/api/types/container" containerpkg "github.com/docker/docker/container" + "github.com/docker/docker/image" + "github.com/docker/docker/layer" + "github.com/docker/docker/pkg/idtools" "golang.org/x/net/context" ) @@ -42,11 +45,9 @@ type Backend interface { // ContainerCreateWorkdir creates the workdir ContainerCreateWorkdir(containerID string) error - // ContainerCopy copies/extracts a source FileInfo to a destination path inside a container - // specified by a container object. - // TODO: extract in the builder instead of passing `decompress` - // TODO: use containerd/fs.changestream instead as a source - CopyOnBuild(containerID string, destPath string, srcRoot string, srcPath string, decompress bool) error + CreateImage(config []byte, parent string) (string, error) + + IDMappings() *idtools.IDMappings ImageCacheBuilder } @@ -96,10 +97,13 @@ type ImageCache interface { type Image interface { ImageID() string RunConfig() *container.Config + MarshalJSON() ([]byte, error) + NewChild(child image.ChildConfig) *image.Image } // ReleaseableLayer is an image layer that can be mounted and released type ReleaseableLayer interface { Release() error Mount() (string, error) + DiffID() layer.DiffID } diff --git a/builder/dockerfile/builder.go b/builder/dockerfile/builder.go index 52b540ab5d6b4..c50570a6fd93f 100644 --- a/builder/dockerfile/builder.go +++ b/builder/dockerfile/builder.go @@ -15,6 +15,8 @@ import ( "github.com/docker/docker/builder/dockerfile/command" "github.com/docker/docker/builder/dockerfile/parser" "github.com/docker/docker/builder/remotecontext" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/chrootarchive" "github.com/docker/docker/pkg/streamformatter" "github.com/docker/docker/pkg/stringid" "github.com/pkg/errors" @@ -98,6 +100,7 @@ type Builder struct { docker builder.Backend clientCtx context.Context + archiver *archive.Archiver buildStages *buildStages disableCommit bool buildArgs *buildArgs @@ -121,6 +124,7 @@ func newBuilder(clientCtx context.Context, options builderOptions) *Builder { Aux: options.ProgressWriter.AuxFormatter, Output: options.ProgressWriter.Output, docker: options.Backend, + archiver: chrootarchive.NewArchiver(options.Backend.IDMappings()), buildArgs: newBuildArgs(config.BuildArgs), buildStages: newBuildStages(), imageSources: newImageSources(clientCtx, options), diff --git a/builder/dockerfile/copy.go b/builder/dockerfile/copy.go index db98eb92e19ea..ace6b9878a129 100644 --- a/builder/dockerfile/copy.go +++ b/builder/dockerfile/copy.go @@ -13,9 +13,12 @@ import ( "github.com/docker/docker/builder" "github.com/docker/docker/builder/remotecontext" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/progress" "github.com/docker/docker/pkg/streamformatter" + "github.com/docker/docker/pkg/symlink" "github.com/docker/docker/pkg/system" "github.com/docker/docker/pkg/urlutil" "github.com/pkg/errors" @@ -34,6 +37,10 @@ type copyInfo struct { hash string } +func (c copyInfo) fullPath() (string, error) { + return symlink.FollowSymlinkInScope(filepath.Join(c.root, c.path), c.root) +} + func newCopyInfoFromSource(source builder.Source, path string, hash string) copyInfo { return copyInfo{root: source.Root(), path: path, hash: hash} } @@ -355,3 +362,53 @@ func downloadSource(output io.Writer, stdout io.Writer, srcURL string) (remote b lc, err := remotecontext.NewLazyContext(tmpDir) return lc, filename, err } + +type copyFileOptions struct { + decompress bool + archiver *archive.Archiver +} + +func copyFile(dest copyInfo, source copyInfo, options copyFileOptions) error { + srcPath, err := source.fullPath() + if err != nil { + return err + } + destPath, err := dest.fullPath() + if err != nil { + return err + } + + archiver := options.archiver + rootIDs := archiver.IDMappings.RootPair() + + src, err := os.Stat(srcPath) + if err != nil { + return err // TODO: errors.Wrapf + } + if src.IsDir() { + if err := archiver.CopyWithTar(srcPath, destPath); err != nil { + return err + } + return fixPermissions(srcPath, destPath, rootIDs) + } + + if options.decompress && archive.IsArchivePath(srcPath) { + // To support the untar feature we need to clean up the path a little bit + // because tar is not very forgiving + tarDest := dest.path + // TODO: could this be just TrimSuffix()? + if strings.HasSuffix(tarDest, string(os.PathSeparator)) { + tarDest = filepath.Dir(dest.path) + } + return archiver.UntarPath(srcPath, tarDest) + } + + if err := idtools.MkdirAllAndChownNew(filepath.Dir(destPath), 0755, rootIDs); err != nil { + return err + } + if err := archiver.CopyFileWithTar(srcPath, destPath); err != nil { + return err + } + // TODO: do I have to change destPath to the filename? + return fixPermissions(srcPath, destPath, rootIDs) +} diff --git a/builder/dockerfile/copy_unix.go b/builder/dockerfile/copy_unix.go new file mode 100644 index 0000000000000..ecbbd33368841 --- /dev/null +++ b/builder/dockerfile/copy_unix.go @@ -0,0 +1,64 @@ +package dockerfile + +import ( + "os" + "path/filepath" + + "github.com/docker/docker/pkg/idtools" +) + +func pathExists(path string) (bool, error) { + _, err := os.Stat(path) + switch { + case err == nil: + return true, nil + case os.IsNotExist(err): + return false, nil + } + return false, err +} + +// TODO: review this +func fixPermissions(source, destination string, rootIDs idtools.IDPair) error { + doChownDestination, err := chownDestinationRoot(destination) + if err != nil { + return err + } + + // We Walk on the source rather than on the destination because we don't + // want to change permissions on things we haven't created or modified. + return filepath.Walk(source, func(fullpath string, info os.FileInfo, err error) error { + // Do not alter the walk root iff. it existed before, as it doesn't fall under + // the domain of "things we should chown". + if !doChownDestination && (source == fullpath) { + return nil + } + + // Path is prefixed by source: substitute with destination instead. + cleaned, err := filepath.Rel(source, fullpath) + if err != nil { + return err + } + + fullpath = filepath.Join(destination, cleaned) + return os.Lchown(fullpath, rootIDs.UID, rootIDs.GID) + }) +} + +// If the destination didn't already exist, or the destination isn't a +// directory, then we should Lchown the destination. Otherwise, we shouldn't +// Lchown the destination. +func chownDestinationRoot(destination string) (bool, error) { + destExists, err := pathExists(destination) + if err != nil { + return false, err + } + destStat, err := os.Stat(destination) + if err != nil { + // This should *never* be reached, because the destination must've already + // been created while untar-ing the context. + return false, err + } + + return !destExists || !destStat.IsDir(), nil +} diff --git a/builder/dockerfile/copy_windows.go b/builder/dockerfile/copy_windows.go new file mode 100644 index 0000000000000..78f5b09457b0d --- /dev/null +++ b/builder/dockerfile/copy_windows.go @@ -0,0 +1,8 @@ +package dockerfile + +import "github.com/docker/docker/pkg/idtools" + +func fixPermissions(source, destination string, rootIDs idtools.IDPair) error { + // chown is not supported on Windows + return nil +} diff --git a/builder/dockerfile/dispatchers.go b/builder/dockerfile/dispatchers.go index ca67eb7f3001e..1935ac85c5a88 100644 --- a/builder/dockerfile/dispatchers.go +++ b/builder/dockerfile/dispatchers.go @@ -23,6 +23,7 @@ import ( "github.com/docker/docker/api/types/strslice" "github.com/docker/docker/builder" "github.com/docker/docker/builder/dockerfile/parser" + "github.com/docker/docker/image" "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/pkg/signal" "github.com/docker/go-connections/nat" @@ -251,10 +252,8 @@ func parseBuildStageName(args []string) (string, error) { return stageName, nil } -// scratchImage is used as a token for the empty base image. It uses buildStage -// as a convenient implementation of builder.Image, but is not actually a -// buildStage. -var scratchImage builder.Image = &buildStage{} +// scratchImage is used as a token for the empty base image. +var scratchImage builder.Image = &image.Image{} func (b *Builder) getFromImage(shlex *ShellLex, name string) (builder.Image, error) { substitutionArgs := []string{} @@ -267,8 +266,8 @@ func (b *Builder) getFromImage(shlex *ShellLex, name string) (builder.Image, err return nil, err } - if im, ok := b.buildStages.getByName(name); ok { - return im, nil + if stage, ok := b.buildStages.getByName(name); ok { + name = stage.ImageID() } // Windows cannot support a container with no base image. diff --git a/builder/dockerfile/imagecontext.go b/builder/dockerfile/imagecontext.go index f89cc8d9cee13..5aba2ff9367b5 100644 --- a/builder/dockerfile/imagecontext.go +++ b/builder/dockerfile/imagecontext.go @@ -6,37 +6,29 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/docker/api/types/backend" - "github.com/docker/docker/api/types/container" "github.com/docker/docker/builder" "github.com/docker/docker/builder/remotecontext" + "github.com/docker/docker/layer" "github.com/pkg/errors" "golang.org/x/net/context" ) type buildStage struct { - id string - config *container.Config + id string } -func newBuildStageFromImage(image builder.Image) *buildStage { - return &buildStage{id: image.ImageID(), config: image.RunConfig()} +func newBuildStage(imageID string) *buildStage { + return &buildStage{id: imageID} } func (b *buildStage) ImageID() string { return b.id } -func (b *buildStage) RunConfig() *container.Config { - return b.config -} - -func (b *buildStage) update(imageID string, runConfig *container.Config) { +func (b *buildStage) update(imageID string) { b.id = imageID - b.config = runConfig } -var _ builder.Image = &buildStage{} - // buildStages tracks each stage of a build so they can be retrieved by index // or by name. type buildStages struct { @@ -48,12 +40,12 @@ func newBuildStages() *buildStages { return &buildStages{byName: make(map[string]*buildStage)} } -func (s *buildStages) getByName(name string) (builder.Image, bool) { +func (s *buildStages) getByName(name string) (*buildStage, bool) { stage, ok := s.byName[strings.ToLower(name)] return stage, ok } -func (s *buildStages) get(indexOrName string) (builder.Image, error) { +func (s *buildStages) get(indexOrName string) (*buildStage, error) { index, err := strconv.Atoi(indexOrName) if err == nil { if err := s.validateIndex(index); err != nil { @@ -78,7 +70,7 @@ func (s *buildStages) validateIndex(i int) error { } func (s *buildStages) add(name string, image builder.Image) error { - stage := newBuildStageFromImage(image) + stage := newBuildStage(image.ImageID()) name = strings.ToLower(name) if len(name) > 0 { if _, ok := s.byName[name]; ok { @@ -90,8 +82,8 @@ func (s *buildStages) add(name string, image builder.Image) error { return nil } -func (s *buildStages) update(imageID string, runConfig *container.Config) { - s.sequence[len(s.sequence)-1].update(imageID, runConfig) +func (s *buildStages) update(imageID string) { + s.sequence[len(s.sequence)-1].update(imageID) } type getAndMountFunc func(string) (builder.Image, builder.ReleaseableLayer, error) @@ -190,3 +182,7 @@ func (im *imageMount) Image() builder.Image { func (im *imageMount) ImageID() string { return im.image.ImageID() } + +func (im *imageMount) DiffID() layer.DiffID { + return im.layer.DiffID() +} diff --git a/builder/dockerfile/internals.go b/builder/dockerfile/internals.go index 2d02e57a69c09..ef64455ffb3ce 100644 --- a/builder/dockerfile/internals.go +++ b/builder/dockerfile/internals.go @@ -12,6 +12,8 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/backend" "github.com/docker/docker/api/types/container" + "github.com/docker/docker/builder" + "github.com/docker/docker/image" "github.com/docker/docker/pkg/stringid" "github.com/pkg/errors" ) @@ -37,7 +39,6 @@ func (b *Builder) commit(dispatchState *dispatchState, comment string) error { return b.commitContainer(dispatchState, id, runConfigWithCommentCmd) } -// TODO: see if any args can be dropped func (b *Builder) commitContainer(dispatchState *dispatchState, id string, containerConfig *container.Config) error { if b.disableCommit { return nil @@ -60,10 +61,20 @@ func (b *Builder) commitContainer(dispatchState *dispatchState, id string, conta } dispatchState.imageID = imageID - b.buildStages.update(imageID, dispatchState.runConfig) + b.buildStages.update(imageID) return nil } +func (b *Builder) exportImage(state *dispatchState, image builder.Image) error { + config, err := image.MarshalJSON() + if err != nil { + return errors.Wrap(err, "failed to encode image config") + } + + state.imageID, err = b.docker.CreateImage(config, state.imageID) + return err +} + func (b *Builder) performCopy(state *dispatchState, inst copyInstruction) error { srcHash := getSourceHashFromInfos(inst.infos) @@ -83,12 +94,34 @@ func (b *Builder) performCopy(state *dispatchState, inst copyInstruction) error return err } + imageMount, err := b.imageSources.Get(state.imageID) + if err != nil { + return err + } + destSource, err := imageMount.Source() + if err != nil { + return err + } + + destInfo := newCopyInfoFromSource(destSource, dest, "") + opts := copyFileOptions{ + decompress: inst.allowLocalDecompression, + archiver: b.archiver, + } for _, info := range inst.infos { - if err := b.docker.CopyOnBuild(containerID, dest, info.root, info.path, inst.allowLocalDecompression); err != nil { + if err := copyFile(destInfo, info, opts); err != nil { return err } } - return b.commitContainer(state, containerID, runConfigWithCommentCmd) + + newImage := imageMount.Image().NewChild(image.ChildConfig{ + Author: state.maintainer, + DiffID: imageMount.DiffID(), + ContainerConfig: runConfigWithCommentCmd, + // TODO: ContainerID? + // TODO: Config? + }) + return b.exportImage(state, newImage) } // For backwards compat, if there's just one info then use it as the @@ -182,7 +215,7 @@ func (b *Builder) probeCache(dispatchState *dispatchState, runConfig *container. fmt.Fprint(b.Stdout, " ---> Using cache\n") dispatchState.imageID = string(cachedID) - b.buildStages.update(dispatchState.imageID, runConfig) + b.buildStages.update(dispatchState.imageID) return true, nil } diff --git a/builder/dockerfile/mockbackend_test.go b/builder/dockerfile/mockbackend_test.go index 08ce18c2e87e3..cf4c83edd80c1 100644 --- a/builder/dockerfile/mockbackend_test.go +++ b/builder/dockerfile/mockbackend_test.go @@ -1,6 +1,7 @@ package dockerfile import ( + "encoding/json" "io" "github.com/docker/docker/api/types" @@ -8,6 +9,9 @@ import ( "github.com/docker/docker/api/types/container" "github.com/docker/docker/builder" containerpkg "github.com/docker/docker/container" + "github.com/docker/docker/image" + "github.com/docker/docker/layer" + "github.com/docker/docker/pkg/idtools" "golang.org/x/net/context" ) @@ -76,6 +80,14 @@ func (m *MockBackend) MakeImageCache(cacheFrom []string) builder.ImageCache { return nil } +func (m *MockBackend) CreateImage(config []byte, parent string) (string, error) { + return "c411d1d", nil +} + +func (m *MockBackend) IDMappings() *idtools.IDMappings { + return &idtools.IDMappings{} +} + type mockImage struct { id string config *container.Config @@ -89,6 +101,15 @@ func (i *mockImage) RunConfig() *container.Config { return i.config } +func (i *mockImage) MarshalJSON() ([]byte, error) { + type rawImage mockImage + return json.Marshal(rawImage(*i)) +} + +func (i *mockImage) NewChild(child image.ChildConfig) *image.Image { + return nil +} + type mockImageCache struct { getCacheFunc func(parentID string, cfg *container.Config) (string, error) } @@ -109,3 +130,7 @@ func (l *mockLayer) Release() error { func (l *mockLayer) Mount() (string, error) { return "mountPath", nil } + +func (l *mockLayer) DiffID() layer.DiffID { + return layer.DiffID("abcdef12345") +} diff --git a/daemon/archive.go b/daemon/archive.go index 3fb75bbb226af..5db1ef9826548 100644 --- a/daemon/archive.go +++ b/daemon/archive.go @@ -10,9 +10,7 @@ import ( "github.com/docker/docker/container" "github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/chrootarchive" - "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/ioutils" - "github.com/docker/docker/pkg/symlink" "github.com/docker/docker/pkg/system" "github.com/pkg/errors" ) @@ -361,104 +359,4 @@ func (daemon *Daemon) containerCopy(container *container.Container, resource str }) daemon.LogContainerEvent(container, "copy") return reader, nil -} - -// CopyOnBuild copies/extracts a source FileInfo to a destination path inside a container -// specified by a container object. -// TODO: make sure callers don't unnecessarily convert destPath with filepath.FromSlash (Copy does it already). -// CopyOnBuild should take in abstract paths (with slashes) and the implementation should convert it to OS-specific paths. -func (daemon *Daemon) CopyOnBuild(cID, destPath, srcRoot, srcPath string, decompress bool) error { - fullSrcPath, err := symlink.FollowSymlinkInScope(filepath.Join(srcRoot, srcPath), srcRoot) - if err != nil { - return err - } - - destExists := true - destDir := false - rootIDs := daemon.idMappings.RootPair() - - // Work in daemon-local OS specific file paths - destPath = filepath.FromSlash(destPath) - - c, err := daemon.GetContainer(cID) - if err != nil { - return err - } - err = daemon.Mount(c) - if err != nil { - return err - } - defer daemon.Unmount(c) - - dest, err := c.GetResourcePath(destPath) - if err != nil { - return err - } - - // Preserve the trailing slash - // TODO: why are we appending another path separator if there was already one? - if strings.HasSuffix(destPath, string(os.PathSeparator)) || destPath == "." { - destDir = true - dest += string(os.PathSeparator) - } - - destPath = dest - - destStat, err := os.Stat(destPath) - if err != nil { - if !os.IsNotExist(err) { - //logrus.Errorf("Error performing os.Stat on %s. %s", destPath, err) - return err - } - destExists = false - } - - archiver := chrootarchive.NewArchiver(daemon.idMappings) - src, err := os.Stat(fullSrcPath) - if err != nil { - return err - } - - if src.IsDir() { - // copy as directory - if err := archiver.CopyWithTar(fullSrcPath, destPath); err != nil { - return err - } - return fixPermissions(fullSrcPath, destPath, rootIDs.UID, rootIDs.GID, destExists) - } - if decompress && archive.IsArchivePath(fullSrcPath) { - // Only try to untar if it is a file and that we've been told to decompress (when ADD-ing a remote file) - - // First try to unpack the source as an archive - // to support the untar feature we need to clean up the path a little bit - // because tar is very forgiving. First we need to strip off the archive's - // filename from the path but this is only added if it does not end in slash - tarDest := destPath - if strings.HasSuffix(tarDest, string(os.PathSeparator)) { - tarDest = filepath.Dir(destPath) - } - - // try to successfully untar the orig - err := archiver.UntarPath(fullSrcPath, tarDest) - /* - if err != nil { - logrus.Errorf("Couldn't untar to %s: %v", tarDest, err) - } - */ - return err - } - - // only needed for fixPermissions, but might as well put it before CopyFileWithTar - if destDir || (destExists && destStat.IsDir()) { - destPath = filepath.Join(destPath, filepath.Base(srcPath)) - } - - if err := idtools.MkdirAllAndChownNew(filepath.Dir(destPath), 0755, rootIDs); err != nil { - return err - } - if err := archiver.CopyFileWithTar(fullSrcPath, destPath); err != nil { - return err - } - - return fixPermissions(fullSrcPath, destPath, rootIDs.UID, rootIDs.GID, destExists) -} +} \ No newline at end of file diff --git a/daemon/archive_unix.go b/daemon/archive_unix.go index 8806e2e198efc..d5dfad78cb9d9 100644 --- a/daemon/archive_unix.go +++ b/daemon/archive_unix.go @@ -3,9 +3,6 @@ package daemon import ( - "os" - "path/filepath" - "github.com/docker/docker/container" ) @@ -25,38 +22,6 @@ func checkIfPathIsInAVolume(container *container.Container, absPath string) (boo return toVolume, nil } -func fixPermissions(source, destination string, uid, gid int, destExisted bool) error { - // If the destination didn't already exist, or the destination isn't a - // directory, then we should Lchown the destination. Otherwise, we shouldn't - // Lchown the destination. - destStat, err := os.Stat(destination) - if err != nil { - // This should *never* be reached, because the destination must've already - // been created while untar-ing the context. - return err - } - doChownDestination := !destExisted || !destStat.IsDir() - - // We Walk on the source rather than on the destination because we don't - // want to change permissions on things we haven't created or modified. - return filepath.Walk(source, func(fullpath string, info os.FileInfo, err error) error { - // Do not alter the walk root iff. it existed before, as it doesn't fall under - // the domain of "things we should chown". - if !doChownDestination && (source == fullpath) { - return nil - } - - // Path is prefixed by source: substitute with destination instead. - cleaned, err := filepath.Rel(source, fullpath) - if err != nil { - return err - } - - fullpath = filepath.Join(destination, cleaned) - return os.Lchown(fullpath, uid, gid) - }) -} - // isOnlineFSOperationPermitted returns an error if an online filesystem operation // is not permitted. func (daemon *Daemon) isOnlineFSOperationPermitted(container *container.Container) error { diff --git a/daemon/archive_windows.go b/daemon/archive_windows.go index 1c5a894f7ca2b..ab105607d55d0 100644 --- a/daemon/archive_windows.go +++ b/daemon/archive_windows.go @@ -17,11 +17,6 @@ func checkIfPathIsInAVolume(container *container.Container, absPath string) (boo return false, nil } -func fixPermissions(source, destination string, uid, gid int, destExisted bool) error { - // chown is not supported on Windows - return nil -} - // isOnlineFSOperationPermitted returns an error if an online filesystem operation // is not permitted (such as stat or for copying). Running Hyper-V containers // cannot have their file-system interrogated from the host as the filter is diff --git a/daemon/build.go b/daemon/build.go index 1bf05ded2cfe1..3005ede7d1932 100644 --- a/daemon/build.go +++ b/daemon/build.go @@ -8,6 +8,7 @@ import ( "github.com/docker/docker/builder" "github.com/docker/docker/image" "github.com/docker/docker/layer" + "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/stringid" "github.com/docker/docker/registry" "github.com/pkg/errors" @@ -40,6 +41,10 @@ func (rl *releaseableLayer) Release() error { return rl.releaseROLayer() } +func (rl *releaseableLayer) DiffID() layer.DiffID { + return rl.roLayer.DiffID() +} + func (rl *releaseableLayer) releaseRWLayer() error { if rl.rwLayer == nil { return nil @@ -120,3 +125,26 @@ func (daemon *Daemon) GetImageAndReleasableLayer(ctx context.Context, refOrID st layer, err := newReleasableLayerForImage(image, daemon.layerStore) return image, layer, err } + +// CreateImage creates a new image by adding a config and ID to the image store. +// This is similar to LoadImage() except that it receives JSON encoded bytes of +// an image instead of a tar archive. +func (daemon *Daemon) CreateImage(config []byte, parent string) (string, error) { + id, err := daemon.imageStore.Create(config) + if err != nil { + return "", err + } + + if parent != "" { + if err := daemon.imageStore.SetParent(id, image.ID(parent)); err != nil { + return "", err + } + } + // TODO: do we need any daemon.LogContainerEventWithAttributes? + return id.String(), nil +} + +// IDMappings returns uid/gid mappings for the builder +func (daemon *Daemon) IDMappings() *idtools.IDMappings { + return daemon.idMappings +} diff --git a/daemon/commit.go b/daemon/commit.go index e64c7d333384d..8125d28f65fef 100644 --- a/daemon/commit.go +++ b/daemon/commit.go @@ -12,7 +12,6 @@ import ( containertypes "github.com/docker/docker/api/types/container" "github.com/docker/docker/builder/dockerfile" "github.com/docker/docker/container" - "github.com/docker/docker/dockerversion" "github.com/docker/docker/image" "github.com/docker/docker/layer" "github.com/docker/docker/pkg/ioutils" @@ -129,11 +128,6 @@ func (daemon *Daemon) Commit(name string, c *backend.ContainerCommitConfig) (str return "", err } - containerConfig := c.ContainerConfig - if containerConfig == nil { - containerConfig = container.Config - } - // It is not possible to commit a running container on Windows and on Solaris. if (runtime.GOOS == "windows" || runtime.GOOS == "solaris") && container.IsRunning() { return "", errors.Errorf("%+v does not support commit of a running container", runtime.GOOS) @@ -165,60 +159,36 @@ func (daemon *Daemon) Commit(name string, c *backend.ContainerCommitConfig) (str } }() - var history []image.History - rootFS := image.NewRootFS() - osVersion := "" - var osFeatures []string - - if container.ImageID != "" { - img, err := daemon.imageStore.Get(container.ImageID) + var parent *image.Image + if container.ImageID == "" { + parent = new(image.Image) + parent.RootFS = image.NewRootFS() + } else { + parent, err = daemon.imageStore.Get(container.ImageID) if err != nil { return "", err } - history = img.History - rootFS = img.RootFS - osVersion = img.OSVersion - osFeatures = img.OSFeatures } - l, err := daemon.layerStore.Register(rwTar, rootFS.ChainID()) + l, err := daemon.layerStore.Register(rwTar, parent.RootFS.ChainID()) if err != nil { return "", err } defer layer.ReleaseAndLog(daemon.layerStore, l) - h := image.History{ - Author: c.Author, - Created: time.Now().UTC(), - CreatedBy: strings.Join(containerConfig.Cmd, " "), - Comment: c.Comment, - EmptyLayer: true, + containerConfig := c.ContainerConfig + if containerConfig == nil { + containerConfig = container.Config } - - if diffID := l.DiffID(); layer.DigestSHA256EmptyTar != diffID { - h.EmptyLayer = false - rootFS.Append(diffID) + cc := image.ChildConfig{ + ContainerID: container.ID, + Author: c.Author, + Comment: c.Comment, + ContainerConfig: containerConfig, + Config: newConfig, + DiffID: l.DiffID(), } - - history = append(history, h) - - config, err := json.Marshal(&image.Image{ - V1Image: image.V1Image{ - DockerVersion: dockerversion.Version, - Config: newConfig, - Architecture: runtime.GOARCH, - OS: runtime.GOOS, - Container: container.ID, - ContainerConfig: *containerConfig, - Author: c.Author, - Created: h.Created, - }, - RootFS: rootFS, - History: history, - OSFeatures: osFeatures, - OSVersion: osVersion, - }) - + config, err := json.Marshal(parent.NewChild(cc)) if err != nil { return "", err } diff --git a/image/image.go b/image/image.go index 17935ac2342a6..20650a8429afc 100644 --- a/image/image.go +++ b/image/image.go @@ -7,7 +7,11 @@ import ( "time" "github.com/docker/docker/api/types/container" + "github.com/docker/docker/dockerversion" + "github.com/docker/docker/layer" "github.com/opencontainers/go-digest" + "runtime" + "strings" ) // ID is the content-addressable ID of an image. @@ -110,6 +114,48 @@ func (img *Image) MarshalJSON() ([]byte, error) { return json.Marshal(c) } +// ChildConfig is the configuration to apply to an Image to create a new +// Child image. Other properties of the image are copied from the parent. +type ChildConfig struct { + ContainerID string + Author string + Comment string + DiffID layer.DiffID + ContainerConfig *container.Config + Config *container.Config +} + +// NewChild creates a new Image as a child of this image. +func (img *Image) NewChild(child ChildConfig) *Image { + isEmptyLayer := layer.IsEmpty(child.DiffID) + rootFS := img.RootFS + if !isEmptyLayer { + rootFS.Append(child.DiffID) + } + imgHistory := NewHistory( + child.Author, + child.Comment, + strings.Join(child.ContainerConfig.Cmd, " "), + isEmptyLayer) + + return &Image{ + V1Image: V1Image{ + DockerVersion: dockerversion.Version, + Config: child.Config, + Architecture: runtime.GOARCH, + OS: runtime.GOOS, + Container: child.ContainerID, + ContainerConfig: *child.ContainerConfig, + Author: child.Author, + Created: imgHistory.Created, + }, + RootFS: rootFS, + History: append(img.History, imgHistory), + OSFeatures: img.OSFeatures, + OSVersion: img.OSVersion, + } +} + // History stores build commands that were used to create an image type History struct { // Created is the timestamp at which the image was created @@ -126,6 +172,18 @@ type History struct { EmptyLayer bool `json:"empty_layer,omitempty"` } +// NewHistory creates a new history struct from arguments, and sets the created +// time to the current time in UTC +func NewHistory(author, comment, createdBy string, isEmptyLayer bool) History { + return History{ + Author: author, + Created: time.Now().UTC(), + CreatedBy: createdBy, + Comment: comment, + EmptyLayer: isEmptyLayer, + } +} + // Exporter provides interface for loading and saving images type Exporter interface { Load(io.ReadCloser, io.Writer, bool) error diff --git a/layer/empty.go b/layer/empty.go index 3b6ffc82f71c0..80f2c1243958e 100644 --- a/layer/empty.go +++ b/layer/empty.go @@ -54,3 +54,8 @@ func (el *emptyLayer) DiffSize() (size int64, err error) { func (el *emptyLayer) Metadata() (map[string]string, error) { return make(map[string]string), nil } + +// IsEmpty returns true if the layer is an EmptyLayer +func IsEmpty(diffID DiffID) bool { + return diffID == DigestSHA256EmptyTar +}