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>
| ... | ... |
@@ -11,6 +11,9 @@ import ( |
| 11 | 11 |
"github.com/docker/docker/api/types/backend" |
| 12 | 12 |
"github.com/docker/docker/api/types/container" |
| 13 | 13 |
containerpkg "github.com/docker/docker/container" |
| 14 |
+ "github.com/docker/docker/image" |
|
| 15 |
+ "github.com/docker/docker/layer" |
|
| 16 |
+ "github.com/docker/docker/pkg/idtools" |
|
| 14 | 17 |
"golang.org/x/net/context" |
| 15 | 18 |
) |
| 16 | 19 |
|
| ... | ... |
@@ -42,11 +45,9 @@ type Backend interface {
|
| 42 | 42 |
// ContainerCreateWorkdir creates the workdir |
| 43 | 43 |
ContainerCreateWorkdir(containerID string) error |
| 44 | 44 |
|
| 45 |
- // ContainerCopy copies/extracts a source FileInfo to a destination path inside a container |
|
| 46 |
- // specified by a container object. |
|
| 47 |
- // TODO: extract in the builder instead of passing `decompress` |
|
| 48 |
- // TODO: use containerd/fs.changestream instead as a source |
|
| 49 |
- CopyOnBuild(containerID string, destPath string, srcRoot string, srcPath string, decompress bool) error |
|
| 45 |
+ CreateImage(config []byte, parent string) (string, error) |
|
| 46 |
+ |
|
| 47 |
+ IDMappings() *idtools.IDMappings |
|
| 50 | 48 |
|
| 51 | 49 |
ImageCacheBuilder |
| 52 | 50 |
} |
| ... | ... |
@@ -96,10 +97,13 @@ type ImageCache interface {
|
| 96 | 96 |
type Image interface {
|
| 97 | 97 |
ImageID() string |
| 98 | 98 |
RunConfig() *container.Config |
| 99 |
+ MarshalJSON() ([]byte, error) |
|
| 100 |
+ NewChild(child image.ChildConfig) *image.Image |
|
| 99 | 101 |
} |
| 100 | 102 |
|
| 101 | 103 |
// ReleaseableLayer is an image layer that can be mounted and released |
| 102 | 104 |
type ReleaseableLayer interface {
|
| 103 | 105 |
Release() error |
| 104 | 106 |
Mount() (string, error) |
| 107 |
+ DiffID() layer.DiffID |
|
| 105 | 108 |
} |
| ... | ... |
@@ -15,6 +15,8 @@ import ( |
| 15 | 15 |
"github.com/docker/docker/builder/dockerfile/command" |
| 16 | 16 |
"github.com/docker/docker/builder/dockerfile/parser" |
| 17 | 17 |
"github.com/docker/docker/builder/remotecontext" |
| 18 |
+ "github.com/docker/docker/pkg/archive" |
|
| 19 |
+ "github.com/docker/docker/pkg/chrootarchive" |
|
| 18 | 20 |
"github.com/docker/docker/pkg/streamformatter" |
| 19 | 21 |
"github.com/docker/docker/pkg/stringid" |
| 20 | 22 |
"github.com/pkg/errors" |
| ... | ... |
@@ -98,6 +100,7 @@ type Builder struct {
|
| 98 | 98 |
docker builder.Backend |
| 99 | 99 |
clientCtx context.Context |
| 100 | 100 |
|
| 101 |
+ archiver *archive.Archiver |
|
| 101 | 102 |
buildStages *buildStages |
| 102 | 103 |
disableCommit bool |
| 103 | 104 |
buildArgs *buildArgs |
| ... | ... |
@@ -121,6 +124,7 @@ func newBuilder(clientCtx context.Context, options builderOptions) *Builder {
|
| 121 | 121 |
Aux: options.ProgressWriter.AuxFormatter, |
| 122 | 122 |
Output: options.ProgressWriter.Output, |
| 123 | 123 |
docker: options.Backend, |
| 124 |
+ archiver: chrootarchive.NewArchiver(options.Backend.IDMappings()), |
|
| 124 | 125 |
buildArgs: newBuildArgs(config.BuildArgs), |
| 125 | 126 |
buildStages: newBuildStages(), |
| 126 | 127 |
imageSources: newImageSources(clientCtx, options), |
| ... | ... |
@@ -13,9 +13,12 @@ import ( |
| 13 | 13 |
|
| 14 | 14 |
"github.com/docker/docker/builder" |
| 15 | 15 |
"github.com/docker/docker/builder/remotecontext" |
| 16 |
+ "github.com/docker/docker/pkg/archive" |
|
| 17 |
+ "github.com/docker/docker/pkg/idtools" |
|
| 16 | 18 |
"github.com/docker/docker/pkg/ioutils" |
| 17 | 19 |
"github.com/docker/docker/pkg/progress" |
| 18 | 20 |
"github.com/docker/docker/pkg/streamformatter" |
| 21 |
+ "github.com/docker/docker/pkg/symlink" |
|
| 19 | 22 |
"github.com/docker/docker/pkg/system" |
| 20 | 23 |
"github.com/docker/docker/pkg/urlutil" |
| 21 | 24 |
"github.com/pkg/errors" |
| ... | ... |
@@ -34,6 +37,10 @@ type copyInfo struct {
|
| 34 | 34 |
hash string |
| 35 | 35 |
} |
| 36 | 36 |
|
| 37 |
+func (c copyInfo) fullPath() (string, error) {
|
|
| 38 |
+ return symlink.FollowSymlinkInScope(filepath.Join(c.root, c.path), c.root) |
|
| 39 |
+} |
|
| 40 |
+ |
|
| 37 | 41 |
func newCopyInfoFromSource(source builder.Source, path string, hash string) copyInfo {
|
| 38 | 42 |
return copyInfo{root: source.Root(), path: path, hash: hash}
|
| 39 | 43 |
} |
| ... | ... |
@@ -355,3 +362,53 @@ func downloadSource(output io.Writer, stdout io.Writer, srcURL string) (remote b |
| 355 | 355 |
lc, err := remotecontext.NewLazyContext(tmpDir) |
| 356 | 356 |
return lc, filename, err |
| 357 | 357 |
} |
| 358 |
+ |
|
| 359 |
+type copyFileOptions struct {
|
|
| 360 |
+ decompress bool |
|
| 361 |
+ archiver *archive.Archiver |
|
| 362 |
+} |
|
| 363 |
+ |
|
| 364 |
+func copyFile(dest copyInfo, source copyInfo, options copyFileOptions) error {
|
|
| 365 |
+ srcPath, err := source.fullPath() |
|
| 366 |
+ if err != nil {
|
|
| 367 |
+ return err |
|
| 368 |
+ } |
|
| 369 |
+ destPath, err := dest.fullPath() |
|
| 370 |
+ if err != nil {
|
|
| 371 |
+ return err |
|
| 372 |
+ } |
|
| 373 |
+ |
|
| 374 |
+ archiver := options.archiver |
|
| 375 |
+ rootIDs := archiver.IDMappings.RootPair() |
|
| 376 |
+ |
|
| 377 |
+ src, err := os.Stat(srcPath) |
|
| 378 |
+ if err != nil {
|
|
| 379 |
+ return err // TODO: errors.Wrapf |
|
| 380 |
+ } |
|
| 381 |
+ if src.IsDir() {
|
|
| 382 |
+ if err := archiver.CopyWithTar(srcPath, destPath); err != nil {
|
|
| 383 |
+ return err |
|
| 384 |
+ } |
|
| 385 |
+ return fixPermissions(srcPath, destPath, rootIDs) |
|
| 386 |
+ } |
|
| 387 |
+ |
|
| 388 |
+ if options.decompress && archive.IsArchivePath(srcPath) {
|
|
| 389 |
+ // To support the untar feature we need to clean up the path a little bit |
|
| 390 |
+ // because tar is not very forgiving |
|
| 391 |
+ tarDest := dest.path |
|
| 392 |
+ // TODO: could this be just TrimSuffix()? |
|
| 393 |
+ if strings.HasSuffix(tarDest, string(os.PathSeparator)) {
|
|
| 394 |
+ tarDest = filepath.Dir(dest.path) |
|
| 395 |
+ } |
|
| 396 |
+ return archiver.UntarPath(srcPath, tarDest) |
|
| 397 |
+ } |
|
| 398 |
+ |
|
| 399 |
+ if err := idtools.MkdirAllAndChownNew(filepath.Dir(destPath), 0755, rootIDs); err != nil {
|
|
| 400 |
+ return err |
|
| 401 |
+ } |
|
| 402 |
+ if err := archiver.CopyFileWithTar(srcPath, destPath); err != nil {
|
|
| 403 |
+ return err |
|
| 404 |
+ } |
|
| 405 |
+ // TODO: do I have to change destPath to the filename? |
|
| 406 |
+ return fixPermissions(srcPath, destPath, rootIDs) |
|
| 407 |
+} |
| 358 | 408 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,64 @@ |
| 0 |
+package dockerfile |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "os" |
|
| 4 |
+ "path/filepath" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/docker/docker/pkg/idtools" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+func pathExists(path string) (bool, error) {
|
|
| 10 |
+ _, err := os.Stat(path) |
|
| 11 |
+ switch {
|
|
| 12 |
+ case err == nil: |
|
| 13 |
+ return true, nil |
|
| 14 |
+ case os.IsNotExist(err): |
|
| 15 |
+ return false, nil |
|
| 16 |
+ } |
|
| 17 |
+ return false, err |
|
| 18 |
+} |
|
| 19 |
+ |
|
| 20 |
+// TODO: review this |
|
| 21 |
+func fixPermissions(source, destination string, rootIDs idtools.IDPair) error {
|
|
| 22 |
+ doChownDestination, err := chownDestinationRoot(destination) |
|
| 23 |
+ if err != nil {
|
|
| 24 |
+ return err |
|
| 25 |
+ } |
|
| 26 |
+ |
|
| 27 |
+ // We Walk on the source rather than on the destination because we don't |
|
| 28 |
+ // want to change permissions on things we haven't created or modified. |
|
| 29 |
+ return filepath.Walk(source, func(fullpath string, info os.FileInfo, err error) error {
|
|
| 30 |
+ // Do not alter the walk root iff. it existed before, as it doesn't fall under |
|
| 31 |
+ // the domain of "things we should chown". |
|
| 32 |
+ if !doChownDestination && (source == fullpath) {
|
|
| 33 |
+ return nil |
|
| 34 |
+ } |
|
| 35 |
+ |
|
| 36 |
+ // Path is prefixed by source: substitute with destination instead. |
|
| 37 |
+ cleaned, err := filepath.Rel(source, fullpath) |
|
| 38 |
+ if err != nil {
|
|
| 39 |
+ return err |
|
| 40 |
+ } |
|
| 41 |
+ |
|
| 42 |
+ fullpath = filepath.Join(destination, cleaned) |
|
| 43 |
+ return os.Lchown(fullpath, rootIDs.UID, rootIDs.GID) |
|
| 44 |
+ }) |
|
| 45 |
+} |
|
| 46 |
+ |
|
| 47 |
+// If the destination didn't already exist, or the destination isn't a |
|
| 48 |
+// directory, then we should Lchown the destination. Otherwise, we shouldn't |
|
| 49 |
+// Lchown the destination. |
|
| 50 |
+func chownDestinationRoot(destination string) (bool, error) {
|
|
| 51 |
+ destExists, err := pathExists(destination) |
|
| 52 |
+ if err != nil {
|
|
| 53 |
+ return false, err |
|
| 54 |
+ } |
|
| 55 |
+ destStat, err := os.Stat(destination) |
|
| 56 |
+ if err != nil {
|
|
| 57 |
+ // This should *never* be reached, because the destination must've already |
|
| 58 |
+ // been created while untar-ing the context. |
|
| 59 |
+ return false, err |
|
| 60 |
+ } |
|
| 61 |
+ |
|
| 62 |
+ return !destExists || !destStat.IsDir(), nil |
|
| 63 |
+} |
| ... | ... |
@@ -23,6 +23,7 @@ import ( |
| 23 | 23 |
"github.com/docker/docker/api/types/strslice" |
| 24 | 24 |
"github.com/docker/docker/builder" |
| 25 | 25 |
"github.com/docker/docker/builder/dockerfile/parser" |
| 26 |
+ "github.com/docker/docker/image" |
|
| 26 | 27 |
"github.com/docker/docker/pkg/jsonmessage" |
| 27 | 28 |
"github.com/docker/docker/pkg/signal" |
| 28 | 29 |
"github.com/docker/go-connections/nat" |
| ... | ... |
@@ -251,10 +252,8 @@ func parseBuildStageName(args []string) (string, error) {
|
| 251 | 251 |
return stageName, nil |
| 252 | 252 |
} |
| 253 | 253 |
|
| 254 |
-// scratchImage is used as a token for the empty base image. It uses buildStage |
|
| 255 |
-// as a convenient implementation of builder.Image, but is not actually a |
|
| 256 |
-// buildStage. |
|
| 257 |
-var scratchImage builder.Image = &buildStage{}
|
|
| 254 |
+// scratchImage is used as a token for the empty base image. |
|
| 255 |
+var scratchImage builder.Image = &image.Image{}
|
|
| 258 | 256 |
|
| 259 | 257 |
func (b *Builder) getFromImage(shlex *ShellLex, name string) (builder.Image, error) {
|
| 260 | 258 |
substitutionArgs := []string{}
|
| ... | ... |
@@ -267,8 +266,8 @@ func (b *Builder) getFromImage(shlex *ShellLex, name string) (builder.Image, err |
| 267 | 267 |
return nil, err |
| 268 | 268 |
} |
| 269 | 269 |
|
| 270 |
- if im, ok := b.buildStages.getByName(name); ok {
|
|
| 271 |
- return im, nil |
|
| 270 |
+ if stage, ok := b.buildStages.getByName(name); ok {
|
|
| 271 |
+ name = stage.ImageID() |
|
| 272 | 272 |
} |
| 273 | 273 |
|
| 274 | 274 |
// Windows cannot support a container with no base image. |
| ... | ... |
@@ -6,37 +6,29 @@ import ( |
| 6 | 6 |
|
| 7 | 7 |
"github.com/Sirupsen/logrus" |
| 8 | 8 |
"github.com/docker/docker/api/types/backend" |
| 9 |
- "github.com/docker/docker/api/types/container" |
|
| 10 | 9 |
"github.com/docker/docker/builder" |
| 11 | 10 |
"github.com/docker/docker/builder/remotecontext" |
| 11 |
+ "github.com/docker/docker/layer" |
|
| 12 | 12 |
"github.com/pkg/errors" |
| 13 | 13 |
"golang.org/x/net/context" |
| 14 | 14 |
) |
| 15 | 15 |
|
| 16 | 16 |
type buildStage struct {
|
| 17 |
- id string |
|
| 18 |
- config *container.Config |
|
| 17 |
+ id string |
|
| 19 | 18 |
} |
| 20 | 19 |
|
| 21 |
-func newBuildStageFromImage(image builder.Image) *buildStage {
|
|
| 22 |
- return &buildStage{id: image.ImageID(), config: image.RunConfig()}
|
|
| 20 |
+func newBuildStage(imageID string) *buildStage {
|
|
| 21 |
+ return &buildStage{id: imageID}
|
|
| 23 | 22 |
} |
| 24 | 23 |
|
| 25 | 24 |
func (b *buildStage) ImageID() string {
|
| 26 | 25 |
return b.id |
| 27 | 26 |
} |
| 28 | 27 |
|
| 29 |
-func (b *buildStage) RunConfig() *container.Config {
|
|
| 30 |
- return b.config |
|
| 31 |
-} |
|
| 32 |
- |
|
| 33 |
-func (b *buildStage) update(imageID string, runConfig *container.Config) {
|
|
| 28 |
+func (b *buildStage) update(imageID string) {
|
|
| 34 | 29 |
b.id = imageID |
| 35 |
- b.config = runConfig |
|
| 36 | 30 |
} |
| 37 | 31 |
|
| 38 |
-var _ builder.Image = &buildStage{}
|
|
| 39 |
- |
|
| 40 | 32 |
// buildStages tracks each stage of a build so they can be retrieved by index |
| 41 | 33 |
// or by name. |
| 42 | 34 |
type buildStages struct {
|
| ... | ... |
@@ -48,12 +40,12 @@ func newBuildStages() *buildStages {
|
| 48 | 48 |
return &buildStages{byName: make(map[string]*buildStage)}
|
| 49 | 49 |
} |
| 50 | 50 |
|
| 51 |
-func (s *buildStages) getByName(name string) (builder.Image, bool) {
|
|
| 51 |
+func (s *buildStages) getByName(name string) (*buildStage, bool) {
|
|
| 52 | 52 |
stage, ok := s.byName[strings.ToLower(name)] |
| 53 | 53 |
return stage, ok |
| 54 | 54 |
} |
| 55 | 55 |
|
| 56 |
-func (s *buildStages) get(indexOrName string) (builder.Image, error) {
|
|
| 56 |
+func (s *buildStages) get(indexOrName string) (*buildStage, error) {
|
|
| 57 | 57 |
index, err := strconv.Atoi(indexOrName) |
| 58 | 58 |
if err == nil {
|
| 59 | 59 |
if err := s.validateIndex(index); err != nil {
|
| ... | ... |
@@ -78,7 +70,7 @@ func (s *buildStages) validateIndex(i int) error {
|
| 78 | 78 |
} |
| 79 | 79 |
|
| 80 | 80 |
func (s *buildStages) add(name string, image builder.Image) error {
|
| 81 |
- stage := newBuildStageFromImage(image) |
|
| 81 |
+ stage := newBuildStage(image.ImageID()) |
|
| 82 | 82 |
name = strings.ToLower(name) |
| 83 | 83 |
if len(name) > 0 {
|
| 84 | 84 |
if _, ok := s.byName[name]; ok {
|
| ... | ... |
@@ -90,8 +82,8 @@ func (s *buildStages) add(name string, image builder.Image) error {
|
| 90 | 90 |
return nil |
| 91 | 91 |
} |
| 92 | 92 |
|
| 93 |
-func (s *buildStages) update(imageID string, runConfig *container.Config) {
|
|
| 94 |
- s.sequence[len(s.sequence)-1].update(imageID, runConfig) |
|
| 93 |
+func (s *buildStages) update(imageID string) {
|
|
| 94 |
+ s.sequence[len(s.sequence)-1].update(imageID) |
|
| 95 | 95 |
} |
| 96 | 96 |
|
| 97 | 97 |
type getAndMountFunc func(string) (builder.Image, builder.ReleaseableLayer, error) |
| ... | ... |
@@ -190,3 +182,7 @@ func (im *imageMount) Image() builder.Image {
|
| 190 | 190 |
func (im *imageMount) ImageID() string {
|
| 191 | 191 |
return im.image.ImageID() |
| 192 | 192 |
} |
| 193 |
+ |
|
| 194 |
+func (im *imageMount) DiffID() layer.DiffID {
|
|
| 195 |
+ return im.layer.DiffID() |
|
| 196 |
+} |
| ... | ... |
@@ -12,6 +12,8 @@ import ( |
| 12 | 12 |
"github.com/docker/docker/api/types" |
| 13 | 13 |
"github.com/docker/docker/api/types/backend" |
| 14 | 14 |
"github.com/docker/docker/api/types/container" |
| 15 |
+ "github.com/docker/docker/builder" |
|
| 16 |
+ "github.com/docker/docker/image" |
|
| 15 | 17 |
"github.com/docker/docker/pkg/stringid" |
| 16 | 18 |
"github.com/pkg/errors" |
| 17 | 19 |
) |
| ... | ... |
@@ -37,7 +39,6 @@ func (b *Builder) commit(dispatchState *dispatchState, comment string) error {
|
| 37 | 37 |
return b.commitContainer(dispatchState, id, runConfigWithCommentCmd) |
| 38 | 38 |
} |
| 39 | 39 |
|
| 40 |
-// TODO: see if any args can be dropped |
|
| 41 | 40 |
func (b *Builder) commitContainer(dispatchState *dispatchState, id string, containerConfig *container.Config) error {
|
| 42 | 41 |
if b.disableCommit {
|
| 43 | 42 |
return nil |
| ... | ... |
@@ -60,10 +61,20 @@ func (b *Builder) commitContainer(dispatchState *dispatchState, id string, conta |
| 60 | 60 |
} |
| 61 | 61 |
|
| 62 | 62 |
dispatchState.imageID = imageID |
| 63 |
- b.buildStages.update(imageID, dispatchState.runConfig) |
|
| 63 |
+ b.buildStages.update(imageID) |
|
| 64 | 64 |
return nil |
| 65 | 65 |
} |
| 66 | 66 |
|
| 67 |
+func (b *Builder) exportImage(state *dispatchState, image builder.Image) error {
|
|
| 68 |
+ config, err := image.MarshalJSON() |
|
| 69 |
+ if err != nil {
|
|
| 70 |
+ return errors.Wrap(err, "failed to encode image config") |
|
| 71 |
+ } |
|
| 72 |
+ |
|
| 73 |
+ state.imageID, err = b.docker.CreateImage(config, state.imageID) |
|
| 74 |
+ return err |
|
| 75 |
+} |
|
| 76 |
+ |
|
| 67 | 77 |
func (b *Builder) performCopy(state *dispatchState, inst copyInstruction) error {
|
| 68 | 78 |
srcHash := getSourceHashFromInfos(inst.infos) |
| 69 | 79 |
|
| ... | ... |
@@ -83,12 +94,34 @@ func (b *Builder) performCopy(state *dispatchState, inst copyInstruction) error |
| 83 | 83 |
return err |
| 84 | 84 |
} |
| 85 | 85 |
|
| 86 |
+ imageMount, err := b.imageSources.Get(state.imageID) |
|
| 87 |
+ if err != nil {
|
|
| 88 |
+ return err |
|
| 89 |
+ } |
|
| 90 |
+ destSource, err := imageMount.Source() |
|
| 91 |
+ if err != nil {
|
|
| 92 |
+ return err |
|
| 93 |
+ } |
|
| 94 |
+ |
|
| 95 |
+ destInfo := newCopyInfoFromSource(destSource, dest, "") |
|
| 96 |
+ opts := copyFileOptions{
|
|
| 97 |
+ decompress: inst.allowLocalDecompression, |
|
| 98 |
+ archiver: b.archiver, |
|
| 99 |
+ } |
|
| 86 | 100 |
for _, info := range inst.infos {
|
| 87 |
- if err := b.docker.CopyOnBuild(containerID, dest, info.root, info.path, inst.allowLocalDecompression); err != nil {
|
|
| 101 |
+ if err := copyFile(destInfo, info, opts); err != nil {
|
|
| 88 | 102 |
return err |
| 89 | 103 |
} |
| 90 | 104 |
} |
| 91 |
- return b.commitContainer(state, containerID, runConfigWithCommentCmd) |
|
| 105 |
+ |
|
| 106 |
+ newImage := imageMount.Image().NewChild(image.ChildConfig{
|
|
| 107 |
+ Author: state.maintainer, |
|
| 108 |
+ DiffID: imageMount.DiffID(), |
|
| 109 |
+ ContainerConfig: runConfigWithCommentCmd, |
|
| 110 |
+ // TODO: ContainerID? |
|
| 111 |
+ // TODO: Config? |
|
| 112 |
+ }) |
|
| 113 |
+ return b.exportImage(state, newImage) |
|
| 92 | 114 |
} |
| 93 | 115 |
|
| 94 | 116 |
// 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. |
| 182 | 182 |
fmt.Fprint(b.Stdout, " ---> Using cache\n") |
| 183 | 183 |
|
| 184 | 184 |
dispatchState.imageID = string(cachedID) |
| 185 |
- b.buildStages.update(dispatchState.imageID, runConfig) |
|
| 185 |
+ b.buildStages.update(dispatchState.imageID) |
|
| 186 | 186 |
return true, nil |
| 187 | 187 |
} |
| 188 | 188 |
|
| ... | ... |
@@ -1,6 +1,7 @@ |
| 1 | 1 |
package dockerfile |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
+ "encoding/json" |
|
| 4 | 5 |
"io" |
| 5 | 6 |
|
| 6 | 7 |
"github.com/docker/docker/api/types" |
| ... | ... |
@@ -8,6 +9,9 @@ import ( |
| 8 | 8 |
"github.com/docker/docker/api/types/container" |
| 9 | 9 |
"github.com/docker/docker/builder" |
| 10 | 10 |
containerpkg "github.com/docker/docker/container" |
| 11 |
+ "github.com/docker/docker/image" |
|
| 12 |
+ "github.com/docker/docker/layer" |
|
| 13 |
+ "github.com/docker/docker/pkg/idtools" |
|
| 11 | 14 |
"golang.org/x/net/context" |
| 12 | 15 |
) |
| 13 | 16 |
|
| ... | ... |
@@ -76,6 +80,14 @@ func (m *MockBackend) MakeImageCache(cacheFrom []string) builder.ImageCache {
|
| 76 | 76 |
return nil |
| 77 | 77 |
} |
| 78 | 78 |
|
| 79 |
+func (m *MockBackend) CreateImage(config []byte, parent string) (string, error) {
|
|
| 80 |
+ return "c411d1d", nil |
|
| 81 |
+} |
|
| 82 |
+ |
|
| 83 |
+func (m *MockBackend) IDMappings() *idtools.IDMappings {
|
|
| 84 |
+ return &idtools.IDMappings{}
|
|
| 85 |
+} |
|
| 86 |
+ |
|
| 79 | 87 |
type mockImage struct {
|
| 80 | 88 |
id string |
| 81 | 89 |
config *container.Config |
| ... | ... |
@@ -89,6 +101,15 @@ func (i *mockImage) RunConfig() *container.Config {
|
| 89 | 89 |
return i.config |
| 90 | 90 |
} |
| 91 | 91 |
|
| 92 |
+func (i *mockImage) MarshalJSON() ([]byte, error) {
|
|
| 93 |
+ type rawImage mockImage |
|
| 94 |
+ return json.Marshal(rawImage(*i)) |
|
| 95 |
+} |
|
| 96 |
+ |
|
| 97 |
+func (i *mockImage) NewChild(child image.ChildConfig) *image.Image {
|
|
| 98 |
+ return nil |
|
| 99 |
+} |
|
| 100 |
+ |
|
| 92 | 101 |
type mockImageCache struct {
|
| 93 | 102 |
getCacheFunc func(parentID string, cfg *container.Config) (string, error) |
| 94 | 103 |
} |
| ... | ... |
@@ -109,3 +130,7 @@ func (l *mockLayer) Release() error {
|
| 109 | 109 |
func (l *mockLayer) Mount() (string, error) {
|
| 110 | 110 |
return "mountPath", nil |
| 111 | 111 |
} |
| 112 |
+ |
|
| 113 |
+func (l *mockLayer) DiffID() layer.DiffID {
|
|
| 114 |
+ return layer.DiffID("abcdef12345")
|
|
| 115 |
+} |
| ... | ... |
@@ -10,9 +10,7 @@ import ( |
| 10 | 10 |
"github.com/docker/docker/container" |
| 11 | 11 |
"github.com/docker/docker/pkg/archive" |
| 12 | 12 |
"github.com/docker/docker/pkg/chrootarchive" |
| 13 |
- "github.com/docker/docker/pkg/idtools" |
|
| 14 | 13 |
"github.com/docker/docker/pkg/ioutils" |
| 15 |
- "github.com/docker/docker/pkg/symlink" |
|
| 16 | 14 |
"github.com/docker/docker/pkg/system" |
| 17 | 15 |
"github.com/pkg/errors" |
| 18 | 16 |
) |
| ... | ... |
@@ -361,104 +359,4 @@ func (daemon *Daemon) containerCopy(container *container.Container, resource str |
| 361 | 361 |
}) |
| 362 | 362 |
daemon.LogContainerEvent(container, "copy") |
| 363 | 363 |
return reader, nil |
| 364 |
-} |
|
| 365 |
- |
|
| 366 |
-// CopyOnBuild copies/extracts a source FileInfo to a destination path inside a container |
|
| 367 |
-// specified by a container object. |
|
| 368 |
-// TODO: make sure callers don't unnecessarily convert destPath with filepath.FromSlash (Copy does it already). |
|
| 369 |
-// CopyOnBuild should take in abstract paths (with slashes) and the implementation should convert it to OS-specific paths. |
|
| 370 |
-func (daemon *Daemon) CopyOnBuild(cID, destPath, srcRoot, srcPath string, decompress bool) error {
|
|
| 371 |
- fullSrcPath, err := symlink.FollowSymlinkInScope(filepath.Join(srcRoot, srcPath), srcRoot) |
|
| 372 |
- if err != nil {
|
|
| 373 |
- return err |
|
| 374 |
- } |
|
| 375 |
- |
|
| 376 |
- destExists := true |
|
| 377 |
- destDir := false |
|
| 378 |
- rootIDs := daemon.idMappings.RootPair() |
|
| 379 |
- |
|
| 380 |
- // Work in daemon-local OS specific file paths |
|
| 381 |
- destPath = filepath.FromSlash(destPath) |
|
| 382 |
- |
|
| 383 |
- c, err := daemon.GetContainer(cID) |
|
| 384 |
- if err != nil {
|
|
| 385 |
- return err |
|
| 386 |
- } |
|
| 387 |
- err = daemon.Mount(c) |
|
| 388 |
- if err != nil {
|
|
| 389 |
- return err |
|
| 390 |
- } |
|
| 391 |
- defer daemon.Unmount(c) |
|
| 392 |
- |
|
| 393 |
- dest, err := c.GetResourcePath(destPath) |
|
| 394 |
- if err != nil {
|
|
| 395 |
- return err |
|
| 396 |
- } |
|
| 397 |
- |
|
| 398 |
- // Preserve the trailing slash |
|
| 399 |
- // TODO: why are we appending another path separator if there was already one? |
|
| 400 |
- if strings.HasSuffix(destPath, string(os.PathSeparator)) || destPath == "." {
|
|
| 401 |
- destDir = true |
|
| 402 |
- dest += string(os.PathSeparator) |
|
| 403 |
- } |
|
| 404 |
- |
|
| 405 |
- destPath = dest |
|
| 406 |
- |
|
| 407 |
- destStat, err := os.Stat(destPath) |
|
| 408 |
- if err != nil {
|
|
| 409 |
- if !os.IsNotExist(err) {
|
|
| 410 |
- //logrus.Errorf("Error performing os.Stat on %s. %s", destPath, err)
|
|
| 411 |
- return err |
|
| 412 |
- } |
|
| 413 |
- destExists = false |
|
| 414 |
- } |
|
| 415 |
- |
|
| 416 |
- archiver := chrootarchive.NewArchiver(daemon.idMappings) |
|
| 417 |
- src, err := os.Stat(fullSrcPath) |
|
| 418 |
- if err != nil {
|
|
| 419 |
- return err |
|
| 420 |
- } |
|
| 421 |
- |
|
| 422 |
- if src.IsDir() {
|
|
| 423 |
- // copy as directory |
|
| 424 |
- if err := archiver.CopyWithTar(fullSrcPath, destPath); err != nil {
|
|
| 425 |
- return err |
|
| 426 |
- } |
|
| 427 |
- return fixPermissions(fullSrcPath, destPath, rootIDs.UID, rootIDs.GID, destExists) |
|
| 428 |
- } |
|
| 429 |
- if decompress && archive.IsArchivePath(fullSrcPath) {
|
|
| 430 |
- // Only try to untar if it is a file and that we've been told to decompress (when ADD-ing a remote file) |
|
| 431 |
- |
|
| 432 |
- // First try to unpack the source as an archive |
|
| 433 |
- // to support the untar feature we need to clean up the path a little bit |
|
| 434 |
- // because tar is very forgiving. First we need to strip off the archive's |
|
| 435 |
- // filename from the path but this is only added if it does not end in slash |
|
| 436 |
- tarDest := destPath |
|
| 437 |
- if strings.HasSuffix(tarDest, string(os.PathSeparator)) {
|
|
| 438 |
- tarDest = filepath.Dir(destPath) |
|
| 439 |
- } |
|
| 440 |
- |
|
| 441 |
- // try to successfully untar the orig |
|
| 442 |
- err := archiver.UntarPath(fullSrcPath, tarDest) |
|
| 443 |
- /* |
|
| 444 |
- if err != nil {
|
|
| 445 |
- logrus.Errorf("Couldn't untar to %s: %v", tarDest, err)
|
|
| 446 |
- } |
|
| 447 |
- */ |
|
| 448 |
- return err |
|
| 449 |
- } |
|
| 450 |
- |
|
| 451 |
- // only needed for fixPermissions, but might as well put it before CopyFileWithTar |
|
| 452 |
- if destDir || (destExists && destStat.IsDir()) {
|
|
| 453 |
- destPath = filepath.Join(destPath, filepath.Base(srcPath)) |
|
| 454 |
- } |
|
| 455 |
- |
|
| 456 |
- if err := idtools.MkdirAllAndChownNew(filepath.Dir(destPath), 0755, rootIDs); err != nil {
|
|
| 457 |
- return err |
|
| 458 |
- } |
|
| 459 |
- if err := archiver.CopyFileWithTar(fullSrcPath, destPath); err != nil {
|
|
| 460 |
- return err |
|
| 461 |
- } |
|
| 462 |
- |
|
| 463 |
- return fixPermissions(fullSrcPath, destPath, rootIDs.UID, rootIDs.GID, destExists) |
|
| 464 |
-} |
|
| 364 |
+} |
|
| 465 | 365 |
\ No newline at end of file |
| ... | ... |
@@ -3,9 +3,6 @@ |
| 3 | 3 |
package daemon |
| 4 | 4 |
|
| 5 | 5 |
import ( |
| 6 |
- "os" |
|
| 7 |
- "path/filepath" |
|
| 8 |
- |
|
| 9 | 6 |
"github.com/docker/docker/container" |
| 10 | 7 |
) |
| 11 | 8 |
|
| ... | ... |
@@ -25,38 +22,6 @@ func checkIfPathIsInAVolume(container *container.Container, absPath string) (boo |
| 25 | 25 |
return toVolume, nil |
| 26 | 26 |
} |
| 27 | 27 |
|
| 28 |
-func fixPermissions(source, destination string, uid, gid int, destExisted bool) error {
|
|
| 29 |
- // If the destination didn't already exist, or the destination isn't a |
|
| 30 |
- // directory, then we should Lchown the destination. Otherwise, we shouldn't |
|
| 31 |
- // Lchown the destination. |
|
| 32 |
- destStat, err := os.Stat(destination) |
|
| 33 |
- if err != nil {
|
|
| 34 |
- // This should *never* be reached, because the destination must've already |
|
| 35 |
- // been created while untar-ing the context. |
|
| 36 |
- return err |
|
| 37 |
- } |
|
| 38 |
- doChownDestination := !destExisted || !destStat.IsDir() |
|
| 39 |
- |
|
| 40 |
- // We Walk on the source rather than on the destination because we don't |
|
| 41 |
- // want to change permissions on things we haven't created or modified. |
|
| 42 |
- return filepath.Walk(source, func(fullpath string, info os.FileInfo, err error) error {
|
|
| 43 |
- // Do not alter the walk root iff. it existed before, as it doesn't fall under |
|
| 44 |
- // the domain of "things we should chown". |
|
| 45 |
- if !doChownDestination && (source == fullpath) {
|
|
| 46 |
- return nil |
|
| 47 |
- } |
|
| 48 |
- |
|
| 49 |
- // Path is prefixed by source: substitute with destination instead. |
|
| 50 |
- cleaned, err := filepath.Rel(source, fullpath) |
|
| 51 |
- if err != nil {
|
|
| 52 |
- return err |
|
| 53 |
- } |
|
| 54 |
- |
|
| 55 |
- fullpath = filepath.Join(destination, cleaned) |
|
| 56 |
- return os.Lchown(fullpath, uid, gid) |
|
| 57 |
- }) |
|
| 58 |
-} |
|
| 59 |
- |
|
| 60 | 28 |
// isOnlineFSOperationPermitted returns an error if an online filesystem operation |
| 61 | 29 |
// is not permitted. |
| 62 | 30 |
func (daemon *Daemon) isOnlineFSOperationPermitted(container *container.Container) error {
|
| ... | ... |
@@ -17,11 +17,6 @@ func checkIfPathIsInAVolume(container *container.Container, absPath string) (boo |
| 17 | 17 |
return false, nil |
| 18 | 18 |
} |
| 19 | 19 |
|
| 20 |
-func fixPermissions(source, destination string, uid, gid int, destExisted bool) error {
|
|
| 21 |
- // chown is not supported on Windows |
|
| 22 |
- return nil |
|
| 23 |
-} |
|
| 24 |
- |
|
| 25 | 20 |
// isOnlineFSOperationPermitted returns an error if an online filesystem operation |
| 26 | 21 |
// is not permitted (such as stat or for copying). Running Hyper-V containers |
| 27 | 22 |
// cannot have their file-system interrogated from the host as the filter is |
| ... | ... |
@@ -8,6 +8,7 @@ import ( |
| 8 | 8 |
"github.com/docker/docker/builder" |
| 9 | 9 |
"github.com/docker/docker/image" |
| 10 | 10 |
"github.com/docker/docker/layer" |
| 11 |
+ "github.com/docker/docker/pkg/idtools" |
|
| 11 | 12 |
"github.com/docker/docker/pkg/stringid" |
| 12 | 13 |
"github.com/docker/docker/registry" |
| 13 | 14 |
"github.com/pkg/errors" |
| ... | ... |
@@ -40,6 +41,10 @@ func (rl *releaseableLayer) Release() error {
|
| 40 | 40 |
return rl.releaseROLayer() |
| 41 | 41 |
} |
| 42 | 42 |
|
| 43 |
+func (rl *releaseableLayer) DiffID() layer.DiffID {
|
|
| 44 |
+ return rl.roLayer.DiffID() |
|
| 45 |
+} |
|
| 46 |
+ |
|
| 43 | 47 |
func (rl *releaseableLayer) releaseRWLayer() error {
|
| 44 | 48 |
if rl.rwLayer == nil {
|
| 45 | 49 |
return nil |
| ... | ... |
@@ -120,3 +125,26 @@ func (daemon *Daemon) GetImageAndReleasableLayer(ctx context.Context, refOrID st |
| 120 | 120 |
layer, err := newReleasableLayerForImage(image, daemon.layerStore) |
| 121 | 121 |
return image, layer, err |
| 122 | 122 |
} |
| 123 |
+ |
|
| 124 |
+// CreateImage creates a new image by adding a config and ID to the image store. |
|
| 125 |
+// This is similar to LoadImage() except that it receives JSON encoded bytes of |
|
| 126 |
+// an image instead of a tar archive. |
|
| 127 |
+func (daemon *Daemon) CreateImage(config []byte, parent string) (string, error) {
|
|
| 128 |
+ id, err := daemon.imageStore.Create(config) |
|
| 129 |
+ if err != nil {
|
|
| 130 |
+ return "", err |
|
| 131 |
+ } |
|
| 132 |
+ |
|
| 133 |
+ if parent != "" {
|
|
| 134 |
+ if err := daemon.imageStore.SetParent(id, image.ID(parent)); err != nil {
|
|
| 135 |
+ return "", err |
|
| 136 |
+ } |
|
| 137 |
+ } |
|
| 138 |
+ // TODO: do we need any daemon.LogContainerEventWithAttributes? |
|
| 139 |
+ return id.String(), nil |
|
| 140 |
+} |
|
| 141 |
+ |
|
| 142 |
+// IDMappings returns uid/gid mappings for the builder |
|
| 143 |
+func (daemon *Daemon) IDMappings() *idtools.IDMappings {
|
|
| 144 |
+ return daemon.idMappings |
|
| 145 |
+} |
| ... | ... |
@@ -12,7 +12,6 @@ import ( |
| 12 | 12 |
containertypes "github.com/docker/docker/api/types/container" |
| 13 | 13 |
"github.com/docker/docker/builder/dockerfile" |
| 14 | 14 |
"github.com/docker/docker/container" |
| 15 |
- "github.com/docker/docker/dockerversion" |
|
| 16 | 15 |
"github.com/docker/docker/image" |
| 17 | 16 |
"github.com/docker/docker/layer" |
| 18 | 17 |
"github.com/docker/docker/pkg/ioutils" |
| ... | ... |
@@ -129,11 +128,6 @@ func (daemon *Daemon) Commit(name string, c *backend.ContainerCommitConfig) (str |
| 129 | 129 |
return "", err |
| 130 | 130 |
} |
| 131 | 131 |
|
| 132 |
- containerConfig := c.ContainerConfig |
|
| 133 |
- if containerConfig == nil {
|
|
| 134 |
- containerConfig = container.Config |
|
| 135 |
- } |
|
| 136 |
- |
|
| 137 | 132 |
// It is not possible to commit a running container on Windows and on Solaris. |
| 138 | 133 |
if (runtime.GOOS == "windows" || runtime.GOOS == "solaris") && container.IsRunning() {
|
| 139 | 134 |
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 |
| 165 | 165 |
} |
| 166 | 166 |
}() |
| 167 | 167 |
|
| 168 |
- var history []image.History |
|
| 169 |
- rootFS := image.NewRootFS() |
|
| 170 |
- osVersion := "" |
|
| 171 |
- var osFeatures []string |
|
| 172 |
- |
|
| 173 |
- if container.ImageID != "" {
|
|
| 174 |
- img, err := daemon.imageStore.Get(container.ImageID) |
|
| 168 |
+ var parent *image.Image |
|
| 169 |
+ if container.ImageID == "" {
|
|
| 170 |
+ parent = new(image.Image) |
|
| 171 |
+ parent.RootFS = image.NewRootFS() |
|
| 172 |
+ } else {
|
|
| 173 |
+ parent, err = daemon.imageStore.Get(container.ImageID) |
|
| 175 | 174 |
if err != nil {
|
| 176 | 175 |
return "", err |
| 177 | 176 |
} |
| 178 |
- history = img.History |
|
| 179 |
- rootFS = img.RootFS |
|
| 180 |
- osVersion = img.OSVersion |
|
| 181 |
- osFeatures = img.OSFeatures |
|
| 182 | 177 |
} |
| 183 | 178 |
|
| 184 |
- l, err := daemon.layerStore.Register(rwTar, rootFS.ChainID()) |
|
| 179 |
+ l, err := daemon.layerStore.Register(rwTar, parent.RootFS.ChainID()) |
|
| 185 | 180 |
if err != nil {
|
| 186 | 181 |
return "", err |
| 187 | 182 |
} |
| 188 | 183 |
defer layer.ReleaseAndLog(daemon.layerStore, l) |
| 189 | 184 |
|
| 190 |
- h := image.History{
|
|
| 191 |
- Author: c.Author, |
|
| 192 |
- Created: time.Now().UTC(), |
|
| 193 |
- CreatedBy: strings.Join(containerConfig.Cmd, " "), |
|
| 194 |
- Comment: c.Comment, |
|
| 195 |
- EmptyLayer: true, |
|
| 185 |
+ containerConfig := c.ContainerConfig |
|
| 186 |
+ if containerConfig == nil {
|
|
| 187 |
+ containerConfig = container.Config |
|
| 196 | 188 |
} |
| 197 |
- |
|
| 198 |
- if diffID := l.DiffID(); layer.DigestSHA256EmptyTar != diffID {
|
|
| 199 |
- h.EmptyLayer = false |
|
| 200 |
- rootFS.Append(diffID) |
|
| 189 |
+ cc := image.ChildConfig{
|
|
| 190 |
+ ContainerID: container.ID, |
|
| 191 |
+ Author: c.Author, |
|
| 192 |
+ Comment: c.Comment, |
|
| 193 |
+ ContainerConfig: containerConfig, |
|
| 194 |
+ Config: newConfig, |
|
| 195 |
+ DiffID: l.DiffID(), |
|
| 201 | 196 |
} |
| 202 |
- |
|
| 203 |
- history = append(history, h) |
|
| 204 |
- |
|
| 205 |
- config, err := json.Marshal(&image.Image{
|
|
| 206 |
- V1Image: image.V1Image{
|
|
| 207 |
- DockerVersion: dockerversion.Version, |
|
| 208 |
- Config: newConfig, |
|
| 209 |
- Architecture: runtime.GOARCH, |
|
| 210 |
- OS: runtime.GOOS, |
|
| 211 |
- Container: container.ID, |
|
| 212 |
- ContainerConfig: *containerConfig, |
|
| 213 |
- Author: c.Author, |
|
| 214 |
- Created: h.Created, |
|
| 215 |
- }, |
|
| 216 |
- RootFS: rootFS, |
|
| 217 |
- History: history, |
|
| 218 |
- OSFeatures: osFeatures, |
|
| 219 |
- OSVersion: osVersion, |
|
| 220 |
- }) |
|
| 221 |
- |
|
| 197 |
+ config, err := json.Marshal(parent.NewChild(cc)) |
|
| 222 | 198 |
if err != nil {
|
| 223 | 199 |
return "", err |
| 224 | 200 |
} |
| ... | ... |
@@ -7,7 +7,11 @@ import ( |
| 7 | 7 |
"time" |
| 8 | 8 |
|
| 9 | 9 |
"github.com/docker/docker/api/types/container" |
| 10 |
+ "github.com/docker/docker/dockerversion" |
|
| 11 |
+ "github.com/docker/docker/layer" |
|
| 10 | 12 |
"github.com/opencontainers/go-digest" |
| 13 |
+ "runtime" |
|
| 14 |
+ "strings" |
|
| 11 | 15 |
) |
| 12 | 16 |
|
| 13 | 17 |
// ID is the content-addressable ID of an image. |
| ... | ... |
@@ -110,6 +114,48 @@ func (img *Image) MarshalJSON() ([]byte, error) {
|
| 110 | 110 |
return json.Marshal(c) |
| 111 | 111 |
} |
| 112 | 112 |
|
| 113 |
+// ChildConfig is the configuration to apply to an Image to create a new |
|
| 114 |
+// Child image. Other properties of the image are copied from the parent. |
|
| 115 |
+type ChildConfig struct {
|
|
| 116 |
+ ContainerID string |
|
| 117 |
+ Author string |
|
| 118 |
+ Comment string |
|
| 119 |
+ DiffID layer.DiffID |
|
| 120 |
+ ContainerConfig *container.Config |
|
| 121 |
+ Config *container.Config |
|
| 122 |
+} |
|
| 123 |
+ |
|
| 124 |
+// NewChild creates a new Image as a child of this image. |
|
| 125 |
+func (img *Image) NewChild(child ChildConfig) *Image {
|
|
| 126 |
+ isEmptyLayer := layer.IsEmpty(child.DiffID) |
|
| 127 |
+ rootFS := img.RootFS |
|
| 128 |
+ if !isEmptyLayer {
|
|
| 129 |
+ rootFS.Append(child.DiffID) |
|
| 130 |
+ } |
|
| 131 |
+ imgHistory := NewHistory( |
|
| 132 |
+ child.Author, |
|
| 133 |
+ child.Comment, |
|
| 134 |
+ strings.Join(child.ContainerConfig.Cmd, " "), |
|
| 135 |
+ isEmptyLayer) |
|
| 136 |
+ |
|
| 137 |
+ return &Image{
|
|
| 138 |
+ V1Image: V1Image{
|
|
| 139 |
+ DockerVersion: dockerversion.Version, |
|
| 140 |
+ Config: child.Config, |
|
| 141 |
+ Architecture: runtime.GOARCH, |
|
| 142 |
+ OS: runtime.GOOS, |
|
| 143 |
+ Container: child.ContainerID, |
|
| 144 |
+ ContainerConfig: *child.ContainerConfig, |
|
| 145 |
+ Author: child.Author, |
|
| 146 |
+ Created: imgHistory.Created, |
|
| 147 |
+ }, |
|
| 148 |
+ RootFS: rootFS, |
|
| 149 |
+ History: append(img.History, imgHistory), |
|
| 150 |
+ OSFeatures: img.OSFeatures, |
|
| 151 |
+ OSVersion: img.OSVersion, |
|
| 152 |
+ } |
|
| 153 |
+} |
|
| 154 |
+ |
|
| 113 | 155 |
// History stores build commands that were used to create an image |
| 114 | 156 |
type History struct {
|
| 115 | 157 |
// Created is the timestamp at which the image was created |
| ... | ... |
@@ -126,6 +172,18 @@ type History struct {
|
| 126 | 126 |
EmptyLayer bool `json:"empty_layer,omitempty"` |
| 127 | 127 |
} |
| 128 | 128 |
|
| 129 |
+// NewHistory creates a new history struct from arguments, and sets the created |
|
| 130 |
+// time to the current time in UTC |
|
| 131 |
+func NewHistory(author, comment, createdBy string, isEmptyLayer bool) History {
|
|
| 132 |
+ return History{
|
|
| 133 |
+ Author: author, |
|
| 134 |
+ Created: time.Now().UTC(), |
|
| 135 |
+ CreatedBy: createdBy, |
|
| 136 |
+ Comment: comment, |
|
| 137 |
+ EmptyLayer: isEmptyLayer, |
|
| 138 |
+ } |
|
| 139 |
+} |
|
| 140 |
+ |
|
| 129 | 141 |
// Exporter provides interface for loading and saving images |
| 130 | 142 |
type Exporter interface {
|
| 131 | 143 |
Load(io.ReadCloser, io.Writer, bool) error |
| ... | ... |
@@ -54,3 +54,8 @@ func (el *emptyLayer) DiffSize() (size int64, err error) {
|
| 54 | 54 |
func (el *emptyLayer) Metadata() (map[string]string, error) {
|
| 55 | 55 |
return make(map[string]string), nil |
| 56 | 56 |
} |
| 57 |
+ |
|
| 58 |
+// IsEmpty returns true if the layer is an EmptyLayer |
|
| 59 |
+func IsEmpty(diffID DiffID) bool {
|
|
| 60 |
+ return diffID == DigestSHA256EmptyTar |
|
| 61 |
+} |