From d08a76745479722921936b37331aff2ad3cc647c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordan=20Goasdou=C3=A9?= Date: Sat, 14 Mar 2020 20:44:15 +0100 Subject: [PATCH 01/20] feat: allow injecting through stdin tar.gz on kaniko --- README.md | 24 +++- integration/integration_test.go | 2 +- integration/integration_with_stdin_test.go | 151 +++++++++++++++++++++ pkg/buildcontext/buildcontext.go | 36 ++--- pkg/buildcontext/tar.go | 21 +++ pkg/util/util.go | 10 ++ pkg/util/util_test.go | 32 +++++ 7 files changed, 257 insertions(+), 19 deletions(-) create mode 100644 integration/integration_with_stdin_test.go create mode 100644 pkg/util/util_test.go diff --git a/README.md b/README.md index 0a6d3c8a5..f7a699d3f 100644 --- a/README.md +++ b/README.md @@ -120,11 +120,18 @@ Right now, kaniko supports these storage solutions: - S3 Bucket - Azure Blob Storage - Local Directory +- Local Tar +- Standard Input - Git Repository -_Note: the local directory option refers to a directory within the kaniko container. +_Note about Local Directory: this option refers to a directory within the kaniko container. If you wish to use this option, you will need to mount in your build context into the container as a directory._ +_Note about Local Tar: this option refers to a tar gz file within the kaniko container. +If you wish to use this option, you will need to mount in your build context into the container as a file._ + +_Note about Standard Input: the only Standard Input allowed by kaniko is in `.tar.gz` format._ + If using a GCS or S3 bucket, you will first need to create a compressed tar of your build context and upload it to your bucket. Once running, kaniko will then download and unpack the compressed tar of the build context before starting the image build. @@ -146,6 +153,7 @@ When running kaniko, use the `--context` flag with the appropriate prefix to spe |---------|---------|---------| | Local Directory | dir://[path to a directory in the kaniko container] | `dir:///workspace` | | Local Tar Gz | tar://[path to a .tar.gz in the kaniko container] | `tar://path/to/context.tar.gz` | +| Standard Input | tar://[stdin] | `tar://stdin` | | GCS Bucket | gs://[bucket name]/[path to .tar.gz] | `gs://kaniko-bucket/path/to/context.tar.gz` | | S3 Bucket | s3://[bucket name]/[path to .tar.gz] | `s3://kaniko-bucket/path/to/context.tar.gz` | | Azure Blob Storage| https://[account].[azureblobhostsuffix]/[container]/[path to .tar.gz] | `https://myaccount.blob.core.windows.net/container/path/to/context.tar.gz` | @@ -160,6 +168,20 @@ If you are using Azure Blob Storage for context file, you will need to pass [Azu ### Using Private Git Repository You can use `Personal Access Tokens` for Build Contexts from Private Repositories from [GitHub](https://blog.github.com/2012-09-21-easier-builds-and-deployments-using-git-over-https-and-oauth/). +### Using Standard Input +If running kaniko and using Standard Input build context, you will need to add the docker or kubernetes `-i, --interactive` flag. +Once running, kaniko will then get the data from `STDIN` and create the build context as a compressed tar. +It will then unpack the compressed tar of the build context before starting the image build. +If no data is piped during the interactive run, you will need to send the EOF signal by yourself by pressing `Ctrl+D`. + +Complete example of how to interactively run kaniko with `.tar.gz` Standard Input data, using docker: +```shell +echo -e 'FROM alpine \nRUN echo "created from standard input"' > Dockerfile | tar -cf - Dockerfile | gzip -9 | docker run \ + --interactive -v $(pwd):/workspace gcr.io/kaniko-project/executor:latest \ + --context tar://stdin \ + --destination= +``` + ### Running kaniko There are several different ways to deploy and run kaniko: diff --git a/integration/integration_test.go b/integration/integration_test.go index 5fc4541ee..01630bb09 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -32,11 +32,11 @@ import ( "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/daemon" + "github.com/pkg/errors" "github.com/GoogleContainerTools/kaniko/pkg/timing" "github.com/GoogleContainerTools/kaniko/pkg/util" "github.com/GoogleContainerTools/kaniko/testutil" - "github.com/pkg/errors" ) var config *integrationTestConfig diff --git a/integration/integration_with_stdin_test.go b/integration/integration_with_stdin_test.go new file mode 100644 index 000000000..fc023c934 --- /dev/null +++ b/integration/integration_with_stdin_test.go @@ -0,0 +1,151 @@ +/* +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 integration + +import ( + "compress/gzip" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "sync" + "testing" + + "github.com/GoogleContainerTools/kaniko/pkg/util" + "github.com/GoogleContainerTools/kaniko/testutil" +) + +func TestBuildWithStdin(t *testing.T) { + _, ex, _, _ := runtime.Caller(0) + cwd := filepath.Dir(ex) + + testDir := "test_dir" + testDirLongPath := filepath.Join(cwd, testDir) + + if err := os.MkdirAll(testDirLongPath, 0750); err != nil { + t.Errorf("Failed to create dir_where_to_extract: %v", err) + } + + dockerfile := "Dockerfile_test_stdin" + + files := map[string]string{ + dockerfile: "FROM debian:9.11\nRUN echo \"hey\"", + } + + if err := testutil.SetupFiles(testDir, files); err != nil { + t.Errorf("Failed to setup files %v on %s: %v", files, testDir, err) + } + + if err := os.Chdir(testDir); err != nil { + t.Fatalf("Failed to Chdir on %s: %v", testDir, err) + } + + tarPath := fmt.Sprintf("%s.tar.gz", dockerfile) + + var wg sync.WaitGroup + wg.Add(1) + // Create Tar Gz File with dockerfile inside + go func(wg *sync.WaitGroup) { + defer wg.Done() + tarFile, err := os.Create(tarPath) + if err != nil { + t.Errorf("Failed to create %s: %v", tarPath, err) + } + defer tarFile.Close() + + gw := gzip.NewWriter(tarFile) + defer gw.Close() + + tw := util.NewTar(gw) + defer tw.Close() + + if err := tw.AddFileToTar(dockerfile); err != nil { + t.Errorf("Failed to add %s to %s: %v", dockerfile, tarPath, err) + } + }(&wg) + + // Waiting for the Tar Gz file creation to be done before moving on + wg.Wait() + + // Build with docker + + dockerImage := GetDockerImage(config.imageRepo, dockerfile) + dockerCmd := exec.Command("docker", + append([]string{"build", + "-t", dockerImage, + "-f", dockerfile, + "."})...) + + _, err := RunCommandWithoutTest(dockerCmd) + if err != nil { + t.Fatalf("can't run %s: %v", dockerCmd.String(), err) + } + + // Build with kaniko using Stdin + kanikoImageStdin := GetKanikoImage(config.imageRepo, dockerfile) + tarCmd := exec.Command("tar", "-cf", "-", dockerfile) + gzCmd := exec.Command("gzip", "-9") + + dockerRunFlags := []string{"run", "--interactive", "--net=host", "-v", cwd + ":/workspace"} + dockerRunFlags = addServiceAccountFlags(dockerRunFlags, config.serviceAccount) + dockerRunFlags = append(dockerRunFlags, + ExecutorImage, + "-f", dockerfile, + "-c", "tar://stdin", + "-d", kanikoImageStdin) + + kanikoCmdStdin := exec.Command("docker", dockerRunFlags...) + + gzCmd.Stdin, err = tarCmd.StdoutPipe() + if err != nil { + t.Fatalf("can't set gzCmd stdin: %v", err) + } + kanikoCmdStdin.Stdin, err = gzCmd.StdoutPipe() + if err != nil { + t.Fatalf("can't set kanikoCmd stdin: %v", err) + } + + if err := kanikoCmdStdin.Start(); err != nil { + t.Fatalf("can't start %s: %v", kanikoCmdStdin.String(), err) + } + + if err := gzCmd.Start(); err != nil { + t.Fatalf("can't start %s: %v", gzCmd.String(), err) + } + + if err := tarCmd.Run(); err != nil { + t.Fatalf("can't start %s: %v", tarCmd.String(), err) + } + + if err := gzCmd.Wait(); err != nil { + t.Fatalf("can't wait %s: %v", gzCmd.String(), err) + } + + if err := kanikoCmdStdin.Wait(); err != nil { + t.Fatalf("can't wait %s: %v", kanikoCmdStdin.String(), err) + } + + diff := containerDiff(t, daemonPrefix+dockerImage, kanikoImageStdin, "--no-cache") + + expected := fmt.Sprintf(emptyContainerDiff, dockerImage, kanikoImageStdin, dockerImage, kanikoImageStdin) + checkContainerDiffOutput(t, diff, expected) + + if err := os.RemoveAll(testDirLongPath); err != nil { + t.Errorf("Failed to remove %s: %v", testDirLongPath, err) + } +} diff --git a/pkg/buildcontext/buildcontext.go b/pkg/buildcontext/buildcontext.go index ef39c21e2..5a81c5033 100644 --- a/pkg/buildcontext/buildcontext.go +++ b/pkg/buildcontext/buildcontext.go @@ -38,25 +38,27 @@ type BuildContext interface { // parser func GetBuildContext(srcContext string) (BuildContext, error) { split := strings.SplitAfter(srcContext, "://") - prefix := split[0] - context := split[1] + if len(split) > 1 { + prefix := split[0] + context := split[1] - switch prefix { - case constants.GCSBuildContextPrefix: - return &GCS{context: context}, nil - case constants.S3BuildContextPrefix: - return &S3{context: context}, nil - case constants.LocalDirBuildContextPrefix: - return &Dir{context: context}, nil - case constants.GitBuildContextPrefix: - return &Git{context: context}, nil - case constants.HTTPSBuildContextPrefix: - if util.ValidAzureBlobStorageHost(srcContext) { - return &AzureBlob{context: srcContext}, nil + switch prefix { + case constants.GCSBuildContextPrefix: + return &GCS{context: context}, nil + case constants.S3BuildContextPrefix: + return &S3{context: context}, nil + case constants.LocalDirBuildContextPrefix: + return &Dir{context: context}, nil + case constants.GitBuildContextPrefix: + return &Git{context: context}, nil + case constants.HTTPSBuildContextPrefix: + if util.ValidAzureBlobStorageHost(srcContext) { + return &AzureBlob{context: srcContext}, nil + } + return nil, errors.New("url provided for https context is not in a supported format, please use the https url for Azure Blob Storage") + case TarBuildContextPrefix: + return &Tar{context: context}, nil } - return nil, errors.New("url provided for https context is not in a supported format, please use the https url for Azure Blob Storage") - case TarBuildContextPrefix: - return &Tar{context: context}, nil } return nil, errors.New("unknown build context prefix provided, please use one of the following: gs://, dir://, tar://, s3://, git://, https://") } diff --git a/pkg/buildcontext/tar.go b/pkg/buildcontext/tar.go index d5d1c250a..f7fcf178e 100644 --- a/pkg/buildcontext/tar.go +++ b/pkg/buildcontext/tar.go @@ -17,11 +17,15 @@ limitations under the License. package buildcontext import ( + "fmt" + "io/ioutil" "os" + "path/filepath" "github.com/GoogleContainerTools/kaniko/pkg/constants" "github.com/GoogleContainerTools/kaniko/pkg/util" "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) // Tar unifies calls to download and unpack the build context. @@ -35,6 +39,23 @@ func (t *Tar) UnpackTarFromBuildContext() (string, error) { if err := os.MkdirAll(directory, 0750); err != nil { return "", errors.Wrap(err, "unpacking tar from build context") } + if t.context == "stdin" { + fi, _ := os.Stdin.Stat() + if (fi.Mode() & os.ModeCharDevice) != 0 { + return "", fmt.Errorf("no data found.. don't forget to add the '--interactive, -i' flag") + } + logrus.Infof("To simulate EOF and exit, press 'Ctrl+D'") + // if launched through docker in interactive mode and without piped data + // process will be stuck here until EOF is sent + data, err := util.GetInputFrom(os.Stdin) + if err != nil { + return "", errors.Wrap(err, "fail to get standard input") + } + t.context = filepath.Join(directory, constants.ContextTar) + if err := ioutil.WriteFile(t.context, data, 0644); err != nil { + return "", errors.Wrap(err, "fail to redirect standard input into compressed tar file") + } + } return directory, util.UnpackCompressedTar(t.context, directory) } diff --git a/pkg/util/util.go b/pkg/util/util.go index 83f709801..27340e413 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -21,6 +21,7 @@ import ( "crypto/sha256" "encoding/hex" "io" + "io/ioutil" "os" "runtime" "strconv" @@ -134,3 +135,12 @@ func currentPlatform() v1.Platform { Architecture: runtime.GOARCH, } } + +// GetInputFrom returns Reader content +func GetInputFrom(r io.Reader) ([]byte, error) { + output, err := ioutil.ReadAll(r) + if err != nil { + return nil, err + } + return output, nil +} diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go new file mode 100644 index 000000000..9d8d69285 --- /dev/null +++ b/pkg/util/util_test.go @@ -0,0 +1,32 @@ +/* +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 util + +import ( + "bufio" + "bytes" + "testing" + + "github.com/GoogleContainerTools/kaniko/testutil" +) + +func TestGetInputFrom(t *testing.T) { + validInput := []byte("Valid\n") + validReader := bufio.NewReader(bytes.NewReader((validInput))) + validValue, err := GetInputFrom(validReader) + testutil.CheckErrorAndDeepEqual(t, false, err, validInput, validValue) +} From e5585fded80f92d90cc1b2dbe3319d53dc8e498b Mon Sep 17 00:00:00 2001 From: Gilbert Gilb's Date: Sun, 29 Mar 2020 18:03:32 +0200 Subject: [PATCH 02/20] Always add parent directories of files to snapshots. During a snapshot, when a file changed and not its parent directories, the parent directories weren't added to the layer. This is inconsistent with Docker's behavior which always add parent directories to the layer. In some edge-cases, it could lead to problems with docker considering that parent directories where owned by root in forthcoming layers although they shouldn't (see #1163). Also, Docker seems to be POSIX compliant regarding the name of directories in the archive, which always have a slash appended. This commit also fixes this. Fixes #1163 --- pkg/snapshot/snapshot.go | 18 ++++++++++++++++++ pkg/snapshot/snapshot_test.go | 22 +++++++++++++++++++++- pkg/util/tar_util.go | 4 ++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/pkg/snapshot/snapshot.go b/pkg/snapshot/snapshot.go index e64e28dd3..7e027fe8f 100644 --- a/pkg/snapshot/snapshot.go +++ b/pkg/snapshot/snapshot.go @@ -226,10 +226,28 @@ func writeToTar(t util.Tar, files, whiteouts []string) error { return err } } + + addedPaths := make(map[string]bool) for _, path := range files { + if _, fileExists := addedPaths[path]; fileExists { + continue + } + for _, parentPath := range util.ParentDirectories(path) { + if parentPath == "/" { + continue + } + if _, dirExists := addedPaths[parentPath]; dirExists { + continue + } + if err := t.AddFileToTar(parentPath); err != nil { + return err + } + addedPaths[parentPath] = true + } if err := t.AddFileToTar(path); err != nil { return err } + addedPaths[path] = true } return nil } diff --git a/pkg/snapshot/snapshot_test.go b/pkg/snapshot/snapshot_test.go index 19fc9c4cb..2e2d9cc21 100644 --- a/pkg/snapshot/snapshot_test.go +++ b/pkg/snapshot/snapshot_test.go @@ -63,6 +63,12 @@ func TestSnapshotFSFileChange(t *testing.T) { fooPath: "newbaz1", batPath: "baz", } + for _, path := range util.ParentDirectoriesWithoutLeadingSlash(batPath) { + if path == "/" { + continue + } + snapshotFiles[path+"/"] = "" + } actualFiles := []string{} for { @@ -76,6 +82,9 @@ func TestSnapshotFSFileChange(t *testing.T) { if _, isFile := snapshotFiles[hdr.Name]; !isFile { t.Fatalf("File %s unexpectedly in tar", hdr.Name) } + if hdr.Typeflag == tar.TypeDir { + continue + } contents, _ := ioutil.ReadAll(tr) if string(contents) != snapshotFiles[hdr.Name] { t.Fatalf("Contents of %s incorrect, expected: %s, actual: %s", hdr.Name, snapshotFiles[hdr.Name], string(contents)) @@ -152,6 +161,12 @@ func TestSnapshotFSChangePermissions(t *testing.T) { snapshotFiles := map[string]string{ batPathWithoutLeadingSlash: "baz2", } + for _, path := range util.ParentDirectoriesWithoutLeadingSlash(batPathWithoutLeadingSlash) { + if path == "/" { + continue + } + snapshotFiles[path+"/"] = "" + } foundFiles := []string{} for { @@ -164,6 +179,9 @@ func TestSnapshotFSChangePermissions(t *testing.T) { if _, isFile := snapshotFiles[hdr.Name]; !isFile { t.Fatalf("File %s unexpectedly in tar", hdr.Name) } + if hdr.Typeflag == tar.TypeDir { + continue + } contents, _ := ioutil.ReadAll(tr) if string(contents) != snapshotFiles[hdr.Name] { t.Fatalf("Contents of %s incorrect, expected: %s, actual: %s", hdr.Name, snapshotFiles[hdr.Name], string(contents)) @@ -203,7 +221,9 @@ func TestSnapshotFiles(t *testing.T) { expectedFiles := []string{ filepath.Join(testDirWithoutLeadingSlash, "foo"), } - expectedFiles = append(expectedFiles, util.ParentDirectoriesWithoutLeadingSlash(filepath.Join(testDir, "foo"))...) + for _, path := range util.ParentDirectoriesWithoutLeadingSlash(filepath.Join(testDir, "foo")) { + expectedFiles = append(expectedFiles, strings.TrimRight(path, "/")+"/") + } f, err := os.Open(tarPath) if err != nil { diff --git a/pkg/util/tar_util.go b/pkg/util/tar_util.go index 823e42f04..76a6b5405 100644 --- a/pkg/util/tar_util.go +++ b/pkg/util/tar_util.go @@ -84,6 +84,10 @@ func (t *Tar) AddFileToTar(p string) error { hdr.Name = p } + if hdr.Typeflag == tar.TypeDir && !strings.HasSuffix(hdr.Name, "/") { + hdr.Name = hdr.Name + "/" + } + // rootfs may not have been extracted when using cache, preventing uname/gname from resolving // this makes this layer unnecessarily differ from a cached layer which does contain this information hdr.Uname = "" From 14170aa455fd16ed606bb3afda2ab1c5847e3d90 Mon Sep 17 00:00:00 2001 From: Gilbert Gilb's Date: Tue, 31 Mar 2020 20:12:00 +0200 Subject: [PATCH 03/20] Fix sorting of parent directories. This refactoring reversed the order of the "ParentDirectories" function: https://github.com/GoogleContainerTools/kaniko/pull/1155/commits/ffc372a63bca75e5b7c680c981d2444fc4017897#diff-d36eb675aa49a7b471e3a2be77005b18R465 As a side-effect, parent directories weren't added in lexicographical order, which broke some tests. We now ensure in unit test that the order of the ParentDirectories function is stable. --- pkg/util/fs_util.go | 4 ++-- pkg/util/fs_util_test.go | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pkg/util/fs_util.go b/pkg/util/fs_util.go index a5ecaab1b..230d87669 100644 --- a/pkg/util/fs_util.go +++ b/pkg/util/fs_util.go @@ -468,10 +468,10 @@ func ParentDirectories(path string) []string { } dir, _ = filepath.Split(dir) dir = filepath.Clean(dir) - paths = append(paths, dir) + paths = append([]string{dir}, paths...) } if len(paths) == 0 { - paths = append(paths, config.RootDir) + paths = []string{config.RootDir} } return paths } diff --git a/pkg/util/fs_util_test.go b/pkg/util/fs_util_test.go index b560330b2..5ba37fef9 100644 --- a/pkg/util/fs_util_test.go +++ b/pkg/util/fs_util_test.go @@ -213,8 +213,6 @@ func Test_ParentDirectories(t *testing.T) { defer func() { config.RootDir = original }() config.RootDir = tt.rootDir actual := ParentDirectories(tt.path) - sort.Strings(actual) - sort.Strings(tt.expected) testutil.CheckErrorAndDeepEqual(t, false, nil, tt.expected, actual) }) From 484d03550c83d1998a32a6b63765268fda991fa0 Mon Sep 17 00:00:00 2001 From: Tom Prince Date: Mon, 13 Apr 2020 15:54:27 -0600 Subject: [PATCH 04/20] Handle `MAINTAINERS` when passing `--single-snapshot`. --- .../dockerfiles/Dockerfile_test_maintainer | 4 +++ integration/images.go | 7 ++-- pkg/executor/build.go | 2 +- pkg/executor/build_test.go | 32 +++++++------------ 4 files changed, 20 insertions(+), 25 deletions(-) create mode 100644 integration/dockerfiles/Dockerfile_test_maintainer diff --git a/integration/dockerfiles/Dockerfile_test_maintainer b/integration/dockerfiles/Dockerfile_test_maintainer new file mode 100644 index 000000000..ae44c4265 --- /dev/null +++ b/integration/dockerfiles/Dockerfile_test_maintainer @@ -0,0 +1,4 @@ +FROM scratch +MAINTAINER nobody@domain.test +# Add a file to the image to work around https://github.com/moby/moby/issues/38039 +COPY context/foo /foo diff --git a/integration/images.go b/integration/images.go index e6af511fc..bb254f110 100644 --- a/integration/images.go +++ b/integration/images.go @@ -73,9 +73,10 @@ 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_scratch": {"--single-snapshot"}, - "Dockerfile_test_target": {"--target=second"}, + "Dockerfile_test_add": {"--single-snapshot"}, + "Dockerfile_test_scratch": {"--single-snapshot"}, + "Dockerfile_test_maintainer": {"--single-snapshot"}, + "Dockerfile_test_target": {"--target=second"}, } // output check to do when building with kaniko diff --git a/pkg/executor/build.go b/pkg/executor/build.go index 9ccd969e5..7884a6ed5 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -415,7 +415,7 @@ func (s *stageBuilder) takeSnapshot(files []string) (string, error) { } func (s *stageBuilder) shouldTakeSnapshot(index int, files []string) bool { - isLastCommand := index == len(s.stage.Commands)-1 + isLastCommand := index == len(s.cmds)-1 // We only snapshot the very end with single snapshot mode on. if s.opts.SingleSnapshot { diff --git a/pkg/executor/build_test.go b/pkg/executor/build_test.go index 831067173..8ff77c724 100644 --- a/pkg/executor/build_test.go +++ b/pkg/executor/build_test.go @@ -88,28 +88,17 @@ func stage(t *testing.T, d string) config.KanikoStage { } } -type MockCommand struct { - name string -} - -func (m *MockCommand) Name() string { - return m.name -} - func Test_stageBuilder_shouldTakeSnapshot(t *testing.T) { - commands := []instructions.Command{ - &MockCommand{name: "command1"}, - &MockCommand{name: "command2"}, - &MockCommand{name: "command3"}, - } - - stage := instructions.Stage{ - Commands: commands, + cmds := []commands.DockerCommand{ + &MockDockerCommand{command: "command1"}, + &MockDockerCommand{command: "command2"}, + &MockDockerCommand{command: "command3"}, } type fields struct { stage config.KanikoStage opts *config.KanikoOptions + cmds []commands.DockerCommand } type args struct { index int @@ -126,8 +115,8 @@ func Test_stageBuilder_shouldTakeSnapshot(t *testing.T) { fields: fields{ stage: config.KanikoStage{ Final: true, - Stage: stage, }, + cmds: cmds, }, args: args{ index: 1, @@ -139,11 +128,11 @@ func Test_stageBuilder_shouldTakeSnapshot(t *testing.T) { fields: fields{ stage: config.KanikoStage{ Final: false, - Stage: stage, }, + cmds: cmds, }, args: args{ - index: len(commands) - 1, + index: len(cmds) - 1, }, want: true, }, @@ -152,8 +141,8 @@ func Test_stageBuilder_shouldTakeSnapshot(t *testing.T) { fields: fields{ stage: config.KanikoStage{ Final: false, - Stage: stage, }, + cmds: cmds, }, args: args{ index: 0, @@ -165,9 +154,9 @@ func Test_stageBuilder_shouldTakeSnapshot(t *testing.T) { fields: fields{ stage: config.KanikoStage{ Final: false, - Stage: stage, }, opts: &config.KanikoOptions{Cache: true}, + cmds: cmds, }, args: args{ index: 0, @@ -184,6 +173,7 @@ func Test_stageBuilder_shouldTakeSnapshot(t *testing.T) { s := &stageBuilder{ stage: tt.fields.stage, opts: tt.fields.opts, + cmds: tt.fields.cmds, } if got := s.shouldTakeSnapshot(tt.args.index, tt.args.files); got != tt.want { t.Errorf("stageBuilder.shouldTakeSnapshot() = %v, want %v", got, tt.want) From 5061b72e421490e53b12a38e18cc314414cce92d Mon Sep 17 00:00:00 2001 From: Tom Prince Date: Mon, 13 Apr 2020 16:26:19 -0600 Subject: [PATCH 05/20] Ignore the target of dangling symlinks. --- integration/dockerfiles/Dockerfile_test_dangling_symlink | 2 ++ pkg/filesystem/resolve.go | 1 + 2 files changed, 3 insertions(+) create mode 100644 integration/dockerfiles/Dockerfile_test_dangling_symlink diff --git a/integration/dockerfiles/Dockerfile_test_dangling_symlink b/integration/dockerfiles/Dockerfile_test_dangling_symlink new file mode 100644 index 000000000..07a5b17a0 --- /dev/null +++ b/integration/dockerfiles/Dockerfile_test_dangling_symlink @@ -0,0 +1,2 @@ +FROM busybox:latest@sha256:b26cd013274a657b86e706210ddd5cc1f82f50155791199d29b9e86e935ce135 +RUN ["/bin/ln", "-s", "nowhere", "/link"] diff --git a/pkg/filesystem/resolve.go b/pkg/filesystem/resolve.go index a753e8776..e34714040 100644 --- a/pkg/filesystem/resolve.go +++ b/pkg/filesystem/resolve.go @@ -72,6 +72,7 @@ func ResolvePaths(paths []string, wl []util.WhitelistEntry) (pathsToAdd []string } logrus.Debugf("symlink path %s, target does not exist", f) + continue } // If the given path is a symlink and the target is part of the whitelist From c520218cec1956ac2ccbb3e7b51a999db70fcb63 Mon Sep 17 00:00:00 2001 From: Tom Prince Date: Mon, 13 Apr 2020 16:50:13 -0600 Subject: [PATCH 06/20] Don't generate cache key, if not caching builds. The cache key generation does environment subsitution in places that running the commands doesn't. This causes issues if a command uses complex shell substitutions. The cache key is generated even if caching isn't enabled. This disables the cache key generation if caching is not enabled. This doesn't fix the underlying issue, but limits it to when the cache is being used. --- .../Dockerfile_test_complex_substitution | 2 ++ pkg/executor/build.go | 32 +++++++++++-------- 2 files changed, 20 insertions(+), 14 deletions(-) create mode 100644 integration/dockerfiles/Dockerfile_test_complex_substitution diff --git a/integration/dockerfiles/Dockerfile_test_complex_substitution b/integration/dockerfiles/Dockerfile_test_complex_substitution new file mode 100644 index 000000000..9db50545f --- /dev/null +++ b/integration/dockerfiles/Dockerfile_test_complex_substitution @@ -0,0 +1,2 @@ +FROM docker.io/library/busybox:latest@sha256:afe605d272837ce1732f390966166c2afff5391208ddd57de10942748694049d +RUN echo ${s%s} diff --git a/pkg/executor/build.go b/pkg/executor/build.go index 9ccd969e5..f05c34528 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -334,9 +334,11 @@ func (s *stageBuilder) build() error { return errors.Wrap(err, "failed to get files used from context") } - *compositeKey, err = s.populateCompositeKey(command, files, *compositeKey, s.args, s.cf.Config.Env) - if err != nil { - return err + if s.opts.Cache { + *compositeKey, err = s.populateCompositeKey(command, files, *compositeKey, s.args, s.cf.Config.Env) + if err != nil && s.opts.Cache { + return err + } } logrus.Info(command.String()) @@ -372,19 +374,21 @@ func (s *stageBuilder) build() error { return errors.Wrap(err, "failed to take snapshot") } - logrus.Debugf("build: composite key for command %v %v", command.String(), compositeKey) - ck, err := compositeKey.Hash() - if err != nil { - return errors.Wrap(err, "failed to hash composite key") - } + if s.opts.Cache { + logrus.Debugf("build: composite key for command %v %v", command.String(), compositeKey) + ck, err := compositeKey.Hash() + if err != nil { + return errors.Wrap(err, "failed to hash composite key") + } - logrus.Debugf("build: cache key for command %v %v", command.String(), ck) + logrus.Debugf("build: cache key for command %v %v", command.String(), ck) - // Push layer to cache (in parallel) now along with new config file - if s.opts.Cache && command.ShouldCacheOutput() { - cacheGroup.Go(func() error { - return s.pushLayerToCache(s.opts, ck, tarPath, command.String()) - }) + // Push layer to cache (in parallel) now along with new config file + if command.ShouldCacheOutput() { + cacheGroup.Go(func() error { + return s.pushLayerToCache(s.opts, ck, tarPath, command.String()) + }) + } } if err := s.saveSnapshotToImage(command.String(), tarPath); err != nil { return errors.Wrap(err, "failed to save snapshot to image") From 34a6ec250ff898280658245821aa056b4b9aa872 Mon Sep 17 00:00:00 2001 From: yw-liu <43599588+yw-liu@users.noreply.github.com> Date: Tue, 14 Apr 2020 20:25:20 +0800 Subject: [PATCH 07/20] add http support for git pull usage: set the GIT_PULL_METHOD env var to http or https for starting the container --- pkg/buildcontext/git.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/pkg/buildcontext/git.go b/pkg/buildcontext/git.go index 9908350b1..fe4eae1ac 100644 --- a/pkg/buildcontext/git.go +++ b/pkg/buildcontext/git.go @@ -25,6 +25,14 @@ import ( "gopkg.in/src-d/go-git.v4/plumbing" ) +const ( + gitPullMethodEnvKey = "GIT_PULL_METHOD" +) + +var ( + supportedGitPullMethods = map[string]bool{"https":true, "http":true} +) + // Git unifies calls to download and unpack the build context. type Git struct { context string @@ -35,7 +43,7 @@ func (g *Git) UnpackTarFromBuildContext() (string, error) { directory := constants.BuildContextDir parts := strings.Split(g.context, "#") options := git.CloneOptions{ - URL: "https://" + parts[0], + URL: getGitPullMethod() + "://" + parts[0], Progress: os.Stdout, } if len(parts) > 1 { @@ -44,3 +52,11 @@ func (g *Git) UnpackTarFromBuildContext() (string, error) { _, err := git.PlainClone(directory, false, &options) return directory, err } + +func getGitPullMethod() string { + gitPullMethod := os.Getenv(gitPullMethodEnvKey) + if ok := supportedGitPullMethods[gitPullMethod]; !ok { + gitPullMethod = "https" + } + return gitPullMethod +} From 2cac43619faeaf32e7442765256e5bb57e1d7145 Mon Sep 17 00:00:00 2001 From: Liubov Grinkevich <33186227+luba239@users.noreply.github.com> Date: Thu, 16 Apr 2020 15:35:50 +0300 Subject: [PATCH 08/20] Fix line endings in shell script Add escapes to line endings, remove extra quote --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 726dc2708..9a17c2ec0 100644 --- a/README.md +++ b/README.md @@ -270,9 +270,9 @@ docker run \ -v "$HOME"/.config/gcloud:/root/.config/gcloud \ -v /path/to/context:/workspace \ gcr.io/kaniko-project/executor:latest \ - --dockerfile /workspace/Dockerfile - --destination "gcr.io/$PROJECT_ID/$IMAGE_NAME:$TAG" - --context dir:///workspace/" + --dockerfile /workspace/Dockerfile \ + --destination "gcr.io/$PROJECT_ID/$IMAGE_NAME:$TAG" \ + --context dir:///workspace/ ``` There is also a utility script [`run_in_docker.sh`](./run_in_docker.sh) that can be used as follows: From 4f8d074e00aac7105cc8d8d9cf3b1c7a5d6edbad Mon Sep 17 00:00:00 2001 From: yw-liu <43599588+yw-liu@users.noreply.github.com> Date: Thu, 16 Apr 2020 22:21:09 +0800 Subject: [PATCH 09/20] add unit-test --- pkg/buildcontext/git_test.go | 42 ++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 pkg/buildcontext/git_test.go diff --git a/pkg/buildcontext/git_test.go b/pkg/buildcontext/git_test.go new file mode 100644 index 000000000..c56159b95 --- /dev/null +++ b/pkg/buildcontext/git_test.go @@ -0,0 +1,42 @@ +package buildcontext + +import ( + "github.com/GoogleContainerTools/kaniko/testutil" + "os" + "testing" +) + +func TestGetGitPullMethod(t *testing.T) { + tests := []struct { + setEnv func() + expectedValue string + }{ + { + setEnv: func() {}, + expectedValue: "https", + }, + { + setEnv: func() { + _ = os.Setenv(gitPullMethodEnvKey, "http") + }, + expectedValue: "http", + }, + { + setEnv: func() { + _ = os.Setenv(gitPullMethodEnvKey, "https") + }, + expectedValue: "https", + }, + { + setEnv: func() { + _ = os.Setenv(gitPullMethodEnvKey, "unknown") + }, + expectedValue: "https", + }, + } + + for _, tt := range tests { + tt.setEnv() + testutil.CheckDeepEqual(t, getGitPullMethod(), tt.expectedValue) + } +} From d8b8e811ddbb0bbc28bf88b379f37a30cd64d9b1 Mon Sep 17 00:00:00 2001 From: yw-liu <43599588+yw-liu@users.noreply.github.com> Date: Thu, 16 Apr 2020 23:44:37 +0800 Subject: [PATCH 10/20] modify code format and unit-test --- pkg/buildcontext/git.go | 2 +- pkg/buildcontext/git_test.go | 56 +++++++++++++++++++++++++----------- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/pkg/buildcontext/git.go b/pkg/buildcontext/git.go index fe4eae1ac..11daea4c8 100644 --- a/pkg/buildcontext/git.go +++ b/pkg/buildcontext/git.go @@ -30,7 +30,7 @@ const ( ) var ( - supportedGitPullMethods = map[string]bool{"https":true, "http":true} + supportedGitPullMethods = map[string]bool{"https": true, "http": true} ) // Git unifies calls to download and unpack the build context. diff --git a/pkg/buildcontext/git_test.go b/pkg/buildcontext/git_test.go index c56159b95..c9ba88b21 100644 --- a/pkg/buildcontext/git_test.go +++ b/pkg/buildcontext/git_test.go @@ -1,42 +1,66 @@ package buildcontext import ( - "github.com/GoogleContainerTools/kaniko/testutil" "os" "testing" + + "github.com/GoogleContainerTools/kaniko/testutil" ) func TestGetGitPullMethod(t *testing.T) { tests := []struct { - setEnv func() - expectedValue string + testName string + setEnv func() (expectedValue string) }{ { - setEnv: func() {}, - expectedValue: "https", - }, - { - setEnv: func() { - _ = os.Setenv(gitPullMethodEnvKey, "http") + testName: "noEnv", + setEnv: func() (expectedValue string) { + expectedValue = "https" + return }, - expectedValue: "http", }, { - setEnv: func() { + testName: "emptyEnv", + setEnv: func() (expectedValue string) { + _ = os.Setenv(gitPullMethodEnvKey, "") + expectedValue = "https" + return + }, + }, + { + testName: "httpEnv", + setEnv: func() (expectedValue string) { + err := os.Setenv(gitPullMethodEnvKey, "http") + if nil != err { + expectedValue = "https" + } else { + expectedValue = "http" + } + return + }, + }, + { + testName: "httpsEnv", + setEnv: func() (expectedValue string) { _ = os.Setenv(gitPullMethodEnvKey, "https") + expectedValue = "https" + return }, - expectedValue: "https", }, { - setEnv: func() { + testName: "unknownEnv", + setEnv: func() (expectedValue string) { _ = os.Setenv(gitPullMethodEnvKey, "unknown") + expectedValue = "https" + return }, - expectedValue: "https", }, } for _, tt := range tests { - tt.setEnv() - testutil.CheckDeepEqual(t, getGitPullMethod(), tt.expectedValue) + t.Run(tt.testName, func(t *testing.T) { + expectedValue := tt.setEnv() + testutil.CheckDeepEqual(t, getGitPullMethod(), expectedValue) + }) } } From 7912e4c87bce4446992ba3377dae7264aad63c85 Mon Sep 17 00:00:00 2001 From: yw-liu <43599588+yw-liu@users.noreply.github.com> Date: Thu, 16 Apr 2020 23:48:43 +0800 Subject: [PATCH 11/20] modify unit-test --- pkg/buildcontext/git_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/buildcontext/git_test.go b/pkg/buildcontext/git_test.go index c9ba88b21..8505897f0 100644 --- a/pkg/buildcontext/git_test.go +++ b/pkg/buildcontext/git_test.go @@ -60,7 +60,7 @@ func TestGetGitPullMethod(t *testing.T) { for _, tt := range tests { t.Run(tt.testName, func(t *testing.T) { expectedValue := tt.setEnv() - testutil.CheckDeepEqual(t, getGitPullMethod(), expectedValue) + testutil.CheckDeepEqual(t, expectedValue, getGitPullMethod()) }) } } From 0fc311a8b73594fb7dac0e61235b64337a612779 Mon Sep 17 00:00:00 2001 From: yw-liu <43599588+yw-liu@users.noreply.github.com> Date: Fri, 17 Apr 2020 00:00:24 +0800 Subject: [PATCH 12/20] make string var as constant --- pkg/buildcontext/git.go | 4 +++- pkg/buildcontext/git_test.go | 16 ++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/pkg/buildcontext/git.go b/pkg/buildcontext/git.go index 11daea4c8..291f834f6 100644 --- a/pkg/buildcontext/git.go +++ b/pkg/buildcontext/git.go @@ -27,10 +27,12 @@ import ( const ( gitPullMethodEnvKey = "GIT_PULL_METHOD" + gitPullMethodHttps = "https" + gitPullMethodHttp = "http" ) var ( - supportedGitPullMethods = map[string]bool{"https": true, "http": true} + supportedGitPullMethods = map[string]bool{gitPullMethodHttps: true, gitPullMethodHttp: true} ) // Git unifies calls to download and unpack the build context. diff --git a/pkg/buildcontext/git_test.go b/pkg/buildcontext/git_test.go index 8505897f0..6f293a6ef 100644 --- a/pkg/buildcontext/git_test.go +++ b/pkg/buildcontext/git_test.go @@ -15,7 +15,7 @@ func TestGetGitPullMethod(t *testing.T) { { testName: "noEnv", setEnv: func() (expectedValue string) { - expectedValue = "https" + expectedValue = gitPullMethodHttps return }, }, @@ -23,18 +23,18 @@ func TestGetGitPullMethod(t *testing.T) { testName: "emptyEnv", setEnv: func() (expectedValue string) { _ = os.Setenv(gitPullMethodEnvKey, "") - expectedValue = "https" + expectedValue = gitPullMethodHttps return }, }, { testName: "httpEnv", setEnv: func() (expectedValue string) { - err := os.Setenv(gitPullMethodEnvKey, "http") + err := os.Setenv(gitPullMethodEnvKey, gitPullMethodHttp) if nil != err { - expectedValue = "https" + expectedValue = gitPullMethodHttps } else { - expectedValue = "http" + expectedValue = gitPullMethodHttp } return }, @@ -42,8 +42,8 @@ func TestGetGitPullMethod(t *testing.T) { { testName: "httpsEnv", setEnv: func() (expectedValue string) { - _ = os.Setenv(gitPullMethodEnvKey, "https") - expectedValue = "https" + _ = os.Setenv(gitPullMethodEnvKey, gitPullMethodHttps) + expectedValue = gitPullMethodHttps return }, }, @@ -51,7 +51,7 @@ func TestGetGitPullMethod(t *testing.T) { testName: "unknownEnv", setEnv: func() (expectedValue string) { _ = os.Setenv(gitPullMethodEnvKey, "unknown") - expectedValue = "https" + expectedValue = gitPullMethodHttps return }, }, From c9fc6b5bcfa9db57ed6656cb01030425cde05c19 Mon Sep 17 00:00:00 2001 From: yw-liu <43599588+yw-liu@users.noreply.github.com> Date: Fri, 17 Apr 2020 00:21:48 +0800 Subject: [PATCH 13/20] fix golint problem --- pkg/buildcontext/git.go | 6 +++--- pkg/buildcontext/git_test.go | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pkg/buildcontext/git.go b/pkg/buildcontext/git.go index 291f834f6..931a07089 100644 --- a/pkg/buildcontext/git.go +++ b/pkg/buildcontext/git.go @@ -27,12 +27,12 @@ import ( const ( gitPullMethodEnvKey = "GIT_PULL_METHOD" - gitPullMethodHttps = "https" - gitPullMethodHttp = "http" + gitPullMethodHTTPS = "https" + gitPullMethodHTTP = "http" ) var ( - supportedGitPullMethods = map[string]bool{gitPullMethodHttps: true, gitPullMethodHttp: true} + supportedGitPullMethods = map[string]bool{gitPullMethodHTTPS: true, gitPullMethodHTTP: true} ) // Git unifies calls to download and unpack the build context. diff --git a/pkg/buildcontext/git_test.go b/pkg/buildcontext/git_test.go index 6f293a6ef..e165bd508 100644 --- a/pkg/buildcontext/git_test.go +++ b/pkg/buildcontext/git_test.go @@ -15,7 +15,7 @@ func TestGetGitPullMethod(t *testing.T) { { testName: "noEnv", setEnv: func() (expectedValue string) { - expectedValue = gitPullMethodHttps + expectedValue = gitPullMethodHTTPS return }, }, @@ -23,18 +23,18 @@ func TestGetGitPullMethod(t *testing.T) { testName: "emptyEnv", setEnv: func() (expectedValue string) { _ = os.Setenv(gitPullMethodEnvKey, "") - expectedValue = gitPullMethodHttps + expectedValue = gitPullMethodHTTPS return }, }, { testName: "httpEnv", setEnv: func() (expectedValue string) { - err := os.Setenv(gitPullMethodEnvKey, gitPullMethodHttp) + err := os.Setenv(gitPullMethodEnvKey, gitPullMethodHTTP) if nil != err { - expectedValue = gitPullMethodHttps + expectedValue = gitPullMethodHTTPS } else { - expectedValue = gitPullMethodHttp + expectedValue = gitPullMethodHTTP } return }, @@ -42,8 +42,8 @@ func TestGetGitPullMethod(t *testing.T) { { testName: "httpsEnv", setEnv: func() (expectedValue string) { - _ = os.Setenv(gitPullMethodEnvKey, gitPullMethodHttps) - expectedValue = gitPullMethodHttps + _ = os.Setenv(gitPullMethodEnvKey, gitPullMethodHTTPS) + expectedValue = gitPullMethodHTTPS return }, }, @@ -51,7 +51,7 @@ func TestGetGitPullMethod(t *testing.T) { testName: "unknownEnv", setEnv: func() (expectedValue string) { _ = os.Setenv(gitPullMethodEnvKey, "unknown") - expectedValue = gitPullMethodHttps + expectedValue = gitPullMethodHTTPS return }, }, From 2e901732384ea2142c1bf79094bdad54e7727437 Mon Sep 17 00:00:00 2001 From: yw-liu <43599588+yw-liu@users.noreply.github.com> Date: Fri, 17 Apr 2020 00:37:12 +0800 Subject: [PATCH 14/20] fix boilerplate --- pkg/buildcontext/git_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pkg/buildcontext/git_test.go b/pkg/buildcontext/git_test.go index e165bd508..ccc53fcb9 100644 --- a/pkg/buildcontext/git_test.go +++ b/pkg/buildcontext/git_test.go @@ -1,3 +1,19 @@ +/* +Copyright 2020 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 buildcontext import ( From 73eb47c7520f8a1cd2f46ea46f6ac4615a6c3e39 Mon Sep 17 00:00:00 2001 From: yw-liu <43599588+yw-liu@users.noreply.github.com> Date: Fri, 17 Apr 2020 09:07:38 +0800 Subject: [PATCH 15/20] replace string literal with constant --- pkg/buildcontext/git.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/buildcontext/git.go b/pkg/buildcontext/git.go index 931a07089..7c2dddc6d 100644 --- a/pkg/buildcontext/git.go +++ b/pkg/buildcontext/git.go @@ -58,7 +58,7 @@ func (g *Git) UnpackTarFromBuildContext() (string, error) { func getGitPullMethod() string { gitPullMethod := os.Getenv(gitPullMethodEnvKey) if ok := supportedGitPullMethods[gitPullMethod]; !ok { - gitPullMethod = "https" + gitPullMethod = gitPullMethodHTTPS } return gitPullMethod } From 8cbc7a8ca2f47109bcd8930b2405eea6011e5347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordan=20Goasdou=C3=A9?= Date: Sun, 29 Mar 2020 22:43:37 +0200 Subject: [PATCH 16/20] feat: multistages is now built without unusued stages --- README.md | 13 +- cmd/executor/cmd/root.go | 1 + pkg/config/options.go | 1 + pkg/dockerfile/dockerfile.go | 54 +++++++++ pkg/dockerfile/dockerfile_test.go | 190 ++++++++++++++++++++++++++++++ 5 files changed, 256 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 726dc2708..625d61113 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ![kaniko logo](logo/Kaniko-Logo.png) -kaniko is a tool to build container images from a Dockerfile, inside a container or Kubernetes cluster. +kaniko is a tool to build container images from a Dockerfile, inside a container or Kubernetes cluster. kaniko doesn't depend on a Docker daemon and executes each command within a Dockerfile completely in userspace. This enables building container images in environments that can't easily or securely run a Docker daemon, such as a standard Kubernetes cluster. @@ -15,7 +15,7 @@ We'd love to hear from you! Join us on [#kaniko Kubernetes Slack](https://kuber :mega: **Please fill out our [quick 5-question survey](https://forms.gle/HhZGEM33x4FUz9Qa6)** so that we can learn how satisfied you are with Kaniko, and what improvements we should make. Thank you! :dancers: -Kaniko is not an officially supported Google project. +Kaniko is not an officially supported Google project. _If you are interested in contributing to kaniko, see [DEVELOPMENT.md](DEVELOPMENT.md) and [CONTRIBUTING.md](CONTRIBUTING.md)._ @@ -50,6 +50,7 @@ _If you are interested in contributing to kaniko, see [DEVELOPMENT.md](DEVELOPME - [--cache](#--cache) - [--cache-dir](#--cache-dir) - [--cache-repo](#--cache-repo) + - [--context-sub-path](#context-sub-path) - [--digest-file](#--digest-file) - [--oci-layout-path](#--oci-layout-path) - [--insecure-registry](#--insecure-registry) @@ -69,6 +70,7 @@ _If you are interested in contributing to kaniko, see [DEVELOPMENT.md](DEVELOPME - [--verbosity](#--verbosity) - [--whitelist-var-run](#--whitelist-var-run) - [--label](#--label) + - [--skip-unused-stages](#skip-unused-stages) - [Debug Image](#debug-image) - [Security](#security) - [Comparison with Other Tools](#comparison-with-other-tools) @@ -280,7 +282,7 @@ There is also a utility script [`run_in_docker.sh`](./run_in_docker.sh) that can ./run_in_docker.sh ``` -_NOTE: `run_in_docker.sh` expects a path to a +_NOTE: `run_in_docker.sh` expects a path to a Dockerfile relative to the absolute path of the build context._ An example run, specifying the Dockerfile in the container directory `/workspace`, the build @@ -536,6 +538,11 @@ Ignore /var/run when taking image snapshot. Set it to false to preserve /var/run Set this flag as `--label key=value` to set some metadata to the final image. This is equivalent as using the `LABEL` within the Dockerfile. +#### --skip-unused-stages + +This flag builds 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 + ### Debug Image The kaniko executor image is based on scratch and doesn't contain a shell. diff --git a/cmd/executor/cmd/root.go b/cmd/executor/cmd/root.go index db27d75f3..f0b50cc61 100644 --- a/cmd/executor/cmd/root.go +++ b/cmd/executor/cmd/root.go @@ -161,6 +161,7 @@ func addKanikoOptionsFlags() { RootCmd.PersistentFlags().StringVarP(&opts.RegistryMirror, "registry-mirror", "", "", "Registry mirror to use has pull-through cache instead of docker.io.") RootCmd.PersistentFlags().BoolVarP(&opts.WhitelistVarRun, "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") } // addHiddenFlags marks certain flags as hidden from the executor help text diff --git a/pkg/config/options.go b/pkg/config/options.go index 47436a5fd..6c8690284 100644 --- a/pkg/config/options.go +++ b/pkg/config/options.go @@ -56,6 +56,7 @@ type KanikoOptions struct { Cache bool Cleanup bool WhitelistVarRun bool + SkipUnusedStages bool } // WarmerOptions are options that are set by command line arguments to the cache warmer. diff --git a/pkg/dockerfile/dockerfile.go b/pkg/dockerfile/dockerfile.go index 3e41f40de..5ca8f1eea 100644 --- a/pkg/dockerfile/dockerfile.go +++ b/pkg/dockerfile/dockerfile.go @@ -22,6 +22,7 @@ import ( "io/ioutil" "net/http" "regexp" + "strconv" "strings" v1 "github.com/google/go-containerregistry/pkg/v1" @@ -253,6 +254,9 @@ func MakeKanikoStages(opts *config.KanikoOptions, stages []instructions.Stage, m if err := resolveStagesArgs(stages, args); err != nil { return nil, errors.Wrap(err, "resolving args") } + if opts.SkipUnusedStages { + stages = skipUnusedStages(stages, &targetStage, opts.Target) + } var kanikoStages []config.KanikoStage for index, stage := range stages { if len(stage.Name) > 0 { @@ -312,3 +316,53 @@ func unifyArgs(metaArgs []instructions.ArgCommand, buildArgs []string) []string } return args } + +// skipUnusedStages returns the list of used stages without the unnecessaries ones +func skipUnusedStages(stages []instructions.Stage, lastStageIndex *int, target string) []instructions.Stage { + stagesDependencies := make(map[string]bool) + var onlyUsedStages []instructions.Stage + idx := *lastStageIndex + + lastStageBaseName := stages[idx].BaseName + + for i := idx; i >= 0; i-- { + s := stages[i] + if (s.Name != "" && stagesDependencies[s.Name]) || s.Name == lastStageBaseName || i == idx { + for _, c := range s.Commands { + switch cmd := c.(type) { + case *instructions.CopyCommand: + stageName := cmd.From + if copyFromIndex, err := strconv.Atoi(stageName); err == nil { + stageName = stages[copyFromIndex].Name + } + if !stagesDependencies[stageName] { + stagesDependencies[stageName] = true + } + } + } + if i != idx { + stagesDependencies[s.BaseName] = true + } + } + } + dependenciesLen := len(stagesDependencies) + if target == "" && dependenciesLen == 0 { + return stages + } else if dependenciesLen > 0 { + for i := 0; i < idx; i++ { + if stages[i].Name == "" { + continue + } + s := stages[i] + if stagesDependencies[s.Name] || s.Name == lastStageBaseName { + onlyUsedStages = append(onlyUsedStages, s) + } + } + } + onlyUsedStages = append(onlyUsedStages, stages[idx]) + if idx > len(onlyUsedStages)-1 { + *lastStageIndex = len(onlyUsedStages) - 1 + } + + return onlyUsedStages +} diff --git a/pkg/dockerfile/dockerfile_test.go b/pkg/dockerfile/dockerfile_test.go index 8fb612297..63c66f4b2 100644 --- a/pkg/dockerfile/dockerfile_test.go +++ b/pkg/dockerfile/dockerfile_test.go @@ -456,3 +456,193 @@ func Test_ResolveStagesArgs(t *testing.T) { } } } + +func Test_SkipingUnusedStages(t *testing.T) { + tests := []struct { + description string + dockerfile string + targets []string + expectedSourceCodes map[string][]string + expectedTargetIndexBeforeSkip map[string]int + expectedTargetIndexAfterSkip map[string]int + }{ + { + description: "dockerfile_without_copyFrom", + dockerfile: ` + FROM alpine:3.11 AS base-dev + RUN echo dev > /hi + FROM alpine:3.11 AS base-prod + RUN echo prod > /hi + FROM base-dev as final-stage + RUN cat /hi + `, + targets: []string{"base-dev", "base-prod", ""}, + expectedSourceCodes: map[string][]string{ + "base-dev": {"FROM alpine:3.11 AS base-dev"}, + "base-prod": {"FROM alpine:3.11 AS base-prod"}, + "": {"FROM alpine:3.11 AS base-dev", "FROM base-dev as final-stage"}, + }, + expectedTargetIndexBeforeSkip: map[string]int{ + "base-dev": 0, + "base-prod": 1, + "": 2, + }, + expectedTargetIndexAfterSkip: map[string]int{ + "base-dev": 0, + "base-prod": 0, + "": 1, + }, + }, + { + description: "dockerfile_with_copyFrom", + dockerfile: ` + FROM alpine:3.11 AS base-dev + RUN echo dev > /hi + FROM alpine:3.11 AS base-prod + RUN echo prod > /hi + FROM alpine:3.11 + COPY --from=base-prod /hi /finalhi + RUN cat /finalhi + `, + targets: []string{"base-dev", "base-prod", ""}, + expectedSourceCodes: map[string][]string{ + "base-dev": {"FROM alpine:3.11 AS base-dev"}, + "base-prod": {"FROM alpine:3.11 AS base-prod"}, + "": {"FROM alpine:3.11 AS base-prod", "FROM alpine:3.11"}, + }, + expectedTargetIndexBeforeSkip: map[string]int{ + "base-dev": 0, + "base-prod": 1, + "": 2, + }, + expectedTargetIndexAfterSkip: map[string]int{ + "base-dev": 0, + "base-prod": 0, + "": 1, + }, + }, + { + description: "dockerfile_with_two_copyFrom", + dockerfile: ` + FROM alpine:3.11 AS base-dev + RUN echo dev > /hi + FROM alpine:3.11 AS base-prod + RUN echo prod > /hi + FROM alpine:3.11 + COPY --from=base-dev /hi /finalhidev + COPY --from=base-prod /hi /finalhiprod + RUN cat /finalhidev + RUN cat /finalhiprod + `, + targets: []string{"base-dev", "base-prod", ""}, + expectedSourceCodes: map[string][]string{ + "base-dev": {"FROM alpine:3.11 AS base-dev"}, + "base-prod": {"FROM alpine:3.11 AS base-prod"}, + "": {"FROM alpine:3.11 AS base-dev", "FROM alpine:3.11 AS base-prod", "FROM alpine:3.11"}, + }, + expectedTargetIndexBeforeSkip: map[string]int{ + "base-dev": 0, + "base-prod": 1, + "": 2, + }, + expectedTargetIndexAfterSkip: map[string]int{ + "base-dev": 0, + "base-prod": 0, + "": 2, + }, + }, + { + description: "dockerfile_with_two_copyFrom_and_arg", + dockerfile: ` + FROM debian:9.11 as base + COPY . . + FROM scratch as second + ENV foopath context/foo + COPY --from=0 $foopath context/b* /foo/ + FROM second as third + COPY --from=base /context/foo /new/foo + FROM base as fourth + # Make sure that we snapshot intermediate images correctly + RUN date > /date + ENV foo bar + # This base image contains symlinks with relative paths to whitelisted directories + # We need to test they're extracted correctly + FROM fedora@sha256:c4cc32b09c6ae3f1353e7e33a8dda93dc41676b923d6d89afa996b421cc5aa48 + FROM fourth + ARG file=/foo2 + COPY --from=second /foo ${file} + COPY --from=debian:9.11 /etc/os-release /new + `, + targets: []string{"base", ""}, + expectedSourceCodes: map[string][]string{ + "base": {"FROM debian:9.11 as base"}, + "second": {"FROM debian:9.11 as base", "FROM scratch as second"}, + "": {"FROM debian:9.11 as base", "FROM scratch as second", "FROM base as fourth", "FROM fourth"}, + }, + expectedTargetIndexBeforeSkip: map[string]int{ + "base": 0, + "second": 1, + "": 5, + }, + expectedTargetIndexAfterSkip: map[string]int{ + "base": 0, + "second": 1, + "": 3, + }, + }, + { + description: "dockerfile_without_final_dependencies", + dockerfile: ` + FROM alpine:3.11 + FROM debian:9.11 as base + RUN echo foo > /foo + FROM debian:9.11 as fizz + RUN echo fizz >> /fizz + COPY --from=base /foo /fizz + FROM alpine:3.11 as buzz + RUN echo buzz > /buzz + FROM alpine:3.11 as final + RUN echo bar > /bar + `, + targets: []string{"final", "buzz", "fizz", ""}, + expectedSourceCodes: map[string][]string{ + "final": {"FROM alpine:3.11 as final"}, + "buzz": {"FROM alpine:3.11 as buzz"}, + "fizz": {"FROM debian:9.11 as base", "FROM debian:9.11 as fizz"}, + "": {"FROM alpine:3.11", "FROM debian:9.11 as base", "FROM debian:9.11 as fizz", "FROM alpine:3.11 as buzz", "FROM alpine:3.11 as final"}, + }, + expectedTargetIndexBeforeSkip: map[string]int{ + "final": 4, + "buzz": 3, + "fizz": 2, + "": 4, + }, + expectedTargetIndexAfterSkip: map[string]int{ + "final": 0, + "buzz": 0, + "fizz": 1, + "": 4, + }, + }, + } + + for _, test := range tests { + stages, _, err := Parse([]byte(test.dockerfile)) + testutil.CheckError(t, false, err) + actualSourceCodes := make(map[string][]string) + for _, target := range test.targets { + targetIndex, err := targetStage(stages, target) + testutil.CheckError(t, false, err) + targetIndexBeforeSkip := targetIndex + onlyUsedStages := skipUnusedStages(stages, &targetIndex, target) + for _, s := range onlyUsedStages { + actualSourceCodes[target] = append(actualSourceCodes[target], s.SourceCode) + } + t.Run(test.description, func(t *testing.T) { + testutil.CheckDeepEqual(t, test.expectedSourceCodes[target], actualSourceCodes[target]) + testutil.CheckDeepEqual(t, test.expectedTargetIndexBeforeSkip[target], targetIndexBeforeSkip) + testutil.CheckDeepEqual(t, test.expectedTargetIndexAfterSkip[target], targetIndex) + }) + } + } +} From 04888f269091f89f6fe5b02e0bef541103aba9cd Mon Sep 17 00:00:00 2001 From: Dani Raznikov Date: Sat, 18 Apr 2020 20:24:46 +0300 Subject: [PATCH 17/20] Set loud logs to trace level --- README.md | 2 +- cmd/executor/cmd/root.go | 3 +-- cmd/warmer/cmd/root.go | 2 +- pkg/commands/copy.go | 3 ++- pkg/commands/run.go | 3 ++- pkg/filesystem/resolve.go | 4 ++-- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 726dc2708..c723b0011 100644 --- a/README.md +++ b/README.md @@ -526,7 +526,7 @@ You need to set `--destination` as well (for example `--destination=image`). #### --verbosity -Set this flag as `--verbosity=` to set the logging level. Defaults to `info`. +Set this flag as `--verbosity=` to set the logging level. Defaults to `info`. #### --whitelist-var-run diff --git a/cmd/executor/cmd/root.go b/cmd/executor/cmd/root.go index db27d75f3..88be2d656 100644 --- a/cmd/executor/cmd/root.go +++ b/cmd/executor/cmd/root.go @@ -47,9 +47,8 @@ var ( ) func init() { - RootCmd.PersistentFlags().StringVarP(&logLevel, "verbosity", "v", logging.DefaultLevel, "Log level (debug, info, warn, error, fatal, panic") + RootCmd.PersistentFlags().StringVarP(&logLevel, "verbosity", "v", logging.DefaultLevel, "Log level (trace, debug, info, warn, error, fatal, panic)") RootCmd.PersistentFlags().StringVar(&logFormat, "log-format", logging.FormatColor, "Log format (text, color, json)") - RootCmd.PersistentFlags().BoolVarP(&force, "force", "", false, "Force building outside of a container") addKanikoOptionsFlags() diff --git a/cmd/warmer/cmd/root.go b/cmd/warmer/cmd/root.go index 9bbd08e21..2979981a8 100644 --- a/cmd/warmer/cmd/root.go +++ b/cmd/warmer/cmd/root.go @@ -35,7 +35,7 @@ var ( ) func init() { - RootCmd.PersistentFlags().StringVarP(&logLevel, "verbosity", "v", logging.DefaultLevel, "Log level (debug, info, warn, error, fatal, panic") + RootCmd.PersistentFlags().StringVarP(&logLevel, "verbosity", "v", logging.DefaultLevel, "Log level (trace, debug, info, warn, error, fatal, panic)") RootCmd.PersistentFlags().StringVar(&logFormat, "log-format", logging.FormatColor, "Log format (text, color, json)") addKanikoOptionsFlags() diff --git a/pkg/commands/copy.go b/pkg/commands/copy.go index e31efe2f3..5d58dd6d2 100644 --- a/pkg/commands/copy.go +++ b/pkg/commands/copy.go @@ -207,7 +207,8 @@ func (cr *CachingCopyCommand) FilesUsedFromContext(config *v1.Config, buildArgs func (cr *CachingCopyCommand) FilesToSnapshot() []string { f := cr.extractedFiles - logrus.Debugf("files extracted by caching copy command %s", f) + logrus.Debugf("%d files extracted by caching copy command", len(f)) + logrus.Tracef("Extracted files: %s", f) return f } diff --git a/pkg/commands/run.go b/pkg/commands/run.go index 23b7f5fd7..b5096c5d8 100644 --- a/pkg/commands/run.go +++ b/pkg/commands/run.go @@ -217,7 +217,8 @@ func (cr *CachingRunCommand) ExecuteCommand(config *v1.Config, buildArgs *docker func (cr *CachingRunCommand) FilesToSnapshot() []string { f := cr.extractedFiles - logrus.Debugf("files extracted from caching run command %s", f) + logrus.Debugf("%d files extracted by caching run command", len(f)) + logrus.Tracef("Extracted files: %s", f) return f } diff --git a/pkg/filesystem/resolve.go b/pkg/filesystem/resolve.go index 0ea1ac9b6..93afaa041 100644 --- a/pkg/filesystem/resolve.go +++ b/pkg/filesystem/resolve.go @@ -35,8 +35,8 @@ import ( // output set. // * Add all ancestors of each path to the output set. func ResolvePaths(paths []string, wl []util.WhitelistEntry) (pathsToAdd []string, err error) { - logrus.Info("Resolving paths") - logrus.Debugf("Resolving paths %s", paths) + logrus.Infof("Resolving %d paths", len(paths)) + logrus.Tracef("Resolving paths %s", paths) fileSet := make(map[string]bool) From ad3ed6bcf6f7debf854ae422eeec57b264cc0bd7 Mon Sep 17 00:00:00 2001 From: Giovan Isa Musthofa Date: Sun, 19 Apr 2020 14:44:49 +0700 Subject: [PATCH 18/20] Update Pushing to Docker Hub to use v2 api --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 726dc2708..e4b26525a 100644 --- a/README.md +++ b/README.md @@ -336,7 +336,7 @@ Create a `config.json` file with your Docker registry url and the previous gener ``` { "auths": { - "https://index.docker.io/v1/": { + "https://index.docker.io/v2/": { "auth": "xxxxxxxxxxxxxxx" } } From d763b7e961e65bc0a488f7800af5efac7eca4a36 Mon Sep 17 00:00:00 2001 From: Ben Einaudi Date: Mon, 30 Mar 2020 00:02:48 +0200 Subject: [PATCH 19/20] fix previous name checking in 'executor.build.fetchExtraStages' --- pkg/executor/build.go | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/pkg/executor/build.go b/pkg/executor/build.go index 015466bf7..eecea2ec0 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -705,7 +705,7 @@ func fetchExtraStages(stages []config.KanikoStage, opts *config.KanikoOptions) e t := timing.Start("Fetching Extra Stages") defer timing.DefaultRun.Stop(t) - var names = []string{} + var names []string for stageIndex, s := range stages { for _, cmd := range s.Commands { @@ -722,11 +722,10 @@ func fetchExtraStages(stages []config.KanikoStage, opts *config.KanikoOptions) e continue } // Check if the name is the alias of a previous stage - for _, name := range names { - if name == c.From { - continue - } + if fromPreviousStage(c, names) { + continue } + // This must be an image name, fetch it. logrus.Debugf("Found extra base image stage %s", c.From) sourceImage, err := util.RetrieveRemoteImage(c.From, opts) @@ -747,6 +746,16 @@ func fetchExtraStages(stages []config.KanikoStage, opts *config.KanikoOptions) e } return nil } + +func fromPreviousStage(copyCommand *instructions.CopyCommand, previousStageNames []string) bool { + for _, previousStageName := range previousStageNames { + if previousStageName == copyCommand.From { + return true + } + } + return false +} + func extractImageToDependencyDir(name string, image v1.Image) error { t := timing.Start("Extracting Image to Dependency Dir") defer timing.DefaultRun.Stop(t) From 1d23574cc19eac1c364ee08bdd083b09692fbef8 Mon Sep 17 00:00:00 2001 From: Michel Hollands Date: Sun, 3 May 2020 15:49:41 +0100 Subject: [PATCH 20/20] Add --log-format parameter to README.md The --log-format parameter was introduced in v0.18.0 but was not added to the README.md Closes #1215 --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index f8370fe1e..cefd01002 100644 --- a/README.md +++ b/README.md @@ -552,6 +552,10 @@ You need to set `--destination` as well (for example `--destination=image`). Set this flag as `--verbosity=` to set the logging level. Defaults to `info`. +#### --log-format + +Set this flag as `--log-format=` to set the log format. Defaults to `color`. + #### --whitelist-var-run Ignore /var/run when taking image snapshot. Set it to false to preserve /var/run/* in destination image. (Default true).