Selaa lähdekoodia

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 <dnephin@docker.com>
Daniel Nephin 8 vuotta sitten
vanhempi
commit
bd5f92d263

+ 9 - 5
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
 }

+ 4 - 0
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),

+ 57 - 0
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)
+}

+ 64 - 0
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
+}

+ 8 - 0
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
+}

+ 5 - 6
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.

+ 14 - 18
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()
+}

+ 38 - 5
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
 }
 

+ 25 - 0
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")
+}

+ 1 - 103
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)
-}
+}

+ 0 - 35
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 {

+ 0 - 5
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

+ 28 - 0
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
+}

+ 18 - 48
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
 	}

+ 58 - 0
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

+ 5 - 0
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
+}