diff --git a/cmd/executor/cmd/root.go b/cmd/executor/cmd/root.go index a18908cd6..4d9c019d5 100644 --- a/cmd/executor/cmd/root.go +++ b/cmd/executor/cmd/root.go @@ -219,7 +219,7 @@ func addKanikoOptionsFlags() { 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().BoolVarP(&opts.NoPushCache, "no-push-cache", "", false, "Do not push the cache layers 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.CacheRepo, "cache-repo", "", "", "Specify a repository to use as a cache, otherwise one will be inferred from the destination provided; when prefixed with 'oci:' the repository will be written in OCI image layout format at the path provided") RootCmd.PersistentFlags().StringVarP(&opts.CacheDir, "cache-dir", "", "/cache", "Specify a local directory to use as a cache.") RootCmd.PersistentFlags().StringVarP(&opts.DigestFile, "digest-file", "", "", "Specify a file to save the digest of the built image to.") RootCmd.PersistentFlags().StringVarP(&opts.ImageNameDigestFile, "image-name-with-digest-file", "", "", "Specify a file to save the image name w/ digest of the built image to.") diff --git a/integration/dockerfiles/Dockerfile_test_cache_copy_oci b/integration/dockerfiles/Dockerfile_test_cache_copy_oci new file mode 100644 index 000000000..661397eae --- /dev/null +++ b/integration/dockerfiles/Dockerfile_test_cache_copy_oci @@ -0,0 +1,3 @@ +FROM google/cloud-sdk:256.0.0-alpine + +COPY context/foo /usr/bin diff --git a/integration/dockerfiles/Dockerfile_test_cache_install_oci b/integration/dockerfiles/Dockerfile_test_cache_install_oci new file mode 100644 index 000000000..455fc26c0 --- /dev/null +++ b/integration/dockerfiles/Dockerfile_test_cache_install_oci @@ -0,0 +1,23 @@ +# 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. + +# Test to make sure the cache works properly +# /date should be the same regardless of when this image is built +# if the cache is implemented correctly + +FROM debian:9.11 +WORKDIR /foo +RUN apt-get update && apt-get install -y make +COPY context/bar /context +RUN echo "hey" > foo diff --git a/integration/dockerfiles/Dockerfile_test_cache_oci b/integration/dockerfiles/Dockerfile_test_cache_oci new file mode 100644 index 000000000..d6f78798c --- /dev/null +++ b/integration/dockerfiles/Dockerfile_test_cache_oci @@ -0,0 +1,22 @@ +# 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. + +# Test to make sure the cache works properly +# If the image is built twice, /date should be the same in both images +# if the cache is implemented correctly + +FROM debian:9.11 +RUN date > /date +COPY context/foo /foo +RUN echo hey diff --git a/integration/dockerfiles/Dockerfile_test_cache_perm_oci b/integration/dockerfiles/Dockerfile_test_cache_perm_oci new file mode 100644 index 000000000..06c311fe6 --- /dev/null +++ b/integration/dockerfiles/Dockerfile_test_cache_perm_oci @@ -0,0 +1,8 @@ +# Test to make sure the cache works with special file permissions properly. +# If the image is built twice, directory foo should have the sticky bit, +# and file bar should have the setuid and setgid bits. + +FROM busybox + +RUN mkdir foo && chmod +t foo +RUN touch bar && chmod u+s,g+s bar diff --git a/integration/images.go b/integration/images.go index 444b21b05..58f1737ac 100644 --- a/integration/images.go +++ b/integration/images.go @@ -186,9 +186,10 @@ func FindDockerFiles(dir, dockerfilesPattern 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]struct{} - DockerfilesToIgnore map[string]struct{} - TestCacheDockerfiles map[string]struct{} + filesBuilt map[string]struct{} + DockerfilesToIgnore map[string]struct{} + TestCacheDockerfiles map[string]struct{} + TestOCICacheDockerfiles map[string]struct{} } type logger func(string, ...interface{}) @@ -216,6 +217,12 @@ func NewDockerFileBuilder() *DockerFileBuilder { "Dockerfile_test_cache_perm": {}, "Dockerfile_test_cache_copy": {}, } + d.TestOCICacheDockerfiles = map[string]struct{}{ + "Dockerfile_test_cache_oci": {}, + "Dockerfile_test_cache_install_oci": {}, + "Dockerfile_test_cache_perm_oci": {}, + "Dockerfile_test_cache_copy_oci": {}, + } return &d } @@ -355,42 +362,40 @@ func populateVolumeCache() error { 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(config *integrationTestConfig, cacheRepo, dockerfilesPath string, version int, args []string) error { +// buildCachedImage builds the image for testing caching via kaniko where version is the nth time this image has been built +func (d *DockerFileBuilder) buildCachedImage(config *integrationTestConfig, cacheRepo, dockerfilesPath, dockerfile string, version int, args []string) error { imageRepo, serviceAccount := config.imageRepo, config.serviceAccount _, ex, _, _ := runtime.Caller(0) cwd := filepath.Dir(ex) cacheFlag := "--cache=true" - for dockerfile := range d.TestCacheDockerfiles { - benchmarkEnv := "BENCHMARK_FILE=false" - if b, err := strconv.ParseBool(os.Getenv("BENCHMARK")); err == nil && b { - os.Mkdir("benchmarks", 0755) - benchmarkEnv = "BENCHMARK_FILE=/workspace/benchmarks/" + dockerfile - } - kanikoImage := GetVersionedKanikoImage(imageRepo, dockerfile, version) + benchmarkEnv := "BENCHMARK_FILE=false" + if b, err := strconv.ParseBool(os.Getenv("BENCHMARK")); err == nil && b { + os.Mkdir("benchmarks", 0755) + benchmarkEnv = "BENCHMARK_FILE=/workspace/benchmarks/" + dockerfile + } + kanikoImage := GetVersionedKanikoImage(imageRepo, dockerfile, version) - dockerRunFlags := []string{"run", "--net=host", - "-v", cwd + ":/workspace", - "-e", benchmarkEnv} - dockerRunFlags = addServiceAccountFlags(dockerRunFlags, serviceAccount) - dockerRunFlags = append(dockerRunFlags, ExecutorImage, - "-f", path.Join(buildContextPath, dockerfilesPath, dockerfile), - "-d", kanikoImage, - "-c", buildContextPath, - cacheFlag, - "--cache-repo", cacheRepo, - "--cache-dir", cacheDir) - for _, v := range args { - dockerRunFlags = append(dockerRunFlags, v) - } - kanikoCmd := exec.Command("docker", dockerRunFlags...) + dockerRunFlags := []string{"run", "--net=host", + "-v", cwd + ":/workspace", + "-e", benchmarkEnv} + dockerRunFlags = addServiceAccountFlags(dockerRunFlags, serviceAccount) + dockerRunFlags = append(dockerRunFlags, ExecutorImage, + "-f", path.Join(buildContextPath, dockerfilesPath, dockerfile), + "-d", kanikoImage, + "-c", buildContextPath, + cacheFlag, + "--cache-repo", cacheRepo, + "--cache-dir", cacheDir) + for _, v := range args { + dockerRunFlags = append(dockerRunFlags, v) + } + kanikoCmd := exec.Command("docker", dockerRunFlags...) - _, err := RunCommandWithoutTest(kanikoCmd) - if err != nil { - return fmt.Errorf("Failed to build cached image %s with kaniko command \"%s\": %w", kanikoImage, kanikoCmd.Args, err) - } + _, err := RunCommandWithoutTest(kanikoCmd) + if err != nil { + return fmt.Errorf("Failed to build cached image %s with kaniko command \"%s\": %w", kanikoImage, kanikoCmd.Args, err) } return nil } diff --git a/integration/integration_test.go b/integration/integration_test.go index 0f4ef28dc..b58991690 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -559,32 +559,24 @@ func buildImage(t *testing.T, dockerfile string, imageBuilder *DockerFileBuilder // Build each image with kaniko twice, and then make sure they're exactly the same func TestCache(t *testing.T) { populateVolumeCache() + + // Build dockerfiles with registry cache for dockerfile := range imageBuilder.TestCacheDockerfiles { - args := []string{} - if dockerfile == "Dockerfile_test_cache_copy" { - args = append(args, "--cache-copy-layers=true") - } t.Run("test_cache_"+dockerfile, func(t *testing.T) { dockerfile := dockerfile - t.Parallel() - cache := filepath.Join(config.imageRepo, "cache", fmt.Sprintf("%v", time.Now().UnixNano())) - // Build the initial image which will cache layers - if err := imageBuilder.buildCachedImages(config, cache, dockerfilesPath, 0, args); err != nil { - t.Fatalf("error building cached image for the first time: %v", err) - } - // Build the second image which should pull from the cache - if err := imageBuilder.buildCachedImages(config, cache, dockerfilesPath, 1, args); err != nil { - t.Fatalf("error building cached image for the second time: %v", err) - } - // Make sure both images are the same - kanikoVersion0 := GetVersionedKanikoImage(config.imageRepo, dockerfile, 0) - kanikoVersion1 := GetVersionedKanikoImage(config.imageRepo, dockerfile, 1) + t.Parallel() + verifyBuildWith(t, cache, dockerfile) + }) + } - diff := containerDiff(t, kanikoVersion0, kanikoVersion1) - - expected := fmt.Sprintf(emptyContainerDiff, kanikoVersion0, kanikoVersion1, kanikoVersion0, kanikoVersion1) - checkContainerDiffOutput(t, diff, expected) + // Build dockerfiles with layout cache + for dockerfile := range imageBuilder.TestOCICacheDockerfiles { + t.Run("test_oci_cache_"+dockerfile, func(t *testing.T) { + dockerfile := dockerfile + cache := filepath.Join("oci:", cacheDir, "cached", fmt.Sprintf("%v", time.Now().UnixNano())) + t.Parallel() + verifyBuildWith(t, cache, dockerfile) }) } @@ -593,6 +585,30 @@ func TestCache(t *testing.T) { } } +func verifyBuildWith(t *testing.T, cache, dockerfile string) { + args := []string{} + if strings.HasPrefix(dockerfile, "Dockerfile_test_cache_copy") { + args = append(args, "--cache-copy-layers=true") + } + + // Build the initial image which will cache layers + if err := imageBuilder.buildCachedImage(config, cache, dockerfilesPath, dockerfile, 0, args); err != nil { + t.Fatalf("error building cached image for the first time: %v", err) + } + // Build the second image which should pull from the cache + if err := imageBuilder.buildCachedImage(config, cache, dockerfilesPath, dockerfile, 1, args); err != nil { + t.Fatalf("error building cached image for the second time: %v", err) + } + // Make sure both images are the same + kanikoVersion0 := GetVersionedKanikoImage(config.imageRepo, dockerfile, 0) + kanikoVersion1 := GetVersionedKanikoImage(config.imageRepo, dockerfile, 1) + + diff := containerDiff(t, kanikoVersion0, kanikoVersion1) + + expected := fmt.Sprintf(emptyContainerDiff, kanikoVersion0, kanikoVersion1, kanikoVersion0, kanikoVersion1) + checkContainerDiffOutput(t, diff, expected) +} + func TestRelativePaths(t *testing.T) { dockerfile := "Dockerfile_relative_copy" diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go index 94187a341..699de8c34 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -21,6 +21,7 @@ import ( "os" "path" "path/filepath" + "strings" "time" "github.com/GoogleContainerTools/kaniko/pkg/config" @@ -28,6 +29,7 @@ import ( "github.com/GoogleContainerTools/kaniko/pkg/util" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/layout" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/tarball" "github.com/pkg/errors" @@ -73,25 +75,82 @@ func (rc *RegistryCache) RetrieveLayer(ck string) (v1.Image, error) { return nil, err } + if err = verifyImage(img, rc.Opts.CacheTTL, cache); err != nil { + return nil, err + } + return img, nil +} + +func verifyImage(img v1.Image, cacheTTL time.Duration, cache string) error { cf, err := img.ConfigFile() if err != nil { - return nil, errors.Wrap(err, fmt.Sprintf("retrieving config file for %s", cache)) + return errors.Wrap(err, fmt.Sprintf("retrieving config file for %s", cache)) } - expiry := cf.Created.Add(rc.Opts.CacheTTL) + expiry := cf.Created.Add(cacheTTL) // Layer is stale, rebuild it. if expiry.Before(time.Now()) { logrus.Infof("Cache entry expired: %s", cache) - return nil, fmt.Errorf("Cache entry expired: %s", cache) + return fmt.Errorf("Cache entry expired: %s", cache) } // Force the manifest to be populated if _, err := img.RawManifest(); err != nil { + return err + } + return nil +} + +// LayoutCache is the OCI image layout cache +type LayoutCache struct { + Opts *config.KanikoOptions +} + +func (lc *LayoutCache) RetrieveLayer(ck string) (v1.Image, error) { + cache, err := Destination(lc.Opts, ck) + if err != nil { + return nil, errors.Wrap(err, "getting cache destination") + } + logrus.Infof("Checking for cached layer %s...", cache) + + var img v1.Image + if img, err = locateImage(strings.TrimPrefix(cache, "oci:")); err != nil { + return nil, errors.Wrap(err, "locating cache image") + } + + if err = verifyImage(img, lc.Opts.CacheTTL, cache); err != nil { return nil, err } return img, nil } +func locateImage(path string) (v1.Image, error) { + var img v1.Image + layoutPath, err := layout.FromPath(path) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("constructing layout path from %s", path)) + } + index, err := layoutPath.ImageIndex() + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("retrieving index file for %s", layoutPath)) + } + manifest, err := index.IndexManifest() + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("retrieving manifest file for %s", layoutPath)) + } + for _, m := range manifest.Manifests { + // assume there is only one image + img, err = layoutPath.Image(m.Digest) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("initializing image with digest %s", m.Digest.String())) + } + } + if img == nil { + return nil, fmt.Errorf("path contains no images") + } + return img, nil +} + // Destination returns the repo where the layer should be stored // If no cache is specified, one is inferred from the destination provided func Destination(opts *config.KanikoOptions, cacheKey string) (string, error) { diff --git a/pkg/executor/build.go b/pkg/executor/build.go index e27a02605..2bf379abb 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -127,9 +127,7 @@ func newStageBuilder(args *dockerfile.BuildArgs, opts *config.KanikoOptions, sta crossStageDeps: crossStageDeps, digestToCacheKey: dcm, stageIdxToDigest: sid, - layerCache: &cache.RegistryCache{ - Opts: opts, - }, + layerCache: newLayerCache(opts), pushLayerToCache: pushLayerToCache, } @@ -184,6 +182,21 @@ func initConfig(img partial.WithConfigFile, opts *config.KanikoOptions) (*v1.Con return imageConfig, nil } +func newLayerCache(opts *config.KanikoOptions) cache.LayerCache { + if isOCILayout(opts.CacheRepo) { + return &cache.LayoutCache{ + Opts: opts, + } + } + return &cache.RegistryCache{ + Opts: opts, + } +} + +func isOCILayout(path string) bool { + return strings.HasPrefix(path, "oci:") +} + func (s *stageBuilder) populateCompositeKey(command fmt.Stringer, 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) diff --git a/pkg/executor/build_test.go b/pkg/executor/build_test.go index 0fa3652f8..23baf944f 100644 --- a/pkg/executor/build_test.go +++ b/pkg/executor/build_test.go @@ -28,6 +28,7 @@ import ( "strconv" "testing" + "github.com/GoogleContainerTools/kaniko/pkg/cache" "github.com/GoogleContainerTools/kaniko/pkg/commands" "github.com/GoogleContainerTools/kaniko/pkg/config" "github.com/GoogleContainerTools/kaniko/pkg/dockerfile" @@ -522,6 +523,36 @@ func TestInitializeConfig(t *testing.T) { } } +func Test_newLayerCache_defaultCache(t *testing.T) { + t.Run("default layer cache is registry cache", func(t *testing.T) { + layerCache := newLayerCache(&config.KanikoOptions{CacheRepo: "some-cache-repo"}) + foundCache, ok := layerCache.(*cache.RegistryCache) + if !ok { + t.Error("expected layer cache to be a registry cache") + } + if foundCache.Opts.CacheRepo != "some-cache-repo" { + t.Errorf( + "expected cache repo to be 'some-cache-repo'; got %q", foundCache.Opts.CacheRepo, + ) + } + }) +} + +func Test_newLayerCache_layoutCache(t *testing.T) { + t.Run("when cache repo has 'oci:' prefix layer cache is layout cache", func(t *testing.T) { + layerCache := newLayerCache(&config.KanikoOptions{CacheRepo: "oci:/some-cache-repo"}) + foundCache, ok := layerCache.(*cache.LayoutCache) + if !ok { + t.Error("expected layer cache to be a layout cache") + } + if foundCache.Opts.CacheRepo != "oci:/some-cache-repo" { + t.Errorf( + "expected cache repo to be 'oci:/some-cache-repo'; got %q", foundCache.Opts.CacheRepo, + ) + } + }) +} + func Test_stageBuilder_optimize(t *testing.T) { testCases := []struct { opts *config.KanikoOptions diff --git a/pkg/executor/push.go b/pkg/executor/push.go index fb6da95bb..7e2e156a2 100644 --- a/pkg/executor/push.go +++ b/pkg/executor/push.go @@ -84,7 +84,11 @@ func CheckPushPermissions(opts *config.KanikoOptions) error { } else if opts.NoPush && !opts.NoPushCache { // When no push is set, we want to check permissions for the cache repo // instead of the destinations - targets = []string{opts.CacheRepo} + if isOCILayout(opts.CacheRepo) { + targets = []string{} // no need to check push permissions if we're just writing to disk + } else { + targets = []string{opts.CacheRepo} + } } checked := map[string]bool{} @@ -289,8 +293,8 @@ func writeImageOutputs(image v1.Image, destRefs []name.Tag) error { return nil } -// pushLayerToCache pushes layer (tagged with cacheKey) to opts.Cache -// if opts.Cache doesn't exist, infer the cache from the given destination +// pushLayerToCache pushes layer (tagged with cacheKey) to opts.CacheRepo +// if opts.CacheRepo doesn't exist, infer the cache from the given destination func pushLayerToCache(opts *config.KanikoOptions, cacheKey string, tarPath string, createdBy string) error { var layer v1.Layer var err error @@ -331,5 +335,9 @@ func pushLayerToCache(opts *config.KanikoOptions, cacheKey string, tarPath strin cacheOpts.Destinations = []string{cache} cacheOpts.InsecureRegistries = opts.InsecureRegistries cacheOpts.SkipTLSVerifyRegistries = opts.SkipTLSVerifyRegistries + if isOCILayout(cache) { + cacheOpts.OCILayoutPath = strings.TrimPrefix(cache, "oci:") + cacheOpts.NoPush = true + } return DoPush(empty, &cacheOpts) } diff --git a/pkg/executor/push_test.go b/pkg/executor/push_test.go index 5c783b09b..f8535f156 100644 --- a/pkg/executor/push_test.go +++ b/pkg/executor/push_test.go @@ -252,74 +252,99 @@ func TestImageNameTagDigestFile(t *testing.T) { testutil.CheckErrorAndDeepEqual(t, false, err, want, got) } -var calledExecCommand = []bool{} -var calledCheckPushPermission = false +var checkPushPermsCallCount = 0 -func setCalledFalse() { - calledExecCommand = []bool{} - calledCheckPushPermission = false +func resetCalledCount() { + checkPushPermsCallCount = 0 } func fakeCheckPushPermission(ref name.Reference, kc authn.Keychain, t http.RoundTripper) error { - calledCheckPushPermission = true + checkPushPermsCallCount++ return nil } func TestCheckPushPermissions(t *testing.T) { tests := []struct { - description string - Destination []string - ShouldCallExecCommand []bool - ExistingConfig bool + description string + cacheRepo string + checkPushPermsExpectedCallCount int + destinations []string + existingConfig bool + noPush bool + noPushCache bool }{ - {"a gcr image without config", []string{"gcr.io/test-image"}, []bool{true}, false}, - {"a gcr image with config", []string{"gcr.io/test-image"}, []bool{false}, true}, - {"a pkg.dev image without config", []string{"us-docker.pkg.dev/test-image"}, []bool{true}, false}, - {"a pkg.dev image with config", []string{"us-docker.pkg.dev/test-image"}, []bool{false}, true}, - {"localhost registry with config", []string{"localhost:5000/test-image"}, []bool{false}, false}, - {"localhost registry without config", []string{"localhost:5000/test-image"}, []bool{false}, true}, - {"any other registry", []string{"notgcr.io/test-image"}, []bool{false}, false}, - {"multiple destinations pushed to different registry", - []string{ + {description: "a gcr image without config", destinations: []string{"gcr.io/test-image"}, checkPushPermsExpectedCallCount: 1}, + {description: "a gcr image with config", destinations: []string{"gcr.io/test-image"}, existingConfig: true, checkPushPermsExpectedCallCount: 1}, + {description: "a pkg.dev image without config", destinations: []string{"us-docker.pkg.dev/test-image"}, checkPushPermsExpectedCallCount: 1}, + {description: "a pkg.dev image with config", destinations: []string{"us-docker.pkg.dev/test-image"}, existingConfig: true, checkPushPermsExpectedCallCount: 1}, + {description: "localhost registry without config", destinations: []string{"localhost:5000/test-image"}, checkPushPermsExpectedCallCount: 1}, + {description: "localhost registry with config", destinations: []string{"localhost:5000/test-image"}, existingConfig: true, checkPushPermsExpectedCallCount: 1}, + {description: "any other registry", destinations: []string{"notgcr.io/test-image"}, checkPushPermsExpectedCallCount: 1}, + { + description: "multiple destinations pushed to different registry", + destinations: []string{ "us-central1-docker.pkg.dev/prj/test-image", "us-west-docker.pkg.dev/prj/test-image", }, - []bool{true, true}, false, + checkPushPermsExpectedCallCount: 2, }, - {"same image names with different tags", - []string{ + { + description: "same image names with different tags", + destinations: []string{ "us-central1-docker.pkg.dev/prj/test-image:tag1", "us-central1-docker.pkg.dev/prj/test-image:tag2", }, - []bool{true, true}, false, + checkPushPermsExpectedCallCount: 1, }, - {"same destination image multiple times", - []string{ + { + description: "same destination image multiple times", + destinations: []string{ "us-central1-docker.pkg.dev/prj/test-image", "us-central1-docker.pkg.dev/prj/test-image", }, - []bool{true, false}, false, + checkPushPermsExpectedCallCount: 1, + }, + { + description: "no push and no push cache", + destinations: []string{"us-central1-docker.pkg.dev/prj/test-image"}, + checkPushPermsExpectedCallCount: 0, + noPush: true, + noPushCache: true, + }, + { + description: "no push and push cache", + destinations: []string{"us-central1-docker.pkg.dev/prj/test-image"}, + cacheRepo: "us-central1-docker.pkg.dev/prj/cache-image", + checkPushPermsExpectedCallCount: 1, + noPush: true, + }, + { + description: "no push and cache repo is OCI image layout", + destinations: []string{"us-central1-docker.pkg.dev/prj/test-image"}, + cacheRepo: "oci:/some-layout-path", + checkPushPermsExpectedCallCount: 0, + noPush: true, }, } checkRemotePushPermission = fakeCheckPushPermission for _, test := range tests { t.Run(test.description, func(t *testing.T) { - setCalledFalse() + resetCalledCount() fs = afero.NewMemMapFs() opts := config.KanikoOptions{ - Destinations: test.Destination, + CacheRepo: test.cacheRepo, + Destinations: test.destinations, + NoPush: test.noPush, + NoPushCache: test.noPushCache, } - if test.ExistingConfig { + if test.existingConfig { afero.WriteFile(fs, util.DockerConfLocation(), []byte(""), os.FileMode(0644)) defer fs.Remove(util.DockerConfLocation()) } CheckPushPermissions(&opts) - for i, shdCall := range test.ShouldCallExecCommand { - if i < len(calledExecCommand) && shdCall != calledExecCommand[i] { - t.Errorf("Expected calledExecCommand to be %v however it was %v", - calledExecCommand, shdCall) - } + if checkPushPermsCallCount != test.checkPushPermsExpectedCallCount { + t.Errorf("expected check push permissions call count to be %d but it was %d", test.checkPushPermsExpectedCallCount, checkPushPermsCallCount) } }) }