Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add a new run command along with a new flag #1300

Merged
merged 13 commits into from
Jun 13, 2020
1 change: 1 addition & 0 deletions cmd/executor/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions integration/dockerfiles/Dockerfile_test_run_new
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Copyright 2020 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
2 changes: 2 additions & 0 deletions integration/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -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_run_redo": {"file=/file"},
"Dockerfile_test_workdir": {"workdir=/arg/workdir"},
"Dockerfile_test_add": {"file=context/foo"},
Expand Down Expand Up @@ -75,6 +76,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_run_redo": {"--snapshotMode=redo"},
"Dockerfile_test_scratch": {"--single-snapshot"},
"Dockerfile_test_maintainer": {"--single-snapshot"},
Expand Down
4 changes: 4 additions & 0 deletions pkg/commands/base_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,7 @@ func (b *BaseCommand) RequiresUnpackedFS() bool {
func (b *BaseCommand) ShouldCacheOutput() bool {
return false
}

func (b *BaseCommand) ShouldDetectDeletedFiles() bool {
return false
}
8 changes: 7 additions & 1 deletion pkg/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,17 @@ type DockerCommand interface {
RequiresUnpackedFS() bool

ShouldCacheOutput() bool

// ShouldDetectDeletedFiles returns 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
Expand Down
11 changes: 7 additions & 4 deletions pkg/commands/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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])
Expand Down Expand Up @@ -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
}

Expand Down
108 changes: 108 additions & 0 deletions pkg/commands/run_marker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
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 (
"fmt"
"io/ioutil"
"os"
"time"

"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"
"github.com/sirupsen/logrus"
)

type RunMarkerCommand struct {
BaseCommand
cmd *instructions.RunCommand
Files []string
}

func (r *RunMarkerCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error {
// run command `touch filemarker`
logrus.Debugf("using new RunMarker command")
markerFile, err := ioutil.TempFile("", "marker")
if err != nil {
return fmt.Errorf("could not place a marker file")
}
defer func() {
os.Remove(markerFile.Name())
}()
markerInfo, err := os.Stat(markerFile.Name())
if err != nil {
return fmt.Errorf("could not place a marker file")
}
// introduce a delay
time.Sleep(time.Second)
if err := runCommandInExec(config, buildArgs, r.cmd); err != nil {
return err
}

// run command find to find all new files generate
isNewer := func(p string) (bool, error) {
fi, err := os.Stat(p)
if err != nil {
return false, err
}
return fi.ModTime().After(markerInfo.ModTime()), nil
}
r.Files, _ = util.WalkFS("/", map[string]struct{}{}, isNewer)
logrus.Debugf("files changed %s", r.Files)
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 r.Files
}

func (r *RunMarkerCommand) ProvidesFilesToSnapshot() bool {
return true
}

// 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 (r *RunMarkerCommand) ShouldDetectDeletedFiles() bool {
return true
}
1 change: 1 addition & 0 deletions pkg/config/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
29 changes: 10 additions & 19 deletions pkg/executor/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -319,7 +319,7 @@ func (s *stageBuilder) build() error {
}

initSnapshotTaken := false
if s.opts.SingleSnapshot {
if s.opts.SingleSnapshot || s.opts.RunV2 {
if err := s.initSnapshotWithTimings(); err != nil {
return err
}
Expand Down Expand Up @@ -372,7 +372,7 @@ func (s *stageBuilder) build() error {
files = command.FilesToSnapshot()
timing.DefaultRun.Stop(t)

if !s.shouldTakeSnapshot(index, files, command.ProvidesFilesToSnapshot()) {
if !s.shouldTakeSnapshot(index, command.MetadataOnly()) {
continue
}
if isCacheCommand {
Expand All @@ -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")
}
Expand Down Expand Up @@ -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

Expand All @@ -426,13 +426,13 @@ 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
}

func (s *stageBuilder) shouldTakeSnapshot(index int, files []string, provideFiles bool) bool {
func (s *stageBuilder) shouldTakeSnapshot(index int, isMetadatCmd bool) bool {
isLastCommand := index == len(s.cmds)-1

// We only snapshot the very end with single snapshot mode on.
Expand All @@ -445,17 +445,8 @@ func (s *stageBuilder) shouldTakeSnapshot(index int, files []string, provideFile
return true
}

// if command does not provide files, snapshot everything.
if !provideFiles {
return true
}

// Don't snapshot an empty list.
if len(files) == 0 {
return false
}

return true
// if command is a metadata command, do not snapshot.
return !isMetadatCmd
}

func (s *stageBuilder) saveSnapshotToImage(createdBy string, tarPath string) error {
Expand Down
18 changes: 8 additions & 10 deletions pkg/executor/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,8 @@ func Test_stageBuilder_shouldTakeSnapshot(t *testing.T) {
cmds []commands.DockerCommand
}
type args struct {
index int
files []string
hasFiles bool
index int
metadataOnly bool
}
tests := []struct {
name string
Expand Down Expand Up @@ -158,9 +157,8 @@ func Test_stageBuilder_shouldTakeSnapshot(t *testing.T) {
stage: config.KanikoStage{},
},
args: args{
index: 0,
files: []string{},
hasFiles: true,
index: 0,
metadataOnly: true,
},
want: false,
},
Expand All @@ -172,9 +170,8 @@ func Test_stageBuilder_shouldTakeSnapshot(t *testing.T) {
},
},
args: args{
index: 0,
files: nil,
hasFiles: false,
index: 0,
metadataOnly: false,
},
want: true,
},
Expand Down Expand Up @@ -204,7 +201,7 @@ func Test_stageBuilder_shouldTakeSnapshot(t *testing.T) {
opts: tt.fields.opts,
cmds: tt.fields.cmds,
}
if got := s.shouldTakeSnapshot(tt.args.index, tt.args.files, tt.args.hasFiles); got != tt.want {
if got := s.shouldTakeSnapshot(tt.args.index, tt.args.metadataOnly); got != tt.want {
t.Errorf("stageBuilder.shouldTakeSnapshot() = %v, want %v", got, tt.want)
}
})
Expand Down Expand Up @@ -1246,6 +1243,7 @@ func getCommands(dir string, cmds []instructions.Command) []commands.DockerComma
cmd, err := commands.GetCommand(
c,
dir,
false,
)
if err != nil {
panic(err)
Expand Down
Loading