diff --git a/cmd/executor/cmd/root.go b/cmd/executor/cmd/root.go index 7f0339c5d..c03dd616a 100644 --- a/cmd/executor/cmd/root.go +++ b/cmd/executor/cmd/root.go @@ -257,6 +257,8 @@ func addKanikoOptionsFlags() { RootCmd.PersistentFlags().VarP(&opts.Compression, "compression", "", "Compression algorithm (gzip, zstd)") RootCmd.PersistentFlags().IntVarP(&opts.CompressionLevel, "compression-level", "", -1, "Compression level") RootCmd.PersistentFlags().BoolVarP(&opts.Cache, "cache", "", false, "Use cache when building image") + RootCmd.PersistentFlags().VarP(&opts.ExcludeBuildArgsFromCacheKey, "exclude-build-arg-from-cache-key", "", "This flag allows you to exclude ARG values from the cache key. Set it repeatedly for multiple values.") + RootCmd.PersistentFlags().VarP(&opts.ExcludeEnvsFromCacheKey, "exclude-env-from-cache-key", "", "This flag allows you to exclude ENV values from the cache key. Set it repeatedly for multiple values.") RootCmd.PersistentFlags().BoolVarP(&opts.CompressedCaching, "compressed-caching", "", true, "Compress the cached layers. Decreases build time, but increases memory usage.") RootCmd.PersistentFlags().BoolVarP(&opts.Cleanup, "cleanup", "", false, "Clean the filesystem at the end") RootCmd.PersistentFlags().DurationVarP(&opts.CacheTTL, "cache-ttl", "", time.Hour*336, "Cache timeout, requires value and unit of duration -> ex: 6h. Defaults to two weeks.") diff --git a/pkg/config/options.go b/pkg/config/options.go index dbc1e0297..37243a09f 100644 --- a/pkg/config/options.go +++ b/pkg/config/options.go @@ -28,6 +28,9 @@ import ( type CacheOptions struct { CacheDir string CacheTTL time.Duration + + ExcludeBuildArgsFromCacheKey multiArg + ExcludeEnvsFromCacheKey multiArg } // RegistryOptions are all the options related to the registries, set by command line arguments. diff --git a/pkg/executor/build.go b/pkg/executor/build.go index 73b2f0df2..399a51693 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -199,20 +199,39 @@ func isOCILayout(path string) bool { } func (s *stageBuilder) populateCompositeKey(command commands.DockerCommand, files []string, compositeKey CompositeCache, args *dockerfile.BuildArgs, env []string) (CompositeCache, error) { - // First replace all the environment variables or args in the command - replacementEnvs := args.ReplacementEnvs(env) - // The sort order of `replacementEnvs` is basically undefined, sort it - // so we can ensure a stable cache key. - sort.Strings(replacementEnvs) - // Use the special argument "|#" at the start of the args array. This will - // avoid conflicts with any RUN command since commands can not - // start with | (vertical bar). The "#" (number of build envs) is there to - // help ensure proper cache matches. - if command.IsArgsEnvsRequiredInCache() { - if len(replacementEnvs) > 0 { - compositeKey.AddKey(fmt.Sprintf("|%d", len(replacementEnvs))) - compositeKey.AddKey(replacementEnvs...) + filteredEnvs := []string{} + for _, e := range env { + envName := strings.SplitN(e, "=", 2)[0] + if s.opts.ExcludeEnvsFromCacheKey.Contains(envName) { + continue + } + filteredEnvs = append(filteredEnvs, e) + } + + // First replace all the environment variables or args in the command + envsAndBuildArgs := args.ReplacementEnvs(filteredEnvs) + + filteredEnvsAndBuildArgs := []string{} + for i, env := range envsAndBuildArgs { + envArgName := strings.SplitN(env, "=", 2)[0] + if s.opts.ExcludeBuildArgsFromCacheKey.Contains(envArgName) { + continue + } + filteredEnvsAndBuildArgs = append(filteredEnvs, envsAndBuildArgs[i]) + } + + // The sort order of `filteredEnvs` is basically undefined, sort it + // so we can ensure a stable cache key. + sort.Strings(filteredEnvsAndBuildArgs) + + if len(filteredEnvsAndBuildArgs) > 0 { + // Use the special argument "|#" at the start of the args array. This will + // avoid conflicts with any RUN command since commands can not + // start with | (vertical bar). The "#" (number of build envs) is there to + // help ensure proper cache matches. + compositeKey.AddKey(fmt.Sprintf("|%d", len(filteredEnvsAndBuildArgs))) + compositeKey.AddKey(filteredEnvsAndBuildArgs...) } } diff --git a/pkg/executor/build_test.go b/pkg/executor/build_test.go index 7065b8628..9bfaa846c 100644 --- a/pkg/executor/build_test.go +++ b/pkg/executor/build_test.go @@ -655,10 +655,12 @@ func newStageContext(command string, args map[string]string, env []string) stage func Test_stageBuilder_populateCompositeKey(t *testing.T) { type testcase struct { - description string - cmd1 stageContext - cmd2 stageContext - shdEqual bool + description string + cmd1 stageContext + cmd2 stageContext + shdEqual bool + excludeBuildArgsFromCacheKey []string + excludeEnvsFromCacheKey []string } testCases := []testcase{ { @@ -744,6 +746,21 @@ func Test_stageBuilder_populateCompositeKey(t *testing.T) { []string{}, ), }, + { + description: "cache key for same command [RUN] with a build arg values, with excludeBuildArgsFromCacheKey set", + cmd1: newStageContext( + "RUN echo $ARG > test", + map[string]string{"ARG": "foo"}, + []string{}, + ), + cmd2: newStageContext( + "RUN echo $ARG > test", + map[string]string{"ARG": "bar"}, + []string{}, + ), + shdEqual: true, + excludeBuildArgsFromCacheKey: []string{"ARG"}, + }, { description: "cache key for same command [RUN] with different env values", cmd1: newStageContext( @@ -798,6 +815,21 @@ func Test_stageBuilder_populateCompositeKey(t *testing.T) { ), shdEqual: false, }, + { + description: "cache key for command [RUN] with different env values, when excludeEnvsFromCacheKey is set", + cmd1: newStageContext( + "RUN echo ${APP_VERSION%.*} ${APP_VERSION%-*} > test", + map[string]string{"ARG": "foo"}, + []string{"ENV=1"}, + ), + cmd2: newStageContext( + "RUN echo ${APP_VERSION%.*} ${APP_VERSION%-*} > test", + map[string]string{"ARG": "foo"}, + []string{"ENV=2"}, + ), + shdEqual: true, + excludeEnvsFromCacheKey: []string{"ENV"}, + }, func() testcase { dir, files := tempDirAndFile(t) file := files[0] @@ -867,7 +899,15 @@ func Test_stageBuilder_populateCompositeKey(t *testing.T) { } for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { - sb := &stageBuilder{fileContext: util.FileContext{Root: "workspace"}} + sb := &stageBuilder{ + fileContext: util.FileContext{Root: "workspace"}, + opts: &config.KanikoOptions{ + CacheOptions: config.CacheOptions{ + ExcludeBuildArgsFromCacheKey: tc.excludeBuildArgsFromCacheKey, + ExcludeEnvsFromCacheKey: tc.excludeEnvsFromCacheKey, + }, + }, + } ck := CompositeCache{} instructions1, err := dockerfile.ParseCommands([]string{tc.cmd1.command.String()})