From 9a0e29c4416b4accc92524ebf16329f944718880 Mon Sep 17 00:00:00 2001 From: dlorenc Date: Tue, 9 Oct 2018 12:15:17 -0500 Subject: [PATCH 01/42] Refactor the build loop. (#385) This change refactors the build loop a bit to make cache optimization easier in the future. Some notable changes: The special casing around volume snapshots is removed. Every volume is added to the snapshotFiles list for every command that will snapshot anyway. Snapshot saving was extracted to a sub-function The decision on whether or not to snapshot was extracted --- pkg/commands/volume.go | 6 +- pkg/commands/volume_test.go | 1 - pkg/constants/constants.go | 3 - pkg/executor/build.go | 157 ++++++++++++++++++++---------------- 4 files changed, 88 insertions(+), 79 deletions(-) diff --git a/pkg/commands/volume.go b/pkg/commands/volume.go index af2d95675..4dd337850 100644 --- a/pkg/commands/volume.go +++ b/pkg/commands/volume.go @@ -30,8 +30,7 @@ import ( type VolumeCommand struct { BaseCommand - cmd *instructions.VolumeCommand - snapshotFiles []string + cmd *instructions.VolumeCommand } func (v *VolumeCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error { @@ -57,7 +56,6 @@ func (v *VolumeCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile. // Only create and snapshot the dir if it didn't exist already if _, err := os.Stat(volume); os.IsNotExist(err) { logrus.Infof("Creating directory %s", volume) - v.snapshotFiles = append(v.snapshotFiles, volume) if err := os.MkdirAll(volume, 0755); err != nil { return fmt.Errorf("Could not create directory for volume %s: %s", volume, err) } @@ -69,7 +67,7 @@ func (v *VolumeCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile. } func (v *VolumeCommand) FilesToSnapshot() []string { - return v.snapshotFiles + return []string{} } func (v *VolumeCommand) String() string { diff --git a/pkg/commands/volume_test.go b/pkg/commands/volume_test.go index c85ed7f6d..9616006b8 100644 --- a/pkg/commands/volume_test.go +++ b/pkg/commands/volume_test.go @@ -43,7 +43,6 @@ func TestUpdateVolume(t *testing.T) { cmd: &instructions.VolumeCommand{ Volumes: volumes, }, - snapshotFiles: []string{}, } expectedVolumes := map[string]struct{}{ diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 1c8d774b8..d8fcc722e 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -62,9 +62,6 @@ const ( // Docker command names Cmd = "cmd" Entrypoint = "entrypoint" - - // VolumeCmdName is the name of the volume command - VolumeCmdName = "volume" ) // KanikoBuildFiles is the list of files required to build kaniko diff --git a/pkg/executor/build.go b/pkg/executor/build.go index e517dafa0..1f90037d8 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -49,6 +49,7 @@ type stageBuilder struct { cf *v1.ConfigFile snapshotter *snapshot.Snapshotter baseImageDigest string + opts *config.KanikoOptions } // newStageBuilder returns a new type stageBuilder which contains all the information required to build the stage @@ -81,6 +82,7 @@ func newStageBuilder(opts *config.KanikoOptions, stage config.KanikoStage) (*sta cf: imageConfig, snapshotter: snapshotter, baseImageDigest: digest.String(), + opts: opts, }, nil } @@ -111,7 +113,7 @@ func (s *stageBuilder) extractCachedLayer(layer v1.Image, createdBy string) erro return err } -func (s *stageBuilder) build(opts *config.KanikoOptions) error { +func (s *stageBuilder) build() error { // Unpack file system to root if _, err := util.GetFSFromImage(constants.RootDir, s.image); err != nil { return err @@ -120,23 +122,26 @@ func (s *stageBuilder) build(opts *config.KanikoOptions) error { if err := s.snapshotter.Init(); err != nil { return err } - var volumes []string // Set the initial cache key to be the base image digest, the build args and the SrcContext. compositeKey := NewCompositeCache(s.baseImageDigest) - contextHash, err := HashDir(opts.SrcContext) + contextHash, err := HashDir(s.opts.SrcContext) if err != nil { return err } - compositeKey.AddKey(opts.BuildArgs...) + compositeKey.AddKey(s.opts.BuildArgs...) - args := dockerfile.NewBuildArgs(opts.BuildArgs) - for index, cmd := range s.stage.Commands { - finalCmd := index == len(s.stage.Commands)-1 - command, err := commands.GetCommand(cmd, opts.SrcContext) + cmds := []commands.DockerCommand{} + for _, cmd := range s.stage.Commands { + command, err := commands.GetCommand(cmd, s.opts.SrcContext) if err != nil { return err } + cmds = append(cmds, command) + } + + args := dockerfile.NewBuildArgs(s.opts.BuildArgs) + for index, command := range cmds { if command == nil { continue } @@ -153,8 +158,8 @@ func (s *stageBuilder) build(opts *config.KanikoOptions) error { return err } - if command.CacheCommand() && opts.Cache { - image, err := cache.RetrieveLayer(opts, ck) + if command.CacheCommand() && s.opts.Cache { + image, err := cache.RetrieveLayer(s.opts, ck) if err == nil { if err := s.extractCachedLayer(image, command.String()); err != nil { return errors.Wrap(err, "extracting cached layer") @@ -163,84 +168,94 @@ func (s *stageBuilder) build(opts *config.KanikoOptions) error { } logrus.Info("No cached layer found, executing command...") } + if err := command.ExecuteCommand(&s.cf.Config, args); err != nil { return err } files := command.FilesToSnapshot() - if cmd.Name() == constants.VolumeCmdName { - volumes = append(volumes, files...) - continue - } var contents []byte - // If this is an intermediate stage, we only snapshot for the last command and we - // want to snapshot the entire filesystem since we aren't tracking what was changed - // by previous commands. - if !s.stage.Final { - if finalCmd { - contents, err = s.snapshotter.TakeSnapshotFS() - } - } else { - // If we are in single snapshot mode, we only take a snapshot once, after all - // commands have completed. - if opts.SingleSnapshot { - if finalCmd { - contents, err = s.snapshotter.TakeSnapshotFS() - } - } else { - // Otherwise, in the final stage we take a snapshot at each command. If we know - // the files that were changed, we'll snapshot those explicitly, otherwise we'll - // check if anything in the filesystem changed. - if files != nil { - if len(files) > 0 { - files = append(files, volumes...) - volumes = []string{} - } - contents, err = s.snapshotter.TakeSnapshot(files) - } else { - contents, err = s.snapshotter.TakeSnapshotFS() - volumes = []string{} - } - } - } - if err != nil { - return fmt.Errorf("Error taking snapshot of files for command %s: %s", command, err) - } - - if contents == nil { - logrus.Info("No files were changed, appending empty layer to config. No layer added to image.") + if !s.shouldTakeSnapshot(index, files) { continue } - // Append the layer to the image - opener := func() (io.ReadCloser, error) { - return ioutil.NopCloser(bytes.NewReader(contents)), nil + + if files == nil || s.opts.SingleSnapshot { + contents, err = s.snapshotter.TakeSnapshotFS() + } else { + // Volumes are very weird. They get created in their command, but snapshotted in the next one. + // Add them to the list of files to snapshot. + for v := range s.cf.Config.Volumes { + files = append(files, v) + } + contents, err = s.snapshotter.TakeSnapshot(files) } - layer, err := tarball.LayerFromOpener(opener) if err != nil { return err } - // Push layer to cache now along with new config file - if command.CacheCommand() && opts.Cache { - if err := pushLayerToCache(opts, ck, layer, command.String()); err != nil { - return err - } - } - s.image, err = mutate.Append(s.image, - mutate.Addendum{ - Layer: layer, - History: v1.History{ - Author: constants.Author, - CreatedBy: command.String(), - }, - }, - ) - if err != nil { + if err := s.saveSnapshot(command, ck, contents); err != nil { return err } } return nil } +func (s *stageBuilder) shouldTakeSnapshot(index int, files []string) bool { + isLastCommand := index == len(s.stage.Commands)-1 + + // We only snapshot the very end of intermediate stages. + if !s.stage.Final { + return isLastCommand + } + + // We only snapshot the very end with single snapshot mode on. + if s.opts.SingleSnapshot { + return isLastCommand + } + + // nil means snapshot everything. + if files == nil { + return true + } + + // Don't snapshot an empty list. + if len(files) == 0 { + return false + } + return true +} + +func (s *stageBuilder) saveSnapshot(command commands.DockerCommand, ck string, contents []byte) error { + if contents == nil { + logrus.Info("No files were changed, appending empty layer to config. No layer added to image.") + return nil + } + // Append the layer to the image + opener := func() (io.ReadCloser, error) { + return ioutil.NopCloser(bytes.NewReader(contents)), nil + } + layer, err := tarball.LayerFromOpener(opener) + if err != nil { + return err + } + // Push layer to cache now along with new config file + if command.CacheCommand() && s.opts.Cache { + if err := pushLayerToCache(s.opts, ck, layer, command.String()); err != nil { + return err + } + } + s.image, err = mutate.Append(s.image, + mutate.Addendum{ + Layer: layer, + History: v1.History{ + Author: constants.Author, + CreatedBy: command.String(), + }, + }, + ) + return err + +} + // DoBuild executes building the Dockerfile func DoBuild(opts *config.KanikoOptions) (v1.Image, error) { // Parse dockerfile and unpack base image to root @@ -253,7 +268,7 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) { if err != nil { return nil, errors.Wrap(err, fmt.Sprintf("getting stage builder for stage %d", index)) } - if err := sb.build(opts); err != nil { + if err := sb.build(); err != nil { return nil, errors.Wrap(err, "error building stage") } reviewConfig(stage, &sb.cf.Config) From a72970712f3216d14b916540d994fd1218707aa0 Mon Sep 17 00:00:00 2001 From: Ian Beringer Date: Wed, 10 Oct 2018 20:44:19 +0200 Subject: [PATCH 02/42] Update go-containerregistry dependency #245 --- Gopkg.lock | 4 +- .../pkg/v1/partial/compressed.go | 5 +++ .../pkg/v1/partial/uncompressed.go | 38 +++++++++++-------- .../pkg/v1/remote/transport/bearer.go | 35 ++++++++++++----- 4 files changed, 54 insertions(+), 28 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 47e3f9662..410e2aae1 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -431,7 +431,7 @@ [[projects]] branch = "master" - digest = "1:edf64d541c12aaf4f279642ea9939f035dcc9fc2edf649aba295e9cbca2c28d4" + digest = "1:764d0a9bb2c987d9333c5b0a256bc94791db50c0b5be2fa10f2247b1dcbd7a04" name = "github.com/google/go-containerregistry" packages = [ "pkg/authn", @@ -450,7 +450,7 @@ "pkg/v1/v1util", ] pruneopts = "NUT" - revision = "03167950e20ac82689f50828811e69cdd9e02af2" + revision = "24bbadfcffb5e05b1578cb2bd5438992ada3b546" [[projects]] digest = "1:f4f203acd8b11b8747bdcd91696a01dbc95ccb9e2ca2db6abf81c3a4f5e950ce" diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/partial/compressed.go b/vendor/github.com/google/go-containerregistry/pkg/v1/partial/compressed.go index e6e4f4d42..6c3998e52 100644 --- a/vendor/github.com/google/go-containerregistry/pkg/v1/partial/compressed.go +++ b/vendor/github.com/google/go-containerregistry/pkg/v1/partial/compressed.go @@ -125,6 +125,11 @@ func (i *compressedImageExtender) Layers() ([]v1.Layer, error) { // LayerByDigest implements v1.Image func (i *compressedImageExtender) LayerByDigest(h v1.Hash) (v1.Layer, error) { + if cfgName, err := i.ConfigName(); err != nil { + return nil, err + } else if cfgName == h { + return ConfigLayer(i) + } cl, err := i.CompressedImageCore.LayerByDigest(h) if err != nil { return nil, err diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/partial/uncompressed.go b/vendor/github.com/google/go-containerregistry/pkg/v1/partial/uncompressed.go index 7afa187b8..f7055dcad 100644 --- a/vendor/github.com/google/go-containerregistry/pkg/v1/partial/uncompressed.go +++ b/vendor/github.com/google/go-containerregistry/pkg/v1/partial/uncompressed.go @@ -37,8 +37,12 @@ type UncompressedLayer interface { // uncompressedLayerExtender implements v1.Image using the uncompressed base properties. type uncompressedLayerExtender struct { UncompressedLayer - // TODO(mattmoor): Memoize size/hash so that the methods aren't twice as + // Memoize size/hash so that the methods aren't twice as // expensive as doing this manually. + hash v1.Hash + size int64 + hashSizeError error + once sync.Once } // Compressed implements v1.Layer @@ -52,29 +56,31 @@ func (ule *uncompressedLayerExtender) Compressed() (io.ReadCloser, error) { // Digest implements v1.Layer func (ule *uncompressedLayerExtender) Digest() (v1.Hash, error) { - r, err := ule.Compressed() - if err != nil { - return v1.Hash{}, err - } - defer r.Close() - h, _, err := v1.SHA256(r) - return h, err + ule.calcSizeHash() + return ule.hash, ule.hashSizeError } // Size implements v1.Layer func (ule *uncompressedLayerExtender) Size() (int64, error) { - r, err := ule.Compressed() - if err != nil { - return -1, err - } - defer r.Close() - _, i, err := v1.SHA256(r) - return i, err + ule.calcSizeHash() + return ule.size, ule.hashSizeError +} + +func (ule *uncompressedLayerExtender) calcSizeHash() { + ule.once.Do(func() { + var r io.ReadCloser + r, ule.hashSizeError = ule.Compressed() + if ule.hashSizeError != nil { + return + } + defer r.Close() + ule.hash, ule.size, ule.hashSizeError = v1.SHA256(r) + }) } // UncompressedToLayer fills in the missing methods from an UncompressedLayer so that it implements v1.Layer func UncompressedToLayer(ul UncompressedLayer) (v1.Layer, error) { - return &uncompressedLayerExtender{ul}, nil + return &uncompressedLayerExtender{UncompressedLayer: ul}, nil } // UncompressedImageCore represents the bare minimum interface a natively diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/bearer.go b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/bearer.go index 7dd49ae6f..a45121390 100644 --- a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/bearer.go +++ b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/bearer.go @@ -46,22 +46,37 @@ var _ http.RoundTripper = (*bearerTransport)(nil) // RoundTrip implements http.RoundTripper func (bt *bearerTransport) RoundTrip(in *http.Request) (*http.Response, error) { - hdr, err := bt.bearer.Authorization() + sendRequest := func() (*http.Response, error) { + hdr, err := bt.bearer.Authorization() + if err != nil { + return nil, err + } + + // http.Client handles redirects at a layer above the http.RoundTripper + // abstraction, so to avoid forwarding Authorization headers to places + // we are redirected, only set it when the authorization header matches + // the registry with which we are interacting. + if in.Host == bt.registry.RegistryStr() { + in.Header.Set("Authorization", hdr) + } + in.Header.Set("User-Agent", transportName) + return bt.inner.RoundTrip(in) + } + + res, err := sendRequest() if err != nil { return nil, err } - // http.Client handles redirects at a layer above the http.RoundTripper - // abstraction, so to avoid forwarding Authorization headers to places - // we are redirected, only set it when the authorization header matches - // the registry with which we are interacting. - if in.Host == bt.registry.RegistryStr() { - in.Header.Set("Authorization", hdr) + // Perform a token refresh() and retry the request in case the token has expired + if res.StatusCode == http.StatusUnauthorized { + if err = bt.refresh(); err != nil { + return nil, err + } + return sendRequest() } - in.Header.Set("User-Agent", transportName) - // TODO(mattmoor): On 401s perform a single refresh() and retry. - return bt.inner.RoundTrip(in) + return res, err } func (bt *bearerTransport) refresh() error { From 5695ebc3d5bdfb5bd8f785ed7f5e71e0971f4ded Mon Sep 17 00:00:00 2001 From: peter-evans Date: Thu, 11 Oct 2018 09:28:55 +0900 Subject: [PATCH 03/42] Remove all at path to make way for new symlink --- pkg/util/fs_util.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/util/fs_util.go b/pkg/util/fs_util.go index 07e3fbc59..b43ee4df5 100644 --- a/pkg/util/fs_util.go +++ b/pkg/util/fs_util.go @@ -260,7 +260,7 @@ func extractFile(dest string, hdr *tar.Header, tr io.Reader) error { // Check if something already exists at path // If so, delete it if FilepathExists(path) { - if err := os.Remove(path); err != nil { + if err := os.RemoveAll(path); err != nil { return errors.Wrapf(err, "error removing %s to make way for new symlink", hdr.Name) } } From 796b2515a7819b908683a62b29e1586130ea575a Mon Sep 17 00:00:00 2001 From: peter-evans Date: Thu, 11 Oct 2018 11:18:26 +0900 Subject: [PATCH 04/42] Add test for symlink extraction to non-empty dir --- integration/dockerfiles/Dockerfile_test_extraction | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 integration/dockerfiles/Dockerfile_test_extraction diff --git a/integration/dockerfiles/Dockerfile_test_extraction b/integration/dockerfiles/Dockerfile_test_extraction new file mode 100644 index 000000000..8e64d8ade --- /dev/null +++ b/integration/dockerfiles/Dockerfile_test_extraction @@ -0,0 +1,2 @@ +# Tests extraction of a symlink to a path that is a non-empty directory +FROM registry.access.redhat.com/jboss-eap-7/eap71-openshift:1.3-10 From 38e8dc2cdd20c09c4f641f1fdd162e2f80854f03 Mon Sep 17 00:00:00 2001 From: peter-evans Date: Thu, 11 Oct 2018 15:33:25 +0900 Subject: [PATCH 05/42] Remove all at path to make way for new reg files and links --- pkg/util/fs_util.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/util/fs_util.go b/pkg/util/fs_util.go index b43ee4df5..3af1c5e85 100644 --- a/pkg/util/fs_util.go +++ b/pkg/util/fs_util.go @@ -193,7 +193,7 @@ func extractFile(dest string, hdr *tar.Header, tr io.Reader) error { // Check if something already exists at path (symlinks etc.) // If so, delete it if FilepathExists(path) { - if err := os.Remove(path); err != nil { + if err := os.RemoveAll(path); err != nil { return errors.Wrapf(err, "error removing %s to make way for new file.", path) } } @@ -242,7 +242,7 @@ func extractFile(dest string, hdr *tar.Header, tr io.Reader) error { // Check if something already exists at path // If so, delete it if FilepathExists(path) { - if err := os.Remove(path); err != nil { + if err := os.RemoveAll(path); err != nil { return errors.Wrapf(err, "error removing %s to make way for new link", hdr.Name) } } From 073abff176d88e6aceffb49fa801a8cfc2ef7657 Mon Sep 17 00:00:00 2001 From: Peter van Zetten Date: Thu, 11 Oct 2018 11:00:50 +0100 Subject: [PATCH 06/42] Improve IsDestDir functionality with filesystem info Add a check for FileInfo to determine whether a given string is a directory path. If any error occurs, fall back to the naive string check. Fixes #365 --- pkg/util/command_util.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/util/command_util.go b/pkg/util/command_util.go index b6574faa8..828b9e827 100644 --- a/pkg/util/command_util.go +++ b/pkg/util/command_util.go @@ -133,7 +133,14 @@ func matchSources(srcs, files []string) ([]string, error) { } func IsDestDir(path string) bool { - return strings.HasSuffix(path, "/") || path == "." + // try to stat the path + fileInfo, err := os.Stat(path) + if err != nil { + // fall back to string-based determination + return strings.HasSuffix(path, "/") || path == "." + } + // if it's a real path, check the fs response + return fileInfo.IsDir() } // DestinationFilepath returns the destination filepath from the build context to the image filesystem From effac9dfc34f3bbe7217d37221688b71a72ac33f Mon Sep 17 00:00:00 2001 From: Sharif Elgamal Date: Thu, 11 Oct 2018 13:38:05 -0700 Subject: [PATCH 07/42] Persistent volume caching for base images (#383) * comments * initial commit for persisent volume caching * cache warmer works * general cleanup * adding some debugging * adding missing files * Fixing up cache retrieval and cleanup * fix tests * removing auth since we only cache public images * simplifying the caching logic * fixing logic * adding volume cache to integration tests. remove auth from cache warmer image. * add building warmer to integration-test * move sample yaml files to examples dir * small test fix --- Makefile | 5 +++ cmd/executor/cmd/root.go | 1 + cmd/warmer/cmd/root.go | 74 +++++++++++++++++++++++++++++++ cmd/warmer/main.go | 29 ++++++++++++ deploy/Dockerfile_warmer | 32 +++++++++++++ examples/kaniko-cache-claim.yaml | 11 +++++ examples/kaniko-cache-volume.yaml | 14 ++++++ examples/kaniko-test.yaml | 30 +++++++++++++ examples/kaniko-warmer.yaml | 27 +++++++++++ integration-test.sh | 1 + integration/images.go | 26 ++++++++++- integration/integration_test.go | 8 ++++ pkg/cache/cache.go | 19 ++++++++ pkg/cache/warm.go | 61 +++++++++++++++++++++++++ pkg/config/options.go | 7 +++ pkg/executor/build.go | 2 +- pkg/util/image_util.go | 42 +++++++++++++++++- pkg/util/image_util_test.go | 6 +-- 18 files changed, 389 insertions(+), 6 deletions(-) create mode 100644 cmd/warmer/cmd/root.go create mode 100644 cmd/warmer/main.go create mode 100644 deploy/Dockerfile_warmer create mode 100644 examples/kaniko-cache-claim.yaml create mode 100644 examples/kaniko-cache-volume.yaml create mode 100644 examples/kaniko-test.yaml create mode 100644 examples/kaniko-warmer.yaml create mode 100644 pkg/cache/warm.go diff --git a/Makefile b/Makefile index 8d20c2b03..94c18ad40 100644 --- a/Makefile +++ b/Makefile @@ -35,11 +35,15 @@ GO_LDFLAGS += -w -s # Drop debugging symbols. GO_LDFLAGS += ' EXECUTOR_PACKAGE = $(REPOPATH)/cmd/executor +WARMER_PACKAGE = $(REPOPATH)/cmd/warmer KANIKO_PROJECT = $(REPOPATH)/kaniko out/executor: $(GO_FILES) GOARCH=$(GOARCH) GOOS=linux CGO_ENABLED=0 go build -ldflags $(GO_LDFLAGS) -o $@ $(EXECUTOR_PACKAGE) +out/warmer: $(GO_FILES) + GOARCH=$(GOARCH) GOOS=linux CGO_ENABLED=0 go build -ldflags $(GO_LDFLAGS) -o $@ $(WARMER_PACKAGE) + .PHONY: test test: out/executor @ ./test.sh @@ -52,3 +56,4 @@ integration-test: images: docker build -t $(REGISTRY)/executor:latest -f deploy/Dockerfile . docker build -t $(REGISTRY)/executor:debug -f deploy/Dockerfile_debug . + docker build -t $(REGISTRY)/warmer:latest -f deploy/Dockerfile_warmer . diff --git a/cmd/executor/cmd/root.go b/cmd/executor/cmd/root.go index bdd4e52c6..55d256f28 100644 --- a/cmd/executor/cmd/root.go +++ b/cmd/executor/cmd/root.go @@ -99,6 +99,7 @@ func addKanikoOptionsFlags(cmd *cobra.Command) { RootCmd.PersistentFlags().StringVarP(&opts.Target, "target", "", "", "Set the target build stage to build") RootCmd.PersistentFlags().BoolVarP(&opts.NoPush, "no-push", "", false, "Do not push the image to the registry") RootCmd.PersistentFlags().StringVarP(&opts.CacheRepo, "cache-repo", "", "", "Specify a repository to use as a cache, otherwise one will be inferred from the destination provided") + RootCmd.PersistentFlags().StringVarP(&opts.CacheDir, "cache-dir", "", "/cache", "Specify a local directory to use as a cache.") RootCmd.PersistentFlags().BoolVarP(&opts.Cache, "cache", "", false, "Use cache when building image") RootCmd.PersistentFlags().BoolVarP(&opts.Cleanup, "cleanup", "", false, "Clean the filesystem at the end") } diff --git a/cmd/warmer/cmd/root.go b/cmd/warmer/cmd/root.go new file mode 100644 index 000000000..0e4908d2b --- /dev/null +++ b/cmd/warmer/cmd/root.go @@ -0,0 +1,74 @@ +/* +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 cmd + +import ( + "fmt" + "os" + + "github.com/GoogleContainerTools/kaniko/pkg/cache" + "github.com/GoogleContainerTools/kaniko/pkg/config" + "github.com/GoogleContainerTools/kaniko/pkg/constants" + "github.com/GoogleContainerTools/kaniko/pkg/util" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + opts = &config.WarmerOptions{} + logLevel string +) + +func init() { + RootCmd.PersistentFlags().StringVarP(&logLevel, "verbosity", "v", constants.DefaultLogLevel, "Log level (debug, info, warn, error, fatal, panic") + addKanikoOptionsFlags(RootCmd) + addHiddenFlags(RootCmd) +} + +var RootCmd = &cobra.Command{ + Use: "cache warmer", + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if err := util.ConfigureLogging(logLevel); err != nil { + return err + } + if len(opts.Images) == 0 { + return errors.New("You must select at least one image to cache") + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + if err := cache.WarmCache(opts); err != nil { + exit(errors.Wrap(err, "Failed warming cache")) + } + }, +} + +// addKanikoOptionsFlags configures opts +func addKanikoOptionsFlags(cmd *cobra.Command) { + RootCmd.PersistentFlags().VarP(&opts.Images, "image", "i", "Image to cache. Set it repeatedly for multiple images.") + RootCmd.PersistentFlags().StringVarP(&opts.CacheDir, "cache-dir", "c", "/cache", "Directory of the cache.") +} + +// addHiddenFlags marks certain flags as hidden from the executor help text +func addHiddenFlags(cmd *cobra.Command) { + RootCmd.PersistentFlags().MarkHidden("azure-container-registry-config") +} + +func exit(err error) { + fmt.Println(err) + os.Exit(1) +} diff --git a/cmd/warmer/main.go b/cmd/warmer/main.go new file mode 100644 index 000000000..c91eba44a --- /dev/null +++ b/cmd/warmer/main.go @@ -0,0 +1,29 @@ +/* +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 main + +import ( + "os" + + "github.com/GoogleContainerTools/kaniko/cmd/warmer/cmd" +) + +func main() { + if err := cmd.RootCmd.Execute(); err != nil { + os.Exit(1) + } +} diff --git a/deploy/Dockerfile_warmer b/deploy/Dockerfile_warmer new file mode 100644 index 000000000..2cee0968c --- /dev/null +++ b/deploy/Dockerfile_warmer @@ -0,0 +1,32 @@ +# Copyright 2018 Google, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Builds the static Go image to execute in a Kubernetes job + +FROM golang:1.10 +WORKDIR /go/src/github.com/GoogleContainerTools/kaniko +COPY . . +RUN make + +FROM scratch +COPY --from=0 /go/src/github.com/GoogleContainerTools/kaniko/out/warmer /kaniko/warmer +COPY files/ca-certificates.crt /kaniko/ssl/certs/ +COPY files/config.json /kaniko/.docker/ +ENV HOME /root +ENV USER /root +ENV PATH /usr/local/bin:/kaniko +ENV SSL_CERT_DIR=/kaniko/ssl/certs +ENV DOCKER_CONFIG /kaniko/.docker/ +WORKDIR /workspace +ENTRYPOINT ["/kaniko/warmer"] diff --git a/examples/kaniko-cache-claim.yaml b/examples/kaniko-cache-claim.yaml new file mode 100644 index 000000000..dc30c3a8d --- /dev/null +++ b/examples/kaniko-cache-claim.yaml @@ -0,0 +1,11 @@ +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: kaniko-cache-claim +spec: + storageClassName: manual + accessModes: + - ReadOnlyMany + resources: + requests: + storage: 8Gi diff --git a/examples/kaniko-cache-volume.yaml b/examples/kaniko-cache-volume.yaml new file mode 100644 index 000000000..700aa2999 --- /dev/null +++ b/examples/kaniko-cache-volume.yaml @@ -0,0 +1,14 @@ +kind: PersistentVolume +apiVersion: v1 +metadata: + name: kaniko-cache-volume + labels: + type: local +spec: + storageClassName: manual + capacity: + storage: 10Gi + accessModes: + - ReadOnlyMany + hostPath: + path: "/tmp/kaniko-cache" diff --git a/examples/kaniko-test.yaml b/examples/kaniko-test.yaml new file mode 100644 index 000000000..d6f990433 --- /dev/null +++ b/examples/kaniko-test.yaml @@ -0,0 +1,30 @@ +apiVersion: v1 +kind: Pod +metadata: + name: kaniko +spec: + containers: + - name: kaniko + image: gcr.io/kaniko-project/executor:latest + args: ["--dockerfile=", + "--context=", + "--destination=", + "--cache", + "--cache-dir=/cache"] + volumeMounts: + - name: kaniko-secret + mountPath: /secret + - name: kaniko-cache + mountPath: /cache + env: + - name: GOOGLE_APPLICATION_CREDENTIALS + value: /secret/kaniko-secret.json + restartPolicy: Never + volumes: + - name: kaniko-secret + secret: + secretName: kaniko-secret + - name: kaniko-cache + persistentVolumeClaim: + claimName: kaniko-cache-claim + diff --git a/examples/kaniko-warmer.yaml b/examples/kaniko-warmer.yaml new file mode 100644 index 000000000..318f62878 --- /dev/null +++ b/examples/kaniko-warmer.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +kind: Pod +metadata: + name: kaniko-warmer +spec: + containers: + - name: kaniko-warmer + image: gcr.io/kaniko-project/warmer:latest + args: ["--cache-dir=/cache", + "--image=gcr.io/google-appengine/debian9"] + volumeMounts: + - name: kaniko-secret + mountPath: /secret + - name: kaniko-cache + mountPath: /cache + env: + - name: GOOGLE_APPLICATION_CREDENTIALS + value: /secret/kaniko-secret.json + restartPolicy: Never + volumes: + - name: kaniko-secret + secret: + secretName: kaniko-secret + - name: kaniko-cache + persistentVolumeClaim: + claimName: kaniko-cache-claim + diff --git a/integration-test.sh b/integration-test.sh index 51fcb6e20..3217f767a 100755 --- a/integration-test.sh +++ b/integration-test.sh @@ -34,5 +34,6 @@ fi echo "Running integration tests..." make out/executor +make out/warmer pushd integration go test -v --bucket "${GCS_BUCKET}" --repo "${IMAGE_REPO}" --timeout 30m diff --git a/integration/images.go b/integration/images.go index cf1f90105..464db8cc8 100644 --- a/integration/images.go +++ b/integration/images.go @@ -30,10 +30,13 @@ import ( const ( // ExecutorImage is the name of the kaniko executor image ExecutorImage = "executor-image" + WarmerImage = "warmer-image" dockerPrefix = "docker-" kanikoPrefix = "kaniko-" buildContextPath = "/workspace" + cacheDir = "/workspace/cache" + baseImageToCache = "gcr.io/google-appengine/debian9@sha256:1d6a9a6d106bd795098f60f4abb7083626354fa6735e81743c7f8cfca11259f0" ) // Arguments to build Dockerfiles with, used for both docker and kaniko builds @@ -201,6 +204,26 @@ func (d *DockerFileBuilder) BuildImage(imageRepo, gcsBucket, dockerfilesPath, do return nil } +func populateVolumeCache() error { + _, ex, _, _ := runtime.Caller(0) + cwd := filepath.Dir(ex) + warmerCmd := exec.Command("docker", + append([]string{"run", + "-v", os.Getenv("HOME") + "/.config/gcloud:/root/.config/gcloud", + "-v", cwd + ":/workspace", + WarmerImage, + "-c", cacheDir, + "-i", baseImageToCache}, + )..., + ) + + if _, err := RunCommandWithoutTest(warmerCmd); err != nil { + return fmt.Errorf("Failed to warm kaniko cache: %s", err) + } + + return nil +} + // buildCachedImages builds the images for testing caching via kaniko where version is the nth time this image has been built func (d *DockerFileBuilder) buildCachedImages(imageRepo, cacheRepo, dockerfilesPath string, version int) error { _, ex, _, _ := runtime.Caller(0) @@ -219,7 +242,8 @@ func (d *DockerFileBuilder) buildCachedImages(imageRepo, cacheRepo, dockerfilesP "-d", kanikoImage, "-c", buildContextPath, cacheFlag, - "--cache-repo", cacheRepo})..., + "--cache-repo", cacheRepo, + "--cache-dir", cacheDir})..., ) if _, err := RunCommandWithoutTest(kanikoCmd); err != nil { diff --git a/integration/integration_test.go b/integration/integration_test.go index e6e4f8959..313d8f053 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -145,6 +145,13 @@ func TestMain(m *testing.M) { os.Exit(1) } + fmt.Println("Building cache warmer image") + cmd = exec.Command("docker", "build", "-t", WarmerImage, "-f", "../deploy/Dockerfile_warmer", "..") + if _, err = RunCommandWithoutTest(cmd); err != nil { + fmt.Printf("Building kaniko's cache warmer failed: %s", err) + os.Exit(1) + } + fmt.Println("Building onbuild base image") buildOnbuildBase := exec.Command("docker", "build", "-t", config.onbuildBaseImage, "-f", "dockerfiles/Dockerfile_onbuild_base", ".") if err := buildOnbuildBase.Run(); err != nil { @@ -238,6 +245,7 @@ func TestLayers(t *testing.T) { // Build each image with kaniko twice, and then make sure they're exactly the same func TestCache(t *testing.T) { + populateVolumeCache() for dockerfile := range imageBuilder.TestCacheDockerfiles { t.Run("test_cache_"+dockerfile, func(t *testing.T) { cache := filepath.Join(config.imageRepo, "cache", fmt.Sprintf("%v", time.Now().UnixNano())) diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go index 69682ba48..fca66e631 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -18,6 +18,7 @@ package cache import ( "fmt" + "path" "github.com/GoogleContainerTools/kaniko/pkg/config" "github.com/google/go-containerregistry/pkg/authn" @@ -25,6 +26,7 @@ import ( "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/tarball" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -68,3 +70,20 @@ func Destination(opts *config.KanikoOptions, cacheKey string) (string, error) { } return fmt.Sprintf("%s:%s", cache, cacheKey), nil } + +func LocalSource(opts *config.KanikoOptions, cacheKey string) (v1.Image, error) { + cache := opts.CacheDir + if cache == "" { + return nil, nil + } + + path := path.Join(cache, cacheKey) + + imgTar, err := tarball.ImageFromPath(path, nil) + if err != nil { + return nil, errors.Wrap(err, "getting image from path") + } + + logrus.Infof("Found %s in local cache", cacheKey) + return imgTar, nil +} diff --git a/pkg/cache/warm.go b/pkg/cache/warm.go new file mode 100644 index 000000000..fc7b6b6c8 --- /dev/null +++ b/pkg/cache/warm.go @@ -0,0 +1,61 @@ +/* +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 cache + +import ( + "fmt" + "path" + + "github.com/GoogleContainerTools/kaniko/pkg/config" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +func WarmCache(opts *config.WarmerOptions) error { + cacheDir := opts.CacheDir + images := opts.Images + logrus.Debugf("%s\n", cacheDir) + logrus.Debugf("%s\n", images) + + for _, image := range images { + cacheRef, err := name.NewTag(image, name.WeakValidation) + if err != nil { + errors.Wrap(err, fmt.Sprintf("Failed to verify image name: %s", image)) + } + img, err := remote.Image(cacheRef) + if err != nil { + errors.Wrap(err, fmt.Sprintf("Failed to retrieve image: %s", image)) + } + + digest, err := img.Digest() + if err != nil { + errors.Wrap(err, fmt.Sprintf("Failed to retrieve digest: %s", image)) + } + cachePath := path.Join(cacheDir, digest.String()) + err = tarball.WriteToFile(cachePath, cacheRef, img) + if err != nil { + errors.Wrap(err, fmt.Sprintf("Failed to write %s to cache", image)) + } else { + logrus.Debugf("Wrote %s to cache", image) + } + + } + return nil +} diff --git a/pkg/config/options.go b/pkg/config/options.go index 26fecec29..fc15c1f21 100644 --- a/pkg/config/options.go +++ b/pkg/config/options.go @@ -25,6 +25,7 @@ type KanikoOptions struct { TarPath string Target string CacheRepo string + CacheDir string Destinations multiArg BuildArgs multiArg InsecurePush bool @@ -35,3 +36,9 @@ type KanikoOptions struct { Cache bool Cleanup bool } + +// WarmerOptions are options that are set by command line arguments to the cache warmer. +type WarmerOptions struct { + Images multiArg + CacheDir string +} diff --git a/pkg/executor/build.go b/pkg/executor/build.go index 1f90037d8..7d991069d 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -54,7 +54,7 @@ type stageBuilder struct { // newStageBuilder returns a new type stageBuilder which contains all the information required to build the stage func newStageBuilder(opts *config.KanikoOptions, stage config.KanikoStage) (*stageBuilder, error) { - sourceImage, err := util.RetrieveSourceImage(stage, opts.BuildArgs) + sourceImage, err := util.RetrieveSourceImage(stage, opts.BuildArgs, opts) if err != nil { return nil, err } diff --git a/pkg/util/image_util.go b/pkg/util/image_util.go index d19e684f6..6eb41aa57 100644 --- a/pkg/util/image_util.go +++ b/pkg/util/image_util.go @@ -30,6 +30,7 @@ import ( "github.com/google/go-containerregistry/pkg/v1/tarball" "github.com/sirupsen/logrus" + "github.com/GoogleContainerTools/kaniko/pkg/cache" "github.com/GoogleContainerTools/kaniko/pkg/config" "github.com/GoogleContainerTools/kaniko/pkg/constants" ) @@ -41,7 +42,7 @@ var ( ) // RetrieveSourceImage returns the base image of the stage at index -func RetrieveSourceImage(stage config.KanikoStage, buildArgs []string) (v1.Image, error) { +func RetrieveSourceImage(stage config.KanikoStage, buildArgs []string, opts *config.KanikoOptions) (v1.Image, error) { currentBaseName, err := ResolveEnvironmentReplacement(stage.BaseName, buildArgs, false) if err != nil { return nil, err @@ -57,6 +58,19 @@ func RetrieveSourceImage(stage config.KanikoStage, buildArgs []string) (v1.Image return retrieveTarImage(stage.BaseImageIndex) } + // Next, check if local caching is enabled + // If so, look in the local cache before trying the remote registry + if opts.Cache && opts.CacheDir != "" { + cachedImage, err := cachedImage(opts, currentBaseName) + if cachedImage != nil { + return cachedImage, nil + } + + if err != nil { + logrus.Warnf("Error while retrieving image from cache: %v", err) + } + } + // Otherwise, initialize image as usual return retrieveRemoteImage(currentBaseName) } @@ -92,3 +106,29 @@ func remoteImage(image string) (v1.Image, error) { kc := authn.NewMultiKeychain(authn.DefaultKeychain, k8sc) return remote.Image(ref, remote.WithAuthFromKeychain(kc)) } + +func cachedImage(opts *config.KanikoOptions, image string) (v1.Image, error) { + ref, err := name.ParseReference(image, name.WeakValidation) + if err != nil { + return nil, err + } + + var cacheKey string + if d, ok := ref.(name.Digest); ok { + cacheKey = d.DigestStr() + } else { + img, err := remote.Image(ref) + if err != nil { + return nil, err + } + + d, err := img.Digest() + if err != nil { + return nil, err + } + + cacheKey = d.String() + } + + return cache.LocalSource(opts, cacheKey) +} diff --git a/pkg/util/image_util_test.go b/pkg/util/image_util_test.go index dbd7b8ee1..44897fe72 100644 --- a/pkg/util/image_util_test.go +++ b/pkg/util/image_util_test.go @@ -57,7 +57,7 @@ func Test_StandardImage(t *testing.T) { retrieveRemoteImage = mock actual, err := RetrieveSourceImage(config.KanikoStage{ Stage: stages[0], - }, nil) + }, nil, &config.KanikoOptions{}) testutil.CheckErrorAndDeepEqual(t, false, err, nil, actual) } func Test_ScratchImage(t *testing.T) { @@ -67,7 +67,7 @@ func Test_ScratchImage(t *testing.T) { } actual, err := RetrieveSourceImage(config.KanikoStage{ Stage: stages[1], - }, nil) + }, nil, &config.KanikoOptions{}) expected := empty.Image testutil.CheckErrorAndDeepEqual(t, false, err, expected, actual) } @@ -89,7 +89,7 @@ func Test_TarImage(t *testing.T) { BaseImageStoredLocally: true, BaseImageIndex: 0, Stage: stages[2], - }, nil) + }, nil, &config.KanikoOptions{}) testutil.CheckErrorAndDeepEqual(t, false, err, nil, actual) } From 129eb9b8a8938eecc81eb440e458b5c2f8668d76 Mon Sep 17 00:00:00 2001 From: Deniz Zoeteman Date: Fri, 12 Oct 2018 16:16:48 +0200 Subject: [PATCH 08/42] Change loglevel for copying files to debug (#303) --- pkg/util/fs_util.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/util/fs_util.go b/pkg/util/fs_util.go index 07e3fbc59..f755b335a 100644 --- a/pkg/util/fs_util.go +++ b/pkg/util/fs_util.go @@ -468,7 +468,7 @@ func CopyDir(src, dest string) ([]string, error) { } destPath := filepath.Join(dest, file) if fi.IsDir() { - logrus.Infof("Creating directory %s", destPath) + logrus.Debugf("Creating directory %s", destPath) uid := int(fi.Sys().(*syscall.Stat_t).Uid) gid := int(fi.Sys().(*syscall.Stat_t).Gid) @@ -511,7 +511,7 @@ func CopyFile(src, dest string) error { if err != nil { return err } - logrus.Infof("Copying file %s to %s", src, dest) + logrus.Debugf("Copying file %s to %s", src, dest) srcFile, err := os.Open(src) if err != nil { return err From a572774bbe9b0aac0da29228682cd16fe88938f3 Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Fri, 12 Oct 2018 11:55:09 -0700 Subject: [PATCH 09/42] Add --ignore flag Added a --ignore flag to ignore packages and files in the build context. This should mimic the .dockerignore file. Before starting the build, we go through and delete ignored files from the build context. --- README.md | 4 ++ cmd/executor/cmd/root.go | 33 +++++++++++ .../dockerfiles/Dockerfile_test_ignore | 2 + integration/images.go | 59 ++++++++++++++++++- pkg/config/options.go | 1 + 5 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 integration/dockerfiles/Dockerfile_test_ignore diff --git a/README.md b/README.md index 39c24e310..de8116141 100644 --- a/README.md +++ b/README.md @@ -321,6 +321,10 @@ _This flag must be used in conjunction with the `--cache=true` flag._ Set this flag to cleanup the filesystem at the end, leaving a clean kaniko container (if you want to build multiple images in the same container, using the debug kaniko image) +#### --ignore + +Set this flag to ignore files in your build context. For examples, set `--ignore pkg/*` to ignore all files in the `pkg` directory. + ### Debug Image The kaniko executor image is based off of scratch and doesn't contain a shell. diff --git a/cmd/executor/cmd/root.go b/cmd/executor/cmd/root.go index 55d256f28..089378442 100644 --- a/cmd/executor/cmd/root.go +++ b/cmd/executor/cmd/root.go @@ -61,6 +61,9 @@ var RootCmd = &cobra.Command{ if err := resolveSourceContext(); err != nil { return errors.Wrap(err, "error resolving source context") } + if err := removeIgnoredFiles(); err != nil { + return errors.Wrap(err, "error removing ignored files from build context") + } return resolveDockerfilePath() }, Run: func(cmd *cobra.Command, args []string) { @@ -91,6 +94,7 @@ func addKanikoOptionsFlags(cmd *cobra.Command) { RootCmd.PersistentFlags().VarP(&opts.Destinations, "destination", "d", "Registry the final image should be pushed to. Set it repeatedly for multiple destinations.") RootCmd.PersistentFlags().StringVarP(&opts.SnapshotMode, "snapshotMode", "", "full", "Change the file attributes inspected during snapshotting") RootCmd.PersistentFlags().VarP(&opts.BuildArgs, "build-arg", "", "This flag allows you to pass in ARG values at build time. Set it repeatedly for multiple values.") + RootCmd.PersistentFlags().VarP(&opts.Ignore, "ignore", "", "Set this flag to ignore files in the build context. Set it repeatedly for multiple values.") RootCmd.PersistentFlags().BoolVarP(&opts.InsecurePush, "insecure", "", false, "Push to insecure registry using plain HTTP") RootCmd.PersistentFlags().BoolVarP(&opts.SkipTLSVerify, "skip-tls-verify", "", false, "Push to insecure registry ignoring TLS verify") RootCmd.PersistentFlags().StringVarP(&opts.TarPath, "tarPath", "", "", "Path to save the image in as a tarball instead of pushing") @@ -182,6 +186,35 @@ func resolveSourceContext() error { return nil } +func removeIgnoredFiles() error { + logrus.Infof("Removing ignored files from build context: %s", opts.Ignore) + for r, i := range opts.Ignore { + opts.Ignore[r] = filepath.Clean(filepath.Join(opts.SrcContext, i)) + } + err := filepath.Walk(opts.SrcContext, func(path string, fi os.FileInfo, _ error) error { + if ignoreFile(path) { + if err := os.RemoveAll(path); err != nil { + logrus.Debugf("error removing %s from buildcontext", path) + } + } + return nil + }) + return err +} + +func ignoreFile(path string) bool { + for _, i := range opts.Ignore { + matched, err := filepath.Match(i, path) + if err != nil { + return false + } + if matched { + return true + } + } + return false +} + func exit(err error) { fmt.Println(err) os.Exit(1) diff --git a/integration/dockerfiles/Dockerfile_test_ignore b/integration/dockerfiles/Dockerfile_test_ignore new file mode 100644 index 000000000..04fdc2701 --- /dev/null +++ b/integration/dockerfiles/Dockerfile_test_ignore @@ -0,0 +1,2 @@ +FROM scratch +COPY . . diff --git a/integration/images.go b/integration/images.go index 464db8cc8..daeeca24b 100644 --- a/integration/images.go +++ b/integration/images.go @@ -54,6 +54,16 @@ var argsMap = map[string][]string{ "Dockerfile_test_multistage": {"file=/foo2"}, } +var filesToIgnore = []string{"context/bar/*", "context/tars/"} + +func ignoreFlags() []string { + var f []string + for _, i := range filesToIgnore { + f = append(f, fmt.Sprintf("--ignore=%s", i)) + } + return f +} + // Arguments to build Dockerfiles with when building with docker var additionalDockerFlagsMap = map[string][]string{ "Dockerfile_test_target": {"--target=second"}, @@ -64,6 +74,7 @@ var additionalKanikoFlagsMap = map[string][]string{ "Dockerfile_test_add": {"--single-snapshot"}, "Dockerfile_test_scratch": {"--single-snapshot"}, "Dockerfile_test_target": {"--target=second"}, + "Dockerfile_test_ignore": ignoreFlags(), } var bucketContextTests = []string{"Dockerfile_test_copy_bucket"} @@ -110,9 +121,10 @@ func FindDockerFiles(dockerfilesPath string) ([]string, error) { // keeps track of which files have been built. type DockerFileBuilder struct { // Holds all available docker files and whether or not they've been built - FilesBuilt map[string]bool - DockerfilesToIgnore map[string]struct{} - TestCacheDockerfiles map[string]struct{} + FilesBuilt map[string]bool + DockerfilesToIgnore map[string]struct{} + TestCacheDockerfiles map[string]struct{} + TestIgnoreDockerfiles map[string]struct{} } // NewDockerFileBuilder will create a DockerFileBuilder initialized with dockerfiles, which @@ -130,6 +142,9 @@ func NewDockerFileBuilder(dockerfiles []string) *DockerFileBuilder { "Dockerfile_test_cache": {}, "Dockerfile_test_cache_install": {}, } + d.TestIgnoreDockerfiles = map[string]struct{}{ + "Dockerfile_test_ignore": {}, + } return &d } @@ -158,11 +173,23 @@ func (d *DockerFileBuilder) BuildImage(imageRepo, gcsBucket, dockerfilesPath, do "."}, additionalFlags...)..., ) + if d.includeDockerIgnore(dockerfile) { + if err := generateDockerIgnore(); err != nil { + return err + } + } + _, err := RunCommandWithoutTest(dockerCmd) if err != nil { return fmt.Errorf("Failed to build image %s with docker command \"%s\": %s", dockerImage, dockerCmd.Args, err) } + if d.includeDockerIgnore(dockerfilesPath) { + if err := deleteDockerIgnore(); err != nil { + return err + } + } + contextFlag := "-c" contextPath := buildContextPath for _, d := range bucketContextTests { @@ -252,3 +279,29 @@ func (d *DockerFileBuilder) buildCachedImages(imageRepo, cacheRepo, dockerfilesP } return nil } + +func (d *DockerFileBuilder) includeDockerIgnore(dockerfile string) bool { + for i := range d.TestIgnoreDockerfiles { + if i == dockerfile { + return true + } + } + return false +} + +func generateDockerIgnore() error { + f, err := os.Create(".dockerignore") + if err != nil { + return err + } + defer f.Close() + contents := strings.Join(filesToIgnore, "\n") + if _, err := f.Write([]byte(contents)); err != nil { + return err + } + return nil +} + +func deleteDockerIgnore() error { + return os.Remove(".dockerignore") +} diff --git a/pkg/config/options.go b/pkg/config/options.go index fc15c1f21..2c9195598 100644 --- a/pkg/config/options.go +++ b/pkg/config/options.go @@ -28,6 +28,7 @@ type KanikoOptions struct { CacheDir string Destinations multiArg BuildArgs multiArg + Ignore multiArg InsecurePush bool SkipTLSVerify bool SingleSnapshot bool From adac8d495d0a8fd3ab0b21f30d684a7569346243 Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Fri, 12 Oct 2018 12:48:16 -0700 Subject: [PATCH 10/42] Add test dir for ignore files It seems like .dockerignore deletes files containeed within the file locally upon docker build, so I created a temporary test dir to make sure the --ignore flag works. We make sure it exists before building docker and kaniko for Dockerfile_test_ignore, and then delete it after the builds have completed. --- integration/images.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/integration/images.go b/integration/images.go index daeeca24b..b190e61f6 100644 --- a/integration/images.go +++ b/integration/images.go @@ -37,6 +37,7 @@ const ( buildContextPath = "/workspace" cacheDir = "/workspace/cache" baseImageToCache = "gcr.io/google-appengine/debian9@sha256:1d6a9a6d106bd795098f60f4abb7083626354fa6735e81743c7f8cfca11259f0" + testDirPath = "test/dir/path" ) // Arguments to build Dockerfiles with, used for both docker and kaniko builds @@ -54,7 +55,7 @@ var argsMap = map[string][]string{ "Dockerfile_test_multistage": {"file=/foo2"}, } -var filesToIgnore = []string{"context/bar/*", "context/tars/"} +var filesToIgnore = []string{"test/*"} func ignoreFlags() []string { var f []string @@ -174,6 +175,9 @@ func (d *DockerFileBuilder) BuildImage(imageRepo, gcsBucket, dockerfilesPath, do additionalFlags...)..., ) if d.includeDockerIgnore(dockerfile) { + if err := setupTestDir(); err != nil { + return err + } if err := generateDockerIgnore(); err != nil { return err } @@ -188,7 +192,11 @@ func (d *DockerFileBuilder) BuildImage(imageRepo, gcsBucket, dockerfilesPath, do if err := deleteDockerIgnore(); err != nil { return err } + if err := setupTestDir(); err != nil { + return err + } } + defer removeTestDir() contextFlag := "-c" contextPath := buildContextPath @@ -289,6 +297,14 @@ func (d *DockerFileBuilder) includeDockerIgnore(dockerfile string) bool { return false } +func setupTestDir() error { + return os.MkdirAll(testDirPath, 0644) +} + +func removeTestDir() error { + return os.RemoveAll(testDirPath) +} + func generateDockerIgnore() error { f, err := os.Create(".dockerignore") if err != nil { From d8beff6f28019345e979d6ec9fe192ca88441099 Mon Sep 17 00:00:00 2001 From: peter-evans Date: Sat, 13 Oct 2018 08:04:27 +0900 Subject: [PATCH 11/42] Update test for link and file extraction to non-empty dir --- integration/dockerfiles/Dockerfile_test_extraction | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration/dockerfiles/Dockerfile_test_extraction b/integration/dockerfiles/Dockerfile_test_extraction index 8e64d8ade..d2a3eaedd 100644 --- a/integration/dockerfiles/Dockerfile_test_extraction +++ b/integration/dockerfiles/Dockerfile_test_extraction @@ -1,2 +1,2 @@ -# Tests extraction of a symlink to a path that is a non-empty directory -FROM registry.access.redhat.com/jboss-eap-7/eap71-openshift:1.3-10 +# Tests extraction of symlink, hardlink and regular files to a path that is a non-empty directory +FROM gcr.io/kaniko-test/extraction-base-image:latest From 38c8c89b6a3f6ceedab46a40ab110cc2f987f921 Mon Sep 17 00:00:00 2001 From: Cyrille Hemidy Date: Sun, 14 Oct 2018 00:38:18 +0200 Subject: [PATCH 12/42] fix mispell --- integration/gcs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/gcs.go b/integration/gcs.go index 2604f1397..a67b5fb00 100644 --- a/integration/gcs.go +++ b/integration/gcs.go @@ -36,7 +36,7 @@ func CreateIntegrationTarball() (string, error) { } tempDir, err := ioutil.TempDir("", "") if err != nil { - return "", fmt.Errorf("Failed to create temporary directoy to hold tarball: %s", err) + return "", fmt.Errorf("Failed to create temporary directory to hold tarball: %s", err) } contextFile := fmt.Sprintf("%s/context_%d.tar.gz", tempDir, time.Now().UnixNano()) cmd := exec.Command("tar", "-C", dir, "-zcvf", contextFile, ".") From 5ac29a97734170a0547fea33b348dc7c328e2f8a Mon Sep 17 00:00:00 2001 From: dlorenc Date: Mon, 15 Oct 2018 08:56:34 -0500 Subject: [PATCH 13/42] Use only the necessary files in the cache keys. (#387) --- pkg/commands/add.go | 39 ++++++++++++++++++-------- pkg/commands/base_command.go | 16 +++++++---- pkg/commands/commands.go | 2 +- pkg/commands/copy.go | 49 +++++++++++++++++++++++++-------- pkg/executor/build.go | 18 +++++++----- pkg/executor/composite_cache.go | 26 +++++++++++++++++ 6 files changed, 114 insertions(+), 36 deletions(-) diff --git a/pkg/commands/add.go b/pkg/commands/add.go index b0f07a599..27cf0c968 100644 --- a/pkg/commands/add.go +++ b/pkg/commands/add.go @@ -19,12 +19,13 @@ package commands import ( "path/filepath" + "github.com/moby/buildkit/frontend/dockerfile/instructions" + "github.com/GoogleContainerTools/kaniko/pkg/dockerfile" "github.com/google/go-containerregistry/pkg/v1" "github.com/GoogleContainerTools/kaniko/pkg/util" - "github.com/moby/buildkit/frontend/dockerfile/instructions" "github.com/sirupsen/logrus" ) @@ -44,18 +45,13 @@ type AddCommand struct { // 2. If is a local tar archive: // -If is a local tar archive, it is unpacked at the dest, as 'tar -x' would func (a *AddCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error { - // First, resolve any environment replacement replacementEnvs := buildArgs.ReplacementEnvs(config.Env) - resolvedEnvs, err := util.ResolveEnvironmentReplacementList(a.cmd.SourcesAndDest, replacementEnvs, true) - if err != nil { - return err - } - dest := resolvedEnvs[len(resolvedEnvs)-1] - // Resolve wildcards and get a list of resolved sources - srcs, err := util.ResolveSources(resolvedEnvs, a.buildcontext) + + srcs, dest, err := resolveEnvAndWildcards(a.cmd.SourcesAndDest, a.buildcontext, replacementEnvs) if err != nil { return err } + var unresolvedSrcs []string // If any of the sources are local tar archives: // 1. Unpack them to the specified destination @@ -94,6 +90,7 @@ func (a *AddCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bui }, buildcontext: a.buildcontext, } + if err := copyCmd.ExecuteCommand(config, buildArgs); err != nil { return err } @@ -111,6 +108,26 @@ func (a *AddCommand) String() string { return a.cmd.String() } -func (a *AddCommand) UsesContext() bool { - return true +func (a *AddCommand) FilesUsedFromContext(config *v1.Config, buildArgs *dockerfile.BuildArgs) ([]string, error) { + replacementEnvs := buildArgs.ReplacementEnvs(config.Env) + + srcs, _, err := resolveEnvAndWildcards(a.cmd.SourcesAndDest, a.buildcontext, replacementEnvs) + if err != nil { + return nil, err + } + + files := []string{} + for _, src := range srcs { + if util.IsSrcRemoteFileURL(src) { + continue + } + if util.IsFileLocalTarArchive(src) { + continue + } + fullPath := filepath.Join(a.buildcontext, src) + files = append(files, fullPath) + } + + logrus.Infof("Using files from context: %v", files) + return files, nil } diff --git a/pkg/commands/base_command.go b/pkg/commands/base_command.go index bcb6448c5..d2a746ee6 100644 --- a/pkg/commands/base_command.go +++ b/pkg/commands/base_command.go @@ -16,19 +16,23 @@ limitations under the License. package commands +import ( + "github.com/GoogleContainerTools/kaniko/pkg/dockerfile" + "github.com/google/go-containerregistry/pkg/v1" +) + type BaseCommand struct { - cache bool - usesContext bool + cache bool } func (b *BaseCommand) CacheCommand() bool { return b.cache } -func (b *BaseCommand) UsesContext() bool { - return b.usesContext -} - func (b *BaseCommand) FilesToSnapshot() []string { return []string{} } + +func (b *BaseCommand) FilesUsedFromContext(_ *v1.Config, _ *dockerfile.BuildArgs) ([]string, error) { + return []string{}, nil +} diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go index 422272765..cbb960e68 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -39,7 +39,7 @@ type DockerCommand interface { CacheCommand() bool // Return true if this command depends on the build context. - UsesContext() bool + FilesUsedFromContext(*v1.Config, *dockerfile.BuildArgs) ([]string, error) } func GetCommand(cmd instructions.Command, buildcontext string) (DockerCommand, error) { diff --git a/pkg/commands/copy.go b/pkg/commands/copy.go index 7bb582ea6..d70d98483 100644 --- a/pkg/commands/copy.go +++ b/pkg/commands/copy.go @@ -20,12 +20,14 @@ import ( "os" "path/filepath" + "github.com/moby/buildkit/frontend/dockerfile/instructions" + "github.com/sirupsen/logrus" + "github.com/GoogleContainerTools/kaniko/pkg/constants" "github.com/GoogleContainerTools/kaniko/pkg/dockerfile" "github.com/GoogleContainerTools/kaniko/pkg/util" "github.com/google/go-containerregistry/pkg/v1" - "github.com/moby/buildkit/frontend/dockerfile/instructions" ) type CopyCommand struct { @@ -40,18 +42,14 @@ func (c *CopyCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bu if c.cmd.From != "" { c.buildcontext = filepath.Join(constants.KanikoDir, c.cmd.From) } + replacementEnvs := buildArgs.ReplacementEnvs(config.Env) - // First, resolve any environment replacement - resolvedEnvs, err := util.ResolveEnvironmentReplacementList(c.cmd.SourcesAndDest, replacementEnvs, true) - if err != nil { - return err - } - dest := resolvedEnvs[len(resolvedEnvs)-1] - // Resolve wildcards and get a list of resolved sources - srcs, err := util.ResolveSources(resolvedEnvs, c.buildcontext) + + srcs, dest, err := resolveEnvAndWildcards(c.cmd.SourcesAndDest, c.buildcontext, replacementEnvs) if err != nil { return err } + // For each source, iterate through and copy it over for _, src := range srcs { fullPath := filepath.Join(c.buildcontext, src) @@ -94,6 +92,18 @@ func (c *CopyCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bu return nil } +func resolveEnvAndWildcards(sd instructions.SourcesAndDest, buildcontext string, envs []string) ([]string, string, error) { + // First, resolve any environment replacement + resolvedEnvs, err := util.ResolveEnvironmentReplacementList(sd, envs, true) + if err != nil { + return nil, "", err + } + dest := resolvedEnvs[len(resolvedEnvs)-1] + // Resolve wildcards and get a list of resolved sources + srcs, err := util.ResolveSources(resolvedEnvs, buildcontext) + return srcs, dest, err +} + // FilesToSnapshot should return an empty array if still nil; no files were changed func (c *CopyCommand) FilesToSnapshot() []string { return c.snapshotFiles @@ -104,6 +114,23 @@ func (c *CopyCommand) String() string { return c.cmd.String() } -func (c *CopyCommand) UsesContext() bool { - return true +func (c *CopyCommand) FilesUsedFromContext(config *v1.Config, buildArgs *dockerfile.BuildArgs) ([]string, error) { + // We don't use the context if we're performing a copy --from. + if c.cmd.From != "" { + return nil, nil + } + + replacementEnvs := buildArgs.ReplacementEnvs(config.Env) + srcs, _, err := resolveEnvAndWildcards(c.cmd.SourcesAndDest, c.buildcontext, replacementEnvs) + if err != nil { + return nil, err + } + + files := []string{} + for _, src := range srcs { + fullPath := filepath.Join(c.buildcontext, src) + files = append(files, fullPath) + } + logrus.Infof("Using files from context: %v", files) + return files, nil } diff --git a/pkg/executor/build.go b/pkg/executor/build.go index 7d991069d..29a1cf314 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -125,10 +125,6 @@ func (s *stageBuilder) build() error { // Set the initial cache key to be the base image digest, the build args and the SrcContext. compositeKey := NewCompositeCache(s.baseImageDigest) - contextHash, err := HashDir(s.opts.SrcContext) - if err != nil { - return err - } compositeKey.AddKey(s.opts.BuildArgs...) cmds := []commands.DockerCommand{} @@ -148,8 +144,16 @@ func (s *stageBuilder) build() error { // Add the next command to the cache key. compositeKey.AddKey(command.String()) - if command.UsesContext() { - compositeKey.AddKey(contextHash) + + // If the command uses files from the context, add them. + files, err := command.FilesUsedFromContext(&s.cf.Config, args) + if err != nil { + return err + } + for _, f := range files { + if err := compositeKey.AddPath(f); err != nil { + return err + } } logrus.Info(command.String()) @@ -172,7 +176,7 @@ func (s *stageBuilder) build() error { if err := command.ExecuteCommand(&s.cf.Config, args); err != nil { return err } - files := command.FilesToSnapshot() + files = command.FilesToSnapshot() var contents []byte if !s.shouldTakeSnapshot(index, files) { diff --git a/pkg/executor/composite_cache.go b/pkg/executor/composite_cache.go index 0a832e636..15a88a40b 100644 --- a/pkg/executor/composite_cache.go +++ b/pkg/executor/composite_cache.go @@ -53,6 +53,32 @@ func (s *CompositeCache) Hash() (string, error) { return util.SHA256(strings.NewReader(s.Key())) } +func (s *CompositeCache) AddPath(p string) error { + sha := sha256.New() + fi, err := os.Lstat(p) + if err != nil { + return err + } + if fi.Mode().IsDir() { + k, err := HashDir(p) + if err != nil { + return err + } + s.keys = append(s.keys, k) + return nil + } + fh, err := util.CacheHasher()(p) + if err != nil { + return err + } + if _, err := sha.Write([]byte(fh)); err != nil { + return err + } + + s.keys = append(s.keys, string(sha.Sum(nil))) + return nil +} + // HashDir returns a hash of the directory. func HashDir(p string) (string, error) { sha := sha256.New() From 579b866abf7775840c899634f8dd23632249ec42 Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Tue, 16 Oct 2018 10:04:18 -0700 Subject: [PATCH 14/42] Release v0.5.0 --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55426e2bd..5962a0732 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +# v0.5.0 Release - 10/16/2018 + +## New Features +* Persistent volume caching for base images [#383](https://github.com/GoogleContainerTools/kaniko/pull/383) + +## Updates +* Use only the necessary files in the cache keys. [#387](https://github.com/GoogleContainerTools/kaniko/pull/387) +* Change loglevel for copying files to debug (#303) [#393](https://github.com/GoogleContainerTools/kaniko/pull/393) +* Improve IsDestDir functionality with filesystem info [#390](https://github.com/GoogleContainerTools/kaniko/pull/390) +* Refactor the build loop. [#385](https://github.com/GoogleContainerTools/kaniko/pull/385) +* Rework cache key generation a bit. [#375](https://github.com/GoogleContainerTools/kaniko/pull/375) + +## Bug Fixes +* fix mispell [#396](https://github.com/GoogleContainerTools/kaniko/pull/396) +* Update go-containerregistry dependency [#388](https://github.com/GoogleContainerTools/kaniko/pull/388) +* chore: fix broken markdown (CHANGELOG.md) [#382](https://github.com/GoogleContainerTools/kaniko/pull/382) +* Don't cut everything after an equals sign [#381](https://github.com/GoogleContainerTools/kaniko/pull/381) + + # v0.4.0 Release - 10/01/2018 ## New Features From d4b54cbbcac97db727d8c8fa613b81a8180efbe1 Mon Sep 17 00:00:00 2001 From: peter-evans Date: Thu, 18 Oct 2018 17:46:43 +0900 Subject: [PATCH 15/42] Add test for setting root user explicitly --- pkg/commands/run_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pkg/commands/run_test.go b/pkg/commands/run_test.go index fd3afb591..8609ac67e 100644 --- a/pkg/commands/run_test.go +++ b/pkg/commands/run_test.go @@ -62,6 +62,17 @@ func Test_addDefaultHOME(t *testing.T) { "HOME=/", }, }, + { + name: "HOME isn't set, user is set to root", + user: "root", + initial: []string{ + "PATH=/something/else", + }, + expected: []string{ + "PATH=/something/else", + "HOME=/root", + }, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { From 49b2fa5199034ee554c409b3014aa3a33ca9a9a5 Mon Sep 17 00:00:00 2001 From: peter-evans Date: Thu, 18 Oct 2018 17:59:07 +0900 Subject: [PATCH 16/42] Set root user to default home --- pkg/commands/run.go | 2 +- pkg/constants/constants.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/commands/run.go b/pkg/commands/run.go index dba619680..d8957786c 100644 --- a/pkg/commands/run.go +++ b/pkg/commands/run.go @@ -127,7 +127,7 @@ func addDefaultHOME(u string, envs []string) []string { } // If user isn't set, set default value of HOME - if u == "" { + if u == "" || u == constants.RootUser { return append(envs, fmt.Sprintf("%s=%s", constants.HOME, constants.DefaultHOMEValue)) } diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index d8fcc722e..c3a4ac909 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -58,6 +58,7 @@ const ( HOME = "HOME" // DefaultHOMEValue is the default value Docker sets for $HOME DefaultHOMEValue = "/root" + RootUser = "root" // Docker command names Cmd = "cmd" From 39d6dc1ce736e76c54c185cdc1ec78525023f1ca Mon Sep 17 00:00:00 2001 From: peter-evans Date: Thu, 18 Oct 2018 18:08:34 +0900 Subject: [PATCH 17/42] Modify integration test for setting root user explicitly --- integration/dockerfiles/Dockerfile_test_user_run | 2 ++ 1 file changed, 2 insertions(+) diff --git a/integration/dockerfiles/Dockerfile_test_user_run b/integration/dockerfiles/Dockerfile_test_user_run index bdad4f4b9..3732e625b 100644 --- a/integration/dockerfiles/Dockerfile_test_user_run +++ b/integration/dockerfiles/Dockerfile_test_user_run @@ -21,6 +21,8 @@ USER testuser:1001 RUN echo "hey2" >> /tmp/foo USER root +RUN echo "hi" > $HOME/file +COPY context/foo $HOME/foo RUN useradd -ms /bin/bash newuser USER newuser From 3fc43f4c736ad47488d594e9effdde4a7e3b391c Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Fri, 19 Oct 2018 09:54:49 -0700 Subject: [PATCH 18/42] Add support for .dockerignore file --- README.md | 4 --- cmd/executor/cmd/root.go | 29 +++++++++++------ integration/images.go | 61 ++++++++---------------------------- pkg/config/options.go | 1 - pkg/dockerfile/dockerfile.go | 21 +++++++++++++ 5 files changed, 54 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index de8116141..39c24e310 100644 --- a/README.md +++ b/README.md @@ -321,10 +321,6 @@ _This flag must be used in conjunction with the `--cache=true` flag._ Set this flag to cleanup the filesystem at the end, leaving a clean kaniko container (if you want to build multiple images in the same container, using the debug kaniko image) -#### --ignore - -Set this flag to ignore files in your build context. For examples, set `--ignore pkg/*` to ignore all files in the `pkg` directory. - ### Debug Image The kaniko executor image is based off of scratch and doesn't contain a shell. diff --git a/cmd/executor/cmd/root.go b/cmd/executor/cmd/root.go index 089378442..5bdeb0c87 100644 --- a/cmd/executor/cmd/root.go +++ b/cmd/executor/cmd/root.go @@ -25,6 +25,7 @@ import ( "github.com/GoogleContainerTools/kaniko/pkg/buildcontext" "github.com/GoogleContainerTools/kaniko/pkg/config" "github.com/GoogleContainerTools/kaniko/pkg/constants" + "github.com/GoogleContainerTools/kaniko/pkg/dockerfile" "github.com/GoogleContainerTools/kaniko/pkg/executor" "github.com/GoogleContainerTools/kaniko/pkg/util" "github.com/genuinetools/amicontained/container" @@ -94,7 +95,6 @@ func addKanikoOptionsFlags(cmd *cobra.Command) { RootCmd.PersistentFlags().VarP(&opts.Destinations, "destination", "d", "Registry the final image should be pushed to. Set it repeatedly for multiple destinations.") RootCmd.PersistentFlags().StringVarP(&opts.SnapshotMode, "snapshotMode", "", "full", "Change the file attributes inspected during snapshotting") RootCmd.PersistentFlags().VarP(&opts.BuildArgs, "build-arg", "", "This flag allows you to pass in ARG values at build time. Set it repeatedly for multiple values.") - RootCmd.PersistentFlags().VarP(&opts.Ignore, "ignore", "", "Set this flag to ignore files in the build context. Set it repeatedly for multiple values.") RootCmd.PersistentFlags().BoolVarP(&opts.InsecurePush, "insecure", "", false, "Push to insecure registry using plain HTTP") RootCmd.PersistentFlags().BoolVarP(&opts.SkipTLSVerify, "skip-tls-verify", "", false, "Push to insecure registry ignoring TLS verify") RootCmd.PersistentFlags().StringVarP(&opts.TarPath, "tarPath", "", "", "Path to save the image in as a tarball instead of pushing") @@ -187,23 +187,34 @@ func resolveSourceContext() error { } func removeIgnoredFiles() error { - logrus.Infof("Removing ignored files from build context: %s", opts.Ignore) - for r, i := range opts.Ignore { - opts.Ignore[r] = filepath.Clean(filepath.Join(opts.SrcContext, i)) + if !dockerfile.DockerignoreExists(opts) { + return nil } - err := filepath.Walk(opts.SrcContext, func(path string, fi os.FileInfo, _ error) error { - if ignoreFile(path) { + ignore, err := dockerfile.ParseDockerignore(opts) + if err != nil { + return err + } + logrus.Infof("Removing ignored files from build context: %s", ignore) + for r, i := range ignore { + ignore[r] = filepath.Clean(filepath.Join(opts.SrcContext, i)) + } + err = filepath.Walk(opts.SrcContext, func(path string, fi os.FileInfo, _ error) error { + if ignoreFile(path, ignore) { if err := os.RemoveAll(path); err != nil { logrus.Debugf("error removing %s from buildcontext", path) } } return nil }) - return err + if err != nil { + return err + } + path := filepath.Join(opts.SrcContext, ".dockerignore") + return os.Remove(path) } -func ignoreFile(path string) bool { - for _, i := range opts.Ignore { +func ignoreFile(path string, ignore []string) bool { + for _, i := range ignore { matched, err := filepath.Match(i, path) if err != nil { return false diff --git a/integration/images.go b/integration/images.go index b190e61f6..19d7a91d2 100644 --- a/integration/images.go +++ b/integration/images.go @@ -57,14 +57,6 @@ var argsMap = map[string][]string{ var filesToIgnore = []string{"test/*"} -func ignoreFlags() []string { - var f []string - for _, i := range filesToIgnore { - f = append(f, fmt.Sprintf("--ignore=%s", i)) - } - return f -} - // Arguments to build Dockerfiles with when building with docker var additionalDockerFlagsMap = map[string][]string{ "Dockerfile_test_target": {"--target=second"}, @@ -75,7 +67,6 @@ var additionalKanikoFlagsMap = map[string][]string{ "Dockerfile_test_add": {"--single-snapshot"}, "Dockerfile_test_scratch": {"--single-snapshot"}, "Dockerfile_test_target": {"--target=second"}, - "Dockerfile_test_ignore": ignoreFlags(), } var bucketContextTests = []string{"Dockerfile_test_copy_bucket"} @@ -122,10 +113,9 @@ func FindDockerFiles(dockerfilesPath string) ([]string, error) { // keeps track of which files have been built. type DockerFileBuilder struct { // Holds all available docker files and whether or not they've been built - FilesBuilt map[string]bool - DockerfilesToIgnore map[string]struct{} - TestCacheDockerfiles map[string]struct{} - TestIgnoreDockerfiles map[string]struct{} + FilesBuilt map[string]bool + DockerfilesToIgnore map[string]struct{} + TestCacheDockerfiles map[string]struct{} } // NewDockerFileBuilder will create a DockerFileBuilder initialized with dockerfiles, which @@ -143,9 +133,6 @@ func NewDockerFileBuilder(dockerfiles []string) *DockerFileBuilder { "Dockerfile_test_cache": {}, "Dockerfile_test_cache_install": {}, } - d.TestIgnoreDockerfiles = map[string]struct{}{ - "Dockerfile_test_ignore": {}, - } return &d } @@ -174,13 +161,11 @@ func (d *DockerFileBuilder) BuildImage(imageRepo, gcsBucket, dockerfilesPath, do "."}, additionalFlags...)..., ) - if d.includeDockerIgnore(dockerfile) { - if err := setupTestDir(); err != nil { - return err - } - if err := generateDockerIgnore(); err != nil { - return err - } + if err := setupTestDir(); err != nil { + return err + } + if err := generateDockerIgnore(); err != nil { + return err } _, err := RunCommandWithoutTest(dockerCmd) @@ -188,15 +173,12 @@ func (d *DockerFileBuilder) BuildImage(imageRepo, gcsBucket, dockerfilesPath, do return fmt.Errorf("Failed to build image %s with docker command \"%s\": %s", dockerImage, dockerCmd.Args, err) } - if d.includeDockerIgnore(dockerfilesPath) { - if err := deleteDockerIgnore(); err != nil { - return err - } - if err := setupTestDir(); err != nil { - return err - } + if err := setupTestDir(); err != nil { + return err + } + if err := generateDockerIgnore(); err != nil { + return err } - defer removeTestDir() contextFlag := "-c" contextPath := buildContextPath @@ -288,23 +270,10 @@ func (d *DockerFileBuilder) buildCachedImages(imageRepo, cacheRepo, dockerfilesP return nil } -func (d *DockerFileBuilder) includeDockerIgnore(dockerfile string) bool { - for i := range d.TestIgnoreDockerfiles { - if i == dockerfile { - return true - } - } - return false -} - func setupTestDir() error { return os.MkdirAll(testDirPath, 0644) } -func removeTestDir() error { - return os.RemoveAll(testDirPath) -} - func generateDockerIgnore() error { f, err := os.Create(".dockerignore") if err != nil { @@ -317,7 +286,3 @@ func generateDockerIgnore() error { } return nil } - -func deleteDockerIgnore() error { - return os.Remove(".dockerignore") -} diff --git a/pkg/config/options.go b/pkg/config/options.go index 2c9195598..fc15c1f21 100644 --- a/pkg/config/options.go +++ b/pkg/config/options.go @@ -28,7 +28,6 @@ type KanikoOptions struct { CacheDir string Destinations multiArg BuildArgs multiArg - Ignore multiArg InsecurePush bool SkipTLSVerify bool SingleSnapshot bool diff --git a/pkg/dockerfile/dockerfile.go b/pkg/dockerfile/dockerfile.go index 8b06985b6..c2eb9bff7 100644 --- a/pkg/dockerfile/dockerfile.go +++ b/pkg/dockerfile/dockerfile.go @@ -20,6 +20,7 @@ import ( "bytes" "fmt" "io/ioutil" + "path/filepath" "strconv" "strings" @@ -168,3 +169,23 @@ func saveStage(index int, stages []instructions.Stage) bool { } return false } + +// DockerignoreExists returns true if .dockerignore exists in the source context +func DockerignoreExists(opts *config.KanikoOptions) bool { + path := filepath.Join(opts.SrcContext, ".dockerignore") + return util.FilepathExists(path) +} + +// ParseDockerignore returns a list of all paths in .dockerignore +func ParseDockerignore(opts *config.KanikoOptions) ([]string, error) { + path := filepath.Join(opts.SrcContext, ".dockerignore") + contents, err := ioutil.ReadFile(path) + if err != nil { + return nil, errors.Wrap(err, "parsing .dockerignore") + } + return strings.FieldsFunc(string(contents), split), nil +} + +func split(r rune) bool { + return r == '\n' || r == ' ' +} From 05e3250043fad5cf80fe898e8aab6ef849b86ef5 Mon Sep 17 00:00:00 2001 From: Daisuke Taniwaki Date: Tue, 23 Oct 2018 06:33:41 +0900 Subject: [PATCH 19/42] Support insecure pull (#401) --- cmd/executor/cmd/root.go | 2 +- pkg/config/options.go | 2 +- pkg/executor/push.go | 2 +- pkg/util/image_util.go | 31 ++++++++++++++++++++++++++++--- pkg/util/image_util_test.go | 6 +++--- 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/cmd/executor/cmd/root.go b/cmd/executor/cmd/root.go index 55d256f28..eef088ade 100644 --- a/cmd/executor/cmd/root.go +++ b/cmd/executor/cmd/root.go @@ -91,7 +91,7 @@ func addKanikoOptionsFlags(cmd *cobra.Command) { RootCmd.PersistentFlags().VarP(&opts.Destinations, "destination", "d", "Registry the final image should be pushed to. Set it repeatedly for multiple destinations.") RootCmd.PersistentFlags().StringVarP(&opts.SnapshotMode, "snapshotMode", "", "full", "Change the file attributes inspected during snapshotting") RootCmd.PersistentFlags().VarP(&opts.BuildArgs, "build-arg", "", "This flag allows you to pass in ARG values at build time. Set it repeatedly for multiple values.") - RootCmd.PersistentFlags().BoolVarP(&opts.InsecurePush, "insecure", "", false, "Push to insecure registry using plain HTTP") + RootCmd.PersistentFlags().BoolVarP(&opts.Insecure, "insecure", "", false, "Pull and push to insecure registry using plain HTTP") RootCmd.PersistentFlags().BoolVarP(&opts.SkipTLSVerify, "skip-tls-verify", "", false, "Push to insecure registry ignoring TLS verify") RootCmd.PersistentFlags().StringVarP(&opts.TarPath, "tarPath", "", "", "Path to save the image in as a tarball instead of pushing") RootCmd.PersistentFlags().BoolVarP(&opts.SingleSnapshot, "single-snapshot", "", false, "Take a single snapshot at the end of the build.") diff --git a/pkg/config/options.go b/pkg/config/options.go index fc15c1f21..7cf6c07d2 100644 --- a/pkg/config/options.go +++ b/pkg/config/options.go @@ -28,7 +28,7 @@ type KanikoOptions struct { CacheDir string Destinations multiArg BuildArgs multiArg - InsecurePush bool + Insecure bool SkipTLSVerify bool SingleSnapshot bool Reproducible bool diff --git a/pkg/executor/push.go b/pkg/executor/push.go index 6cfd39e9e..dcf49a647 100644 --- a/pkg/executor/push.go +++ b/pkg/executor/push.go @@ -71,7 +71,7 @@ func DoPush(image v1.Image, opts *config.KanikoOptions) error { // continue pushing unless an error occurs for _, destRef := range destRefs { - if opts.InsecurePush { + if opts.Insecure { newReg, err := name.NewInsecureRegistry(destRef.Repository.Registry.Name(), name.WeakValidation) if err != nil { return errors.Wrap(err, "getting new insecure registry") diff --git a/pkg/util/image_util.go b/pkg/util/image_util.go index 6eb41aa57..8c2c30141 100644 --- a/pkg/util/image_util.go +++ b/pkg/util/image_util.go @@ -17,6 +17,8 @@ limitations under the License. package util import ( + "crypto/tls" + "net/http" "path/filepath" "strconv" @@ -72,7 +74,7 @@ func RetrieveSourceImage(stage config.KanikoStage, buildArgs []string, opts *con } // Otherwise, initialize image as usual - return retrieveRemoteImage(currentBaseName) + return retrieveRemoteImage(currentBaseName, opts) } // RetrieveConfigFile returns the config file for an image @@ -93,18 +95,41 @@ func tarballImage(index int) (v1.Image, error) { return tarball.ImageFromPath(tarPath, nil) } -func remoteImage(image string) (v1.Image, error) { +func remoteImage(image string, opts *config.KanikoOptions) (v1.Image, error) { logrus.Infof("Downloading base image %s", image) ref, err := name.ParseReference(image, name.WeakValidation) if err != nil { return nil, err } + + if opts.Insecure { + newReg, err := name.NewInsecureRegistry(ref.Context().RegistryStr(), name.WeakValidation) + if err != nil { + return nil, err + } + if tag, ok := ref.(name.Tag); ok { + tag.Repository.Registry = newReg + ref = tag + } + if digest, ok := ref.(name.Digest); ok { + digest.Repository.Registry = newReg + ref = digest + } + } + + tr := http.DefaultTransport.(*http.Transport) + if opts.SkipTLSVerify { + tr.TLSClientConfig = &tls.Config{ + InsecureSkipVerify: true, + } + } + k8sc, err := k8schain.NewNoClient() if err != nil { return nil, err } kc := authn.NewMultiKeychain(authn.DefaultKeychain, k8sc) - return remote.Image(ref, remote.WithAuthFromKeychain(kc)) + return remote.Image(ref, remote.WithTransport(tr), remote.WithAuthFromKeychain(kc)) } func cachedImage(opts *config.KanikoOptions, image string) (v1.Image, error) { diff --git a/pkg/util/image_util_test.go b/pkg/util/image_util_test.go index 44897fe72..272e9e284 100644 --- a/pkg/util/image_util_test.go +++ b/pkg/util/image_util_test.go @@ -32,11 +32,11 @@ var ( dockerfile = ` FROM gcr.io/distroless/base:latest as base COPY . . - + FROM scratch as second ENV foopath context/foo COPY --from=0 $foopath context/b* /foo/ - + FROM base ARG file COPY --from=second /foo $file` @@ -51,7 +51,7 @@ func Test_StandardImage(t *testing.T) { defer func() { retrieveRemoteImage = original }() - mock := func(image string) (v1.Image, error) { + mock := func(image string, opts *config.KanikoOptions) (v1.Image, error) { return nil, nil } retrieveRemoteImage = mock From e04a922dc34189a32c501065cc23e9b3d8393de4 Mon Sep 17 00:00:00 2001 From: Daisuke Taniwaki Date: Thu, 25 Oct 2018 06:33:58 +0900 Subject: [PATCH 20/42] Separate insecure pull options --- cmd/executor/cmd/root.go | 4 +++- pkg/config/options.go | 36 +++++++++++++++++++----------------- pkg/util/image_util.go | 4 ++-- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/cmd/executor/cmd/root.go b/cmd/executor/cmd/root.go index eef088ade..b43a85b68 100644 --- a/cmd/executor/cmd/root.go +++ b/cmd/executor/cmd/root.go @@ -91,8 +91,10 @@ func addKanikoOptionsFlags(cmd *cobra.Command) { RootCmd.PersistentFlags().VarP(&opts.Destinations, "destination", "d", "Registry the final image should be pushed to. Set it repeatedly for multiple destinations.") RootCmd.PersistentFlags().StringVarP(&opts.SnapshotMode, "snapshotMode", "", "full", "Change the file attributes inspected during snapshotting") RootCmd.PersistentFlags().VarP(&opts.BuildArgs, "build-arg", "", "This flag allows you to pass in ARG values at build time. Set it repeatedly for multiple values.") - RootCmd.PersistentFlags().BoolVarP(&opts.Insecure, "insecure", "", false, "Pull and push to insecure registry using plain HTTP") + RootCmd.PersistentFlags().BoolVarP(&opts.Insecure, "insecure", "", false, "Push to insecure registry using plain HTTP") RootCmd.PersistentFlags().BoolVarP(&opts.SkipTLSVerify, "skip-tls-verify", "", false, "Push to insecure registry ignoring TLS verify") + RootCmd.PersistentFlags().BoolVarP(&opts.InsecurePull, "insecure-pull", "", false, "Pull from insecure registry using plain HTTP") + RootCmd.PersistentFlags().BoolVarP(&opts.SkipTLSVerifyPull, "skip-tls-verify-pull", "", false, "Pull from insecure registry ignoring TLS verify") RootCmd.PersistentFlags().StringVarP(&opts.TarPath, "tarPath", "", "", "Path to save the image in as a tarball instead of pushing") RootCmd.PersistentFlags().BoolVarP(&opts.SingleSnapshot, "single-snapshot", "", false, "Take a single snapshot at the end of the build.") RootCmd.PersistentFlags().BoolVarP(&opts.Reproducible, "reproducible", "", false, "Strip timestamps out of the image to make it reproducible") diff --git a/pkg/config/options.go b/pkg/config/options.go index 7cf6c07d2..a9a57c9e4 100644 --- a/pkg/config/options.go +++ b/pkg/config/options.go @@ -18,23 +18,25 @@ package config // KanikoOptions are options that are set by command line arguments type KanikoOptions struct { - DockerfilePath string - SrcContext string - SnapshotMode string - Bucket string - TarPath string - Target string - CacheRepo string - CacheDir string - Destinations multiArg - BuildArgs multiArg - Insecure bool - SkipTLSVerify bool - SingleSnapshot bool - Reproducible bool - NoPush bool - Cache bool - Cleanup bool + DockerfilePath string + SrcContext string + SnapshotMode string + Bucket string + TarPath string + Target string + CacheRepo string + CacheDir string + Destinations multiArg + BuildArgs multiArg + Insecure bool + SkipTLSVerify bool + InsecurePull bool + SkipTLSVerifyPull bool + SingleSnapshot bool + Reproducible bool + NoPush bool + Cache bool + Cleanup bool } // WarmerOptions are options that are set by command line arguments to the cache warmer. diff --git a/pkg/util/image_util.go b/pkg/util/image_util.go index 8c2c30141..d0db22321 100644 --- a/pkg/util/image_util.go +++ b/pkg/util/image_util.go @@ -102,7 +102,7 @@ func remoteImage(image string, opts *config.KanikoOptions) (v1.Image, error) { return nil, err } - if opts.Insecure { + if opts.InsecurePull { newReg, err := name.NewInsecureRegistry(ref.Context().RegistryStr(), name.WeakValidation) if err != nil { return nil, err @@ -118,7 +118,7 @@ func remoteImage(image string, opts *config.KanikoOptions) (v1.Image, error) { } tr := http.DefaultTransport.(*http.Transport) - if opts.SkipTLSVerify { + if opts.SkipTLSVerifyPull { tr.TLSClientConfig = &tls.Config{ InsecureSkipVerify: true, } From 1639d1d71c69537288f84ae586d9fd34d536aaf5 Mon Sep 17 00:00:00 2001 From: Sharif Elgamal Date: Thu, 25 Oct 2018 16:42:34 -0700 Subject: [PATCH 21/42] adding the cache warmer to the release process (#412) --- deploy/cloudbuild-release.yaml | 11 ++++++++++- deploy/cloudbuild.yaml | 11 ++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/deploy/cloudbuild-release.yaml b/deploy/cloudbuild-release.yaml index a86b0b0da..ee5b1d694 100644 --- a/deploy/cloudbuild-release.yaml +++ b/deploy/cloudbuild-release.yaml @@ -14,7 +14,16 @@ steps: - name: "gcr.io/cloud-builders/docker" args: ["tag", "gcr.io/kaniko-project/executor:debug-$TAG_NAME", "gcr.io/kaniko-project/executor:debug"] + # Then, we want to build the cache warmer + - name: "gcr.io/cloud-builders/docker" + args: ["build", "-f", "deploy/Dockerfile_warmer", + "-t", "gcr.io/kaniko-project/warmer:$TAG_NAME", "."] + - name: "gcr.io/cloud-builders/docker" + args: ["tag", "gcr.io/kaniko-project/warmer:$TAG_NAME", + "gcr.io/kaniko-project/warmer:latest"] images: ["gcr.io/kaniko-project/executor:$TAG_NAME", "gcr.io/kaniko-project/executor:latest", "gcr.io/kaniko-project/executor:debug-$TAG_NAME", - "gcr.io/kaniko-project/executor:debug"] + "gcr.io/kaniko-project/executor:debug", + "gcr.io/kaniko-project/warmer:$TAG_NAME", + "gcr.io/kaniko-project/warmer:latest"] diff --git a/deploy/cloudbuild.yaml b/deploy/cloudbuild.yaml index 6e2e074af..e01b52390 100644 --- a/deploy/cloudbuild.yaml +++ b/deploy/cloudbuild.yaml @@ -13,7 +13,16 @@ steps: - name: "gcr.io/cloud-builders/docker" args: ["build", "-f", "deploy/Dockerfile_debug", "-t", "gcr.io/kaniko-project/executor:debug", "."] + # Then, we want to build the cache warmer + - name: "gcr.io/cloud-builders/docker" + args: ["build", "-f", "deploy/Dockerfile_warmer", + "-t", "gcr.io/kaniko-project/warmer:${COMMIT_SHA}", "."] + - name: "gcr.io/cloud-builders/docker" + args: ["build", "-f", "deploy/Dockerfile_warmer", + "-t", "gcr.io/kaniko-project/warmer:latest", "."] images: ["gcr.io/kaniko-project/executor:${COMMIT_SHA}", "gcr.io/kaniko-project/executor:latest", "gcr.io/kaniko-project/executor:debug-${COMMIT_SHA}", - "gcr.io/kaniko-project/executor:debug"] + "gcr.io/kaniko-project/executor:debug", + "gcr.io/kaniko-project/warmer:${COMMIT_SHA}", + "gcr.io/kaniko-project/warmer:latest"] From e8aab7e17e1d8baab5da91543fcf7e5a32231144 Mon Sep 17 00:00:00 2001 From: Daisuke Taniwaki Date: Fri, 26 Oct 2018 12:20:54 +0900 Subject: [PATCH 22/42] Update README --- README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 39c24e310..aad3bdc71 100644 --- a/README.md +++ b/README.md @@ -298,11 +298,19 @@ Set this flag if you only want to build the image, without pushing to a registry #### --insecure -Set this flag if you want to connect to a plain HTTP registry. It is supposed to be used for testing purposes only and should not be used in production! +Set this flag if you want to push images to a plain HTTP registry. It is supposed to be used for testing purposes only and should not be used in production! #### --skip-tls-verify -Set this flag to skip TLS certificate validation when connecting to a registry. It is supposed to be used for testing purposes only and should not be used in production! +Set this flag to skip TLS certificate validation when pushing images to a registry. It is supposed to be used for testing purposes only and should not be used in production! + +#### --insecure-pull + +Set this flag if you want to pull images from a plain HTTP registry. It is supposed to be used for testing purposes only and should not be used in production! + +#### --skip-tls-verify-pull + +Set this flag to skip TLS certificate validation when pulling images from a registry. It is supposed to be used for testing purposes only and should not be used in production! #### --cache @@ -413,4 +421,4 @@ file are made and when the `mtime` is updated. This means: which will still be correct, but it does affect the number of layers. _Note that these issues are currently theoretical only. If you see this issue occur, please -[open an issue](https://github.com/GoogleContainerTools/kaniko/issues)._ \ No newline at end of file +[open an issue](https://github.com/GoogleContainerTools/kaniko/issues)._ From 9908eeb30a773eb4bbf5fa4bd1a9c35229201792 Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Fri, 26 Oct 2018 11:38:32 -0700 Subject: [PATCH 23/42] Use remoteImage function when getting digest for cache Issue #410 experienced an error with base image caching where they were "Not Authorized" to get information for a remote image, but later were able to download and extract the base image. To fix this, we can switch to using the remoteImage function for getting information about the digest, which is the same function used for downloading base images. This way we can also take advantage of the --insecure and --skip-tls-verify flags if users pass those in when trying to get digests for the cache as well. --- pkg/util/image_util.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/util/image_util.go b/pkg/util/image_util.go index 8c2c30141..991a54e5f 100644 --- a/pkg/util/image_util.go +++ b/pkg/util/image_util.go @@ -142,7 +142,7 @@ func cachedImage(opts *config.KanikoOptions, image string) (v1.Image, error) { if d, ok := ref.(name.Digest); ok { cacheKey = d.DigestStr() } else { - img, err := remote.Image(ref) + img, err := remoteImage(image, opts) if err != nil { return nil, err } From b8c1314b3969fd9df5e5a3bf503b6bd9e0a08e17 Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Fri, 26 Oct 2018 16:44:18 -0400 Subject: [PATCH 24/42] "Container Builder" - > "Cloud Build" --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 39c24e310..60c757055 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ We do **not** recommend running the kaniko executor binary in another image, as - [Running kaniko](#running-kaniko) - [Running kaniko in a Kubernetes cluster](#running-kaniko-in-a-kubernetes-cluster) - [Running kaniko in gVisor](#running-kaniko-in-gvisor) - - [Running kaniko in Google Container Builder](#running-kaniko-in-google-container-builder) + - [Running kaniko in Google Cloud Build](#running-kaniko-in-google-cloud-build) - [Running kaniko locally](#running-kaniko-locally) - [Caching](#caching) - [Pushing to Different Registries](#pushing-to-different-registries) @@ -87,7 +87,7 @@ There are several different ways to deploy and run kaniko: - [In a Kubernetes cluster](#running-kaniko-in-a-kubernetes-cluster) - [In gVisor](#running-kaniko-in-gvisor) -- [In Google Container Builder](#running-kaniko-in-google-container-builder) +- [In Google Cloud Build](#running-kaniko-in-google-cloud-build) - [Locally](#running-kaniko-locally) #### Running kaniko in a Kubernetes cluster @@ -154,7 +154,7 @@ gcr.io/kaniko-project/executor:latest \ We pass in `--runtime=runsc` to use gVisor. This example mounts the current directory to `/workspace` for the build context and the `~/.config` directory for GCR credentials. -#### Running kaniko in Google Container Builder +#### Running kaniko in Google Cloud Build To run kaniko in GCB, add it to your build config as a build step: @@ -413,4 +413,4 @@ file are made and when the `mtime` is updated. This means: which will still be correct, but it does affect the number of layers. _Note that these issues are currently theoretical only. If you see this issue occur, please -[open an issue](https://github.com/GoogleContainerTools/kaniko/issues)._ \ No newline at end of file +[open an issue](https://github.com/GoogleContainerTools/kaniko/issues)._ From f1dfc2c68537b06be75640929d6b4e3ab6c31bd1 Mon Sep 17 00:00:00 2001 From: j0shua Date: Fri, 26 Oct 2018 16:47:50 -0400 Subject: [PATCH 25/42] adding exit 1 when there are not enough command line vars passed to `run in docker` script --- run_in_docker.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/run_in_docker.sh b/run_in_docker.sh index dd90ea947..2ffafc932 100755 --- a/run_in_docker.sh +++ b/run_in_docker.sh @@ -17,6 +17,7 @@ set -e if [ $# -ne 3 ]; then echo "Usage: run_in_docker.sh " + exit 1 fi dockerfile=$1 From bf9f13b045e944727ff8aeab06b0c331f8c4a2d9 Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Mon, 29 Oct 2018 11:45:06 -0700 Subject: [PATCH 26/42] Update README Updated README to clarify: 1. What a build context is and how kaniko interacts with it 2. How to set up a Kubernetes secret for auth to push the final image Also made some general fixes to make the docs and the run_in_docker script more clearer. --- README.md | 46 +++++++++++++++++++++++++++++++++------------- run_in_docker.sh | 4 ++-- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 45218ffff..b3557ad66 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ We do **not** recommend running the kaniko executor binary in another image, as - [Running kaniko in a Kubernetes cluster](#running-kaniko-in-a-kubernetes-cluster) - [Running kaniko in gVisor](#running-kaniko-in-gvisor) - [Running kaniko in Google Cloud Build](#running-kaniko-in-google-cloud-build) - - [Running kaniko locally](#running-kaniko-locally) + - [Running kaniko in Docker](#running-kaniko-in-Docker) - [Caching](#caching) - [Pushing to Different Registries](#pushing-to-different-registries) - [Additional Flags](#additional-flags) @@ -57,8 +57,20 @@ To use kaniko to build and push an image for you, you will need: ### kaniko Build Contexts -kaniko currently supports local directories, Google Cloud Storage and Amazon S3 as build contexts. -If using a GCS or S3 bucket, the bucket should contain a compressed tar of the build context, which kaniko will unpack and use. +kaniko's build context is very similar to the build context you would send your Docker daemon for an image build; it represents a directory containing a Dockerfile which kaniko will use to build your image. +For example, a `COPY` command in your Dockerfile should refer to a file in the build context. + +You will need to store your build context in a place that kaniko can access. +Right now, kaniko supports these storage solutions: +- GCS Bucket +- S3 Bucket +- Local Directory + +_Note: the local directory 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._ + +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. To create a compressed tar, you can run: ```shell @@ -70,11 +82,11 @@ For example, we can copy over the compressed tar to a GCS bucket with gsutil: gsutil cp context.tar.gz gs:// ``` -Use the `--context` flag with the appropriate prefix to specify your build context: +When running kaniko, use the `--context` flag with the appropriate prefix to specify the location of your build context: | Source | Prefix | |---------|---------| -| Local Directory | dir://[path to directory] | +| Local Directory | dir://[path to a directory in the kaniko container] | | GCS Bucket | gs://[bucket name]/[path to .tar.gz] | | S3 Bucket | s3://[bucket name]/[path to .tar.gz] | @@ -88,7 +100,7 @@ There are several different ways to deploy and run kaniko: - [In a Kubernetes cluster](#running-kaniko-in-a-kubernetes-cluster) - [In gVisor](#running-kaniko-in-gvisor) - [In Google Cloud Build](#running-kaniko-in-google-cloud-build) -- [Locally](#running-kaniko-locally) +- [In Docker](#running-kaniko-in-docker) #### Running kaniko in a Kubernetes cluster @@ -96,19 +108,24 @@ Requirements: - Standard Kubernetes cluster (e.g. using [GKE](https://cloud.google.com/kubernetes-engine/)) - [Kubernetes Secret](#kubernetes-secret) +- A [build context](#kaniko-build-contexts) ##### Kubernetes secret To run kaniko in a Kubernetes cluster, you will need a standard running Kubernetes cluster and a Kubernetes secret, which contains the auth required to push the final image. -To create the secret, first you will need to create a service account in the Google Cloud Console project you want to push the final image to, with `Storage Admin` permissions. -You can download a JSON key for this service account, and rename it `kaniko-secret.json`. -To create the secret, run: +To create a secret to authenticate to Google Cloud Registry, follow these steps: +1. Create a service account in the Google Cloud Console project you want to push the final image to with `Storage Admin` permissions. +2. Download a JSON key for this service account +3. Rename the key to `kaniko-secret.json` +4. To create the secret, run: ```shell kubectl create secret generic kaniko-secret --from-file= ``` +_Note: If using a GCS bucket in the same GCP project as a build context, this service account should now also have permissions to read from that bucket._ + The Kubernetes Pod spec should look similar to this, with the args parameters filled in: ```yaml @@ -120,7 +137,7 @@ spec: containers: - name: kaniko image: gcr.io/kaniko-project/executor:latest - args: ["--dockerfile=", + args: ["--dockerfile=", "--context=gs:///", "--destination="] volumeMounts: @@ -156,19 +173,22 @@ This example mounts the current directory to `/workspace` for the build context #### Running kaniko in Google Cloud Build +Requirements: +- A [build context](#kaniko-build-contexts) + To run kaniko in GCB, add it to your build config as a build step: ```yaml steps: - name: gcr.io/kaniko-project/executor:latest - args: ["--dockerfile=", + args: ["--dockerfile=", "--context=dir://", "--destination="] ``` kaniko will build and push the final image in this build step. -#### Running kaniko locally +#### Running kaniko in Docker Requirements: @@ -245,7 +265,7 @@ To configure credentials, you will need to do the following: containers: - name: kaniko image: gcr.io/kaniko-project/executor:latest - args: ["--dockerfile=", + args: ["--dockerfile=", "--context=s3:///", "--destination="] volumeMounts: diff --git a/run_in_docker.sh b/run_in_docker.sh index 2ffafc932..623779ca1 100755 --- a/run_in_docker.sh +++ b/run_in_docker.sh @@ -22,7 +22,7 @@ fi dockerfile=$1 context=$2 -tag=$3 +destination=$3 if [[ ! -e $HOME/.config/gcloud/application_default_credentials.json ]]; then echo "Application Default Credentials do not exist. Run [gcloud auth application-default login] to configure them" @@ -33,4 +33,4 @@ docker run \ -v $HOME/.config/gcloud:/root/.config/gcloud \ -v ${context}:/workspace \ gcr.io/kaniko-project/executor:latest \ - -f ${dockerfile} -d ${tag} -c /workspace/ + --dockerfile ${dockerfile} --destination ${destination} --context dir:///workspace/ From ede93786a13207677f9eb4712ebb2ac457478f15 Mon Sep 17 00:00:00 2001 From: Sharif Elgamal Date: Mon, 29 Oct 2018 12:02:40 -0700 Subject: [PATCH 27/42] fix releasing the cache warmer (#418) --- deploy/Dockerfile_warmer | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/Dockerfile_warmer b/deploy/Dockerfile_warmer index 2cee0968c..1dbfeba00 100644 --- a/deploy/Dockerfile_warmer +++ b/deploy/Dockerfile_warmer @@ -17,7 +17,7 @@ FROM golang:1.10 WORKDIR /go/src/github.com/GoogleContainerTools/kaniko COPY . . -RUN make +RUN make out/warmer FROM scratch COPY --from=0 /go/src/github.com/GoogleContainerTools/kaniko/out/warmer /kaniko/warmer From 9a1a2ef9e334a21a0b74c4874894cba187168e13 Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Mon, 29 Oct 2018 13:44:11 -0700 Subject: [PATCH 28/42] Update go-containerregistry Update go-containerregistry to merge in this [PR](https://github.com/google/go-containerregistry/pull/293) and fix --- Gopkg.lock | 5 ++--- Gopkg.toml | 4 ++++ .../go-containerregistry/pkg/v1/remote/transport/basic.go | 3 ++- .../go-containerregistry/pkg/v1/remote/transport/bearer.go | 3 ++- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 410e2aae1..b11e5ee73 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -430,8 +430,7 @@ version = "v0.2.0" [[projects]] - branch = "master" - digest = "1:764d0a9bb2c987d9333c5b0a256bc94791db50c0b5be2fa10f2247b1dcbd7a04" + digest = "1:f1b23f53418c1b035a5965ac2600a28b16c08643683d5213fb581ecf4e79a02a" name = "github.com/google/go-containerregistry" packages = [ "pkg/authn", @@ -450,7 +449,7 @@ "pkg/v1/v1util", ] pruneopts = "NUT" - revision = "24bbadfcffb5e05b1578cb2bd5438992ada3b546" + revision = "88d8d18eb1bde1fcef23c745205c738074290515" [[projects]] digest = "1:f4f203acd8b11b8747bdcd91696a01dbc95ccb9e2ca2db6abf81c3a4f5e950ce" diff --git a/Gopkg.toml b/Gopkg.toml index bf5263598..9431660cc 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -35,6 +35,10 @@ required = [ name = "k8s.io/client-go" version = "kubernetes-1.11.0" +[[constraint]] + name = "github.com/google/go-containerregistry" + revision = "88d8d18eb1bde1fcef23c745205c738074290515" + [[override]] name = "k8s.io/apimachinery" version = "kubernetes-1.11.0" diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/basic.go b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/basic.go index 752038cb1..e77f47f69 100644 --- a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/basic.go +++ b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/basic.go @@ -39,7 +39,8 @@ func (bt *basicTransport) RoundTrip(in *http.Request) (*http.Response, error) { // abstraction, so to avoid forwarding Authorization headers to places // we are redirected, only set it when the authorization header matches // the host with which we are interacting. - if in.Host == bt.target { + // In case of redirect http.Client can use an empty Host, check URL too. + if in.Host == bt.target || in.URL.Host == bt.target { in.Header.Set("Authorization", hdr) } in.Header.Set("User-Agent", transportName) diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/bearer.go b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/bearer.go index a45121390..2bfdb6e24 100644 --- a/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/bearer.go +++ b/vendor/github.com/google/go-containerregistry/pkg/v1/remote/transport/bearer.go @@ -56,7 +56,8 @@ func (bt *bearerTransport) RoundTrip(in *http.Request) (*http.Response, error) { // abstraction, so to avoid forwarding Authorization headers to places // we are redirected, only set it when the authorization header matches // the registry with which we are interacting. - if in.Host == bt.registry.RegistryStr() { + // In case of redirect http.Client can use an empty Host, check URL too. + if in.Host == bt.registry.RegistryStr() || in.URL.Host == bt.registry.RegistryStr() { in.Header.Set("Authorization", hdr) } in.Header.Set("User-Agent", transportName) From cb0a5e0a18283bb0807ec6714585809b510f90e7 Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Fri, 19 Oct 2018 14:55:56 -0700 Subject: [PATCH 29/42] Fix integration tests --- cmd/executor/cmd/root.go | 6 +++++- integration/images.go | 14 +++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/cmd/executor/cmd/root.go b/cmd/executor/cmd/root.go index 5bdeb0c87..dcbb48430 100644 --- a/cmd/executor/cmd/root.go +++ b/cmd/executor/cmd/root.go @@ -63,7 +63,7 @@ var RootCmd = &cobra.Command{ return errors.Wrap(err, "error resolving source context") } if err := removeIgnoredFiles(); err != nil { - return errors.Wrap(err, "error removing ignored files from build context") + return errors.Wrap(err, "error removing .dockerignore files from build context") } return resolveDockerfilePath() }, @@ -198,9 +198,11 @@ func removeIgnoredFiles() error { for r, i := range ignore { ignore[r] = filepath.Clean(filepath.Join(opts.SrcContext, i)) } + // first, remove all files in .dockerignore err = filepath.Walk(opts.SrcContext, func(path string, fi os.FileInfo, _ error) error { if ignoreFile(path, ignore) { if err := os.RemoveAll(path); err != nil { + // don't return error, because this path could have been removed already logrus.Debugf("error removing %s from buildcontext", path) } } @@ -209,10 +211,12 @@ func removeIgnoredFiles() error { if err != nil { return err } + // then, remove .dockerignore path := filepath.Join(opts.SrcContext, ".dockerignore") return os.Remove(path) } +// ignoreFile returns true if the path matches any of the paths in ignore func ignoreFile(path string, ignore []string) bool { for _, i := range ignore { matched, err := filepath.Match(i, path) diff --git a/integration/images.go b/integration/images.go index 19d7a91d2..1f392be57 100644 --- a/integration/images.go +++ b/integration/images.go @@ -37,7 +37,7 @@ const ( buildContextPath = "/workspace" cacheDir = "/workspace/cache" baseImageToCache = "gcr.io/google-appengine/debian9@sha256:1d6a9a6d106bd795098f60f4abb7083626354fa6735e81743c7f8cfca11259f0" - testDirPath = "test/dir/path" + testDirPath = "context/test" ) // Arguments to build Dockerfiles with, used for both docker and kaniko builds @@ -55,7 +55,7 @@ var argsMap = map[string][]string{ "Dockerfile_test_multistage": {"file=/foo2"}, } -var filesToIgnore = []string{"test/*"} +var filesToIgnore = []string{"context/test/*"} // Arguments to build Dockerfiles with when building with docker var additionalDockerFlagsMap = map[string][]string{ @@ -271,7 +271,15 @@ func (d *DockerFileBuilder) buildCachedImages(imageRepo, cacheRepo, dockerfilesP } func setupTestDir() error { - return os.MkdirAll(testDirPath, 0644) + if err := os.MkdirAll(testDirPath, 0750); err != nil { + return err + } + p := filepath.Join(testDirPath, "foo") + f, err := os.Create(p) + if err != nil { + return err + } + return f.Close() } func generateDockerIgnore() error { From ff4e624c6b42152c451e795f0ac19c1361d01d10 Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Tue, 30 Oct 2018 16:06:46 -0700 Subject: [PATCH 30/42] don't delete .dockerignore --- cmd/executor/cmd/root.go | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/cmd/executor/cmd/root.go b/cmd/executor/cmd/root.go index dcbb48430..fa244d332 100644 --- a/cmd/executor/cmd/root.go +++ b/cmd/executor/cmd/root.go @@ -198,8 +198,8 @@ func removeIgnoredFiles() error { for r, i := range ignore { ignore[r] = filepath.Clean(filepath.Join(opts.SrcContext, i)) } - // first, remove all files in .dockerignore - err = filepath.Walk(opts.SrcContext, func(path string, fi os.FileInfo, _ error) error { + // remove all files in .dockerignore + return filepath.Walk(opts.SrcContext, func(path string, fi os.FileInfo, _ error) error { if ignoreFile(path, ignore) { if err := os.RemoveAll(path); err != nil { // don't return error, because this path could have been removed already @@ -208,12 +208,6 @@ func removeIgnoredFiles() error { } return nil }) - if err != nil { - return err - } - // then, remove .dockerignore - path := filepath.Join(opts.SrcContext, ".dockerignore") - return os.Remove(path) } // ignoreFile returns true if the path matches any of the paths in ignore From 55e6157000286a84f4143e5074b2e4b14292e014 Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Wed, 31 Oct 2018 11:41:58 -0700 Subject: [PATCH 31/42] Fix bugs with .dockerignore and improve integration test I improved handling of the .dockerignore file by: 1. Using docker's parser to parse the .dockerignore and using their helper functions to determine if a file should be deleted 2. Copying the Dockerfile we are building to /kaniko/Dockerfile so that if the Dockerfile is specified in .dockerignore it won't be deleted, and if it is specified in the .dockerignore it won't end up in the final image 3. I also improved the integration test to create a temp directory with files to ignore, and updated the .dockerignore to include exclusions (!) --- cmd/executor/cmd/root.go | 52 +++++++-------- integration/dockerignore.go | 112 ++++++++++++++++++++++++++++++++ integration/images.go | 41 ------------ integration/integration_test.go | 25 +++++++ pkg/constants/constants.go | 3 + pkg/dockerfile/dockerfile.go | 8 +-- 6 files changed, 167 insertions(+), 74 deletions(-) create mode 100644 integration/dockerignore.go diff --git a/cmd/executor/cmd/root.go b/cmd/executor/cmd/root.go index 3172a88d0..262856a76 100644 --- a/cmd/executor/cmd/root.go +++ b/cmd/executor/cmd/root.go @@ -28,6 +28,7 @@ import ( "github.com/GoogleContainerTools/kaniko/pkg/dockerfile" "github.com/GoogleContainerTools/kaniko/pkg/executor" "github.com/GoogleContainerTools/kaniko/pkg/util" + "github.com/docker/docker/pkg/fileutils" "github.com/genuinetools/amicontained/container" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -62,10 +63,10 @@ var RootCmd = &cobra.Command{ if err := resolveSourceContext(); err != nil { return errors.Wrap(err, "error resolving source context") } - if err := removeIgnoredFiles(); err != nil { - return errors.Wrap(err, "error removing .dockerignore files from build context") + if err := resolveDockerfilePath(); err != nil { + return errors.Wrap(err, "error resolving dockerfile path") } - return resolveDockerfilePath() + return removeIgnoredFiles() }, Run: func(cmd *cobra.Command, args []string) { if !checkContained() { @@ -144,7 +145,7 @@ func resolveDockerfilePath() error { return errors.Wrap(err, "getting absolute path for dockerfile") } opts.DockerfilePath = abs - return nil + return copyDockerfile() } // Otherwise, check if the path relative to the build context exists if util.FilepathExists(filepath.Join(opts.SrcContext, opts.DockerfilePath)) { @@ -153,11 +154,21 @@ func resolveDockerfilePath() error { return errors.Wrap(err, "getting absolute path for src context/dockerfile path") } opts.DockerfilePath = abs - return nil + return copyDockerfile() } return errors.New("please provide a valid path to a Dockerfile within the build context with --dockerfile") } +// copy Dockerfile to /kaniko/Dockerfile so that if it's specified in the .dockerignore +// it won't be copied into the image +func copyDockerfile() error { + if err := util.CopyFile(opts.DockerfilePath, constants.DockerfilePath); err != nil { + return errors.Wrap(err, "copying dockerfile") + } + opts.DockerfilePath = constants.DockerfilePath + return nil +} + // resolveSourceContext unpacks the source context if it is a tar in a bucket // it resets srcContext to be the path to the unpacked build context within the image func resolveSourceContext() error { @@ -197,33 +208,18 @@ func removeIgnoredFiles() error { return err } logrus.Infof("Removing ignored files from build context: %s", ignore) - for r, i := range ignore { - ignore[r] = filepath.Clean(filepath.Join(opts.SrcContext, i)) + files, err := util.RelativeFiles("", opts.SrcContext) + if err != nil { + return errors.Wrap(err, "getting all files in src context") } - // remove all files in .dockerignore - return filepath.Walk(opts.SrcContext, func(path string, fi os.FileInfo, _ error) error { - if ignoreFile(path, ignore) { - if err := os.RemoveAll(path); err != nil { - // don't return error, because this path could have been removed already - logrus.Debugf("error removing %s from buildcontext", path) + for _, f := range files { + if rm, _ := fileutils.Matches(f, ignore); rm { + if err := os.RemoveAll(f); err != nil { + logrus.Errorf("Error removing %s from build context", f) } } - return nil - }) -} - -// ignoreFile returns true if the path matches any of the paths in ignore -func ignoreFile(path string, ignore []string) bool { - for _, i := range ignore { - matched, err := filepath.Match(i, path) - if err != nil { - return false - } - if matched { - return true - } } - return false + return nil } func exit(err error) { diff --git a/integration/dockerignore.go b/integration/dockerignore.go new file mode 100644 index 000000000..f16f2fec7 --- /dev/null +++ b/integration/dockerignore.go @@ -0,0 +1,112 @@ +/* +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 ( + "fmt" + "os" + "os/exec" + "path" + "path/filepath" + "runtime" + "strings" +) + +var filesToIgnore = []string{"ignore/fo*", "!ignore/foobar", "ignore/Dockerfile_test_ignore"} + +const ( + ignoreDir = "ignore" + ignoreDockerfile = "Dockerfile_test_ignore" + ignoreDockerfileContents = `FROM scratch + COPY . .` +) + +// Set up a test dir to ignore with the structure: +// ignore +// -- Dockerfile_test_ignore +// -- foo +// -- foobar + +func setupIgnoreTestDir() error { + if err := os.MkdirAll(ignoreDir, 0750); err != nil { + return err + } + // Create and write contents to dockerfile + path := filepath.Join(ignoreDir, ignoreDockerfile) + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + if _, err := f.Write([]byte(ignoreDockerfileContents)); err != nil { + return err + } + + additionalFiles := []string{"ignore/foo", "ignore/foobar"} + for _, add := range additionalFiles { + a, err := os.Create(add) + if err != nil { + return err + } + defer a.Close() + } + return generateDockerIgnore() +} + +// generate the .dockerignore file +func generateDockerIgnore() error { + f, err := os.Create(".dockerignore") + if err != nil { + return err + } + defer f.Close() + contents := strings.Join(filesToIgnore, "\n") + if _, err := f.Write([]byte(contents)); err != nil { + return err + } + return nil +} + +func generateDockerignoreImages(imageRepo string) error { + + dockerfilePath := filepath.Join(ignoreDir, ignoreDockerfile) + + dockerImage := strings.ToLower(imageRepo + dockerPrefix + ignoreDockerfile) + dockerCmd := exec.Command("docker", "build", + "-t", dockerImage, + "-f", path.Join(dockerfilePath), + ".") + _, err := RunCommandWithoutTest(dockerCmd) + if err != nil { + return fmt.Errorf("Failed to build image %s with docker command \"%s\": %s", dockerImage, dockerCmd.Args, err) + } + + _, ex, _, _ := runtime.Caller(0) + cwd := filepath.Dir(ex) + kanikoImage := GetKanikoImage(imageRepo, ignoreDockerfile) + kanikoCmd := exec.Command("docker", + "run", + "-v", os.Getenv("HOME")+"/.config/gcloud:/root/.config/gcloud", + "-v", cwd+":/workspace", + ExecutorImage, + "-f", path.Join(buildContextPath, dockerfilePath), + "-d", kanikoImage, + "-c", buildContextPath) + + _, err = RunCommandWithoutTest(kanikoCmd) + return err +} diff --git a/integration/images.go b/integration/images.go index 1f392be57..f84705ca7 100644 --- a/integration/images.go +++ b/integration/images.go @@ -37,7 +37,6 @@ const ( buildContextPath = "/workspace" cacheDir = "/workspace/cache" baseImageToCache = "gcr.io/google-appengine/debian9@sha256:1d6a9a6d106bd795098f60f4abb7083626354fa6735e81743c7f8cfca11259f0" - testDirPath = "context/test" ) // Arguments to build Dockerfiles with, used for both docker and kaniko builds @@ -55,8 +54,6 @@ var argsMap = map[string][]string{ "Dockerfile_test_multistage": {"file=/foo2"}, } -var filesToIgnore = []string{"context/test/*"} - // Arguments to build Dockerfiles with when building with docker var additionalDockerFlagsMap = map[string][]string{ "Dockerfile_test_target": {"--target=second"}, @@ -161,25 +158,12 @@ func (d *DockerFileBuilder) BuildImage(imageRepo, gcsBucket, dockerfilesPath, do "."}, additionalFlags...)..., ) - if err := setupTestDir(); err != nil { - return err - } - if err := generateDockerIgnore(); err != nil { - return err - } _, err := RunCommandWithoutTest(dockerCmd) if err != nil { return fmt.Errorf("Failed to build image %s with docker command \"%s\": %s", dockerImage, dockerCmd.Args, err) } - if err := setupTestDir(); err != nil { - return err - } - if err := generateDockerIgnore(); err != nil { - return err - } - contextFlag := "-c" contextPath := buildContextPath for _, d := range bucketContextTests { @@ -269,28 +253,3 @@ func (d *DockerFileBuilder) buildCachedImages(imageRepo, cacheRepo, dockerfilesP } return nil } - -func setupTestDir() error { - if err := os.MkdirAll(testDirPath, 0750); err != nil { - return err - } - p := filepath.Join(testDirPath, "foo") - f, err := os.Create(p) - if err != nil { - return err - } - return f.Close() -} - -func generateDockerIgnore() error { - f, err := os.Create(".dockerignore") - if err != nil { - return err - } - defer f.Close() - contents := strings.Join(filesToIgnore, "\n") - if _, err := f.Write([]byte(contents)); err != nil { - return err - } - return nil -} diff --git a/integration/integration_test.go b/integration/integration_test.go index 313d8f053..594663e8f 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -275,6 +275,31 @@ func TestCache(t *testing.T) { } } +func TestDockerignore(t *testing.T) { + t.Run(fmt.Sprintf("test_%s", ignoreDockerfile), func(t *testing.T) { + if err := setupIgnoreTestDir(); err != nil { + t.Fatalf("error setting up ignore test dir: %v", err) + } + if err := generateDockerignoreImages(config.imageRepo); err != nil { + t.Fatalf("error generating dockerignore test images: %v", err) + } + + dockerImage := GetDockerImage(config.imageRepo, ignoreDockerfile) + kanikoImage := GetKanikoImage(config.imageRepo, ignoreDockerfile) + + // container-diff + daemonDockerImage := daemonPrefix + dockerImage + containerdiffCmd := exec.Command("container-diff", "diff", + daemonDockerImage, kanikoImage, + "-q", "--type=file", "--type=metadata", "--json") + diff := RunCommand(containerdiffCmd, t) + t.Logf("diff = %s", string(diff)) + + expected := fmt.Sprintf(emptyContainerDiff, dockerImage, kanikoImage, dockerImage, kanikoImage) + checkContainerDiffOutput(t, diff, expected) + }) +} + type fileDiff struct { Name string Size int diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index c3a4ac909..4b8a763eb 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -33,6 +33,9 @@ const ( Author = "kaniko" + // DockerfilePath is the path the Dockerfile is copied to + DockerfilePath = "/kaniko/Dockerfile" + // ContextTar is the default name of the tar uploaded to GCS buckets ContextTar = "context.tar.gz" diff --git a/pkg/dockerfile/dockerfile.go b/pkg/dockerfile/dockerfile.go index c2eb9bff7..fac939e07 100644 --- a/pkg/dockerfile/dockerfile.go +++ b/pkg/dockerfile/dockerfile.go @@ -26,6 +26,7 @@ import ( "github.com/GoogleContainerTools/kaniko/pkg/config" "github.com/GoogleContainerTools/kaniko/pkg/util" + "github.com/docker/docker/builder/dockerignore" "github.com/moby/buildkit/frontend/dockerfile/instructions" "github.com/moby/buildkit/frontend/dockerfile/parser" "github.com/pkg/errors" @@ -183,9 +184,6 @@ func ParseDockerignore(opts *config.KanikoOptions) ([]string, error) { if err != nil { return nil, errors.Wrap(err, "parsing .dockerignore") } - return strings.FieldsFunc(string(contents), split), nil -} - -func split(r rune) bool { - return r == '\n' || r == ' ' + reader := bytes.NewBuffer(contents) + return dockerignore.ReadAll(reader) } From 3c100508b6155d1ded9f76d263dc7b8233eb97d3 Mon Sep 17 00:00:00 2001 From: Sharif Elgamal Date: Wed, 31 Oct 2018 13:33:24 -0700 Subject: [PATCH 32/42] adding documentation for base image caching (#421) * adding documentation for base image caching * add --cache-repo to list of params --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index b3557ad66..5e155706c 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,8 @@ We can run the kaniko executor image locally in a Docker daemon to build and pus ``` ### Caching + +#### Caching Layers kaniko currently can cache layers created by `RUN` commands in a remote repository. Before executing a command, kaniko checks the cache for the layer. If it exists, kaniko will pull and extract the cached layer instead of executing the command. @@ -219,6 +221,21 @@ Users can opt in to caching by setting the `--cache=true` flag. A remote repository for storing cached layers can be provided via the `--cache-repo` flag. If this flag isn't provided, a cached repo will be inferred from the `--destination` provided. +#### Caching Base Images +kaniko can cache images in a local directory that can be volume mounted into the kaniko image. +To do so, the cache must first be populated, as it is read-only. We provide a kaniko cache warming +image at `gcr.io/kaniko-project/warmer`: + +```shell +docker run -v $(pwd):/workspace gcr.io/kaniko-project/warmer:latest --cache-dir=/workspace/cache --image= --image= +``` + +`--image` can be specified for any number of desired images. +This command will cache those images by digest in a local directory named `cache`. +Once the cache is populated, caching is opted into with the same `--cache=true` flag as above. +The location of the local cache is provided via the `--cache-dir` flag, defaulting at `/cache` as with the cache warmer. +See the `examples` directory for how to use with kubernetes clusters and persistent cache volumes. + ### Pushing to Different Registries kaniko uses Docker credential helpers to push images to a registry. @@ -345,6 +362,12 @@ If `--destination=gcr.io/kaniko-project/test`, then cached layers will be stored _This flag must be used in conjunction with the `--cache=true` flag._ +#### --cache-dir + +Set this flag to specify a local directory cache for base images. Defaults to `/cache`. + +_This flag must be used in conjunction with the `--cache=true` flag._ + #### --cleanup Set this flag to cleanup the filesystem at the end, leaving a clean kaniko container (if you want to build multiple images in the same container, using the debug kaniko image) From 52a6ce66857c941ecfab87bdd0918b2956c5c1d3 Mon Sep 17 00:00:00 2001 From: dlorenc Date: Thu, 1 Nov 2018 09:11:21 -0700 Subject: [PATCH 33/42] More cache cleanups: (#397) - move the layer cache to an interface - refactor the DockerCommand implementations to support Cached and non-cached implementations. --- pkg/cache/cache.go | 20 ++++++--- pkg/commands/base_command.go | 5 +-- pkg/commands/commands.go | 7 +-- pkg/commands/run.go | 33 +++++++++++++- pkg/executor/build.go | 83 +++++++++++++++--------------------- 5 files changed, 87 insertions(+), 61 deletions(-) diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go index fca66e631..af4c7f122 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -31,9 +31,16 @@ import ( "github.com/sirupsen/logrus" ) -// RetrieveLayer checks the specified cache for a layer with the tag :cacheKey -func RetrieveLayer(opts *config.KanikoOptions, cacheKey string) (v1.Image, error) { - cache, err := Destination(opts, cacheKey) +type LayerCache interface { + RetrieveLayer(string) (v1.Image, error) +} + +type RegistryCache struct { + Opts *config.KanikoOptions +} + +func (rc *RegistryCache) RetrieveLayer(ck string) (v1.Image, error) { + cache, err := Destination(rc.Opts, ck) if err != nil { return nil, errors.Wrap(err, "getting cache destination") } @@ -52,8 +59,11 @@ func RetrieveLayer(opts *config.KanikoOptions, cacheKey string) (v1.Image, error if err != nil { return nil, err } - _, err = img.Layers() - return img, err + // Force the manifest to be populated + if _, err := img.RawManifest(); err != nil { + return nil, err + } + return img, nil } // Destination returns the repo where the layer should be stored diff --git a/pkg/commands/base_command.go b/pkg/commands/base_command.go index d2a746ee6..90ee3e445 100644 --- a/pkg/commands/base_command.go +++ b/pkg/commands/base_command.go @@ -22,11 +22,10 @@ import ( ) type BaseCommand struct { - cache bool } -func (b *BaseCommand) CacheCommand() bool { - return b.cache +func (b *BaseCommand) CacheCommand(v1.Image) DockerCommand { + return nil } func (b *BaseCommand) FilesToSnapshot() []string { diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go index cbb960e68..9ca6e97ae 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -24,6 +24,8 @@ import ( "github.com/sirupsen/logrus" ) +type CurrentCacheKey func() (string, error) + type DockerCommand interface { // ExecuteCommand is responsible for: // 1. Making required changes to the filesystem (ex. copying files for ADD/COPY or setting ENV variables) @@ -34,9 +36,8 @@ type DockerCommand interface { String() string // A list of files to snapshot, empty for metadata commands or nil if we don't know FilesToSnapshot() []string - // Return true if this command should be true - // Currently only true for RUN - CacheCommand() bool + // Return a cache-aware implementation of this command, if it exists. + CacheCommand(v1.Image) DockerCommand // Return true if this command depends on the build context. FilesUsedFromContext(*v1.Config, *dockerfile.BuildArgs) ([]string, error) diff --git a/pkg/commands/run.go b/pkg/commands/run.go index d8957786c..0290e6f7c 100644 --- a/pkg/commands/run.go +++ b/pkg/commands/run.go @@ -153,6 +153,35 @@ func (r *RunCommand) FilesToSnapshot() []string { } // CacheCommand returns true since this command should be cached -func (r *RunCommand) CacheCommand() bool { - return true +func (r *RunCommand) CacheCommand(img v1.Image) DockerCommand { + + return &CachingRunCommand{ + img: img, + cmd: r.cmd, + } +} + +type CachingRunCommand struct { + BaseCommand + img v1.Image + extractedFiles []string + cmd *instructions.RunCommand +} + +func (cr *CachingRunCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error { + logrus.Infof("Found cached layer, extracting to filesystem") + var err error + cr.extractedFiles, err = util.GetFSFromImage(constants.RootDir, cr.img) + if err != nil { + return errors.Wrap(err, "extracting fs from image") + } + return nil +} + +func (cr *CachingRunCommand) FilesToSnapshot() []string { + return cr.extractedFiles +} + +func (cr *CachingRunCommand) String() string { + return cr.cmd.String() } diff --git a/pkg/executor/build.go b/pkg/executor/build.go index 29a1cf314..d0a7d932d 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -86,33 +86,6 @@ func newStageBuilder(opts *config.KanikoOptions, stage config.KanikoStage) (*sta }, nil } -// extractCachedLayer will extract the cached layer and append it to the config file -func (s *stageBuilder) extractCachedLayer(layer v1.Image, createdBy string) error { - logrus.Infof("Found cached layer, extracting to filesystem") - extractedFiles, err := util.GetFSFromImage(constants.RootDir, layer) - if err != nil { - return errors.Wrap(err, "extracting fs from image") - } - if _, err := s.snapshotter.TakeSnapshot(extractedFiles); err != nil { - return err - } - logrus.Infof("Appending cached layer to base image") - l, err := layer.Layers() - if err != nil { - return errors.Wrap(err, "getting cached layer from image") - } - s.image, err = mutate.Append(s.image, - mutate.Addendum{ - Layer: l[0], - History: v1.History{ - Author: constants.Author, - CreatedBy: createdBy, - }, - }, - ) - return err -} - func (s *stageBuilder) build() error { // Unpack file system to root if _, err := util.GetFSFromImage(constants.RootDir, s.image); err != nil { @@ -136,6 +109,32 @@ func (s *stageBuilder) build() error { cmds = append(cmds, command) } + layerCache := &cache.RegistryCache{ + Opts: s.opts, + } + if s.opts.Cache { + // Possibly replace commands with their cached implementations. + for i, command := range cmds { + if command == nil { + continue + } + ck, err := compositeKey.Hash() + if err != nil { + return err + } + img, err := layerCache.RetrieveLayer(ck) + if err != nil { + logrus.Infof("No cached layer found for cmd %s", command.String()) + break + } + + if cacheCmd := command.CacheCommand(img); cacheCmd != nil { + logrus.Infof("Using caching version of cmd: %s", command.String()) + cmds[i] = cacheCmd + } + } + } + args := dockerfile.NewBuildArgs(s.opts.BuildArgs) for index, command := range cmds { if command == nil { @@ -157,22 +156,6 @@ func (s *stageBuilder) build() error { } logrus.Info(command.String()) - ck, err := compositeKey.Hash() - if err != nil { - return err - } - - if command.CacheCommand() && s.opts.Cache { - image, err := cache.RetrieveLayer(s.opts, ck) - if err == nil { - if err := s.extractCachedLayer(image, command.String()); err != nil { - return errors.Wrap(err, "extracting cached layer") - } - continue - } - logrus.Info("No cached layer found, executing command...") - } - if err := command.ExecuteCommand(&s.cf.Config, args); err != nil { return err } @@ -196,7 +179,11 @@ func (s *stageBuilder) build() error { if err != nil { return err } - if err := s.saveSnapshot(command, ck, contents); err != nil { + ck, err := compositeKey.Hash() + if err != nil { + return err + } + if err := s.saveSnapshot(command.String(), ck, contents); err != nil { return err } } @@ -228,7 +215,7 @@ func (s *stageBuilder) shouldTakeSnapshot(index int, files []string) bool { return true } -func (s *stageBuilder) saveSnapshot(command commands.DockerCommand, ck string, contents []byte) error { +func (s *stageBuilder) saveSnapshot(createdBy string, ck string, contents []byte) error { if contents == nil { logrus.Info("No files were changed, appending empty layer to config. No layer added to image.") return nil @@ -242,8 +229,8 @@ func (s *stageBuilder) saveSnapshot(command commands.DockerCommand, ck string, c return err } // Push layer to cache now along with new config file - if command.CacheCommand() && s.opts.Cache { - if err := pushLayerToCache(s.opts, ck, layer, command.String()); err != nil { + if s.opts.Cache { + if err := pushLayerToCache(s.opts, ck, layer, createdBy); err != nil { return err } } @@ -252,7 +239,7 @@ func (s *stageBuilder) saveSnapshot(command commands.DockerCommand, ck string, c Layer: layer, History: v1.History{ Author: constants.Author, - CreatedBy: command.String(), + CreatedBy: createdBy, }, }, ) From fc43e218f0f7f543cb70c3472f3ab6f632a7f330 Mon Sep 17 00:00:00 2001 From: dlorenc Date: Tue, 6 Nov 2018 09:26:54 -0600 Subject: [PATCH 34/42] Buffer layers to disk as they are created. (#428) When building Docker images, layers were previously stored in memory. This caused obvious issues when manipulating large layers, which could cause Kaniko to crash. --- pkg/executor/build.go | 53 +++++++++++--------- pkg/snapshot/layered_map.go | 6 +-- pkg/snapshot/snapshot.go | 91 ++++++++++++----------------------- pkg/snapshot/snapshot_test.go | 89 ++++++++++++++++++++++------------ pkg/util/fs_util.go | 2 +- 5 files changed, 123 insertions(+), 118 deletions(-) diff --git a/pkg/executor/build.go b/pkg/executor/build.go index d0a7d932d..ca155361c 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -17,10 +17,7 @@ limitations under the License. package executor import ( - "bytes" "fmt" - "io" - "io/ioutil" "os" "path/filepath" "strconv" @@ -42,6 +39,9 @@ import ( "github.com/GoogleContainerTools/kaniko/pkg/util" ) +// This is the size of an empty tar in Go +const emptyTarSize = 1024 + // stageBuilder contains all fields necessary to build one stage of a Dockerfile type stageBuilder struct { stage config.KanikoStage @@ -160,36 +160,39 @@ func (s *stageBuilder) build() error { return err } files = command.FilesToSnapshot() - var contents []byte if !s.shouldTakeSnapshot(index, files) { continue } - if files == nil || s.opts.SingleSnapshot { - contents, err = s.snapshotter.TakeSnapshotFS() - } else { - // Volumes are very weird. They get created in their command, but snapshotted in the next one. - // Add them to the list of files to snapshot. - for v := range s.cf.Config.Volumes { - files = append(files, v) - } - contents, err = s.snapshotter.TakeSnapshot(files) - } + tarPath, err := s.takeSnapshot(files) if err != nil { return err } + ck, err := compositeKey.Hash() if err != nil { return err } - if err := s.saveSnapshot(command.String(), ck, contents); err != nil { + if err := s.saveSnapshotToImage(command.String(), ck, tarPath); err != nil { return err } } return nil } +func (s *stageBuilder) takeSnapshot(files []string) (string, error) { + if files == nil || s.opts.SingleSnapshot { + return s.snapshotter.TakeSnapshotFS() + } + // Volumes are very weird. They get created in their command, but snapshotted in the next one. + // Add them to the list of files to snapshot. + for v := range s.cf.Config.Volumes { + files = append(files, v) + } + return s.snapshotter.TakeSnapshot(files) +} + func (s *stageBuilder) shouldTakeSnapshot(index int, files []string) bool { isLastCommand := index == len(s.stage.Commands)-1 @@ -215,16 +218,20 @@ func (s *stageBuilder) shouldTakeSnapshot(index int, files []string) bool { return true } -func (s *stageBuilder) saveSnapshot(createdBy string, ck string, contents []byte) error { - if contents == nil { +func (s *stageBuilder) saveSnapshotToImage(createdBy string, ck string, tarPath string) error { + if tarPath == "" { + return nil + } + fi, err := os.Stat(tarPath) + if err != nil { + return err + } + if fi.Size() <= emptyTarSize { logrus.Info("No files were changed, appending empty layer to config. No layer added to image.") return nil } - // Append the layer to the image - opener := func() (io.ReadCloser, error) { - return ioutil.NopCloser(bytes.NewReader(contents)), nil - } - layer, err := tarball.LayerFromOpener(opener) + + layer, err := tarball.LayerFromFile(tarPath) if err != nil { return err } @@ -306,7 +313,7 @@ func extractImageToDependecyDir(index int, image v1.Image) error { if err := os.MkdirAll(dependencyDir, 0755); err != nil { return err } - logrus.Infof("trying to extract to %s", dependencyDir) + logrus.Debugf("trying to extract to %s", dependencyDir) _, err := util.GetFSFromImage(dependencyDir, image) return err } diff --git a/pkg/snapshot/layered_map.go b/pkg/snapshot/layered_map.go index 9a356b0e2..c84340e36 100644 --- a/pkg/snapshot/layered_map.go +++ b/pkg/snapshot/layered_map.go @@ -92,13 +92,13 @@ func (l *LayeredMap) GetWhiteout(s string) (string, bool) { return "", false } -func (l *LayeredMap) MaybeAddWhiteout(s string) (bool, error) { +func (l *LayeredMap) MaybeAddWhiteout(s string) bool { whiteout, ok := l.GetWhiteout(s) if ok && whiteout == s { - return false, nil + return false } l.whiteouts[len(l.whiteouts)-1][s] = s - return true, nil + return true } // Add will add the specified file s to the layered map. diff --git a/pkg/snapshot/snapshot.go b/pkg/snapshot/snapshot.go index 4175d4761..a5f739689 100644 --- a/pkg/snapshot/snapshot.go +++ b/pkg/snapshot/snapshot.go @@ -17,18 +17,21 @@ limitations under the License. package snapshot import ( - "bytes" "fmt" - "io" "io/ioutil" "os" "path/filepath" "syscall" + "github.com/GoogleContainerTools/kaniko/pkg/constants" + "github.com/GoogleContainerTools/kaniko/pkg/util" "github.com/sirupsen/logrus" ) +// For testing +var snapshotPathPrefix = constants.KanikoDir + // Snapshotter holds the root directory from which to take snapshots, and a list of snapshots taken type Snapshotter struct { l *LayeredMap @@ -42,10 +45,8 @@ func NewSnapshotter(l *LayeredMap, d string) *Snapshotter { // Init initializes a new snapshotter func (s *Snapshotter) Init() error { - if _, err := s.snapShotFS(ioutil.Discard); err != nil { - return err - } - return nil + _, err := s.TakeSnapshotFS() + return err } // Key returns a string based on the current state of the file system @@ -55,46 +56,21 @@ func (s *Snapshotter) Key() (string, error) { // TakeSnapshot takes a snapshot of the specified files, avoiding directories in the whitelist, and creates // a tarball of the changed files. Return contents of the tarball, and whether or not any files were changed -func (s *Snapshotter) TakeSnapshot(files []string) ([]byte, error) { - buf := bytes.NewBuffer([]byte{}) - filesAdded, err := s.snapshotFiles(buf, files) +func (s *Snapshotter) TakeSnapshot(files []string) (string, error) { + f, err := ioutil.TempFile(snapshotPathPrefix, "") if err != nil { - return nil, err + return "", err } - contents := buf.Bytes() - if !filesAdded { - return nil, nil - } - return contents, err -} + defer f.Close() -// TakeSnapshotFS takes a snapshot of the filesystem, avoiding directories in the whitelist, and creates -// a tarball of the changed files. Return contents of the tarball, and whether or not any files were changed -func (s *Snapshotter) TakeSnapshotFS() ([]byte, error) { - buf := bytes.NewBuffer([]byte{}) - filesAdded, err := s.snapShotFS(buf) - if err != nil { - return nil, err - } - contents := buf.Bytes() - if !filesAdded { - return nil, nil - } - return contents, err -} - -// snapshotFiles creates a snapshot (tar) and adds the specified files. -// It will not add files which are whitelisted. -func (s *Snapshotter) snapshotFiles(f io.Writer, files []string) (bool, error) { s.l.Snapshot() if len(files) == 0 { logrus.Info("No files changed in this command, skipping snapshotting.") - return false, nil + return "", nil } logrus.Info("Taking snapshot of files...") logrus.Debugf("Taking snapshot of files %v", files) snapshottedFiles := make(map[string]bool) - filesAdded := false t := util.NewTar(f) defer t.Close() @@ -114,15 +90,14 @@ func (s *Snapshotter) snapshotFiles(f io.Writer, files []string) (bool, error) { fileAdded, err := s.l.MaybeAdd(file) if err != nil { - return false, fmt.Errorf("Unable to add parent dir %s to layered map: %s", file, err) + return "", fmt.Errorf("Unable to add parent dir %s to layered map: %s", file, err) } if fileAdded { err = t.AddFileToTar(file) if err != nil { - return false, fmt.Errorf("Error adding parent dir %s to tar: %s", file, err) + return "", fmt.Errorf("Error adding parent dir %s to tar: %s", file, err) } - filesAdded = true } } // Next add the files themselves to the tar @@ -134,21 +109,26 @@ func (s *Snapshotter) snapshotFiles(f io.Writer, files []string) (bool, error) { snapshottedFiles[file] = true if err := s.l.Add(file); err != nil { - return false, fmt.Errorf("Unable to add file %s to layered map: %s", file, err) + return "", fmt.Errorf("Unable to add file %s to layered map: %s", file, err) } if err := t.AddFileToTar(file); err != nil { - return false, fmt.Errorf("Error adding file %s to tar: %s", file, err) + return "", fmt.Errorf("Error adding file %s to tar: %s", file, err) } - filesAdded = true } - return filesAdded, nil + return f.Name(), nil } -// shapShotFS creates a snapshot (tar) of all files in the system which are not -// whitelisted and which have changed. -func (s *Snapshotter) snapShotFS(f io.Writer) (bool, error) { +// TakeSnapshotFS takes a snapshot of the filesystem, avoiding directories in the whitelist, and creates +// a tarball of the changed files. +func (s *Snapshotter) TakeSnapshotFS() (string, error) { logrus.Info("Taking snapshot of full filesystem...") + f, err := ioutil.TempFile(snapshotPathPrefix, "") + if err != nil { + return "", err + } + defer f.Close() + // Some of the operations that follow (e.g. hashing) depend on the file system being synced, // for example the hashing function that determines if files are equal uses the mtime of the files, // which can lag if sync is not called. Unfortunately there can still be lag if too much data needs @@ -157,7 +137,6 @@ func (s *Snapshotter) snapShotFS(f io.Writer) (bool, error) { s.l.Snapshot() existingPaths := s.l.GetFlattenedPathsForWhiteOut() - filesAdded := false t := util.NewTar(f) defer t.Close() @@ -176,15 +155,10 @@ func (s *Snapshotter) snapShotFS(f io.Writer) (bool, error) { // Only add the whiteout if the directory for the file still exists. dir := filepath.Dir(path) if _, ok := memFs[dir]; ok { - addWhiteout, err := s.l.MaybeAddWhiteout(path) - if err != nil { - return false, nil - } - if addWhiteout { + if s.l.MaybeAddWhiteout(path) { logrus.Infof("Adding whiteout for %s", path) - filesAdded = true if err := t.Whiteout(path); err != nil { - return false, err + return "", err } } } @@ -194,7 +168,7 @@ func (s *Snapshotter) snapShotFS(f io.Writer) (bool, error) { for path := range memFs { whitelisted, err := util.CheckWhitelist(path) if err != nil { - return false, err + return "", err } if whitelisted { logrus.Debugf("Not adding %s to layer, as it's whitelisted", path) @@ -204,16 +178,15 @@ func (s *Snapshotter) snapShotFS(f io.Writer) (bool, error) { // Only add to the tar if we add it to the layeredmap. maybeAdd, err := s.l.MaybeAdd(path) if err != nil { - return false, err + return "", err } if maybeAdd { logrus.Debugf("Adding %s to layer, because it was changed.", path) - filesAdded = true if err := t.AddFileToTar(path); err != nil { - return false, err + return "", err } } } - return filesAdded, nil + return f.Name(), nil } diff --git a/pkg/snapshot/snapshot_test.go b/pkg/snapshot/snapshot_test.go index 95daa88f6..11c63c791 100644 --- a/pkg/snapshot/snapshot_test.go +++ b/pkg/snapshot/snapshot_test.go @@ -17,7 +17,6 @@ package snapshot import ( "archive/tar" - "bytes" "io" "io/ioutil" "os" @@ -30,9 +29,8 @@ import ( ) func TestSnapshotFSFileChange(t *testing.T) { - - testDir, snapshotter, err := setUpTestDir() - defer os.RemoveAll(testDir) + testDir, snapshotter, cleanup, err := setUpTestDir() + defer cleanup() if err != nil { t.Fatal(err) } @@ -45,16 +43,17 @@ func TestSnapshotFSFileChange(t *testing.T) { t.Fatalf("Error setting up fs: %s", err) } // Take another snapshot - contents, err := snapshotter.TakeSnapshotFS() + tarPath, err := snapshotter.TakeSnapshotFS() if err != nil { t.Fatalf("Error taking snapshot of fs: %s", err) } - if contents == nil { - t.Fatal("No files added to snapshot.") + + f, err := os.Open(tarPath) + if err != nil { + t.Fatal(err) } // Check contents of the snapshot, make sure contents is equivalent to snapshotFiles - reader := bytes.NewReader(contents) - tr := tar.NewReader(reader) + tr := tar.NewReader(f) fooPath := filepath.Join(testDir, "foo") batPath := filepath.Join(testDir, "bar/bat") snapshotFiles := map[string]string{ @@ -82,8 +81,8 @@ func TestSnapshotFSFileChange(t *testing.T) { } func TestSnapshotFSChangePermissions(t *testing.T) { - testDir, snapshotter, err := setUpTestDir() - defer os.RemoveAll(testDir) + testDir, snapshotter, cleanup, err := setUpTestDir() + defer cleanup() if err != nil { t.Fatal(err) } @@ -93,16 +92,16 @@ func TestSnapshotFSChangePermissions(t *testing.T) { t.Fatalf("Error changing permissions on %s: %v", batPath, err) } // Take another snapshot - contents, err := snapshotter.TakeSnapshotFS() + tarPath, err := snapshotter.TakeSnapshotFS() if err != nil { t.Fatalf("Error taking snapshot of fs: %s", err) } - if contents == nil { - t.Fatal("No files added to snapshot.") + f, err := os.Open(tarPath) + if err != nil { + t.Fatal(err) } // Check contents of the snapshot, make sure contents is equivalent to snapshotFiles - reader := bytes.NewReader(contents) - tr := tar.NewReader(reader) + tr := tar.NewReader(f) snapshotFiles := map[string]string{ batPath: "baz2", } @@ -127,8 +126,8 @@ func TestSnapshotFSChangePermissions(t *testing.T) { } func TestSnapshotFiles(t *testing.T) { - testDir, snapshotter, err := setUpTestDir() - defer os.RemoveAll(testDir) + testDir, snapshotter, cleanup, err := setUpTestDir() + defer cleanup() if err != nil { t.Fatal(err) } @@ -142,15 +141,20 @@ func TestSnapshotFiles(t *testing.T) { filesToSnapshot := []string{ filepath.Join(testDir, "foo"), } - contents, err := snapshotter.TakeSnapshot(filesToSnapshot) + tarPath, err := snapshotter.TakeSnapshot(filesToSnapshot) if err != nil { t.Fatal(err) } + defer os.Remove(tarPath) + expectedFiles := []string{"/", "/tmp", filepath.Join(testDir, "foo")} + f, err := os.Open(tarPath) + if err != nil { + t.Fatal(err) + } // Check contents of the snapshot, make sure contents is equivalent to snapshotFiles - reader := bytes.NewReader(contents) - tr := tar.NewReader(reader) + tr := tar.NewReader(f) var actualFiles []string for { hdr, err := tr.Next() @@ -166,27 +170,42 @@ func TestSnapshotFiles(t *testing.T) { } func TestEmptySnapshotFS(t *testing.T) { - testDir, snapshotter, err := setUpTestDir() - defer os.RemoveAll(testDir) + _, snapshotter, cleanup, err := setUpTestDir() if err != nil { t.Fatal(err) } + defer cleanup() + // Take snapshot with no changes - contents, err := snapshotter.TakeSnapshotFS() + tarPath, err := snapshotter.TakeSnapshotFS() if err != nil { t.Fatalf("Error taking snapshot of fs: %s", err) } - // Since we took a snapshot with no changes, contents should be nil - if contents != nil { - t.Fatal("Files added even though no changes to file system were made.") + + f, err := os.Open(tarPath) + if err != nil { + t.Fatal(err) + } + tr := tar.NewReader(f) + + if _, err := tr.Next(); err != io.EOF { + t.Fatal("no files expected in tar, found files.") } } -func setUpTestDir() (string, *Snapshotter, error) { +func setUpTestDir() (string, *Snapshotter, func(), error) { testDir, err := ioutil.TempDir("", "") if err != nil { - return testDir, nil, errors.Wrap(err, "setting up temp dir") + return "", nil, nil, errors.Wrap(err, "setting up temp dir") } + + snapshotPath, err := ioutil.TempDir("", "") + if err != nil { + return "", nil, nil, errors.Wrap(err, "setting up temp dir") + } + + snapshotPathPrefix = snapshotPath + files := map[string]string{ "foo": "baz1", "bar/bat": "baz2", @@ -194,14 +213,20 @@ func setUpTestDir() (string, *Snapshotter, error) { } // Set up initial files if err := testutil.SetupFiles(testDir, files); err != nil { - return testDir, nil, errors.Wrap(err, "setting up file system") + return "", nil, nil, errors.Wrap(err, "setting up file system") } // Take the initial snapshot l := NewLayeredMap(util.Hasher(), util.CacheHasher()) snapshotter := NewSnapshotter(l, testDir) if err := snapshotter.Init(); err != nil { - return testDir, nil, errors.Wrap(err, "initializing snapshotter") + return "", nil, nil, errors.Wrap(err, "initializing snapshotter") } - return testDir, snapshotter, nil + + cleanup := func() { + os.RemoveAll(snapshotPath) + os.RemoveAll(testDir) + } + + return testDir, snapshotter, cleanup, nil } diff --git a/pkg/util/fs_util.go b/pkg/util/fs_util.go index ef16ceb57..fd53ee7bf 100644 --- a/pkg/util/fs_util.go +++ b/pkg/util/fs_util.go @@ -74,7 +74,7 @@ func GetFSFromImage(root string, img v1.Image) ([]string, error) { extractedFiles := []string{} for i, l := range layers { - logrus.Infof("Extracting layer %d", i) + logrus.Debugf("Extracting layer %d", i) r, err := l.Uncompressed() if err != nil { return nil, err From 5ed45ed2fb7dd5ec8aa6cee9724c4ee1ea1e952d Mon Sep 17 00:00:00 2001 From: Carlos Sanchez Date: Tue, 6 Nov 2018 22:44:44 +0100 Subject: [PATCH 35/42] Preserve options when doing a cache push (#423) * Preserve options when doing a cache push Otherwise options like `insecure` are lost * Do not override original object --- pkg/executor/push.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/executor/push.go b/pkg/executor/push.go index dcf49a647..796c17cf7 100644 --- a/pkg/executor/push.go +++ b/pkg/executor/push.go @@ -126,7 +126,7 @@ func pushLayerToCache(opts *config.KanikoOptions, cacheKey string, layer v1.Laye if err != nil { return errors.Wrap(err, "appending layer onto empty image") } - return DoPush(empty, &config.KanikoOptions{ - Destinations: []string{cache}, - }) + cacheOpts := *opts + cacheOpts.Destinations = []string{cache} + return DoPush(empty, &cacheOpts) } From 224b7e2b417e6063d9bb197809fa39f34f1f3555 Mon Sep 17 00:00:00 2001 From: Sharif Elgamal Date: Tue, 6 Nov 2018 15:27:09 -0800 Subject: [PATCH 36/42] parse arg commands at the top of dockerfiles (#404) * parse arg commands at the top of dockerfiles * fix pointer reference bug and remove debugging * fixing tests * account for meta args with no value * don't take fs snapshot if / is the only changed path * move metaArgs inside KanikoStage * removing unused property * check for any directory instead of just / * remove unnecessary check --- Makefile | 2 +- .../dockerfiles/Dockerfile_test_meta_arg | 17 +++++++++++++++++ pkg/commands/arg.go | 6 ++++++ pkg/config/stage.go | 1 + pkg/dockerfile/buildargs.go | 9 +++++++++ pkg/dockerfile/dockerfile.go | 14 ++++++++------ pkg/dockerfile/dockerfile_test.go | 8 ++++---- pkg/executor/build.go | 3 ++- pkg/executor/build_test.go | 2 +- pkg/util/image_util.go | 9 ++++++++- pkg/util/image_util_test.go | 6 +++--- 11 files changed, 60 insertions(+), 17 deletions(-) create mode 100644 integration/dockerfiles/Dockerfile_test_meta_arg diff --git a/Makefile b/Makefile index 94c18ad40..224a8f336 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ # Bump these on release VERSION_MAJOR ?= 0 -VERSION_MINOR ?= 3 +VERSION_MINOR ?= 5 VERSION_BUILD ?= 0 VERSION ?= v$(VERSION_MAJOR).$(VERSION_MINOR).$(VERSION_BUILD) diff --git a/integration/dockerfiles/Dockerfile_test_meta_arg b/integration/dockerfiles/Dockerfile_test_meta_arg new file mode 100644 index 000000000..a98f82e3a --- /dev/null +++ b/integration/dockerfiles/Dockerfile_test_meta_arg @@ -0,0 +1,17 @@ +ARG REGISTRY=gcr.io +ARG REPO=google-appengine +ARG WORD=hello +ARG W0RD2=hey + +FROM ${REGISTRY}/${REPO}/debian9 as stage1 + +# Should evaluate WORD and create /tmp/hello +ARG WORD +RUN touch /${WORD} + +FROM ${REGISTRY}/${REPO}/debian9 + +COPY --from=stage1 /hello /tmp + +# /tmp/hey should not get created without the ARG statement +RUN touch /tmp/${WORD2} diff --git a/pkg/commands/arg.go b/pkg/commands/arg.go index 9f5533fcf..6b890cff0 100644 --- a/pkg/commands/arg.go +++ b/pkg/commands/arg.go @@ -42,7 +42,13 @@ func (r *ArgCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bui return err } resolvedValue = &value + } else { + meta := buildArgs.GetAllMeta() + if value, ok := meta[resolvedKey]; ok { + resolvedValue = &value + } } + buildArgs.AddArg(resolvedKey, resolvedValue) return nil } diff --git a/pkg/config/stage.go b/pkg/config/stage.go index b3f16a32e..2cdfaad15 100644 --- a/pkg/config/stage.go +++ b/pkg/config/stage.go @@ -25,4 +25,5 @@ type KanikoStage struct { Final bool BaseImageStoredLocally bool SaveStage bool + MetaArgs []instructions.ArgCommand } diff --git a/pkg/dockerfile/buildargs.go b/pkg/dockerfile/buildargs.go index b929528ef..dac5fd08f 100644 --- a/pkg/dockerfile/buildargs.go +++ b/pkg/dockerfile/buildargs.go @@ -20,6 +20,7 @@ import ( "strings" d "github.com/docker/docker/builder/dockerfile" + "github.com/moby/buildkit/frontend/dockerfile/instructions" ) type BuildArgs struct { @@ -53,3 +54,11 @@ func (b *BuildArgs) ReplacementEnvs(envs []string) []string { filtered := b.FilterAllowed(envs) return append(envs, filtered...) } + +// AddMetaArgs adds the supplied args map to b's allowedMetaArgs +func (b *BuildArgs) AddMetaArgs(metaArgs []instructions.ArgCommand) { + for _, arg := range metaArgs { + v := arg.Value + b.AddMetaArg(arg.Key, v) + } +} diff --git a/pkg/dockerfile/dockerfile.go b/pkg/dockerfile/dockerfile.go index fac939e07..2a886d0f6 100644 --- a/pkg/dockerfile/dockerfile.go +++ b/pkg/dockerfile/dockerfile.go @@ -38,7 +38,7 @@ func Stages(opts *config.KanikoOptions) ([]config.KanikoStage, error) { if err != nil { return nil, errors.Wrap(err, fmt.Sprintf("reading dockerfile at path %s", opts.DockerfilePath)) } - stages, err := Parse(d) + stages, metaArgs, err := Parse(d) if err != nil { return nil, errors.Wrap(err, "parsing dockerfile") } @@ -60,11 +60,13 @@ func Stages(opts *config.KanikoOptions) ([]config.KanikoStage, error) { BaseImageStoredLocally: (baseImageIndex(index, stages) != -1), SaveStage: saveStage(index, stages), Final: index == targetStage, + MetaArgs: metaArgs, }) if index == targetStage { break } } + return kanikoStages, nil } @@ -83,16 +85,16 @@ func baseImageIndex(currentStage int, stages []instructions.Stage) int { } // Parse parses the contents of a Dockerfile and returns a list of commands -func Parse(b []byte) ([]instructions.Stage, error) { +func Parse(b []byte) ([]instructions.Stage, []instructions.ArgCommand, error) { p, err := parser.Parse(bytes.NewReader(b)) if err != nil { - return nil, err + return nil, nil, err } - stages, _, err := instructions.Parse(p.AST) + stages, metaArgs, err := instructions.Parse(p.AST) if err != nil { - return nil, err + return nil, nil, err } - return stages, err + return stages, metaArgs, err } // targetStage returns the index of the target stage kaniko is trying to build diff --git a/pkg/dockerfile/dockerfile_test.go b/pkg/dockerfile/dockerfile_test.go index cd83c79ff..829a59b7f 100644 --- a/pkg/dockerfile/dockerfile_test.go +++ b/pkg/dockerfile/dockerfile_test.go @@ -35,7 +35,7 @@ func Test_resolveStages(t *testing.T) { FROM scratch COPY --from=second /hi2 /hi3 ` - stages, err := Parse([]byte(dockerfile)) + stages, _, err := Parse([]byte(dockerfile)) if err != nil { t.Fatal(err) } @@ -63,7 +63,7 @@ func Test_targetStage(t *testing.T) { FROM scratch COPY --from=second /hi2 /hi3 ` - stages, err := Parse([]byte(dockerfile)) + stages, _, err := Parse([]byte(dockerfile)) if err != nil { t.Fatal(err) } @@ -142,7 +142,7 @@ func Test_SaveStage(t *testing.T) { expected: false, }, } - stages, err := Parse([]byte(testutil.Dockerfile)) + stages, _, err := Parse([]byte(testutil.Dockerfile)) if err != nil { t.Fatalf("couldn't retrieve stages from Dockerfile: %v", err) } @@ -177,7 +177,7 @@ func Test_baseImageIndex(t *testing.T) { }, } - stages, err := Parse([]byte(testutil.Dockerfile)) + stages, _, err := Parse([]byte(testutil.Dockerfile)) if err != nil { t.Fatalf("couldn't retrieve stages from Dockerfile: %v", err) } diff --git a/pkg/executor/build.go b/pkg/executor/build.go index ca155361c..35a37d70a 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -54,7 +54,7 @@ type stageBuilder struct { // newStageBuilder returns a new type stageBuilder which contains all the information required to build the stage func newStageBuilder(opts *config.KanikoOptions, stage config.KanikoStage) (*stageBuilder, error) { - sourceImage, err := util.RetrieveSourceImage(stage, opts.BuildArgs, opts) + sourceImage, err := util.RetrieveSourceImage(stage, opts) if err != nil { return nil, err } @@ -136,6 +136,7 @@ func (s *stageBuilder) build() error { } args := dockerfile.NewBuildArgs(s.opts.BuildArgs) + args.AddMetaArgs(s.stage.MetaArgs) for index, command := range cmds { if command == nil { continue diff --git a/pkg/executor/build_test.go b/pkg/executor/build_test.go index edf162261..63dbd8a4b 100644 --- a/pkg/executor/build_test.go +++ b/pkg/executor/build_test.go @@ -66,7 +66,7 @@ func Test_reviewConfig(t *testing.T) { } func stage(t *testing.T, d string) config.KanikoStage { - stages, err := dockerfile.Parse([]byte(d)) + stages, _, err := dockerfile.Parse([]byte(d)) if err != nil { t.Fatalf("error parsing dockerfile: %v", err) } diff --git a/pkg/util/image_util.go b/pkg/util/image_util.go index 0a5aa67cd..8e46ce067 100644 --- a/pkg/util/image_util.go +++ b/pkg/util/image_util.go @@ -18,6 +18,7 @@ package util import ( "crypto/tls" + "fmt" "net/http" "path/filepath" "strconv" @@ -44,7 +45,13 @@ var ( ) // RetrieveSourceImage returns the base image of the stage at index -func RetrieveSourceImage(stage config.KanikoStage, buildArgs []string, opts *config.KanikoOptions) (v1.Image, error) { +func RetrieveSourceImage(stage config.KanikoStage, opts *config.KanikoOptions) (v1.Image, error) { + buildArgs := opts.BuildArgs + var metaArgsString []string + for _, arg := range stage.MetaArgs { + metaArgsString = append(metaArgsString, fmt.Sprintf("%s=%s", arg.Key, arg.ValueString())) + } + buildArgs = append(buildArgs, metaArgsString...) currentBaseName, err := ResolveEnvironmentReplacement(stage.BaseName, buildArgs, false) if err != nil { return nil, err diff --git a/pkg/util/image_util_test.go b/pkg/util/image_util_test.go index 272e9e284..a155963bc 100644 --- a/pkg/util/image_util_test.go +++ b/pkg/util/image_util_test.go @@ -57,7 +57,7 @@ func Test_StandardImage(t *testing.T) { retrieveRemoteImage = mock actual, err := RetrieveSourceImage(config.KanikoStage{ Stage: stages[0], - }, nil, &config.KanikoOptions{}) + }, &config.KanikoOptions{}) testutil.CheckErrorAndDeepEqual(t, false, err, nil, actual) } func Test_ScratchImage(t *testing.T) { @@ -67,7 +67,7 @@ func Test_ScratchImage(t *testing.T) { } actual, err := RetrieveSourceImage(config.KanikoStage{ Stage: stages[1], - }, nil, &config.KanikoOptions{}) + }, &config.KanikoOptions{}) expected := empty.Image testutil.CheckErrorAndDeepEqual(t, false, err, expected, actual) } @@ -89,7 +89,7 @@ func Test_TarImage(t *testing.T) { BaseImageStoredLocally: true, BaseImageIndex: 0, Stage: stages[2], - }, nil, &config.KanikoOptions{}) + }, &config.KanikoOptions{}) testutil.CheckErrorAndDeepEqual(t, false, err, nil, actual) } From 80bbb5625560195e307ff7099aeedc788fcfbc1f Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Tue, 6 Nov 2018 15:42:50 -0800 Subject: [PATCH 37/42] Update changelog and makefile for v0.6.0 --- CHANGELOG.md | 25 +++++++++++++++++++++++++ Makefile | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5962a0732..7d83916c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +# v0.6.0 Release - 11/06/2018 + +## New Features +* parse arg commands at the top of dockerfiles [#404](https://github.com/GoogleContainerTools/kaniko/pull/404) +* Add buffering for large layers. [#428](https://github.com/GoogleContainerTools/kaniko/pull/428) +* Separate Insecure Pull Options [#409](https://github.com/GoogleContainerTools/kaniko/pull/409) +* Add support for .dockerignore file [#394](https://github.com/GoogleContainerTools/kaniko/pull/394) +* Support insecure pull [#401](https://github.com/GoogleContainerTools/kaniko/pull/401) + +## Updates +* Preserve options when doing a cache push [#423](https://github.com/GoogleContainerTools/kaniko/pull/423) +* More cache cleanups: [#397](https://github.com/GoogleContainerTools/kaniko/pull/397) +* adding documentation for base image caching [#421](https://github.com/GoogleContainerTools/kaniko/pull/421) +* Update go-containerregistry [#420](https://github.com/GoogleContainerTools/kaniko/pull/420) +* Update README [#419](https://github.com/GoogleContainerTools/kaniko/pull/419) +* Use remoteImage function when getting digest for cache [#413](https://github.com/GoogleContainerTools/kaniko/pull/413) +* adding exit 1 when there are not enough command line vars passed to `… [#415](https://github.com/GoogleContainerTools/kaniko/pull/415) +* "Container Builder" - > "Cloud Build" [#414](https://github.com/GoogleContainerTools/kaniko/pull/414) +* adding the cache warmer to the release process [#412](https://github.com/GoogleContainerTools/kaniko/pull/412) + +## Bug Fixes +* Fix bugs with .dockerignore and improve integration test [#424](https://github.com/GoogleContainerTools/kaniko/pull/424) +* fix releasing the cache warmer [#418](https://github.com/GoogleContainerTools/kaniko/pull/418) + + # v0.5.0 Release - 10/16/2018 ## New Features diff --git a/Makefile b/Makefile index 224a8f336..36bd5a190 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ # Bump these on release VERSION_MAJOR ?= 0 -VERSION_MINOR ?= 5 +VERSION_MINOR ?= 6 VERSION_BUILD ?= 0 VERSION ?= v$(VERSION_MAJOR).$(VERSION_MINOR).$(VERSION_BUILD) From 58b607b4d0de146319af1c4df3702e2a3b9d91b8 Mon Sep 17 00:00:00 2001 From: dlorenc Date: Fri, 9 Nov 2018 12:28:18 -0600 Subject: [PATCH 38/42] Fix caching for multi-step builds. (#441) This change fixes that by properly "replaying" the Dockerfile and mutating the config when calculating cache keys. Previously we were looking at the wrong cache key for each command when there was more than one. --- .../dockerfiles/Dockerfile_test_cache_install | 3 + pkg/commands/add.go | 4 + pkg/commands/base_command.go | 4 + pkg/commands/commands.go | 2 + pkg/commands/copy.go | 4 + pkg/commands/run.go | 4 + pkg/commands/workdir.go | 4 + pkg/executor/build.go | 88 +++++++++++++------ 8 files changed, 87 insertions(+), 26 deletions(-) diff --git a/integration/dockerfiles/Dockerfile_test_cache_install b/integration/dockerfiles/Dockerfile_test_cache_install index 8a6a9a58e..b93eb4ad5 100644 --- a/integration/dockerfiles/Dockerfile_test_cache_install +++ b/integration/dockerfiles/Dockerfile_test_cache_install @@ -17,4 +17,7 @@ # if the cache is implemented correctly FROM gcr.io/google-appengine/debian9@sha256:1d6a9a6d106bd795098f60f4abb7083626354fa6735e81743c7f8cfca11259f0 +WORKDIR /foo RUN apt-get update && apt-get install -y make +COPY context/bar /context +RUN echo "hey" > foo diff --git a/pkg/commands/add.go b/pkg/commands/add.go index 27cf0c968..7bcbce810 100644 --- a/pkg/commands/add.go +++ b/pkg/commands/add.go @@ -131,3 +131,7 @@ func (a *AddCommand) FilesUsedFromContext(config *v1.Config, buildArgs *dockerfi logrus.Infof("Using files from context: %v", files) return files, nil } + +func (a *AddCommand) MetadataOnly() bool { + return false +} diff --git a/pkg/commands/base_command.go b/pkg/commands/base_command.go index 90ee3e445..698b72821 100644 --- a/pkg/commands/base_command.go +++ b/pkg/commands/base_command.go @@ -35,3 +35,7 @@ func (b *BaseCommand) FilesToSnapshot() []string { func (b *BaseCommand) FilesUsedFromContext(_ *v1.Config, _ *dockerfile.BuildArgs) ([]string, error) { return []string{}, nil } + +func (b *BaseCommand) MetadataOnly() bool { + return true +} diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go index 9ca6e97ae..d6ed04b59 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -41,6 +41,8 @@ type DockerCommand interface { // Return true if this command depends on the build context. FilesUsedFromContext(*v1.Config, *dockerfile.BuildArgs) ([]string, error) + + MetadataOnly() bool } func GetCommand(cmd instructions.Command, buildcontext string) (DockerCommand, error) { diff --git a/pkg/commands/copy.go b/pkg/commands/copy.go index d70d98483..1e8f196b8 100644 --- a/pkg/commands/copy.go +++ b/pkg/commands/copy.go @@ -134,3 +134,7 @@ func (c *CopyCommand) FilesUsedFromContext(config *v1.Config, buildArgs *dockerf logrus.Infof("Using files from context: %v", files) return files, nil } + +func (c *CopyCommand) MetadataOnly() bool { + return false +} diff --git a/pkg/commands/run.go b/pkg/commands/run.go index 0290e6f7c..72772922b 100644 --- a/pkg/commands/run.go +++ b/pkg/commands/run.go @@ -161,6 +161,10 @@ func (r *RunCommand) CacheCommand(img v1.Image) DockerCommand { } } +func (r *RunCommand) MetadataOnly() bool { + return false +} + type CachingRunCommand struct { BaseCommand img v1.Image diff --git a/pkg/commands/workdir.go b/pkg/commands/workdir.go index 12a2f44d8..abc1f9c06 100644 --- a/pkg/commands/workdir.go +++ b/pkg/commands/workdir.go @@ -67,3 +67,7 @@ func (w *WorkdirCommand) FilesToSnapshot() []string { func (w *WorkdirCommand) String() string { return w.cmd.String() } + +func (w *WorkdirCommand) MetadataOnly() bool { + return false +} diff --git a/pkg/executor/build.go b/pkg/executor/build.go index 35a37d70a..62ad15629 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -86,6 +86,63 @@ func newStageBuilder(opts *config.KanikoOptions, stage config.KanikoStage) (*sta }, nil } +func (s *stageBuilder) optimize(compositeKey CompositeCache, cfg v1.Config, cmds []commands.DockerCommand) error { + if !s.opts.Cache { + return nil + } + + args := dockerfile.NewBuildArgs(s.opts.BuildArgs) + args.AddMetaArgs(s.stage.MetaArgs) + + layerCache := &cache.RegistryCache{ + Opts: s.opts, + } + + // Possibly replace commands with their cached implementations. + // We walk through all the commands, running any commands that only operate on metadata. + // We throw the metadata away after, but we need it to properly track command dependencies + // for things like COPY ${FOO} or RUN commands that use environment variables. + for i, command := range cmds { + if command == nil { + continue + } + compositeKey.AddKey(command.String()) + // If the command uses files from the context, add them. + files, err := command.FilesUsedFromContext(&cfg, args) + if err != nil { + return err + } + for _, f := range files { + if err := compositeKey.AddPath(f); err != nil { + return err + } + } + + ck, err := compositeKey.Hash() + if err != nil { + return err + } + img, err := layerCache.RetrieveLayer(ck) + if err != nil { + logrus.Infof("No cached layer found for cmd %s", command.String()) + break + } + + if cacheCmd := command.CacheCommand(img); cacheCmd != nil { + logrus.Infof("Using caching version of cmd: %s", command.String()) + cmds[i] = cacheCmd + } + + // Mutate the config for any commands that require it. + if command.MetadataOnly() { + if err := command.ExecuteCommand(&cfg, args); err != nil { + return err + } + } + } + return nil +} + func (s *stageBuilder) build() error { // Unpack file system to root if _, err := util.GetFSFromImage(constants.RootDir, s.image); err != nil { @@ -109,34 +166,13 @@ func (s *stageBuilder) build() error { cmds = append(cmds, command) } - layerCache := &cache.RegistryCache{ - Opts: s.opts, - } - if s.opts.Cache { - // Possibly replace commands with their cached implementations. - for i, command := range cmds { - if command == nil { - continue - } - ck, err := compositeKey.Hash() - if err != nil { - return err - } - img, err := layerCache.RetrieveLayer(ck) - if err != nil { - logrus.Infof("No cached layer found for cmd %s", command.String()) - break - } - - if cacheCmd := command.CacheCommand(img); cacheCmd != nil { - logrus.Infof("Using caching version of cmd: %s", command.String()) - cmds[i] = cacheCmd - } - } - } - args := dockerfile.NewBuildArgs(s.opts.BuildArgs) args.AddMetaArgs(s.stage.MetaArgs) + + // Apply optimizations to the instructions. + if err := s.optimize(*compositeKey, s.cf.Config, cmds); err != nil { + return err + } for index, command := range cmds { if command == nil { continue From 063663e17b630a6eee0571c2f555407c53a3e192 Mon Sep 17 00:00:00 2001 From: dlorenc Date: Mon, 12 Nov 2018 12:51:45 -0600 Subject: [PATCH 39/42] Skip unpacking the base FS if there are no run commands (or only cached ones). (#440) This is the final part of an optimization that I've been refactoring towards for awhile. If the Dockerfile consists of no RUN commands, or cached RUN commands, followed by metadata-only operations, we can skip downloading and unpacking the base image. --- pkg/commands/base_command.go | 4 ++++ pkg/commands/commands.go | 2 ++ pkg/commands/run.go | 4 ++++ pkg/executor/build.go | 39 +++++++++++++++++++++++------------- pkg/util/fs_util.go | 11 +++++----- pkg/util/fs_util_test.go | 5 +++-- 6 files changed, 43 insertions(+), 22 deletions(-) diff --git a/pkg/commands/base_command.go b/pkg/commands/base_command.go index 698b72821..fe6a45c71 100644 --- a/pkg/commands/base_command.go +++ b/pkg/commands/base_command.go @@ -39,3 +39,7 @@ func (b *BaseCommand) FilesUsedFromContext(_ *v1.Config, _ *dockerfile.BuildArgs func (b *BaseCommand) MetadataOnly() bool { return true } + +func (b *BaseCommand) RequiresUnpackedFS() bool { + return false +} diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go index d6ed04b59..b8acade2c 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -43,6 +43,8 @@ type DockerCommand interface { FilesUsedFromContext(*v1.Config, *dockerfile.BuildArgs) ([]string, error) MetadataOnly() bool + + RequiresUnpackedFS() bool } func GetCommand(cmd instructions.Command, buildcontext string) (DockerCommand, error) { diff --git a/pkg/commands/run.go b/pkg/commands/run.go index 72772922b..5295cab82 100644 --- a/pkg/commands/run.go +++ b/pkg/commands/run.go @@ -165,6 +165,10 @@ func (r *RunCommand) MetadataOnly() bool { return false } +func (r *RunCommand) RequiresUnpackedFS() bool { + return true +} + type CachingRunCommand struct { BaseCommand img v1.Image diff --git a/pkg/executor/build.go b/pkg/executor/build.go index 62ad15629..f039fb5ca 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -86,14 +86,11 @@ func newStageBuilder(opts *config.KanikoOptions, stage config.KanikoStage) (*sta }, nil } -func (s *stageBuilder) optimize(compositeKey CompositeCache, cfg v1.Config, cmds []commands.DockerCommand) error { +func (s *stageBuilder) optimize(compositeKey CompositeCache, cfg v1.Config, cmds []commands.DockerCommand, args *dockerfile.BuildArgs) error { if !s.opts.Cache { return nil } - args := dockerfile.NewBuildArgs(s.opts.BuildArgs) - args.AddMetaArgs(s.stage.MetaArgs) - layerCache := &cache.RegistryCache{ Opts: s.opts, } @@ -144,15 +141,6 @@ func (s *stageBuilder) optimize(compositeKey CompositeCache, cfg v1.Config, cmds } func (s *stageBuilder) build() error { - // Unpack file system to root - if _, err := util.GetFSFromImage(constants.RootDir, s.image); err != nil { - return err - } - // Take initial snapshot - if err := s.snapshotter.Init(); err != nil { - return err - } - // Set the initial cache key to be the base image digest, the build args and the SrcContext. compositeKey := NewCompositeCache(s.baseImageDigest) compositeKey.AddKey(s.opts.BuildArgs...) @@ -170,9 +158,32 @@ func (s *stageBuilder) build() error { args.AddMetaArgs(s.stage.MetaArgs) // Apply optimizations to the instructions. - if err := s.optimize(*compositeKey, s.cf.Config, cmds); err != nil { + if err := s.optimize(*compositeKey, s.cf.Config, cmds, args); err != nil { return err } + + // Unpack file system to root if we need to. + shouldUnpack := false + for _, cmd := range cmds { + if cmd.RequiresUnpackedFS() { + logrus.Infof("Unpacking rootfs as cmd %s requires it.", cmd.String()) + shouldUnpack = true + break + } + } + if shouldUnpack { + if _, err := util.GetFSFromImage(constants.RootDir, s.image); err != nil { + return err + } + } + if err := util.DetectFilesystemWhitelist(constants.WhitelistPath); err != nil { + return err + } + // Take initial snapshot + if err := s.snapshotter.Init(); err != nil { + return err + } + for index, command := range cmds { if command == nil { continue diff --git a/pkg/util/fs_util.go b/pkg/util/fs_util.go index fd53ee7bf..a0778bb18 100644 --- a/pkg/util/fs_util.go +++ b/pkg/util/fs_util.go @@ -62,8 +62,7 @@ var whitelist = []WhitelistEntry{ // GetFSFromImage extracts the layers of img to root // It returns a list of all files extracted func GetFSFromImage(root string, img v1.Image) ([]string, error) { - whitelist, err := fileSystemWhitelist(constants.WhitelistPath) - if err != nil { + if err := DetectFilesystemWhitelist(constants.WhitelistPath); err != nil { return nil, err } logrus.Debugf("Mounted directories: %v", whitelist) @@ -303,10 +302,10 @@ func checkWhitelistRoot(root string) bool { // (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11) // Where (5) is the mount point relative to the process's root // From: https://www.kernel.org/doc/Documentation/filesystems/proc.txt -func fileSystemWhitelist(path string) ([]WhitelistEntry, error) { +func DetectFilesystemWhitelist(path string) error { f, err := os.Open(path) if err != nil { - return nil, err + return err } defer f.Close() reader := bufio.NewReader(f) @@ -314,7 +313,7 @@ func fileSystemWhitelist(path string) ([]WhitelistEntry, error) { line, err := reader.ReadString('\n') logrus.Debugf("Read the following line from %s: %s", path, line) if err != nil && err != io.EOF { - return nil, err + return err } lineArr := strings.Split(line, " ") if len(lineArr) < 5 { @@ -336,7 +335,7 @@ func fileSystemWhitelist(path string) ([]WhitelistEntry, error) { break } } - return whitelist, nil + return nil } // RelativeFiles returns a list of all files at the filepath relative to root diff --git a/pkg/util/fs_util_test.go b/pkg/util/fs_util_test.go index 968cacf99..20f80820e 100644 --- a/pkg/util/fs_util_test.go +++ b/pkg/util/fs_util_test.go @@ -29,7 +29,7 @@ import ( "github.com/GoogleContainerTools/kaniko/testutil" ) -func Test_fileSystemWhitelist(t *testing.T) { +func Test_DetectFilesystemWhitelist(t *testing.T) { testDir, err := ioutil.TempDir("", "") if err != nil { t.Fatalf("Error creating tempdir: %s", err) @@ -49,7 +49,7 @@ func Test_fileSystemWhitelist(t *testing.T) { t.Fatalf("Error writing file contents to %s: %s", path, err) } - actualWhitelist, err := fileSystemWhitelist(path) + err = DetectFilesystemWhitelist(path) expectedWhitelist := []WhitelistEntry{ {"/kaniko", false}, {"/proc", false}, @@ -59,6 +59,7 @@ func Test_fileSystemWhitelist(t *testing.T) { {"/var/run", false}, {"/etc/mtab", false}, } + actualWhitelist := whitelist sort.Slice(actualWhitelist, func(i, j int) bool { return actualWhitelist[i].Path < actualWhitelist[j].Path }) From 8408c53aa815d7dc32412aa81c590efd2936e049 Mon Sep 17 00:00:00 2001 From: dlorenc Date: Mon, 12 Nov 2018 16:22:04 -0600 Subject: [PATCH 40/42] Improve cache layer uploads. (#443) This change only uploads layers that were created from cache misses on RUN commands. It also improves the cache-checking logic to handle this case. Finally, it makes cache layer uploads happen in parallel with the rest of the build, logging a warning if any fail. --- pkg/commands/base_command.go | 4 ++++ pkg/commands/commands.go | 3 +++ pkg/commands/run.go | 4 ++++ pkg/executor/build.go | 40 +++++++++++++++++++++--------------- pkg/executor/push.go | 6 +++++- 5 files changed, 40 insertions(+), 17 deletions(-) diff --git a/pkg/commands/base_command.go b/pkg/commands/base_command.go index fe6a45c71..3d2cd8fad 100644 --- a/pkg/commands/base_command.go +++ b/pkg/commands/base_command.go @@ -43,3 +43,7 @@ func (b *BaseCommand) MetadataOnly() bool { func (b *BaseCommand) RequiresUnpackedFS() bool { return false } + +func (b *BaseCommand) ShouldCacheOutput() bool { + return false +} diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go index b8acade2c..2615138be 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -36,6 +36,7 @@ type DockerCommand interface { String() string // A list of files to snapshot, empty for metadata commands or nil if we don't know FilesToSnapshot() []string + // Return a cache-aware implementation of this command, if it exists. CacheCommand(v1.Image) DockerCommand @@ -45,6 +46,8 @@ type DockerCommand interface { MetadataOnly() bool RequiresUnpackedFS() bool + + ShouldCacheOutput() bool } func GetCommand(cmd instructions.Command, buildcontext string) (DockerCommand, error) { diff --git a/pkg/commands/run.go b/pkg/commands/run.go index 5295cab82..7a9bf03dc 100644 --- a/pkg/commands/run.go +++ b/pkg/commands/run.go @@ -169,6 +169,10 @@ func (r *RunCommand) RequiresUnpackedFS() bool { return true } +func (r *RunCommand) ShouldCacheOutput() bool { + return true +} + type CachingRunCommand struct { BaseCommand img v1.Image diff --git a/pkg/executor/build.go b/pkg/executor/build.go index f039fb5ca..0f9f3dd2d 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -23,6 +23,8 @@ import ( "strconv" "time" + "golang.org/x/sync/errgroup" + "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/mutate" @@ -119,15 +121,17 @@ func (s *stageBuilder) optimize(compositeKey CompositeCache, cfg v1.Config, cmds if err != nil { return err } - img, err := layerCache.RetrieveLayer(ck) - if err != nil { - logrus.Infof("No cached layer found for cmd %s", command.String()) - break - } + if command.ShouldCacheOutput() { + img, err := layerCache.RetrieveLayer(ck) + if err != nil { + logrus.Infof("No cached layer found for cmd %s", command.String()) + break + } - if cacheCmd := command.CacheCommand(img); cacheCmd != nil { - logrus.Infof("Using caching version of cmd: %s", command.String()) - cmds[i] = cacheCmd + if cacheCmd := command.CacheCommand(img); cacheCmd != nil { + logrus.Infof("Using caching version of cmd: %s", command.String()) + cmds[i] = cacheCmd + } } // Mutate the config for any commands that require it. @@ -184,6 +188,7 @@ func (s *stageBuilder) build() error { return err } + cacheGroup := errgroup.Group{} for index, command := range cmds { if command == nil { continue @@ -222,10 +227,19 @@ func (s *stageBuilder) build() error { if err != nil { return err } - if err := s.saveSnapshotToImage(command.String(), ck, tarPath); err != nil { + // Push layer to cache (in parallel) now along with new config file + if s.opts.Cache && command.ShouldCacheOutput() { + cacheGroup.Go(func() error { + return pushLayerToCache(s.opts, ck, tarPath, command.String()) + }) + } + if err := s.saveSnapshotToImage(command.String(), tarPath); err != nil { return err } } + if err := cacheGroup.Wait(); err != nil { + logrus.Warnf("error uploading layer to cache: %s", err) + } return nil } @@ -266,7 +280,7 @@ func (s *stageBuilder) shouldTakeSnapshot(index int, files []string) bool { return true } -func (s *stageBuilder) saveSnapshotToImage(createdBy string, ck string, tarPath string) error { +func (s *stageBuilder) saveSnapshotToImage(createdBy string, tarPath string) error { if tarPath == "" { return nil } @@ -283,12 +297,6 @@ func (s *stageBuilder) saveSnapshotToImage(createdBy string, ck string, tarPath if err != nil { return err } - // Push layer to cache now along with new config file - if s.opts.Cache { - if err := pushLayerToCache(s.opts, ck, layer, createdBy); err != nil { - return err - } - } s.image, err = mutate.Append(s.image, mutate.Addendum{ Layer: layer, diff --git a/pkg/executor/push.go b/pkg/executor/push.go index 796c17cf7..c2fe76d41 100644 --- a/pkg/executor/push.go +++ b/pkg/executor/push.go @@ -107,7 +107,11 @@ func DoPush(image v1.Image, opts *config.KanikoOptions) error { // pushLayerToCache pushes layer (tagged with cacheKey) to opts.Cache // if opts.Cache doesn't exist, infer the cache from the given destination -func pushLayerToCache(opts *config.KanikoOptions, cacheKey string, layer v1.Layer, createdBy string) error { +func pushLayerToCache(opts *config.KanikoOptions, cacheKey string, tarPath string, createdBy string) error { + layer, err := tarball.LayerFromFile(tarPath) + if err != nil { + return err + } cache, err := cache.Destination(opts, cacheKey) if err != nil { return errors.Wrap(err, "getting cache destination") From e7d11230732123cce729f65f5461854d074679ee Mon Sep 17 00:00:00 2001 From: Adrian Duong Date: Tue, 13 Nov 2018 07:50:55 -0800 Subject: [PATCH 41/42] Hide --azure-container-registry-config flag (#445) ACR is not supported and the flag is a side-effect of vendoring. The ACR credentials pkg uses pflag.String directly. --- cmd/executor/cmd/root.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cmd/executor/cmd/root.go b/cmd/executor/cmd/root.go index 262856a76..22ff0f456 100644 --- a/cmd/executor/cmd/root.go +++ b/cmd/executor/cmd/root.go @@ -33,6 +33,7 @@ import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) var ( @@ -114,9 +115,9 @@ func addKanikoOptionsFlags(cmd *cobra.Command) { // addHiddenFlags marks certain flags as hidden from the executor help text func addHiddenFlags(cmd *cobra.Command) { // This flag is added in a vendored directory, hide so that it doesn't come up via --help - RootCmd.PersistentFlags().MarkHidden("azure-container-registry-config") + pflag.CommandLine.MarkHidden("azure-container-registry-config") // Hide this flag as we want to encourage people to use the --context flag instead - RootCmd.PersistentFlags().MarkHidden("bucket") + cmd.PersistentFlags().MarkHidden("bucket") } func checkContained() bool { From 5df363a0f6493bb334572d9218448a8cc3b448ee Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Tue, 13 Nov 2018 10:12:03 -0800 Subject: [PATCH 42/42] Check if command is nil before optimizing MAINTAINER returns nil since it's deprecated, so we should make sure we don't add to the list of commands to optimize. --- pkg/executor/build.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/executor/build.go b/pkg/executor/build.go index 0f9f3dd2d..e11c0fd53 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -155,6 +155,9 @@ func (s *stageBuilder) build() error { if err != nil { return err } + if command == nil { + continue + } cmds = append(cmds, command) }