diff --git a/cmd/executor/cmd/root.go b/cmd/executor/cmd/root.go index 8b60da706..eed04861f 100644 --- a/cmd/executor/cmd/root.go +++ b/cmd/executor/cmd/root.go @@ -174,6 +174,7 @@ func addKanikoOptionsFlags() { RootCmd.PersistentFlags().BoolVarP(&opts.IgnoreVarRun, "whitelist-var-run", "", true, "Ignore /var/run directory when taking image snapshot. Set it to false to preserve /var/run/ in destination image. (Default true).") RootCmd.PersistentFlags().VarP(&opts.Labels, "label", "", "Set metadata for an image. Set it repeatedly for multiple labels.") RootCmd.PersistentFlags().BoolVarP(&opts.SkipUnusedStages, "skip-unused-stages", "", false, "Build only used stages if defined to true. Otherwise it builds by default all stages, even the unnecessaries ones until it reaches the target stage / end of Dockerfile") + RootCmd.PersistentFlags().BoolVarP(&opts.RunV2, "use-new-run", "", false, "Experimental run command to detect file system changes. This new run command does no rely on snapshotting to detect changes.") } // addHiddenFlags marks certain flags as hidden from the executor help text diff --git a/integration/dockerfiles/Dockerfile_test_run_new b/integration/dockerfiles/Dockerfile_test_run_new new file mode 100644 index 000000000..9891a4381 --- /dev/null +++ b/integration/dockerfiles/Dockerfile_test_run_new @@ -0,0 +1,26 @@ +# Copyright 2018 Google, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM debian:9.11 +RUN echo "hey" > /etc/foo +RUN echo "baz" > /etc/baz +RUN cp /etc/baz /etc/bar +RUN rm /etc/baz + +# Test with ARG +ARG file +RUN echo "run" > $file + +RUN echo "test home" > $HOME/file +COPY context/foo $HOME/foo diff --git a/integration/images.go b/integration/images.go index 8865e836a..c221d6504 100644 --- a/integration/images.go +++ b/integration/images.go @@ -48,6 +48,7 @@ const ( // Arguments to build Dockerfiles with, used for both docker and kaniko builds var argsMap = map[string][]string{ "Dockerfile_test_run": {"file=/file"}, + "Dockerfile_test_run_new": {"file=/file"}, "Dockerfile_test_workdir": {"workdir=/arg/workdir"}, "Dockerfile_test_add": {"file=context/foo"}, "Dockerfile_test_arg_secret": {"SSH_PRIVATE_KEY", "SSH_PUBLIC_KEY=Pµbl1cK€Y"}, @@ -74,6 +75,7 @@ var additionalDockerFlagsMap = map[string][]string{ // Arguments to build Dockerfiles with when building with kaniko var additionalKanikoFlagsMap = map[string][]string{ "Dockerfile_test_add": {"--single-snapshot"}, + "Dockerfile_test_run_new": {"--use-new-run=true"}, "Dockerfile_test_scratch": {"--single-snapshot"}, "Dockerfile_test_maintainer": {"--single-snapshot"}, "Dockerfile_test_target": {"--target=second"}, diff --git a/pkg/commands/base_command.go b/pkg/commands/base_command.go index 94c4fe156..cf19c35cb 100644 --- a/pkg/commands/base_command.go +++ b/pkg/commands/base_command.go @@ -51,3 +51,7 @@ func (b *BaseCommand) RequiresUnpackedFS() bool { func (b *BaseCommand) ShouldCacheOutput() bool { return false } + +func (a *BaseCommand) ShouldDetectDeletedFiles() bool { + return false +} diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go index 0a4e57134..a5d431735 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -52,11 +52,17 @@ type DockerCommand interface { RequiresUnpackedFS() bool ShouldCacheOutput() bool + + // ShouldDetectDeletedFiles turns true if the command could delete files. + ShouldDetectDeletedFiles() bool } -func GetCommand(cmd instructions.Command, buildcontext string) (DockerCommand, error) { +func GetCommand(cmd instructions.Command, buildcontext string, useNewRun bool) (DockerCommand, error) { switch c := cmd.(type) { case *instructions.RunCommand: + if useNewRun { + return &RunMarkerCommand{cmd: c}, nil + } return &RunCommand{cmd: c}, nil case *instructions.CopyCommand: return &CopyCommand{cmd: c, buildcontext: buildcontext}, nil diff --git a/pkg/commands/run.go b/pkg/commands/run.go index e0dd1beef..b99461eaa 100644 --- a/pkg/commands/run.go +++ b/pkg/commands/run.go @@ -46,8 +46,12 @@ var ( ) func (r *RunCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error { + return runCommandInExec(config, buildArgs, r.cmd) +} + +func runCommandInExec(config *v1.Config, buildArgs *dockerfile.BuildArgs, cmdRun *instructions.RunCommand) error { var newCommand []string - if r.cmd.PrependShell { + if cmdRun.PrependShell { // This is the default shell on Linux var shell []string if len(config.Shell) > 0 { @@ -56,9 +60,9 @@ func (r *RunCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bui shell = append(shell, "/bin/sh", "-c") } - newCommand = append(shell, strings.Join(r.cmd.CmdLine, " ")) + newCommand = append(shell, strings.Join(cmdRun.CmdLine, " ")) } else { - newCommand = r.cmd.CmdLine + newCommand = cmdRun.CmdLine } logrus.Infof("cmd: %s", newCommand[0]) @@ -111,7 +115,6 @@ func (r *RunCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bui if err := syscall.Kill(-pgid, syscall.SIGKILL); err != nil && err.Error() != "no such process" { return err } - return nil } diff --git a/pkg/commands/run_marker.go b/pkg/commands/run_marker.go new file mode 100644 index 000000000..780ba0814 --- /dev/null +++ b/pkg/commands/run_marker.go @@ -0,0 +1,106 @@ +/* +Copyright 2018 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package commands + +import ( + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/GoogleContainerTools/kaniko/pkg/dockerfile" + "github.com/GoogleContainerTools/kaniko/pkg/util" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/moby/buildkit/frontend/dockerfile/instructions" +) + +type RunMarkerCommand struct { + BaseCommand + cmd *instructions.RunCommand + Files []string +} + +func (r *RunMarkerCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error { + // run command `touch filemarker` + markerFile, err := ioutil.TempFile("", "marker") + defer func() { + os.Remove(markerFile.Name()) + }() + + if err := runCommandInExec(config, buildArgs, r.cmd); err != nil { + return err + } + + // run command find to find all new files generated + find := exec.Command("find", "/", "-newer", markerFile.Name()) + out, err := find.Output() + if err != nil { + r.Files = []string{} + return nil + } + + r.Files = []string{} + s := strings.Split(string(out), "\n") + for _, path := range s { + path = filepath.Clean(path) + if util.IsDestDir(path) || util.CheckIgnoreList(path) { + continue + } + r.Files = append(r.Files, path) + } + return nil +} + +// String returns some information about the command for the image config +func (r *RunMarkerCommand) String() string { + return r.cmd.String() +} + +func (r *RunMarkerCommand) FilesToSnapshot() []string { + return nil +} + +func (r *RunMarkerCommand) ProvidesFilesToSnapshot() bool { + return false +} + +// CacheCommand returns true since this command should be cached +func (r *RunMarkerCommand) CacheCommand(img v1.Image) DockerCommand { + + return &CachingRunCommand{ + img: img, + cmd: r.cmd, + extractFn: util.ExtractFile, + } +} + +func (r *RunMarkerCommand) MetadataOnly() bool { + return false +} + +func (r *RunMarkerCommand) RequiresUnpackedFS() bool { + return true +} + +func (r *RunMarkerCommand) ShouldCacheOutput() bool { + return true +} + +func (b *BaseCommand) ShouldDetectDelete() bool { + return true +} diff --git a/pkg/config/options.go b/pkg/config/options.go index 576d42f09..74db79b6f 100644 --- a/pkg/config/options.go +++ b/pkg/config/options.go @@ -57,6 +57,7 @@ type KanikoOptions struct { Cleanup bool IgnoreVarRun bool SkipUnusedStages bool + RunV2 bool } // WarmerOptions are options that are set by command line arguments to the cache warmer. diff --git a/pkg/executor/build.go b/pkg/executor/build.go index d05ddecaa..0ecd60962 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -62,7 +62,7 @@ type cachePusher func(*config.KanikoOptions, string, string, string) error type snapShotter interface { Init() error TakeSnapshotFS() (string, error) - TakeSnapshot([]string) (string, error) + TakeSnapshot([]string, bool) (string, error) } // stageBuilder contains all fields necessary to build one stage of a Dockerfile @@ -127,7 +127,7 @@ func newStageBuilder(opts *config.KanikoOptions, stage config.KanikoStage, cross } for _, cmd := range s.stage.Commands { - command, err := commands.GetCommand(cmd, opts.SrcContext) + command, err := commands.GetCommand(cmd, opts.SrcContext, opts.RunV2) if err != nil { return nil, err } @@ -382,7 +382,7 @@ func (s *stageBuilder) build() error { return errors.Wrap(err, "failed to save layer") } } else { - tarPath, err := s.takeSnapshot(files) + tarPath, err := s.takeSnapshot(files, command.ShouldDetectDeletedFiles()) if err != nil { return errors.Wrap(err, "failed to take snapshot") } @@ -416,7 +416,7 @@ func (s *stageBuilder) build() error { return nil } -func (s *stageBuilder) takeSnapshot(files []string) (string, error) { +func (s *stageBuilder) takeSnapshot(files []string, shdDelete bool) (string, error) { var snapshot string var err error @@ -426,7 +426,7 @@ func (s *stageBuilder) takeSnapshot(files []string) (string, error) { } else { // Volumes are very weird. They get snapshotted in the next command. files = append(files, util.Volumes()...) - snapshot, err = s.snapshotter.TakeSnapshot(files) + snapshot, err = s.snapshotter.TakeSnapshot(files, shdDelete) } timing.DefaultRun.Stop(t) return snapshot, err diff --git a/pkg/executor/build_test.go b/pkg/executor/build_test.go index dd1a377ce..5182322a1 100644 --- a/pkg/executor/build_test.go +++ b/pkg/executor/build_test.go @@ -1246,6 +1246,7 @@ func getCommands(dir string, cmds []instructions.Command) []commands.DockerComma cmd, err := commands.GetCommand( c, dir, + false, ) if err != nil { panic(err) diff --git a/pkg/executor/fakes.go b/pkg/executor/fakes.go index 41793af12..cc85fb17f 100644 --- a/pkg/executor/fakes.go +++ b/pkg/executor/fakes.go @@ -73,6 +73,9 @@ func (m MockDockerCommand) RequiresUnpackedFS() bool { func (m MockDockerCommand) ShouldCacheOutput() bool { return true } +func (m MockDockerCommand) ShouldDetectDeletedFiles() bool { + return false +} type MockCachedDockerCommand struct { contextFiles []string @@ -93,6 +96,9 @@ func (m MockCachedDockerCommand) ProvidesFilesToSnapshot() bool { func (m MockCachedDockerCommand) CacheCommand(image v1.Image) commands.DockerCommand { return nil } +func (m MockCachedDockerCommand) ShouldDetectDeletedFiles() bool { + return false +} func (m MockCachedDockerCommand) FilesUsedFromContext(c *v1.Config, args *dockerfile.BuildArgs) ([]string, error) { return m.contextFiles, nil } diff --git a/pkg/snapshot/snapshot.go b/pkg/snapshot/snapshot.go index f253dfbd2..06f10f7d5 100644 --- a/pkg/snapshot/snapshot.go +++ b/pkg/snapshot/snapshot.go @@ -62,7 +62,7 @@ func (s *Snapshotter) Key() (string, error) { // TakeSnapshot takes a snapshot of the specified files, avoiding directories in the ignorelist, and creates // a tarball of the changed files. Return contents of the tarball, and whether or not any files were changed -func (s *Snapshotter) TakeSnapshot(files []string) (string, error) { +func (s *Snapshotter) TakeSnapshot(files []string, shdCheckDelete bool) (string, error) { f, err := ioutil.TempFile(config.KanikoDir, "") if err != nil { return "", err @@ -92,9 +92,30 @@ func (s *Snapshotter) TakeSnapshot(files []string) (string, error) { } } + // Get whiteout paths + filesToWhiteout := []string{} + if shdCheckDelete { + existingPaths := s.l.getFlattenedPathsForWhiteOut() + foundFiles := walkFS(s.directory) + for _, file := range foundFiles { + delete(existingPaths, file) + } + // The paths left here are the ones that have been deleted in this layer. + filesToWhiteOut := []string{} + for path := range existingPaths { + // Only add the whiteout if the directory for the file still exists. + dir := filepath.Dir(path) + if _, ok := existingPaths[dir]; !ok { + if s.l.MaybeAddWhiteout(path) { + logrus.Debugf("Adding whiteout for %s", path) + filesToWhiteOut = append(filesToWhiteOut, path) + } + } + } + } t := util.NewTar(f) defer t.Close() - if err := writeToTar(t, filesToAdd, nil); err != nil { + if err := writeToTar(t, filesToAdd, filesToWhiteout); err != nil { return "", err } return f.Name(), nil @@ -133,31 +154,8 @@ func (s *Snapshotter) scanFullFilesystem() ([]string, []string, error) { s.l.Snapshot() - timer := timing.Start("Walking filesystem") - - foundPaths := make([]string, 0) - - godirwalk.Walk(s.directory, &godirwalk.Options{ - Callback: func(path string, ent *godirwalk.Dirent) error { - if util.IsInIgnoreList(path) { - if util.IsDestDir(path) { - logrus.Tracef("Skipping paths under %s, as it is a ignored directory", path) - - return filepath.SkipDir - } - - return nil - } - - foundPaths = append(foundPaths, path) - - return nil - }, - Unsorted: true, - }, - ) - timing.DefaultRun.Stop(timer) - timer = timing.Start("Resolving Paths") + foundPaths := walkFS(s.directory) + timer := timing.Start("Resolving Paths") // First handle whiteouts // Get a list of all the files that existed before this layer existingPaths := s.l.getFlattenedPathsForWhiteOut() @@ -267,3 +265,29 @@ func filesWithLinks(path string) ([]string, error) { } return []string{path, link}, nil } + +func walkFS(dir string) []string { + foundPaths := make([]string, 0) + timer := timing.Start("Walking filesystem") + godirwalk.Walk(dir, &godirwalk.Options{ + Callback: func(path string, ent *godirwalk.Dirent) error { + if util.IsInIgnoreList(path) { + if util.IsDestDir(path) { + logrus.Tracef("Skipping paths under %s, as it is a ignored directory", path) + + return filepath.SkipDir + } + + return nil + } + + foundPaths = append(foundPaths, path) + + return nil + }, + Unsorted: true, + }, + ) + timing.DefaultRun.Stop(timer) + return foundPaths +} diff --git a/pkg/snapshot/snapshot_test.go b/pkg/snapshot/snapshot_test.go index 1b03bbd4f..314ddec9f 100644 --- a/pkg/snapshot/snapshot_test.go +++ b/pkg/snapshot/snapshot_test.go @@ -212,7 +212,7 @@ func TestSnapshotFiles(t *testing.T) { filesToSnapshot := []string{ filepath.Join(testDir, "foo"), } - tarPath, err := snapshotter.TakeSnapshot(filesToSnapshot) + tarPath, err := snapshotter.TakeSnapshot(filesToSnapshot, false) if err != nil { t.Fatal(err) } @@ -359,7 +359,7 @@ func TestSnasphotPreservesFileOrder(t *testing.T) { } // Take a snapshot - tarPath, err := snapshotter.TakeSnapshot(filesToSnapshot) + tarPath, err := snapshotter.TakeSnapshot(filesToSnapshot, false) if err != nil { t.Fatalf("Error taking snapshot of fs: %s", err)