diff --git a/README.md b/README.md index 10fb58ab6..f2f0dca2c 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,7 @@ _If you are interested in contributing to kaniko, see - [Flag `--no-push`](#flag---no-push) - [Flag `--no-push-cache`](#flag---no-push-cache) - [Flag `--oci-layout-path`](#flag---oci-layout-path) + - [Flag `--preserve-context`](#flag---preserve-context) - [Flag `--push-retry`](#flag---push-retry) - [Flag `--registry-certificate`](#flag---registry-certificate) - [Flag `--registry-client-cert`](#flag---registry-client-cert) @@ -978,6 +979,17 @@ _Note: Depending on the built image, the media type of the image manifest might be either `application/vnd.oci.image.manifest.v1+json` or `application/vnd.docker.distribution.manifest.v2+json`._ +#### Flag `--preserve-context` + +Set this boolean flag to `true` if you want kaniko to restore the build-context for multi-stage builds. +If set, kaniko will take a snapshot of the full filesystem before it starts building to later restore to that state. If combined with the `--cleanup` flag it will also restore the state after cleanup. + +This is useful if you want to pass in secrets via files or if you want to execute commands after the build completes. + +It will only take the snapshot if we are building a multistage image or if we plan to cleanup the filesystem after the build. + +Defaults to `false` + #### Flag `--push-ignore-immutable-tag-errors` Set this boolean flag to `true` if you want the Kaniko process to exit with diff --git a/cmd/executor/cmd/root.go b/cmd/executor/cmd/root.go index 7f0339c5d..ea5257e05 100644 --- a/cmd/executor/cmd/root.go +++ b/cmd/executor/cmd/root.go @@ -280,6 +280,7 @@ func addKanikoOptionsFlags() { RootCmd.PersistentFlags().VarP(&opts.IgnorePaths, "ignore-path", "", "Ignore these paths when taking a snapshot. Set it repeatedly for multiple paths.") RootCmd.PersistentFlags().BoolVarP(&opts.ForceBuildMetadata, "force-build-metadata", "", false, "Force add metadata layers to build image") RootCmd.PersistentFlags().BoolVarP(&opts.SkipPushPermissionCheck, "skip-push-permission-check", "", false, "Skip check of the push permission") + RootCmd.PersistentFlags().BoolVarP(&opts.PreserveContext, "preserve-context", "", false, "Preserve build context accross build stages by taking a snapshot of the full filesystem before build and restore it after we switch stages. Restores in the end too if passed together with 'cleanup'") // Deprecated flags. RootCmd.PersistentFlags().StringVarP(&opts.SnapshotModeDeprecated, "snapshotMode", "", "", "This flag is deprecated. Please use '--snapshot-mode'.") diff --git a/pkg/config/options.go b/pkg/config/options.go index dbc1e0297..ea666ceff 100644 --- a/pkg/config/options.go +++ b/pkg/config/options.go @@ -91,6 +91,7 @@ type KanikoOptions struct { ForceBuildMetadata bool InitialFSUnpacked bool SkipPushPermissionCheck bool + PreserveContext bool } type KanikoGitOptions struct { diff --git a/pkg/executor/build.go b/pkg/executor/build.go index 73b2f0df2..638acbcb5 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -85,6 +85,20 @@ type stageBuilder struct { pushLayerToCache cachePusher } +func makeSnapshotter(opts *config.KanikoOptions) (*snapshot.Snapshotter, error) { + err := util.InitIgnoreList() + if err != nil { + return nil, errors.Wrap(err, "failed to initialize ignore list") + } + + hasher, err := getHasher(opts.SnapshotMode) + if err != nil { + return nil, err + } + l := snapshot.NewLayeredMap(hasher) + return snapshot.NewSnapshotter(l, config.RootDir), nil +} + // newStageBuilder returns a new type stageBuilder which contains all the information required to build the stage func newStageBuilder(args *dockerfile.BuildArgs, opts *config.KanikoOptions, stage config.KanikoStage, crossStageDeps map[int][]string, dcm map[string]string, sid map[string]string, stageNameToIdx map[string]string, fileContext util.FileContext) (*stageBuilder, error) { sourceImage, err := image_util.RetrieveSourceImage(stage, opts) @@ -101,17 +115,10 @@ func newStageBuilder(args *dockerfile.BuildArgs, opts *config.KanikoOptions, sta return nil, err } - err = util.InitIgnoreList() - if err != nil { - return nil, errors.Wrap(err, "failed to initialize ignore list") - } - - hasher, err := getHasher(opts.SnapshotMode) + snapshotter, err := makeSnapshotter(opts) if err != nil { return nil, err } - l := snapshot.NewLayeredMap(hasher) - snapshotter := snapshot.NewSnapshotter(l, config.RootDir) digest, err := sourceImage.Digest() if err != nil { @@ -688,6 +695,23 @@ func CalculateDependencies(stages []config.KanikoStage, opts *config.KanikoOptio return depGraph, nil } +func restoreFilesystem(tarball string, opts *config.KanikoOptions) error { + if err := util.DeleteFilesystem(); err != nil { + return err + } + if opts.PreserveContext { + if tarball == "" { + return fmt.Errorf("context snapshot is missing") + } + _, err := util.UnpackLocalTarArchive(tarball, config.RootDir) + if err != nil { + return errors.Wrap(err, "failed to unpack context snapshot") + } + logrus.Info("Context restored") + } + return nil +} + // DoBuild executes building the Dockerfile func DoBuild(opts *config.KanikoOptions) (v1.Image, error) { t := timing.Start("Total Build Time") @@ -722,6 +746,24 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) { var args *dockerfile.BuildArgs + var tarball string + if opts.PreserveContext { + if len(kanikoStages) > 1 || opts.Cleanup { + logrus.Info("Creating snapshot of build context") + snapshotter, err := makeSnapshotter(opts) + if err != nil { + return nil, err + } + + tarball, err = snapshotter.TakeSnapshotFS() + if err != nil { + return nil, err + } + } else { + logrus.Info("Skipping context snapshot as no-one requires it") + } + } + for index, stage := range kanikoStages { sb, err := newStageBuilder( args, opts, stage, @@ -787,7 +829,8 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) { } } if opts.Cleanup { - if err = util.DeleteFilesystem(); err != nil { + err = restoreFilesystem(tarball, opts) + if err != nil { return nil, err } } @@ -819,8 +862,9 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) { } // Delete the filesystem - if err := util.DeleteFilesystem(); err != nil { - return nil, errors.Wrap(err, fmt.Sprintf("deleting file system after stage %d", index)) + err = restoreFilesystem(tarball, opts) + if err != nil { + return nil, err } }