From faf1d92a03176805db6368c4e47dba5cfbfc31c4 Mon Sep 17 00:00:00 2001 From: Eldar Yusupov Date: Tue, 16 Jul 2019 18:49:28 +0300 Subject: [PATCH 1/8] Don't recreate cached layers This caching optimization reuses existing layer from a cache image for a RUN command instead of unpacking it and recreating. It also helps with multi-stage build caching issues - when recreating cached layers, some attributes like timestamps are not recreated and the digest for the new first stage image was different, breaking caching for subsequent stages. --- pkg/commands/base_command.go | 4 ++++ pkg/commands/commands.go | 3 +++ pkg/commands/run.go | 14 ++++++++------ pkg/executor/build.go | 32 ++++++++++++++++++++++++++++++-- 4 files changed, 45 insertions(+), 8 deletions(-) diff --git a/pkg/commands/base_command.go b/pkg/commands/base_command.go index 3d2cd8fad4..b4be3aa40e 100644 --- a/pkg/commands/base_command.go +++ b/pkg/commands/base_command.go @@ -44,6 +44,10 @@ func (b *BaseCommand) RequiresUnpackedFS() bool { return false } +func (b *BaseCommand) CacheImage() v1.Image { + return nil +} + func (b *BaseCommand) ShouldCacheOutput() bool { return false } diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go index 2615138be4..3d82db6699 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -40,6 +40,9 @@ type DockerCommand interface { // Return a cache-aware implementation of this command, if it exists. CacheCommand(v1.Image) DockerCommand + // Return an image with cached layer for this command, if it exists + CacheImage() v1.Image + // Return true if this command depends on the build context. FilesUsedFromContext(*v1.Config, *dockerfile.BuildArgs) ([]string, error) diff --git a/pkg/commands/run.go b/pkg/commands/run.go index 7a9bf03dc2..04444f3f45 100644 --- a/pkg/commands/run.go +++ b/pkg/commands/run.go @@ -175,23 +175,25 @@ func (r *RunCommand) ShouldCacheOutput() bool { type CachingRunCommand struct { BaseCommand - img v1.Image - extractedFiles []string - cmd *instructions.RunCommand + img v1.Image + cmd *instructions.RunCommand } func (cr *CachingRunCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error { logrus.Infof("Found cached layer, extracting to filesystem") - var err error - cr.extractedFiles, err = util.GetFSFromImage(constants.RootDir, cr.img) + _, err := util.GetFSFromImage(constants.RootDir, cr.img) if err != nil { return errors.Wrap(err, "extracting fs from image") } return nil } +func (cr *CachingRunCommand) CacheImage() v1.Image { + return cr.img +} + func (cr *CachingRunCommand) FilesToSnapshot() []string { - return cr.extractedFiles + return []string{} } func (cr *CachingRunCommand) String() string { diff --git a/pkg/executor/build.go b/pkg/executor/build.go index e134e52b57..5461f42fba 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -223,6 +223,7 @@ func (s *stageBuilder) build() error { if err := util.DetectFilesystemWhitelist(constants.WhitelistPath); err != nil { return err } + // Take initial snapshot t := timing.Start("Initial FS snapshot") if err := s.snapshotter.Init(); err != nil { @@ -254,9 +255,20 @@ func (s *stageBuilder) build() error { if err := command.ExecuteCommand(&s.cf.Config, s.args); err != nil { return err } - files = command.FilesToSnapshot() timing.DefaultRun.Stop(t) + cacheImg := command.CacheImage() + if cacheImg != nil { + layers, err := cacheImg.Layers() + if err != nil { + logrus.Debugf("Failed to retrieve layer from cache image: %s", err) + continue + } + s.addLayerToImage(command.String(), layers[0]) + continue + } + + files = command.FilesToSnapshot() if !s.shouldTakeSnapshot(index, files) { continue } @@ -270,6 +282,7 @@ func (s *stageBuilder) build() error { if err != nil { return err } + // Push layer to cache (in parallel) now along with new config file if s.opts.Cache && command.ShouldCacheOutput() { cacheGroup.Go(func() error { @@ -327,6 +340,21 @@ func (s *stageBuilder) shouldTakeSnapshot(index int, files []string) bool { return true } +func (s *stageBuilder) addLayerToImage(createdBy string, layer v1.Layer) error { + logrus.Infof("Adding cached layer for command %s to the image", createdBy) + var err error + s.image, err = mutate.Append(s.image, + mutate.Addendum{ + Layer: layer, + History: v1.History{ + Author: constants.Author, + CreatedBy: createdBy, + }, + }, + ) + return err +} + func (s *stageBuilder) saveSnapshotToImage(createdBy string, tarPath string) error { if tarPath == "" { return nil @@ -344,6 +372,7 @@ func (s *stageBuilder) saveSnapshotToImage(createdBy string, tarPath string) err if err != nil { return err } + s.image, err = mutate.Append(s.image, mutate.Addendum{ Layer: layer, @@ -354,7 +383,6 @@ func (s *stageBuilder) saveSnapshotToImage(createdBy string, tarPath string) err }, ) return err - } func CalculateDependencies(opts *config.KanikoOptions) (map[int][]string, error) { From 95e06b6f8cf74de88df66c142e63d3a99fd73e75 Mon Sep 17 00:00:00 2001 From: Eldar Yusupov Date: Wed, 17 Jul 2019 02:47:42 +0300 Subject: [PATCH 2/8] Optimize FS snapshotting Defer FS state setup until the first cache miss. Cached commands are now uniformly handled by extracting the layer from the cached image. This allows skipping layers unpacking uniformly when they are not required for the stage build. --- pkg/commands/base_command.go | 11 ++++--- pkg/commands/commands.go | 12 +++---- pkg/commands/run.go | 36 -------------------- pkg/executor/build.go | 64 +++++++++++++++++++----------------- pkg/snapshot/snapshot.go | 4 +-- 5 files changed, 48 insertions(+), 79 deletions(-) diff --git a/pkg/commands/base_command.go b/pkg/commands/base_command.go index b4be3aa40e..02eb23afba 100644 --- a/pkg/commands/base_command.go +++ b/pkg/commands/base_command.go @@ -22,10 +22,7 @@ import ( ) type BaseCommand struct { -} - -func (b *BaseCommand) CacheCommand(v1.Image) DockerCommand { - return nil + cache v1.Image } func (b *BaseCommand) FilesToSnapshot() []string { @@ -45,7 +42,11 @@ func (b *BaseCommand) RequiresUnpackedFS() bool { } func (b *BaseCommand) CacheImage() v1.Image { - return nil + return b.cache +} + +func (b *BaseCommand) SetCacheImage(cache v1.Image) { + b.cache = cache } func (b *BaseCommand) ShouldCacheOutput() bool { diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go index 3d82db6699..9587bb3160 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -37,12 +37,6 @@ type DockerCommand interface { // A list of files to snapshot, empty for metadata commands or nil if we don't know FilesToSnapshot() []string - // Return a cache-aware implementation of this command, if it exists. - CacheCommand(v1.Image) DockerCommand - - // Return an image with cached layer for this command, if it exists - CacheImage() v1.Image - // Return true if this command depends on the build context. FilesUsedFromContext(*v1.Config, *dockerfile.BuildArgs) ([]string, error) @@ -51,6 +45,12 @@ type DockerCommand interface { RequiresUnpackedFS() bool ShouldCacheOutput() bool + + // Returns a cache image for the layer created by this command, if set + CacheImage() v1.Image + + // Sets the cache image for the command's layer + SetCacheImage(cache v1.Image) } func GetCommand(cmd instructions.Command, buildcontext string) (DockerCommand, error) { diff --git a/pkg/commands/run.go b/pkg/commands/run.go index 04444f3f45..85a2f9b8b5 100644 --- a/pkg/commands/run.go +++ b/pkg/commands/run.go @@ -152,15 +152,6 @@ func (r *RunCommand) FilesToSnapshot() []string { return nil } -// CacheCommand returns true since this command should be cached -func (r *RunCommand) CacheCommand(img v1.Image) DockerCommand { - - return &CachingRunCommand{ - img: img, - cmd: r.cmd, - } -} - func (r *RunCommand) MetadataOnly() bool { return false } @@ -172,30 +163,3 @@ func (r *RunCommand) RequiresUnpackedFS() bool { func (r *RunCommand) ShouldCacheOutput() bool { return true } - -type CachingRunCommand struct { - BaseCommand - img v1.Image - cmd *instructions.RunCommand -} - -func (cr *CachingRunCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error { - logrus.Infof("Found cached layer, extracting to filesystem") - _, err := util.GetFSFromImage(constants.RootDir, cr.img) - if err != nil { - return errors.Wrap(err, "extracting fs from image") - } - return nil -} - -func (cr *CachingRunCommand) CacheImage() v1.Image { - return cr.img -} - -func (cr *CachingRunCommand) FilesToSnapshot() []string { - return []string{} -} - -func (cr *CachingRunCommand) String() string { - return cr.cmd.String() -} diff --git a/pkg/executor/build.go b/pkg/executor/build.go index 5461f42fba..cde2ea34c0 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -172,10 +172,8 @@ func (s *stageBuilder) optimize(compositeKey CompositeCache, cfg v1.Config) erro break } - if cacheCmd := command.CacheCommand(img); cacheCmd != nil { - logrus.Infof("Using caching version of cmd: %s", command.String()) - s.cmds[i] = cacheCmd - } + logrus.Infof("Found cached layer for cmd: %s", command.String()) + s.cmds[i].SetCacheImage(img) } // Mutate the config for any commands that require it. @@ -201,13 +199,14 @@ func (s *stageBuilder) build() error { // Unpack file system to root if we need to. shouldUnpack := false for _, cmd := range s.cmds { - if cmd.RequiresUnpackedFS() { + if cmd.CacheImage() == nil && cmd.RequiresUnpackedFS() { logrus.Infof("Unpacking rootfs as cmd %s requires it.", cmd.String()) shouldUnpack = true break } } if len(s.crossStageDeps[s.stage.Index]) > 0 { + logrus.Info("Unpacking rootfs as other stages might require files from it.") shouldUnpack = true } @@ -220,16 +219,6 @@ func (s *stageBuilder) build() error { } else { logrus.Info("Skipping unpacking as no commands require it.") } - if err := util.DetectFilesystemWhitelist(constants.WhitelistPath); err != nil { - return err - } - - // Take initial snapshot - t := timing.Start("Initial FS snapshot") - if err := s.snapshotter.Init(); err != nil { - return err - } - timing.DefaultRun.Stop(t) cacheGroup := errgroup.Group{} for index, command := range s.cmds { @@ -252,17 +241,33 @@ func (s *stageBuilder) build() error { } logrus.Info(command.String()) - if err := command.ExecuteCommand(&s.cf.Config, s.args); err != nil { - return err + cacheImg := command.CacheImage() + if cacheImg == nil { + if command.RequiresUnpackedFS() && !command.MetadataOnly() { + // Prepare initial state for the FS diff + t := timing.Start("Pre-command execution FS snapshot") + s.snapshotter.Init() + timing.DefaultRun.Stop(t) + } + if err := command.ExecuteCommand(&s.cf.Config, s.args); err != nil { + return err + } + } else { + if shouldUnpack { + logrus.Info("Found cached layer, unpacking the filesystem") + if _, err := util.GetFSFromImage(constants.RootDir, cacheImg); err != nil { + return err + } + } else { + logrus.Info("Found cached layer, unpacking is not required") + } } timing.DefaultRun.Stop(t) - cacheImg := command.CacheImage() if cacheImg != nil { layers, err := cacheImg.Layers() if err != nil { - logrus.Debugf("Failed to retrieve layer from cache image: %s", err) - continue + return err } s.addLayerToImage(command.String(), layers[0]) continue @@ -278,13 +283,13 @@ func (s *stageBuilder) build() error { return err } - ck, err := compositeKey.Hash() - if err != nil { - return err - } - // Push layer to cache (in parallel) now along with new config file if s.opts.Cache && command.ShouldCacheOutput() { + ck, err := compositeKey.Hash() + if err != nil { + return err + } + cacheGroup.Go(func() error { return pushLayerToCache(s.opts, ck, tarPath, command.String()) }) @@ -322,11 +327,6 @@ func (s *stageBuilder) shouldTakeSnapshot(index int, files []string) bool { return isLastCommand } - // Always take snapshots if we're using the cache. - if s.opts.Cache { - return true - } - // nil means snapshot everything. if files == nil { return true @@ -469,6 +469,10 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) { } logrus.Infof("Built cross stage deps: %v", crossStageDependencies) + if err := util.DetectFilesystemWhitelist(constants.WhitelistPath); err != nil { + return nil, err + } + for index, stage := range stages { sb, err := newStageBuilder(opts, stage, crossStageDependencies) if err != nil { diff --git a/pkg/snapshot/snapshot.go b/pkg/snapshot/snapshot.go index 5577da087e..24f831e15a 100644 --- a/pkg/snapshot/snapshot.go +++ b/pkg/snapshot/snapshot.go @@ -96,6 +96,8 @@ func (s *Snapshotter) TakeSnapshot(files []string) (string, error) { // TakeSnapshotFS takes a snapshot of the filesystem, avoiding directories in the whitelist, and creates // a tarball of the changed files. func (s *Snapshotter) TakeSnapshotFS() (string, error) { + logrus.Info("Taking snapshot of full filesystem...") + f, err := ioutil.TempFile(snapshotPathPrefix, "") if err != nil { return "", err @@ -117,8 +119,6 @@ func (s *Snapshotter) TakeSnapshotFS() (string, error) { } func (s *Snapshotter) scanFullFilesystem() ([]string, []string, error) { - logrus.Info("Taking snapshot of full filesystem...") - // Some of the operations that follow (e.g. hashing) depend on the file system being synced, // for example the hashing function that determines if files are equal uses the mtime of the files, // which can lag if sync is not called. Unfortunately there can still be lag if too much data needs From c998e924772ee5b3de42bfa4b10df2c2a7d6acf6 Mon Sep 17 00:00:00 2001 From: Eldar Yusupov Date: Wed, 17 Jul 2019 12:37:12 +0300 Subject: [PATCH 3/8] Log base image digest --- pkg/executor/build.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/executor/build.go b/pkg/executor/build.go index cde2ea34c0..c4a21b1181 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -188,6 +188,7 @@ func (s *stageBuilder) optimize(compositeKey CompositeCache, cfg v1.Config) erro func (s *stageBuilder) build() error { // Set the initial cache key to be the base image digest, the build args and the SrcContext. + logrus.Infof("Base image digest is %s", s.baseImageDigest) compositeKey := NewCompositeCache(s.baseImageDigest) compositeKey.AddKey(s.opts.BuildArgs...) From d4b6db4821e536cca34865ada461d4e3a1fb2410 Mon Sep 17 00:00:00 2001 From: Eldar Yusupov Date: Wed, 17 Jul 2019 12:55:22 +0300 Subject: [PATCH 4/8] Do not lookup cache images when caching is disabled --- pkg/executor/build.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/executor/build.go b/pkg/executor/build.go index c4a21b1181..d27b3e4e3c 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -163,7 +163,7 @@ func (s *stageBuilder) optimize(compositeKey CompositeCache, cfg v1.Config) erro if err != nil { return err } - if command.ShouldCacheOutput() { + if s.opts.Cache && command.ShouldCacheOutput() { img, err := layerCache.RetrieveLayer(ck) if err != nil { logrus.Debugf("Failed to retrieve layer: %s", err) From 0352d40251a6562ee828d7c9aec75b7327cadb2d Mon Sep 17 00:00:00 2001 From: Eldar Yusupov Date: Wed, 17 Jul 2019 12:56:43 +0300 Subject: [PATCH 5/8] Cache workdir command It needs unpacked FS to check if the workdir exists. Also it can break caching keys if the workdir did not exist. --- pkg/commands/base_command.go | 4 +++ pkg/commands/commands.go | 6 +++++ pkg/commands/workdir.go | 50 ++++++++++++++++++++++++++++++++---- pkg/commands/workdir_test.go | 4 +-- pkg/executor/build.go | 14 +++++++++- 5 files changed, 70 insertions(+), 8 deletions(-) diff --git a/pkg/commands/base_command.go b/pkg/commands/base_command.go index 02eb23afba..bdca56d78b 100644 --- a/pkg/commands/base_command.go +++ b/pkg/commands/base_command.go @@ -41,6 +41,10 @@ func (b *BaseCommand) RequiresUnpackedFS() bool { return false } +func (b *BaseCommand) CacheCommand() DockerCommand { + return nil +} + func (b *BaseCommand) CacheImage() v1.Image { return b.cache } diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go index 9587bb3160..5efd820c90 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -46,6 +46,12 @@ type DockerCommand interface { ShouldCacheOutput() bool + // Return an implementation of the command to be executed if the cached layer + // exists. This command will be executed after the layer cache is unpacked, + // so it only needs to contain additional (for example, metadata changes) + // operatons. + CacheCommand() DockerCommand + // Returns a cache image for the layer created by this command, if set CacheImage() v1.Image diff --git a/pkg/commands/workdir.go b/pkg/commands/workdir.go index 179a6d4c8e..8fdc26b7d8 100644 --- a/pkg/commands/workdir.go +++ b/pkg/commands/workdir.go @@ -37,9 +37,7 @@ type WorkdirCommand struct { // For testing var mkdir = os.MkdirAll -func (w *WorkdirCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error { - logrus.Info("cmd: workdir") - workdirPath := w.cmd.Path +func updateWorkdir(workdirPath string, config *v1.Config, buildArgs *dockerfile.BuildArgs) error { replacementEnvs := buildArgs.ReplacementEnvs(config.Env) resolvedWorkingDir, err := util.ResolveEnvironmentReplacement(workdirPath, replacementEnvs, true) if err != nil { @@ -51,14 +49,25 @@ func (w *WorkdirCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile config.WorkingDir = filepath.Join(config.WorkingDir, resolvedWorkingDir) } logrus.Infof("Changed working directory to %s", config.WorkingDir) + return nil +} + +func (w *WorkdirCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error { + logrus.Info("cmd: workdir") + + workdirPath := w.cmd.Path + err := updateWorkdir(workdirPath, config, buildArgs) + if err != nil { + return err + } - // Only create and snapshot the dir if it didn't exist already - w.snapshotFiles = []string{} if _, err := os.Stat(config.WorkingDir); os.IsNotExist(err) { logrus.Infof("Creating directory %s", config.WorkingDir) w.snapshotFiles = append(w.snapshotFiles, config.WorkingDir) return mkdir(config.WorkingDir, 0755) } + // Cache the empty layer so we don't have to unpack FS on rerun + w.snapshotFiles = nil return nil } @@ -75,3 +84,34 @@ func (w *WorkdirCommand) String() string { func (w *WorkdirCommand) MetadataOnly() bool { return false } + +func (w *WorkdirCommand) RequiresUnpackedFS() bool { + return true +} + +func (w *WorkdirCommand) ShouldCacheOutput() bool { + return true +} + +func (w *WorkdirCommand) CacheCommand() DockerCommand { + return &CachedWorkdirCommand{cmd: w.cmd} +} + +type CachedWorkdirCommand struct { + BaseCommand + cmd *instructions.WorkdirCommand +} + +func (cw *CachedWorkdirCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error { + logrus.Info("cmd: workdir") + + return updateWorkdir(cw.cmd.Path, config, buildArgs) +} + +func (cw *CachedWorkdirCommand) MetadataOnly() bool { + return true +} + +func (cw *CachedWorkdirCommand) String() string { + return cw.cmd.String() +} diff --git a/pkg/commands/workdir_test.go b/pkg/commands/workdir_test.go index be67098fa1..6eaafea258 100644 --- a/pkg/commands/workdir_test.go +++ b/pkg/commands/workdir_test.go @@ -63,7 +63,7 @@ var workdirTests = []struct { { path: "$home", expectedPath: "/root", - snapshotFiles: []string{}, + snapshotFiles: nil, }, { path: "/foo/$path/$home", @@ -73,7 +73,7 @@ var workdirTests = []struct { { path: "/tmp", expectedPath: "/tmp", - snapshotFiles: []string{}, + snapshotFiles: nil, }, } diff --git a/pkg/executor/build.go b/pkg/executor/build.go index d27b3e4e3c..a6ffa3e26f 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -174,6 +174,11 @@ func (s *stageBuilder) optimize(compositeKey CompositeCache, cfg v1.Config) erro logrus.Infof("Found cached layer for cmd: %s", command.String()) s.cmds[i].SetCacheImage(img) + + if cacheCmd := command.CacheCommand(); cacheCmd != nil { + logrus.Infof("Using caching version of cmd: %s", command.String()) + s.cmds[i] = cacheCmd + } } // Mutate the config for any commands that require it. @@ -342,8 +347,15 @@ func (s *stageBuilder) shouldTakeSnapshot(index int, files []string) bool { } func (s *stageBuilder) addLayerToImage(createdBy string, layer v1.Layer) error { + size, err := layer.Size() + if err != nil { + return err + } + if size <= emptyTarSize { + logrus.Info("No files were changed, appending empty layer to config. No layer added to image.") + return nil + } logrus.Infof("Adding cached layer for command %s to the image", createdBy) - var err error s.image, err = mutate.Append(s.image, mutate.Addendum{ Layer: layer, From 9cfb6d12b474d2e02a313772300c8408b75eca62 Mon Sep 17 00:00:00 2001 From: Eldar Yusupov Date: Wed, 17 Jul 2019 13:16:47 +0300 Subject: [PATCH 6/8] Add more logging --- pkg/executor/build.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/executor/build.go b/pkg/executor/build.go index a6ffa3e26f..9de8b2e22c 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -252,9 +252,12 @@ func (s *stageBuilder) build() error { if command.RequiresUnpackedFS() && !command.MetadataOnly() { // Prepare initial state for the FS diff t := timing.Start("Pre-command execution FS snapshot") + logrus.Debug("Scanning pre-snapshot filesystem state") s.snapshotter.Init() timing.DefaultRun.Stop(t) } + + logrus.Info("Executing command") if err := command.ExecuteCommand(&s.cf.Config, s.args); err != nil { return err } From e8f1819bfffb8ba71ef0e9ac5fc59856d2c826fd Mon Sep 17 00:00:00 2001 From: Eldar Yusupov Date: Tue, 23 Jul 2019 23:07:25 +0300 Subject: [PATCH 7/8] Handle SingleSnaphot option --- pkg/executor/build.go | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/pkg/executor/build.go b/pkg/executor/build.go index 9de8b2e22c..4fff9e18db 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -17,7 +17,9 @@ limitations under the License. package executor import ( + "archive/tar" "fmt" + "io" "os" "path/filepath" "strconv" @@ -202,7 +204,7 @@ func (s *stageBuilder) build() error { return err } - // Unpack file system to root if we need to. + // Check if any uncached command at this stage requires root fs shouldUnpack := false for _, cmd := range s.cmds { if cmd.CacheImage() == nil && cmd.RequiresUnpackedFS() { @@ -226,6 +228,16 @@ func (s *stageBuilder) build() error { logrus.Info("Skipping unpacking as no commands require it.") } + if s.opts.SingleSnapshot { + // Take initial snapshot + t := timing.Start("Initial FS snapshot") + logrus.Debug("Scanning initial filesystem state") + if err := s.snapshotter.Init(); err != nil { + return err + } + timing.DefaultRun.Stop(t) + } + cacheGroup := errgroup.Group{} for index, command := range s.cmds { if command == nil { @@ -256,11 +268,6 @@ func (s *stageBuilder) build() error { s.snapshotter.Init() timing.DefaultRun.Stop(t) } - - logrus.Info("Executing command") - if err := command.ExecuteCommand(&s.cf.Config, s.args); err != nil { - return err - } } else { if shouldUnpack { logrus.Info("Found cached layer, unpacking the filesystem") @@ -273,6 +280,13 @@ func (s *stageBuilder) build() error { } timing.DefaultRun.Stop(t) + if cacheImg == nil || !command.ShouldCacheOutput() { + logrus.Info("Executing command") + if err := command.ExecuteCommand(&s.cf.Config, s.args); err != nil { + return err + } + } + if cacheImg != nil { layers, err := cacheImg.Layers() if err != nil { @@ -350,14 +364,20 @@ func (s *stageBuilder) shouldTakeSnapshot(index int, files []string) bool { } func (s *stageBuilder) addLayerToImage(createdBy string, layer v1.Layer) error { - size, err := layer.Size() + uncompressed, err := layer.Uncompressed() if err != nil { return err } - if size <= emptyTarSize { + + tr := tar.NewReader(uncompressed) + _, err = tr.Next() + if err == io.EOF { logrus.Info("No files were changed, appending empty layer to config. No layer added to image.") return nil + } else if err != nil { + return err } + logrus.Infof("Adding cached layer for command %s to the image", createdBy) s.image, err = mutate.Append(s.image, mutate.Addendum{ From 760acb89dc4acdede13dd6b364ad54989f3ef1fd Mon Sep 17 00:00:00 2001 From: Eldar Yusupov Date: Wed, 24 Jul 2019 04:45:20 +0300 Subject: [PATCH 8/8] Handle volumes --- pkg/commands/volume.go | 2 +- pkg/executor/build.go | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/commands/volume.go b/pkg/commands/volume.go index b6d92bf53a..40fbd59b21 100644 --- a/pkg/commands/volume.go +++ b/pkg/commands/volume.go @@ -50,7 +50,7 @@ func (v *VolumeCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile. existingVolumes[volume] = x util.AddVolumePathToWhitelist(volume) - // Only create and snapshot the dir if it didn't exist already + // Only create the dir if it didn't exist already if _, err := os.Stat(volume); os.IsNotExist(err) { logrus.Infof("Creating directory %s", volume) if err := os.MkdirAll(volume, 0755); err != nil { diff --git a/pkg/executor/build.go b/pkg/executor/build.go index 4fff9e18db..6f7dc8ca02 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -239,6 +239,7 @@ func (s *stageBuilder) build() error { } cacheGroup := errgroup.Group{} + snapshotInitialized := false for index, command := range s.cmds { if command == nil { continue @@ -261,12 +262,13 @@ func (s *stageBuilder) build() error { cacheImg := command.CacheImage() if cacheImg == nil { - if command.RequiresUnpackedFS() && !command.MetadataOnly() { + if !snapshotInitialized && command.RequiresUnpackedFS() && !command.MetadataOnly() { // Prepare initial state for the FS diff t := timing.Start("Pre-command execution FS snapshot") logrus.Debug("Scanning pre-snapshot filesystem state") s.snapshotter.Init() timing.DefaultRun.Stop(t) + snapshotInitialized = true } } else { if shouldUnpack {