add a new run command along with a new flag

This commit is contained in:
Tejal Desai 2020-06-05 16:05:04 -07:00
parent 8fdb78a858
commit 43338d4b2f
13 changed files with 219 additions and 39 deletions

View File

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

View File

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

View File

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

View File

@ -51,3 +51,7 @@ func (b *BaseCommand) RequiresUnpackedFS() bool {
func (b *BaseCommand) ShouldCacheOutput() bool {
return false
}
func (a *BaseCommand) ShouldDetectDeletedFiles() bool {
return false
}

View File

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

View File

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

106
pkg/commands/run_marker.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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