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>
This commit is contained in:
Daniel Nephin 2017-05-14 14:18:48 -04:00
parent 274cc09247
commit bd5f92d263
16 changed files with 334 additions and 225 deletions

View file

@ -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
}

View file

@ -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),

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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.

View file

@ -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()
}

View file

@ -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
}

View file

@ -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")
}

View file

@ -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)
}
}

View file

@ -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 {

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View file

@ -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

View file

@ -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
}