From 7dbc7a04a7e03a8249887474b3b5af8f3e492acc Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Mon, 23 Apr 2018 17:09:29 -0700 Subject: [PATCH 1/6] Support multi stage builds --- cmd/executor/cmd/root.go | 11 +- .../dockerfiles/Dockerfile_test_multistage | 8 + .../dockerfiles/config_test_multistage.json | 12 ++ integration_tests/integration_test_yaml.go | 8 + pkg/commands/copy.go | 15 +- pkg/commands/env.go | 6 + pkg/dockerfile/dockerfile.go | 72 ++++++++- pkg/dockerfile/dockerfile_test.go | 96 +++++++++++ pkg/executor/executor.go | 150 +++++++++++------- pkg/util/command_util.go | 2 +- pkg/util/fs_util.go | 32 +++- test.sh | 2 +- 12 files changed, 345 insertions(+), 69 deletions(-) create mode 100644 integration_tests/dockerfiles/Dockerfile_test_multistage create mode 100644 integration_tests/dockerfiles/config_test_multistage.json create mode 100644 pkg/dockerfile/dockerfile_test.go diff --git a/cmd/executor/cmd/root.go b/cmd/executor/cmd/root.go index 2c8e9b69a..8a03af831 100644 --- a/cmd/executor/cmd/root.go +++ b/cmd/executor/cmd/root.go @@ -23,9 +23,9 @@ import ( "github.com/genuinetools/amicontained/container" - "github.com/GoogleContainerTools/kaniko/pkg/executor" - "github.com/GoogleContainerTools/kaniko/pkg/constants" + "github.com/GoogleContainerTools/kaniko/pkg/executor" + "github.com/GoogleContainerTools/kaniko/pkg/image" "github.com/GoogleContainerTools/kaniko/pkg/util" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -72,7 +72,12 @@ var RootCmd = &cobra.Command{ } logrus.Warn("kaniko is being run outside of a container. This can have dangerous effects on your system") } - if err := executor.DoBuild(dockerfilePath, srcContext, destination, snapshotMode, dockerInsecureSkipTLSVerify); err != nil { + buildImage, err := executor.DoBuild(dockerfilePath, srcContext, snapshotMode) + if err != nil { + logrus.Error(err) + os.Exit(1) + } + if err := image.PushImage(buildImage, destination, dockerInsecureSkipTLSVerify); err != nil { logrus.Error(err) os.Exit(1) } diff --git a/integration_tests/dockerfiles/Dockerfile_test_multistage b/integration_tests/dockerfiles/Dockerfile_test_multistage new file mode 100644 index 000000000..3833e8806 --- /dev/null +++ b/integration_tests/dockerfiles/Dockerfile_test_multistage @@ -0,0 +1,8 @@ +FROM gcr.io/distroless/base:latest +COPY . . + +FROM scratch as second +COPY --from=0 context/foo /foo + +FROM gcr.io/distroless/base:latest +COPY --from=second /foo /foo2 diff --git a/integration_tests/dockerfiles/config_test_multistage.json b/integration_tests/dockerfiles/config_test_multistage.json new file mode 100644 index 000000000..9aa0494cb --- /dev/null +++ b/integration_tests/dockerfiles/config_test_multistage.json @@ -0,0 +1,12 @@ +[ + { + "Image1": "gcr.io/kaniko-test/docker-test-multistage:latest", + "Image2": "gcr.io/kaniko-test/kaniko-test-multistage:latest", + "DiffType": "File", + "Diff": { + "Adds": null, + "Dels": null, + "Mods": null + } + } +] \ No newline at end of file diff --git a/integration_tests/integration_test_yaml.go b/integration_tests/integration_test_yaml.go index fc01d4848..a9f09e3a4 100644 --- a/integration_tests/integration_test_yaml.go +++ b/integration_tests/integration_test_yaml.go @@ -139,6 +139,14 @@ var fileTests = []struct { kanikoContext: buildcontextPath, repo: "test-scratch", }, + { + description: "test multistage", + dockerfilePath: "/workspace/integration_tests/dockerfiles/Dockerfile_test_multistage", + configPath: "/workspace/integration_tests/dockerfiles/config_test_multistage.json", + dockerContext: buildcontextPath, + kanikoContext: buildcontextPath, + repo: "test-multistage", + }, } var structureTests = []struct { diff --git a/pkg/commands/copy.go b/pkg/commands/copy.go index bded4bf99..3d617a3db 100644 --- a/pkg/commands/copy.go +++ b/pkg/commands/copy.go @@ -17,6 +17,7 @@ limitations under the License. package commands import ( + "github.com/GoogleContainerTools/kaniko/pkg/constants" "github.com/GoogleContainerTools/kaniko/pkg/util" "github.com/containers/image/manifest" "github.com/docker/docker/builder/dockerfile/instructions" @@ -39,6 +40,11 @@ func (c *CopyCommand) ExecuteCommand(config *manifest.Schema2Config) error { logrus.Infof("cmd: copy %s", srcs) logrus.Infof("dest: %s", dest) + // Resolve from + if c.cmd.From != "" { + c.buildcontext = filepath.Join(constants.BuildContextDir, c.cmd.From) + } + // First, resolve any environment replacement resolvedEnvs, err := util.ResolveEnvironmentReplacementList(c.cmd.SourcesAndDest, config.Env, true) if err != nil { @@ -57,11 +63,18 @@ func (c *CopyCommand) ExecuteCommand(config *manifest.Schema2Config) error { if err != nil { return err } - destPath, err := util.DestinationFilepath(src, dest, config.WorkingDir) + cwd := config.WorkingDir + if cwd == "" { + cwd = constants.RootDir + } + destPath, err := util.DestinationFilepath(src, dest, cwd) if err != nil { return err } if fi.IsDir() { + if !filepath.IsAbs(dest) { + dest = filepath.Join(cwd, dest) + } if err := util.CopyDir(fullPath, dest); err != nil { return err } diff --git a/pkg/commands/env.go b/pkg/commands/env.go index acb379e23..691315f6d 100644 --- a/pkg/commands/env.go +++ b/pkg/commands/env.go @@ -29,6 +29,12 @@ type EnvCommand struct { cmd *instructions.EnvCommand } +func NewEnvCommand(cmd *instructions.EnvCommand) EnvCommand { + return EnvCommand{ + cmd: cmd, + } +} + func (e *EnvCommand) ExecuteCommand(config *manifest.Schema2Config) error { logrus.Info("cmd: ENV") newEnvs := e.cmd.Env diff --git a/pkg/dockerfile/dockerfile.go b/pkg/dockerfile/dockerfile.go index f755a547e..c5e91f256 100644 --- a/pkg/dockerfile/dockerfile.go +++ b/pkg/dockerfile/dockerfile.go @@ -18,10 +18,15 @@ package dockerfile import ( "bytes" - "strings" - + "github.com/GoogleContainerTools/kaniko/pkg/commands" + "github.com/GoogleContainerTools/kaniko/pkg/constants" + "github.com/GoogleContainerTools/kaniko/pkg/image" + "github.com/GoogleContainerTools/kaniko/pkg/util" "github.com/docker/docker/builder/dockerfile/instructions" "github.com/docker/docker/builder/dockerfile/parser" + "path/filepath" + "strconv" + "strings" ) // Parse parses the contents of a Dockerfile and returns a list of commands @@ -37,6 +42,25 @@ func Parse(b []byte) ([]instructions.Stage, error) { return stages, err } +// ResolveStages resolves any calls to previous stages to the number value of that stage +// Ex. --from=second_stage should be --from=1 for easier processing later on +func ResolveStages(stages []instructions.Stage) { + nameToIndex := make(map[string]string) + for i, stage := range stages { + index := strconv.Itoa(i) + nameToIndex[stage.Name] = index + nameToIndex[index] = index + for _, cmd := range stage.Commands { + switch c := cmd.(type) { + case *instructions.CopyCommand: + if c.From != "" { + c.From = nameToIndex[c.From] + } + } + } + } +} + // ParseCommands parses an array of commands into an array of instructions.Command; used for onbuild func ParseCommands(cmdArray []string) ([]instructions.Command, error) { var cmds []instructions.Command @@ -54,3 +78,47 @@ func ParseCommands(cmdArray []string) ([]instructions.Command, error) { } return cmds, nil } + +// Dependencies returns a list of files in this stage that will be needed in later stages +func Dependencies(index int, stages []instructions.Stage) ([]string, error) { + var dependencies []string + for stageIndex, stage := range stages { + if stageIndex <= index { + continue + } + ms, err := image.NewSourceImage(stage.BaseName) + if err != nil { + return nil, err + } + for _, cmd := range stage.Commands { + switch c := cmd.(type) { + case *instructions.EnvCommand: + envCommand := commands.NewEnvCommand(c) + if err := envCommand.ExecuteCommand(ms.Config()); err != nil { + return nil, err + } + case *instructions.CopyCommand: + if c.From != strconv.Itoa(index) { + continue + } + // First, resolve any environment replacement + resolvedEnvs, err := util.ResolveEnvironmentReplacementList(c.SourcesAndDest, ms.Config().Env, true) + if err != nil { + return nil, err + } + // Resolve wildcards and get a list of resolved sources + srcs, err := util.ResolveSources(resolvedEnvs, constants.RootDir) + if err != nil { + return nil, err + } + for index, src := range srcs { + if !filepath.IsAbs(src) { + srcs[index] = filepath.Join(constants.RootDir, src) + } + } + dependencies = append(dependencies, srcs...) + } + } + } + return dependencies, nil +} diff --git a/pkg/dockerfile/dockerfile_test.go b/pkg/dockerfile/dockerfile_test.go new file mode 100644 index 000000000..4c4c7e39c --- /dev/null +++ b/pkg/dockerfile/dockerfile_test.go @@ -0,0 +1,96 @@ +/* +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 dockerfile + +import ( + "fmt" + "github.com/GoogleContainerTools/kaniko/testutil" + "github.com/docker/docker/builder/dockerfile/instructions" + "io/ioutil" + "os" + "path/filepath" + "strconv" + "testing" +) + +var dockerfile = ` +FROM scratch +RUN echo hi > /hi + +FROM scratch AS second +COPY --from=0 /hi /hi2 + +FROM scratch +COPY --from=second /hi2 /hi3 +` + +func Test_ResolveStages(t *testing.T) { + stages, err := Parse([]byte(dockerfile)) + if err != nil { + t.Fatal(err) + } + ResolveStages(stages) + for index, stage := range stages { + if index == 0 { + continue + } + copyCmd := stage.Commands[0].(*instructions.CopyCommand) + expectedStage := strconv.Itoa(index - 1) + if copyCmd.From != expectedStage { + t.Fatalf("unexpected copy command: %s resolved to stage %s, expected %s", copyCmd.String(), copyCmd.From, expectedStage) + } + } +} + +func Test_Dependencies(t *testing.T) { + testDir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + helloPath := filepath.Join(testDir, "hello") + if err := os.Mkdir(helloPath, 0755); err != nil { + t.Fatal(err) + } + + dockerfile := fmt.Sprintf(` + FROM scratch + COPY %s %s + + FROM scratch AS second + ENV hienv %s + COPY a b + COPY --from=0 /$hienv %s /hi2/ + `, helloPath, helloPath, helloPath, testDir) + + stages, err := Parse([]byte(dockerfile)) + if err != nil { + t.Fatal(err) + } + + expectedDependencies := [][]string{ + { + helloPath, + testDir, + }, + nil, + } + + for index := range stages { + actualDeps, err := Dependencies(index, stages) + testutil.CheckErrorAndDeepEqual(t, false, err, expectedDependencies[index], actualDeps) + } +} diff --git a/pkg/executor/executor.go b/pkg/executor/executor.go index ab9eacd53..3dc14fb55 100644 --- a/pkg/executor/executor.go +++ b/pkg/executor/executor.go @@ -18,9 +18,7 @@ package executor import ( "fmt" - "io/ioutil" - "os" - + img "github.com/GoogleContainerTools/container-diff/pkg/image" "github.com/GoogleContainerTools/kaniko/pkg/commands" "github.com/GoogleContainerTools/kaniko/pkg/constants" "github.com/GoogleContainerTools/kaniko/pkg/dockerfile" @@ -30,72 +28,76 @@ import ( "github.com/containers/image/manifest" "github.com/docker/docker/builder/dockerfile/instructions" "github.com/sirupsen/logrus" + "io/ioutil" + "os" + "path/filepath" + "strconv" ) -func DoBuild(dockerfilePath, srcContext, destination, snapshotMode string, dockerInsecureSkipTLSVerify bool) error { +// DoBuild executes each stage of the build +func DoBuild(dockerfilePath, srcContext, snapshotMode string) (*img.MutableSource, error) { // Parse dockerfile and unpack base image to root d, err := ioutil.ReadFile(dockerfilePath) if err != nil { - return err + return nil, err } stages, err := dockerfile.Parse(d) if err != nil { - return err - } - baseImage := stages[0].BaseName - - // Unpack file system to root - logrus.Infof("Unpacking filesystem of %s...", baseImage) - if err := util.ExtractFileSystemFromImage(baseImage); err != nil { - return err + return nil, err } + dockerfile.ResolveStages(stages) hasher, err := getHasher(snapshotMode) if err != nil { - return err - } - l := snapshot.NewLayeredMap(hasher) - snapshotter := snapshot.NewSnapshotter(l, constants.RootDir) - - // Take initial snapshot - if err := snapshotter.Init(); err != nil { - return err + return nil, err } - // Initialize source image - sourceImage, err := image.NewSourceImage(baseImage) - if err != nil { - return err - } - - // Set environment variables within the image - if err := image.SetEnvVariables(sourceImage); err != nil { - return err - } - - imageConfig := sourceImage.Config() - // Currently only supports single stage builds - for _, stage := range stages { - if err := resolveOnBuild(&stage, imageConfig); err != nil { - return err + for index, stage := range stages { + baseImage := stage.BaseName + finalStage := index == len(stages)-1 + // Unpack file system to root + logrus.Infof("Unpacking filesystem of %s...", baseImage) + if err := util.ExtractFileSystemFromImage(baseImage); err != nil { + return nil, err } + + l := snapshot.NewLayeredMap(hasher) + snapshotter := snapshot.NewSnapshotter(l, constants.RootDir) + + // Take initial snapshot + if err := snapshotter.Init(); err != nil { + return nil, err + } + // Initialize source image + sourceImage, err := image.NewSourceImage(baseImage) + if err != nil { + return nil, err + } + imageConfig := sourceImage.Config() + if err := resolveOnBuild(&stage, imageConfig); err != nil { + return nil, err + } + for _, cmd := range stage.Commands { dockerCommand, err := commands.GetCommand(cmd, srcContext) if err != nil { - return err + return nil, err } if dockerCommand == nil { continue } if err := dockerCommand.ExecuteCommand(imageConfig); err != nil { - return err + return nil, err + } + if !finalStage { + continue } // Now, we get the files to snapshot from this command and take the snapshot snapshotFiles := dockerCommand.FilesToSnapshot() contents, err := snapshotter.TakeSnapshot(snapshotFiles) if err != nil { - return err + return nil, err } util.MoveVolumeWhitelistToWhitelist() if contents == nil { @@ -105,15 +107,58 @@ func DoBuild(dockerfilePath, srcContext, destination, snapshotMode string, docke } // Append the layer to the image if err := sourceImage.AppendLayer(contents, constants.Author); err != nil { + return nil, err + } + } + if finalStage { + return sourceImage, nil + } + if err := saveStageDependencies(index, stages); err != nil { + return nil, err + } + // Delete the filesystem + if err := util.DeleteFilesystem(); err != nil { + return nil, err + } + } + return nil, nil +} + +func saveStageDependencies(index int, stages []instructions.Stage) error { + // First, get the files in this stage later stages will need + dependencies, err := dockerfile.Dependencies(index, stages) + logrus.Infof("saving dependencies %s", dependencies) + if err != nil { + return err + } + // Then, create the directory they will exist in + i := strconv.Itoa(index) + dependencyDir := filepath.Join(constants.BuildContextDir, i) + if err := os.MkdirAll(dependencyDir, 0755); err != nil { + return err + } + // Now, copy over dependencies to this dir + for _, d := range dependencies { + fi, err := os.Lstat(d) + if err != nil { + return err + } + dest := filepath.Join(dependencyDir, d) + if fi.IsDir() { + if err := util.CopyDir(d, dest); err != nil { + return err + } + } else if fi.Mode()&os.ModeSymlink != 0 { + if err := util.CopySymlink(d, dest); err != nil { + return err + } + } else { + if err := util.CopyFile(d, dest); err != nil { return err } } } - // Push the image - if err := setDefaultEnv(); err != nil { - return err - } - return image.PushImage(sourceImage, destination, dockerInsecureSkipTLSVerify) + return nil } func getHasher(snapshotMode string) (func(string) (string, error), error) { @@ -141,18 +186,3 @@ func resolveOnBuild(stage *instructions.Stage, config *manifest.Schema2Config) e logrus.Infof("Executing %v build triggers", len(cmds)) return nil } - -// setDefaultEnv sets default values for HOME and PATH so that -// config.json and docker-credential-gcr can be accessed -func setDefaultEnv() error { - defaultEnvs := map[string]string{ - "HOME": "/root", - "PATH": "/usr/local/bin/", - } - for key, val := range defaultEnvs { - if err := os.Setenv(key, val); err != nil { - return err - } - } - return nil -} diff --git a/pkg/util/command_util.go b/pkg/util/command_util.go index 20efd19a3..c8788a494 100644 --- a/pkg/util/command_util.go +++ b/pkg/util/command_util.go @@ -178,7 +178,7 @@ func IsSrcsValid(srcsAndDest instructions.SourcesAndDest, resolvedSources []stri } if len(resolvedSources) == 1 { - fi, err := os.Stat(filepath.Join(root, resolvedSources[0])) + fi, err := os.Lstat(filepath.Join(root, resolvedSources[0])) if err != nil { return err } diff --git a/pkg/util/fs_util.go b/pkg/util/fs_util.go index f5c96b80b..e32517c9a 100644 --- a/pkg/util/fs_util.go +++ b/pkg/util/fs_util.go @@ -56,6 +56,33 @@ func ExtractFileSystemFromImage(img string) error { return pkgutil.GetFileSystemFromReference(ref, imgSrc, constants.RootDir, whitelist) } +// DeleteFilesystem deletes the extracted image file system +func DeleteFilesystem() error { + logrus.Info("Deleting filesystem...") + err := filepath.Walk(constants.RootDir, func(path string, info os.FileInfo, err error) error { + if PathInWhitelist(path, constants.RootDir) || ChildDirInWhitelist(path, constants.RootDir) { + logrus.Debugf("Not deleting %s, as it's whitelisted", path) + return nil + } + if path == constants.RootDir { + return nil + } + return os.RemoveAll(path) + }) + return err +} + +// ChildDirInWhitelist returns true if there is a child file or directory of the path in the whitelist +func ChildDirInWhitelist(path, directory string) bool { + for _, d := range whitelist { + dirPath := filepath.Join(directory, d) + if pkgutil.HasFilepathPrefix(dirPath, path) { + return true + } + } + return false +} + // PathInWhitelist returns true if the path is whitelisted func PathInWhitelist(path, directory string) bool { for _, c := range constants.KanikoBuildFiles { @@ -135,6 +162,9 @@ func Files(root string) ([]string, error) { var files []string logrus.Debugf("Getting files and contents at root %s", root) err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if PathInWhitelist(path, root) { + return nil + } files = append(files, path) return err }) @@ -219,7 +249,7 @@ func CopyDir(src, dest string) error { } for _, file := range files { fullPath := filepath.Join(src, file) - fi, err := os.Stat(fullPath) + fi, err := os.Lstat(fullPath) if err != nil { return err } diff --git a/test.sh b/test.sh index 1ca29155d..a1b16b244 100755 --- a/test.sh +++ b/test.sh @@ -21,7 +21,7 @@ GREEN='\033[0;32m' RESET='\033[0m' echo "Running go tests..." -go test -cover -v -tags "containers_image_ostree_stub containers_image_openpgp exclude_graphdriver_devicemapper exclude_graphdriver_btrfs" -timeout 60s `go list ./... | grep -v vendor` | sed ''/PASS/s//$(printf "${GREEN}PASS${RESET}")/'' | sed ''/FAIL/s//$(printf "${RED}FAIL${RESET}")/'' +go test -cover -v -tags "containers_image_ostree_stub containers_image_openpgp exclude_graphdriver_devicemapper exclude_graphdriver_btrfs exclude_graphdriver_overlay" -timeout 60s `go list ./... | grep -v vendor` | sed ''/PASS/s//$(printf "${GREEN}PASS${RESET}")/'' | sed ''/FAIL/s//$(printf "${RED}FAIL${RESET}")/'' GO_TEST_EXIT_CODE=${PIPESTATUS[0]} if [[ $GO_TEST_EXIT_CODE -ne 0 ]]; then exit $GO_TEST_EXIT_CODE From cf713fe0cd39745cd08cac794d4357b76acf861b Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Mon, 23 Apr 2018 18:10:34 -0700 Subject: [PATCH 2/6] fixed bug in copy --- integration_tests/dockerfiles/Dockerfile_test_multistage | 3 ++- pkg/commands/copy.go | 3 ++- pkg/dockerfile/dockerfile.go | 2 +- pkg/util/fs_util.go | 3 +++ pkg/util/fs_util_test.go | 2 -- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/integration_tests/dockerfiles/Dockerfile_test_multistage b/integration_tests/dockerfiles/Dockerfile_test_multistage index 3833e8806..18fa2d2c1 100644 --- a/integration_tests/dockerfiles/Dockerfile_test_multistage +++ b/integration_tests/dockerfiles/Dockerfile_test_multistage @@ -2,7 +2,8 @@ FROM gcr.io/distroless/base:latest COPY . . FROM scratch as second -COPY --from=0 context/foo /foo +ENV foopath context/foo +COPY --from=0 $foopath context/b* /foo/ FROM gcr.io/distroless/base:latest COPY --from=second /foo /foo2 diff --git a/pkg/commands/copy.go b/pkg/commands/copy.go index 3d617a3db..c2ca45fc2 100644 --- a/pkg/commands/copy.go +++ b/pkg/commands/copy.go @@ -73,7 +73,8 @@ func (c *CopyCommand) ExecuteCommand(config *manifest.Schema2Config) error { } if fi.IsDir() { if !filepath.IsAbs(dest) { - dest = filepath.Join(cwd, dest) + // we need to add '/' to the end to indicate the destination is a directory + dest = filepath.Join(cwd, dest) + "/" } if err := util.CopyDir(fullPath, dest); err != nil { return err diff --git a/pkg/dockerfile/dockerfile.go b/pkg/dockerfile/dockerfile.go index c5e91f256..8cf98910a 100644 --- a/pkg/dockerfile/dockerfile.go +++ b/pkg/dockerfile/dockerfile.go @@ -42,7 +42,7 @@ func Parse(b []byte) ([]instructions.Stage, error) { return stages, err } -// ResolveStages resolves any calls to previous stages to the number value of that stage +// ResolveStages resolves any calls to previous stages with names to indices // Ex. --from=second_stage should be --from=1 for easier processing later on func ResolveStages(stages []instructions.Stage) { nameToIndex := make(map[string]string) diff --git a/pkg/util/fs_util.go b/pkg/util/fs_util.go index e32517c9a..fa236a771 100644 --- a/pkg/util/fs_util.go +++ b/pkg/util/fs_util.go @@ -144,6 +144,9 @@ func RelativeFiles(fp string, root string) ([]string, error) { fullPath := filepath.Join(root, fp) logrus.Debugf("Getting files and contents at root %s", fullPath) err := filepath.Walk(fullPath, func(path string, info os.FileInfo, err error) error { + if PathInWhitelist(path, root) { + return nil + } if err != nil { return err } diff --git a/pkg/util/fs_util_test.go b/pkg/util/fs_util_test.go index 36583564c..be2574d13 100644 --- a/pkg/util/fs_util_test.go +++ b/pkg/util/fs_util_test.go @@ -106,10 +106,8 @@ var tests = []struct { expectedFiles: []string{ "workspace/foo/a", "workspace/foo/b", - "kaniko/file", "workspace", "workspace/foo", - "kaniko", ".", }, }, From 904575d0cbd476321571671b88cec2ee06c3ab60 Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Thu, 26 Apr 2018 15:40:41 -0700 Subject: [PATCH 3/6] support multi stage builds --- cmd/executor/cmd/root.go | 23 ++- .../dockerfiles/Dockerfile_test_multistage | 9 + .../dockerfiles/config_test_multistage.json | 12 ++ integration_tests/integration_test_yaml.go | 18 +- pkg/commands/copy.go | 13 ++ pkg/commands/env.go | 6 + pkg/dockerfile/dockerfile.go | 96 +++++++++- pkg/dockerfile/dockerfile_test.go | 96 ++++++++++ pkg/executor/executor.go | 181 +++++++++++------- pkg/util/command_util.go | 2 +- pkg/util/fs_util.go | 35 +++- pkg/util/fs_util_test.go | 3 - 12 files changed, 402 insertions(+), 92 deletions(-) create mode 100644 integration_tests/dockerfiles/Dockerfile_test_multistage create mode 100644 integration_tests/dockerfiles/config_test_multistage.json create mode 100644 pkg/dockerfile/dockerfile_test.go diff --git a/cmd/executor/cmd/root.go b/cmd/executor/cmd/root.go index 6a4fd546e..adeed0e58 100644 --- a/cmd/executor/cmd/root.go +++ b/cmd/executor/cmd/root.go @@ -32,14 +32,13 @@ import ( ) var ( - dockerfilePath string - destination string - srcContext string - snapshotMode string - bucket string - dockerInsecureSkipTLSVerify bool - logLevel string - force bool + dockerfilePath string + destination string + srcContext string + snapshotMode string + bucket string + logLevel string + force bool ) func init() { @@ -48,7 +47,6 @@ func init() { RootCmd.PersistentFlags().StringVarP(&bucket, "bucket", "b", "", "Name of the GCS bucket from which to access build context as tarball.") RootCmd.PersistentFlags().StringVarP(&destination, "destination", "d", "", "Registry the final image should be pushed to (ex: gcr.io/test/example:latest)") RootCmd.PersistentFlags().StringVarP(&snapshotMode, "snapshotMode", "", "full", "Set this flag to change the file attributes inspected during snapshotting") - RootCmd.PersistentFlags().BoolVarP(&dockerInsecureSkipTLSVerify, "insecure-skip-tls-verify", "", false, "Push to insecure registry ignoring TLS verify") RootCmd.PersistentFlags().StringVarP(&logLevel, "verbosity", "v", constants.DefaultLogLevel, "Log level (debug, info, warn, error, fatal, panic") RootCmd.PersistentFlags().BoolVarP(&force, "force", "", false, "Force building outside of a container") } @@ -76,7 +74,12 @@ var RootCmd = &cobra.Command{ logrus.Error(err) os.Exit(1) } - if err := executor.DoBuild(dockerfilePath, srcContext, destination, snapshotMode, dockerInsecureSkipTLSVerify); err != nil { + ref, image, err := executor.DoBuild(dockerfilePath, srcContext, snapshotMode) + if err != nil { + logrus.Error(err) + os.Exit(1) + } + if err := executor.DoPush(ref, image, destination); err != nil { logrus.Error(err) os.Exit(1) } diff --git a/integration_tests/dockerfiles/Dockerfile_test_multistage b/integration_tests/dockerfiles/Dockerfile_test_multistage new file mode 100644 index 000000000..18fa2d2c1 --- /dev/null +++ b/integration_tests/dockerfiles/Dockerfile_test_multistage @@ -0,0 +1,9 @@ +FROM gcr.io/distroless/base:latest +COPY . . + +FROM scratch as second +ENV foopath context/foo +COPY --from=0 $foopath context/b* /foo/ + +FROM gcr.io/distroless/base:latest +COPY --from=second /foo /foo2 diff --git a/integration_tests/dockerfiles/config_test_multistage.json b/integration_tests/dockerfiles/config_test_multistage.json new file mode 100644 index 000000000..9aa0494cb --- /dev/null +++ b/integration_tests/dockerfiles/config_test_multistage.json @@ -0,0 +1,12 @@ +[ + { + "Image1": "gcr.io/kaniko-test/docker-test-multistage:latest", + "Image2": "gcr.io/kaniko-test/kaniko-test-multistage:latest", + "DiffType": "File", + "Diff": { + "Adds": null, + "Dels": null, + "Mods": null + } + } +] \ No newline at end of file diff --git a/integration_tests/integration_test_yaml.go b/integration_tests/integration_test_yaml.go index 7c1d64607..a9f09e3a4 100644 --- a/integration_tests/integration_test_yaml.go +++ b/integration_tests/integration_test_yaml.go @@ -115,14 +115,6 @@ var fileTests = []struct { kanikoContext: buildcontextPath, repo: "test-add", }, - { - description: "test mv add", - dockerfilePath: "/workspace/integration_tests/dockerfiles/Dockerfile_test_mv_add", - configPath: "/workspace/integration_tests/dockerfiles/config_test_mv_add.json", - dockerContext: buildcontextPath, - kanikoContext: buildcontextPath, - repo: "test-mv-add", - }, { description: "test registry", dockerfilePath: "/workspace/integration_tests/dockerfiles/Dockerfile_test_registry", @@ -147,6 +139,14 @@ var fileTests = []struct { kanikoContext: buildcontextPath, repo: "test-scratch", }, + { + description: "test multistage", + dockerfilePath: "/workspace/integration_tests/dockerfiles/Dockerfile_test_multistage", + configPath: "/workspace/integration_tests/dockerfiles/config_test_multistage.json", + dockerContext: buildcontextPath, + kanikoContext: buildcontextPath, + repo: "test-multistage", + }, } var structureTests = []struct { @@ -288,7 +288,7 @@ func main() { } compareOutputs := step{ Name: ubuntuImage, - Args: []string{"cmp", "-b", test.configPath, containerDiffOutputFile}, + Args: []string{"cmp", test.configPath, containerDiffOutputFile}, } y.Steps = append(y.Steps, dockerBuild, kaniko, pullKanikoImage, containerDiff, catContainerDiffOutput, compareOutputs) diff --git a/pkg/commands/copy.go b/pkg/commands/copy.go index 91e60d82d..2610799b9 100644 --- a/pkg/commands/copy.go +++ b/pkg/commands/copy.go @@ -17,6 +17,7 @@ limitations under the License. package commands import ( + "github.com/GoogleContainerTools/kaniko/pkg/constants" "os" "path/filepath" "strings" @@ -40,6 +41,10 @@ func (c *CopyCommand) ExecuteCommand(config *v1.Config) error { logrus.Infof("cmd: copy %s", srcs) logrus.Infof("dest: %s", dest) + // Resolve from + if c.cmd.From != "" { + c.buildcontext = filepath.Join(constants.BuildContextDir, c.cmd.From) + } // First, resolve any environment replacement resolvedEnvs, err := util.ResolveEnvironmentReplacementList(c.cmd.SourcesAndDest, config.Env, true) if err != nil { @@ -58,11 +63,19 @@ func (c *CopyCommand) ExecuteCommand(config *v1.Config) error { if err != nil { return err } + cwd := config.WorkingDir + if cwd == "" { + cwd = constants.RootDir + } destPath, err := util.DestinationFilepath(src, dest, config.WorkingDir) if err != nil { return err } if fi.IsDir() { + if !filepath.IsAbs(dest) { + // we need to add '/' to the end to indicate the destination is a directory + dest = filepath.Join(cwd, dest) + "/" + } if err := util.CopyDir(fullPath, dest); err != nil { return err } diff --git a/pkg/commands/env.go b/pkg/commands/env.go index 4e576a3f8..1cb8e974a 100644 --- a/pkg/commands/env.go +++ b/pkg/commands/env.go @@ -29,6 +29,12 @@ type EnvCommand struct { cmd *instructions.EnvCommand } +func NewEnvCommand(cmd *instructions.EnvCommand) EnvCommand { + return EnvCommand{ + cmd: cmd, + } +} + func (e *EnvCommand) ExecuteCommand(config *v1.Config) error { logrus.Info("cmd: ENV") newEnvs := e.cmd.Env diff --git a/pkg/dockerfile/dockerfile.go b/pkg/dockerfile/dockerfile.go index f755a547e..c3eb8ed95 100644 --- a/pkg/dockerfile/dockerfile.go +++ b/pkg/dockerfile/dockerfile.go @@ -18,10 +18,20 @@ package dockerfile import ( "bytes" - "strings" - + "github.com/GoogleContainerTools/kaniko/pkg/commands" + "github.com/GoogleContainerTools/kaniko/pkg/constants" + "github.com/GoogleContainerTools/kaniko/pkg/util" "github.com/docker/docker/builder/dockerfile/instructions" "github.com/docker/docker/builder/dockerfile/parser" + "github.com/google/go-containerregistry/authn" + "github.com/google/go-containerregistry/name" + "github.com/google/go-containerregistry/v1" + "github.com/google/go-containerregistry/v1/empty" + "github.com/google/go-containerregistry/v1/remote" + "net/http" + "path/filepath" + "strconv" + "strings" ) // Parse parses the contents of a Dockerfile and returns a list of commands @@ -37,6 +47,25 @@ func Parse(b []byte) ([]instructions.Stage, error) { return stages, err } +// ResolveStages resolves any calls to previous stages with names to indices +// Ex. --from=second_stage should be --from=1 for easier processing later on +func ResolveStages(stages []instructions.Stage) { + nameToIndex := make(map[string]string) + for i, stage := range stages { + index := strconv.Itoa(i) + nameToIndex[stage.Name] = index + nameToIndex[index] = index + for _, cmd := range stage.Commands { + switch c := cmd.(type) { + case *instructions.CopyCommand: + if c.From != "" { + c.From = nameToIndex[c.From] + } + } + } + } +} + // ParseCommands parses an array of commands into an array of instructions.Command; used for onbuild func ParseCommands(cmdArray []string) ([]instructions.Command, error) { var cmds []instructions.Command @@ -54,3 +83,66 @@ func ParseCommands(cmdArray []string) ([]instructions.Command, error) { } return cmds, nil } + +// Dependencies returns a list of files in this stage that will be needed in later stages +func Dependencies(index int, stages []instructions.Stage) ([]string, error) { + var dependencies []string + for stageIndex, stage := range stages { + if stageIndex <= index { + continue + } + var sourceImage v1.Image + if stage.BaseName == constants.NoBaseImage { + sourceImage = empty.Image + } else { + // Initialize source image + ref, err := name.ParseReference(stage.BaseName, name.WeakValidation) + if err != nil { + return nil, err + + } + auth, err := authn.DefaultKeychain.Resolve(ref.Context().Registry) + if err != nil { + return nil, err + } + sourceImage, err = remote.Image(ref, auth, http.DefaultTransport) + if err != nil { + return nil, err + } + } + imageConfig, err := sourceImage.ConfigFile() + if err != nil { + return nil, err + } + for _, cmd := range stage.Commands { + switch c := cmd.(type) { + case *instructions.EnvCommand: + envCommand := commands.NewEnvCommand(c) + if err := envCommand.ExecuteCommand(&imageConfig.Config); err != nil { + return nil, err + } + case *instructions.CopyCommand: + if c.From != strconv.Itoa(index) { + continue + } + // First, resolve any environment replacement + resolvedEnvs, err := util.ResolveEnvironmentReplacementList(c.SourcesAndDest, imageConfig.Config.Env, true) + if err != nil { + return nil, err + } + // Resolve wildcards and get a list of resolved sources + srcs, err := util.ResolveSources(resolvedEnvs, constants.RootDir) + if err != nil { + return nil, err + } + for index, src := range srcs { + if !filepath.IsAbs(src) { + srcs[index] = filepath.Join(constants.RootDir, src) + } + } + dependencies = append(dependencies, srcs...) + } + } + } + return dependencies, nil +} diff --git a/pkg/dockerfile/dockerfile_test.go b/pkg/dockerfile/dockerfile_test.go new file mode 100644 index 000000000..4c4c7e39c --- /dev/null +++ b/pkg/dockerfile/dockerfile_test.go @@ -0,0 +1,96 @@ +/* +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 dockerfile + +import ( + "fmt" + "github.com/GoogleContainerTools/kaniko/testutil" + "github.com/docker/docker/builder/dockerfile/instructions" + "io/ioutil" + "os" + "path/filepath" + "strconv" + "testing" +) + +var dockerfile = ` +FROM scratch +RUN echo hi > /hi + +FROM scratch AS second +COPY --from=0 /hi /hi2 + +FROM scratch +COPY --from=second /hi2 /hi3 +` + +func Test_ResolveStages(t *testing.T) { + stages, err := Parse([]byte(dockerfile)) + if err != nil { + t.Fatal(err) + } + ResolveStages(stages) + for index, stage := range stages { + if index == 0 { + continue + } + copyCmd := stage.Commands[0].(*instructions.CopyCommand) + expectedStage := strconv.Itoa(index - 1) + if copyCmd.From != expectedStage { + t.Fatalf("unexpected copy command: %s resolved to stage %s, expected %s", copyCmd.String(), copyCmd.From, expectedStage) + } + } +} + +func Test_Dependencies(t *testing.T) { + testDir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + helloPath := filepath.Join(testDir, "hello") + if err := os.Mkdir(helloPath, 0755); err != nil { + t.Fatal(err) + } + + dockerfile := fmt.Sprintf(` + FROM scratch + COPY %s %s + + FROM scratch AS second + ENV hienv %s + COPY a b + COPY --from=0 /$hienv %s /hi2/ + `, helloPath, helloPath, helloPath, testDir) + + stages, err := Parse([]byte(dockerfile)) + if err != nil { + t.Fatal(err) + } + + expectedDependencies := [][]string{ + { + helloPath, + testDir, + }, + nil, + } + + for index := range stages { + actualDeps, err := Dependencies(index, stages) + testutil.CheckErrorAndDeepEqual(t, false, err, expectedDependencies[index], actualDeps) + } +} diff --git a/pkg/executor/executor.go b/pkg/executor/executor.go index d3b2039b1..8f3976da5 100644 --- a/pkg/executor/executor.go +++ b/pkg/executor/executor.go @@ -19,10 +19,13 @@ package executor import ( "bytes" "fmt" + "github.com/GoogleContainerTools/kaniko/pkg/snapshot" "io" "io/ioutil" "net/http" "os" + "path/filepath" + "strconv" "github.com/google/go-containerregistry/v1/empty" @@ -37,98 +40,88 @@ import ( "github.com/GoogleContainerTools/kaniko/pkg/commands" "github.com/GoogleContainerTools/kaniko/pkg/constants" "github.com/GoogleContainerTools/kaniko/pkg/dockerfile" - "github.com/GoogleContainerTools/kaniko/pkg/image" - "github.com/GoogleContainerTools/kaniko/pkg/snapshot" "github.com/GoogleContainerTools/kaniko/pkg/util" "github.com/docker/docker/builder/dockerfile/instructions" "github.com/sirupsen/logrus" ) -func DoBuild(dockerfilePath, srcContext, destination, snapshotMode string, dockerInsecureSkipTLSVerify bool) error { +func DoBuild(dockerfilePath, srcContext, snapshotMode string) (name.Reference, v1.Image, error) { // Parse dockerfile and unpack base image to root d, err := ioutil.ReadFile(dockerfilePath) if err != nil { - return err + return nil, nil, err } stages, err := dockerfile.Parse(d) if err != nil { - return err - } - baseImage := stages[0].BaseName - - // Unpack file system to root - var sourceImage v1.Image - var ref name.Reference - logrus.Infof("Unpacking filesystem of %s...", baseImage) - if baseImage == constants.NoBaseImage { - logrus.Info("No base image, nothing to extract") - sourceImage = empty.Image - } else { - // Initialize source image - ref, err = name.ParseReference(baseImage, name.WeakValidation) - if err != nil { - return err - } - auth, err := authn.DefaultKeychain.Resolve(ref.Context().Registry) - if err != nil { - return err - } - sourceImage, err = remote.Image(ref, auth, http.DefaultTransport) - if err != nil { - return err - } - } - if err := util.GetFSFromImage(sourceImage); err != nil { - return err + return nil, nil, err } + dockerfile.ResolveStages(stages) hasher, err := getHasher(snapshotMode) if err != nil { - return err + return nil, nil, err } - l := snapshot.NewLayeredMap(hasher) - snapshotter := snapshot.NewSnapshotter(l, constants.RootDir) - - // Take initial snapshot - if err := snapshotter.Init(); err != nil { - return err - } - - destRef, err := name.ParseReference(destination, name.WeakValidation) - if err != nil { - return err - } - // Set environment variables within the image - if err := image.SetEnvVariables(sourceImage); err != nil { - return err - } - - imageConfig, err := sourceImage.ConfigFile() - if err != nil { - return err - } - // Currently only supports single stage builds - for _, stage := range stages { + for index, stage := range stages { + baseImage := stage.BaseName + finalStage := index == len(stages)-1 + // Unpack file system to root + logrus.Infof("Unpacking filesystem of %s...", baseImage) + var sourceImage v1.Image + var ref name.Reference + if baseImage == constants.NoBaseImage { + logrus.Info("No base image, nothing to extract") + sourceImage = empty.Image + } else { + // Initialize source image + ref, err = name.ParseReference(baseImage, name.WeakValidation) + if err != nil { + return nil, nil, err + } + auth, err := authn.DefaultKeychain.Resolve(ref.Context().Registry) + if err != nil { + return nil, nil, err + } + sourceImage, err = remote.Image(ref, auth, http.DefaultTransport) + if err != nil { + return nil, nil, err + } + } + if err := util.GetFSFromImage(sourceImage); err != nil { + return nil, nil, err + } + l := snapshot.NewLayeredMap(hasher) + snapshotter := snapshot.NewSnapshotter(l, constants.RootDir) + // Take initial snapshot + if err := snapshotter.Init(); err != nil { + return nil, nil, err + } + imageConfig, err := sourceImage.ConfigFile() + if err != nil { + return nil, nil, err + } if err := resolveOnBuild(&stage, &imageConfig.Config); err != nil { - return err + return nil, nil, err } for _, cmd := range stage.Commands { dockerCommand, err := commands.GetCommand(cmd, srcContext) if err != nil { - return err + return nil, nil, err } if dockerCommand == nil { continue } if err := dockerCommand.ExecuteCommand(&imageConfig.Config); err != nil { - return err + return nil, nil, err + } + if !finalStage { + continue } // Now, we get the files to snapshot from this command and take the snapshot snapshotFiles := dockerCommand.FilesToSnapshot() contents, err := snapshotter.TakeSnapshot(snapshotFiles) if err != nil { - return err + return nil, nil, err } util.MoveVolumeWhitelistToWhitelist() if contents == nil { @@ -141,7 +134,7 @@ func DoBuild(dockerfilePath, srcContext, destination, snapshotMode string, docke } layer, err := tarball.LayerFromOpener(opener) if err != nil { - return err + return nil, nil, err } sourceImage, err = mutate.Append(sourceImage, mutate.Addendum{ @@ -152,15 +145,36 @@ func DoBuild(dockerfilePath, srcContext, destination, snapshotMode string, docke }, ) if err != nil { - return err + return nil, nil, err } } + if finalStage { + return ref, sourceImage, nil + } + if err := saveStageDependencies(index, stages); err != nil { + return nil, nil, err + } + // Delete the filesystem + if err := util.DeleteFilesystem(); err != nil { + return nil, nil, err + } } + return nil, nil, nil +} + +func DoPush(ref name.Reference, image v1.Image, destination string) error { // Push the image if err := setDefaultEnv(); err != nil { return err } - + imageConfig, err := image.ConfigFile() + if err != nil { + return err + } + destRef, err := name.ParseReference(destination, name.WeakValidation) + if err != nil { + return err + } wo := remote.WriteOptions{} if ref != nil { wo.MountPaths = []name.Repository{ref.Context()} @@ -169,12 +183,47 @@ func DoBuild(dockerfilePath, srcContext, destination, snapshotMode string, docke if err != nil { return err } - sourceImage, err = mutate.Config(sourceImage, imageConfig.Config) + image, err = mutate.Config(image, imageConfig.Config) if err != nil { return err } - - return remote.Write(destRef, sourceImage, pushAuth, http.DefaultTransport, wo) + return remote.Write(destRef, image, pushAuth, http.DefaultTransport, wo) +} +func saveStageDependencies(index int, stages []instructions.Stage) error { + // First, get the files in this stage later stages will need + dependencies, err := dockerfile.Dependencies(index, stages) + logrus.Infof("saving dependencies %s", dependencies) + if err != nil { + return err + } + // Then, create the directory they will exist in + i := strconv.Itoa(index) + dependencyDir := filepath.Join(constants.BuildContextDir, i) + if err := os.MkdirAll(dependencyDir, 0755); err != nil { + return err + } + // Now, copy over dependencies to this dir + for _, d := range dependencies { + fi, err := os.Lstat(d) + if err != nil { + return err + } + dest := filepath.Join(dependencyDir, d) + if fi.IsDir() { + if err := util.CopyDir(d, dest); err != nil { + return err + } + } else if fi.Mode()&os.ModeSymlink != 0 { + if err := util.CopySymlink(d, dest); err != nil { + return err + } + } else { + if err := util.CopyFile(d, dest); err != nil { + return err + } + } + } + return nil } func getHasher(snapshotMode string) (func(string) (string, error), error) { diff --git a/pkg/util/command_util.go b/pkg/util/command_util.go index 20efd19a3..c8788a494 100644 --- a/pkg/util/command_util.go +++ b/pkg/util/command_util.go @@ -178,7 +178,7 @@ func IsSrcsValid(srcsAndDest instructions.SourcesAndDest, resolvedSources []stri } if len(resolvedSources) == 1 { - fi, err := os.Stat(filepath.Join(root, resolvedSources[0])) + fi, err := os.Lstat(filepath.Join(root, resolvedSources[0])) if err != nil { return err } diff --git a/pkg/util/fs_util.go b/pkg/util/fs_util.go index d639740df..a103e8bfb 100644 --- a/pkg/util/fs_util.go +++ b/pkg/util/fs_util.go @@ -104,6 +104,33 @@ func GetFSFromImage(img v1.Image) error { return nil } +// DeleteFilesystem deletes the extracted image file system +func DeleteFilesystem() error { + logrus.Info("Deleting filesystem...") + err := filepath.Walk(constants.RootDir, func(path string, info os.FileInfo, err error) error { + if PathInWhitelist(path, constants.RootDir) || ChildDirInWhitelist(path, constants.RootDir) { + logrus.Debugf("Not deleting %s, as it's whitelisted", path) + return nil + } + if path == constants.RootDir { + return nil + } + return os.RemoveAll(path) + }) + return err +} + +// ChildDirInWhitelist returns true if there is a child file or directory of the path in the whitelist +func ChildDirInWhitelist(path, directory string) bool { + for _, d := range whitelist { + dirPath := filepath.Join(directory, d) + if HasFilepathPrefix(dirPath, path) { + return true + } + } + return false +} + func unTar(r io.Reader, dest string) error { tr := tar.NewReader(r) for { @@ -269,6 +296,9 @@ func RelativeFiles(fp string, root string) ([]string, error) { if err != nil { return err } + if PathInWhitelist(path, root) { + return nil + } relPath, err := filepath.Rel(root, path) if err != nil { return err @@ -284,6 +314,9 @@ func Files(root string) ([]string, error) { var files []string logrus.Debugf("Getting files and contents at root %s", root) err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if PathInWhitelist(path, root) { + return nil + } files = append(files, path) return err }) @@ -368,7 +401,7 @@ func CopyDir(src, dest string) error { } for _, file := range files { fullPath := filepath.Join(src, file) - fi, err := os.Stat(fullPath) + fi, err := os.Lstat(fullPath) if err != nil { return err } diff --git a/pkg/util/fs_util_test.go b/pkg/util/fs_util_test.go index 0334d8c56..62e9b6091 100644 --- a/pkg/util/fs_util_test.go +++ b/pkg/util/fs_util_test.go @@ -103,16 +103,13 @@ var tests = []struct { files: map[string]string{ "/workspace/foo/a": "baz1", "/workspace/foo/b": "baz2", - "/kaniko/file": "file", }, directory: "", expectedFiles: []string{ "workspace/foo/a", "workspace/foo/b", - "kaniko/file", "workspace", "workspace/foo", - "kaniko", ".", }, }, From a1acbe8aa82fae7541ce29759cb94f989b9a57f4 Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Mon, 30 Apr 2018 22:37:45 -0400 Subject: [PATCH 4/6] Fixed ResolveStages --- pkg/dockerfile/dockerfile.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/dockerfile/dockerfile.go b/pkg/dockerfile/dockerfile.go index c3eb8ed95..dd2f0311c 100644 --- a/pkg/dockerfile/dockerfile.go +++ b/pkg/dockerfile/dockerfile.go @@ -53,13 +53,16 @@ func ResolveStages(stages []instructions.Stage) { nameToIndex := make(map[string]string) for i, stage := range stages { index := strconv.Itoa(i) - nameToIndex[stage.Name] = index - nameToIndex[index] = index + if stage.Name != index { + nameToIndex[stage.Name] = index + } for _, cmd := range stage.Commands { switch c := cmd.(type) { case *instructions.CopyCommand: if c.From != "" { - c.From = nameToIndex[c.From] + if val, ok := nameToIndex[c.From]; ok { + c.From = val + } } } } From 36933a23d7e8b00e39966582f04eebd411435311 Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Tue, 1 May 2018 13:33:28 -0400 Subject: [PATCH 5/6] Update to new structure tests --- integration_tests/integration_test_yaml.go | 29 ++++++---------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/integration_tests/integration_test_yaml.go b/integration_tests/integration_test_yaml.go index 98b871717..d79ccc876 100644 --- a/integration_tests/integration_test_yaml.go +++ b/integration_tests/integration_test_yaml.go @@ -26,6 +26,7 @@ const ( executorImage = "executor-image" dockerImage = "gcr.io/cloud-builders/docker" ubuntuImage = "ubuntu" + structureTestImage = "gcr.io/gcp-runtimes/container-structure-test" testRepo = "gcr.io/kaniko-test/" dockerPrefix = "docker-" kanikoPrefix = "kaniko-" @@ -205,15 +206,6 @@ func main() { Name: ubuntuImage, Args: []string{"chmod", "+x", "container-diff-linux-amd64"}, } - structureTestsStep := step{ - Name: "gcr.io/cloud-builders/gsutil", - Args: []string{"cp", "gs://container-structure-test/latest/container-structure-test", "."}, - } - structureTestPermissions := step{ - Name: ubuntuImage, - Args: []string{"chmod", "+x", "container-structure-test"}, - } - GCSBucketTarBuildContext := step{ Name: ubuntuImage, Args: []string{"tar", "-C", "/workspace/integration_tests/", "-zcvf", "/workspace/context.tar.gz", "."}, @@ -239,7 +231,7 @@ func main() { Args: []string{"push", onbuildBaseImage}, } y := testyaml{ - Steps: []step{containerDiffStep, containerDiffPermissions, structureTestsStep, structureTestPermissions, GCSBucketTarBuildContext, uploadTarBuildContext, buildExecutorImage, + Steps: []step{containerDiffStep, containerDiffPermissions, GCSBucketTarBuildContext, uploadTarBuildContext, buildExecutorImage, buildOnbuildImage, pushOnbuildBase}, Timeout: "1200s", } @@ -315,20 +307,15 @@ func main() { Args: []string{"pull", kanikoImage}, } // Run structure tests on the kaniko and docker image - args := "container-structure-test -image " + kanikoImage + " " + test.structureTestYamlPath - structureTest := step{ - Name: ubuntuImage, - Args: []string{"sh", "-c", args}, - Env: []string{"PATH=/workspace:/bin"}, + kanikoStructureTest := step{ + Name: structureTestImage, + Args: []string{"test", "--image", kanikoImage, "--config", test.structureTestYamlPath}, } - args = "container-structure-test -image " + dockerImageTag + " " + test.structureTestYamlPath dockerStructureTest := step{ - Name: ubuntuImage, - Args: []string{"sh", "-c", args}, - Env: []string{"PATH=/workspace:/bin"}, + Name: structureTestImage, + Args: []string{"test", "--image", dockerImageTag, "--config", test.structureTestYamlPath}, } - - y.Steps = append(y.Steps, dockerBuild, kaniko, pullKanikoImage, structureTest, dockerStructureTest) + y.Steps = append(y.Steps, dockerBuild, kaniko, pullKanikoImage, kanikoStructureTest, dockerStructureTest) } d, _ := yaml.Marshal(&y) From 459ddffb3cd013a34a0333df1d8e78632a1539ca Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Fri, 11 May 2018 16:16:11 -0700 Subject: [PATCH 6/6] Updated tests --- .../dockerfiles/config_test_scratch.json | 12 ++++++ pkg/commands/env_test.go | 31 ------------- pkg/dockerfile/dockerfile.go | 5 ++- pkg/dockerfile/dockerfile_test.go | 43 ++++++++++++++++++- pkg/executor/executor.go | 3 ++ 5 files changed, 61 insertions(+), 33 deletions(-) create mode 100644 integration_tests/dockerfiles/config_test_scratch.json diff --git a/integration_tests/dockerfiles/config_test_scratch.json b/integration_tests/dockerfiles/config_test_scratch.json new file mode 100644 index 000000000..b9b8930fe --- /dev/null +++ b/integration_tests/dockerfiles/config_test_scratch.json @@ -0,0 +1,12 @@ +[ + { + "Image1": "gcr.io/kaniko-test/docker-test-scratch:latest", + "Image2": "gcr.io/kaniko-test/kaniko-test-scratch:latest", + "DiffType": "File", + "Diff": { + "Adds": null, + "Dels": null, + "Mods": null + } + } +] \ No newline at end of file diff --git a/pkg/commands/env_test.go b/pkg/commands/env_test.go index d4eec7502..16d7b33f7 100644 --- a/pkg/commands/env_test.go +++ b/pkg/commands/env_test.go @@ -23,37 +23,6 @@ import ( "testing" ) -func TestUpdateEnvConfig(t *testing.T) { - cfg := &v1.Config{ - Env: []string{ - "PATH=/path/to/dir", - "hey=hey", - }, - } - - newEnvs := []instructions.KeyValuePair{ - { - Key: "foo", - Value: "foo2", - }, - { - Key: "PATH", - Value: "/new/path/", - }, - { - Key: "foo", - Value: "newfoo", - }, - } - - expectedEnvArray := []string{ - "PATH=/new/path/", - "hey=hey", - "foo=newfoo", - } - updateConfigEnv(newEnvs, cfg) - testutil.CheckErrorAndDeepEqual(t, false, nil, expectedEnvArray, cfg.Env) -} func Test_EnvExecute(t *testing.T) { cfg := &v1.Config{ Env: []string{ diff --git a/pkg/dockerfile/dockerfile.go b/pkg/dockerfile/dockerfile.go index 4e295deba..973120d13 100644 --- a/pkg/dockerfile/dockerfile.go +++ b/pkg/dockerfile/dockerfile.go @@ -123,12 +123,15 @@ func Dependencies(index int, stages []instructions.Stage, buildArgs *BuildArgs) if err := util.UpdateConfigEnv(c.Env, &imageConfig.Config, replacementEnvs); err != nil { return nil, err } + case *instructions.ArgCommand: + buildArgs.AddArg(c.Key, c.Value) case *instructions.CopyCommand: if c.From != strconv.Itoa(index) { continue } // First, resolve any environment replacement - resolvedEnvs, err := util.ResolveEnvironmentReplacementList(c.SourcesAndDest, imageConfig.Config.Env, true) + replacementEnvs := buildArgs.ReplacementEnvs(imageConfig.Config.Env) + resolvedEnvs, err := util.ResolveEnvironmentReplacementList(c.SourcesAndDest, replacementEnvs, true) if err != nil { return nil, err } diff --git a/pkg/dockerfile/dockerfile_test.go b/pkg/dockerfile/dockerfile_test.go index 397ff3801..526bd3efd 100644 --- a/pkg/dockerfile/dockerfile_test.go +++ b/pkg/dockerfile/dockerfile_test.go @@ -89,7 +89,48 @@ func Test_Dependencies(t *testing.T) { } for index := range stages { - actualDeps, err := Dependencies(index, stages) + buildArgs := NewBuildArgs([]string{}) + actualDeps, err := Dependencies(index, stages, buildArgs) + testutil.CheckErrorAndDeepEqual(t, false, err, expectedDependencies[index], actualDeps) + } +} + +func Test_DependenciesWithArg(t *testing.T) { + testDir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + helloPath := filepath.Join(testDir, "hello") + if err := os.Mkdir(helloPath, 0755); err != nil { + t.Fatal(err) + } + + dockerfile := fmt.Sprintf(` + FROM scratch + COPY %s %s + + FROM scratch AS second + ARG hienv + COPY a b + COPY --from=0 /$hienv %s /hi2/ + `, helloPath, helloPath, testDir) + + stages, err := Parse([]byte(dockerfile)) + if err != nil { + t.Fatal(err) + } + + expectedDependencies := [][]string{ + { + helloPath, + testDir, + }, + nil, + } + buildArgs := NewBuildArgs([]string{fmt.Sprintf("hienv=%s", helloPath)}) + + for index := range stages { + actualDeps, err := Dependencies(index, stages, buildArgs) testutil.CheckErrorAndDeepEqual(t, false, err, expectedDependencies[index], actualDeps) } } diff --git a/pkg/executor/executor.go b/pkg/executor/executor.go index cd3ce879b..3b5425340 100644 --- a/pkg/executor/executor.go +++ b/pkg/executor/executor.go @@ -1,9 +1,12 @@ /* 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.