Skip to content

Commit 57ede49

Browse files
author
priyawadhwa
authored
Merge pull request #353 from priyawadhwa/cache
Add layer caching to kaniko
2 parents 8fb220b + e2ca115 commit 57ede49

File tree

10 files changed

+339
-46
lines changed

10 files changed

+339
-46
lines changed

cmd/executor/cmd/root.go

+2
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ func addKanikoOptionsFlags(cmd *cobra.Command) {
9595
RootCmd.PersistentFlags().BoolVarP(&opts.Reproducible, "reproducible", "", false, "Strip timestamps out of the image to make it reproducible")
9696
RootCmd.PersistentFlags().StringVarP(&opts.Target, "target", "", "", "Set the target build stage to build")
9797
RootCmd.PersistentFlags().BoolVarP(&opts.NoPush, "no-push", "", false, "Do not push the image to the registry")
98+
RootCmd.PersistentFlags().StringVarP(&opts.CacheRepo, "cache-repo", "", "", "Specify a repository to use as a cache, otherwise one will be inferred from the destination provided")
99+
RootCmd.PersistentFlags().BoolVarP(&opts.Cache, "cache", "", false, "Use cache when building image")
98100
}
99101

100102
// addHiddenFlags marks certain flags as hidden from the executor help text
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Copyright 2018 Google, Inc. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# Test to make sure the cache works properly
16+
# If the image is built twice, /date should be the same in both images
17+
# if the cache is implemented correctly
18+
19+
FROM gcr.io/google-appengine/debian9@sha256:1d6a9a6d106bd795098f60f4abb7083626354fa6735e81743c7f8cfca11259f0
20+
RUN date > /date
21+
COPY context/foo /foo
22+
RUN echo hey
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright 2018 Google, Inc. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# Test to make sure the cache works properly
16+
# /date should be the same regardless of when this image is built
17+
# if the cache is implemented correctly
18+
19+
FROM gcr.io/google-appengine/debian9@sha256:1d6a9a6d106bd795098f60f4abb7083626354fa6735e81743c7f8cfca11259f0
20+
RUN apt-get update && apt-get install -y make

integration/images.go

+48-6
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"path"
2424
"path/filepath"
2525
"runtime"
26+
"strconv"
2627
"strings"
2728
)
2829

@@ -77,12 +78,16 @@ func GetKanikoImage(imageRepo, dockerfile string) string {
7778
return strings.ToLower(imageRepo + kanikoPrefix + dockerfile)
7879
}
7980

81+
// GetVersionedKanikoImage versions constructs the name of the kaniko image that would be built
82+
// with the dockerfile and versions it for cache testing
83+
func GetVersionedKanikoImage(imageRepo, dockerfile string, version int) string {
84+
return strings.ToLower(imageRepo + kanikoPrefix + dockerfile + strconv.Itoa(version))
85+
}
86+
8087
// FindDockerFiles will look for test docker files in the directory dockerfilesPath.
8188
// These files must start with `Dockerfile_test`. If the file is one we are intentionally
8289
// skipping, it will not be included in the returned list.
8390
func FindDockerFiles(dockerfilesPath string) ([]string, error) {
84-
// TODO: remove test_user_run from this when https://github.com/GoogleContainerTools/container-diff/issues/237 is fixed
85-
testsToIgnore := map[string]bool{"Dockerfile_test_user_run": true}
8691
allDockerfiles, err := filepath.Glob(path.Join(dockerfilesPath, "Dockerfile_test*"))
8792
if err != nil {
8893
return []string{}, fmt.Errorf("Failed to find docker files at %s: %s", dockerfilesPath, err)
@@ -92,9 +97,8 @@ func FindDockerFiles(dockerfilesPath string) ([]string, error) {
9297
for _, dockerfile := range allDockerfiles {
9398
// Remove the leading directory from the path
9499
dockerfile = dockerfile[len("dockerfiles/"):]
95-
if !testsToIgnore[dockerfile] {
96-
dockerfiles = append(dockerfiles, dockerfile)
97-
}
100+
dockerfiles = append(dockerfiles, dockerfile)
101+
98102
}
99103
return dockerfiles, err
100104
}
@@ -103,7 +107,9 @@ func FindDockerFiles(dockerfilesPath string) ([]string, error) {
103107
// keeps track of which files have been built.
104108
type DockerFileBuilder struct {
105109
// Holds all available docker files and whether or not they've been built
106-
FilesBuilt map[string]bool
110+
FilesBuilt map[string]bool
111+
DockerfilesToIgnore map[string]struct{}
112+
TestCacheDockerfiles map[string]struct{}
107113
}
108114

109115
// NewDockerFileBuilder will create a DockerFileBuilder initialized with dockerfiles, which
@@ -113,6 +119,14 @@ func NewDockerFileBuilder(dockerfiles []string) *DockerFileBuilder {
113119
for _, f := range dockerfiles {
114120
d.FilesBuilt[f] = false
115121
}
122+
d.DockerfilesToIgnore = map[string]struct{}{
123+
// TODO: remove test_user_run from this when https://github.com/GoogleContainerTools/container-diff/issues/237 is fixed
124+
"Dockerfile_test_user_run": {},
125+
}
126+
d.TestCacheDockerfiles = map[string]struct{}{
127+
"Dockerfile_test_cache": {},
128+
"Dockerfile_test_cache_install": {},
129+
}
116130
return &d
117131
}
118132

@@ -186,3 +200,31 @@ func (d *DockerFileBuilder) BuildImage(imageRepo, gcsBucket, dockerfilesPath, do
186200
d.FilesBuilt[dockerfile] = true
187201
return nil
188202
}
203+
204+
// buildCachedImages builds the images for testing caching via kaniko where version is the nth time this image has been built
205+
func (d *DockerFileBuilder) buildCachedImages(imageRepo, cacheRepo, dockerfilesPath string, version int) error {
206+
_, ex, _, _ := runtime.Caller(0)
207+
cwd := filepath.Dir(ex)
208+
209+
cacheFlag := "--cache=true"
210+
211+
for dockerfile := range d.TestCacheDockerfiles {
212+
kanikoImage := GetVersionedKanikoImage(imageRepo, dockerfile, version)
213+
kanikoCmd := exec.Command("docker",
214+
append([]string{"run",
215+
"-v", os.Getenv("HOME") + "/.config/gcloud:/root/.config/gcloud",
216+
"-v", cwd + ":/workspace",
217+
ExecutorImage,
218+
"-f", path.Join(buildContextPath, dockerfilesPath, dockerfile),
219+
"-d", kanikoImage,
220+
"-c", buildContextPath,
221+
cacheFlag,
222+
"--cache-repo", cacheRepo})...,
223+
)
224+
225+
if _, err := RunCommandWithoutTest(kanikoCmd); err != nil {
226+
return fmt.Errorf("Failed to build cached image %s with kaniko command \"%s\": %s", kanikoImage, kanikoCmd.Args, err)
227+
}
228+
}
229+
return nil
230+
}

integration/integration_test.go

+65-19
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ import (
2424
"math"
2525
"os"
2626
"os/exec"
27+
"path/filepath"
2728
"strings"
2829
"testing"
30+
"time"
2931

3032
"github.com/google/go-containerregistry/pkg/name"
3133
"github.com/google/go-containerregistry/pkg/v1/daemon"
@@ -148,6 +150,7 @@ func TestMain(m *testing.M) {
148150
fmt.Printf("error building onbuild base: %v", err)
149151
os.Exit(1)
150152
}
153+
151154
pushOnbuildBase := exec.Command("docker", "push", config.onbuildBaseImage)
152155
if err := pushOnbuildBase.Run(); err != nil {
153156
fmt.Printf("error pushing onbuild base %s: %v", config.onbuildBaseImage, err)
@@ -165,7 +168,6 @@ func TestMain(m *testing.M) {
165168
fmt.Printf("error pushing hardlink base %s: %v", config.hardlinkBaseImage, err)
166169
os.Exit(1)
167170
}
168-
169171
dockerfiles, err := FindDockerFiles(dockerfilesPath)
170172
if err != nil {
171173
fmt.Printf("Coudn't create map of dockerfiles: %s", err)
@@ -177,6 +179,12 @@ func TestMain(m *testing.M) {
177179
func TestRun(t *testing.T) {
178180
for dockerfile, built := range imageBuilder.FilesBuilt {
179181
t.Run("test_"+dockerfile, func(t *testing.T) {
182+
if _, ok := imageBuilder.DockerfilesToIgnore[dockerfile]; ok {
183+
t.SkipNow()
184+
}
185+
if _, ok := imageBuilder.TestCacheDockerfiles[dockerfile]; ok {
186+
t.SkipNow()
187+
}
180188
if !built {
181189
err := imageBuilder.BuildImage(config.imageRepo, config.gcsBucket, dockerfilesPath, dockerfile)
182190
if err != nil {
@@ -195,25 +203,8 @@ func TestRun(t *testing.T) {
195203
t.Logf("diff = %s", string(diff))
196204

197205
expected := fmt.Sprintf(emptyContainerDiff, dockerImage, kanikoImage, dockerImage, kanikoImage)
206+
checkContainerDiffOutput(t, diff, expected)
198207

199-
// Let's compare the json objects themselves instead of strings to avoid
200-
// issues with spaces and indents
201-
var diffInt interface{}
202-
var expectedInt interface{}
203-
204-
err := json.Unmarshal(diff, &diffInt)
205-
if err != nil {
206-
t.Error(err)
207-
t.Fail()
208-
}
209-
210-
err = json.Unmarshal([]byte(expected), &expectedInt)
211-
if err != nil {
212-
t.Error(err)
213-
t.Fail()
214-
}
215-
216-
testutil.CheckErrorAndDeepEqual(t, false, nil, expectedInt, diffInt)
217208
})
218209
}
219210
}
@@ -228,6 +219,9 @@ func TestLayers(t *testing.T) {
228219
}
229220
for dockerfile, built := range imageBuilder.FilesBuilt {
230221
t.Run("test_layer_"+dockerfile, func(t *testing.T) {
222+
if _, ok := imageBuilder.DockerfilesToIgnore[dockerfile]; ok {
223+
t.SkipNow()
224+
}
231225
if !built {
232226
err := imageBuilder.BuildImage(config.imageRepo, config.gcsBucket, dockerfilesPath, dockerfile)
233227
if err != nil {
@@ -244,6 +238,58 @@ func TestLayers(t *testing.T) {
244238
}
245239
}
246240

241+
// Build each image with kaniko twice, and then make sure they're exactly the same
242+
func TestCache(t *testing.T) {
243+
for dockerfile := range imageBuilder.TestCacheDockerfiles {
244+
t.Run("test_cache_"+dockerfile, func(t *testing.T) {
245+
cache := filepath.Join(config.imageRepo, "cache", fmt.Sprintf("%v", time.Now().UnixNano()))
246+
// Build the initial image which will cache layers
247+
if err := imageBuilder.buildCachedImages(config.imageRepo, cache, dockerfilesPath, 0); err != nil {
248+
t.Fatalf("error building cached image for the first time: %v", err)
249+
}
250+
// Build the second image which should pull from the cache
251+
if err := imageBuilder.buildCachedImages(config.imageRepo, cache, dockerfilesPath, 1); err != nil {
252+
t.Fatalf("error building cached image for the first time: %v", err)
253+
}
254+
// Make sure both images are the same
255+
kanikoVersion0 := GetVersionedKanikoImage(config.imageRepo, dockerfile, 0)
256+
kanikoVersion1 := GetVersionedKanikoImage(config.imageRepo, dockerfile, 1)
257+
258+
// container-diff
259+
containerdiffCmd := exec.Command("container-diff", "diff",
260+
kanikoVersion0, kanikoVersion1,
261+
"-q", "--type=file", "--type=metadata", "--json")
262+
263+
diff := RunCommand(containerdiffCmd, t)
264+
t.Logf("diff = %s", diff)
265+
266+
expected := fmt.Sprintf(emptyContainerDiff, kanikoVersion0, kanikoVersion1, kanikoVersion0, kanikoVersion1)
267+
checkContainerDiffOutput(t, diff, expected)
268+
})
269+
}
270+
}
271+
272+
func checkContainerDiffOutput(t *testing.T, diff []byte, expected string) {
273+
// Let's compare the json objects themselves instead of strings to avoid
274+
// issues with spaces and indents
275+
t.Helper()
276+
277+
var diffInt interface{}
278+
var expectedInt interface{}
279+
280+
err := json.Unmarshal(diff, &diffInt)
281+
if err != nil {
282+
t.Error(err)
283+
}
284+
285+
err = json.Unmarshal([]byte(expected), &expectedInt)
286+
if err != nil {
287+
t.Error(err)
288+
}
289+
290+
testutil.CheckErrorAndDeepEqual(t, false, nil, expectedInt, diffInt)
291+
}
292+
247293
func checkLayers(t *testing.T, image1, image2 string, offset int) {
248294
t.Helper()
249295
img1, err := getImageDetails(image1)

pkg/cache/cache.go

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
Copyright 2018 Google LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package cache
18+
19+
import (
20+
"fmt"
21+
22+
"github.com/GoogleContainerTools/kaniko/pkg/config"
23+
"github.com/google/go-containerregistry/pkg/authn"
24+
"github.com/google/go-containerregistry/pkg/authn/k8schain"
25+
"github.com/google/go-containerregistry/pkg/name"
26+
"github.com/google/go-containerregistry/pkg/v1"
27+
"github.com/google/go-containerregistry/pkg/v1/remote"
28+
"github.com/pkg/errors"
29+
"github.com/sirupsen/logrus"
30+
)
31+
32+
// RetrieveLayer checks the specified cache for a layer with the tag :cacheKey
33+
func RetrieveLayer(opts *config.KanikoOptions, cacheKey string) (v1.Image, error) {
34+
cache, err := Destination(opts, cacheKey)
35+
if err != nil {
36+
return nil, errors.Wrap(err, "getting cache destination")
37+
}
38+
logrus.Infof("Checking for cached layer %s...", cache)
39+
40+
cacheRef, err := name.NewTag(cache, name.WeakValidation)
41+
if err != nil {
42+
return nil, errors.Wrap(err, fmt.Sprintf("getting reference for %s", cache))
43+
}
44+
k8sc, err := k8schain.NewNoClient()
45+
if err != nil {
46+
return nil, err
47+
}
48+
kc := authn.NewMultiKeychain(authn.DefaultKeychain, k8sc)
49+
img, err := remote.Image(cacheRef, remote.WithAuthFromKeychain(kc))
50+
if err != nil {
51+
return nil, err
52+
}
53+
_, err = img.Layers()
54+
return img, err
55+
}
56+
57+
// Destination returns the repo where the layer should be stored
58+
// If no cache is specified, one is inferred from the destination provided
59+
func Destination(opts *config.KanikoOptions, cacheKey string) (string, error) {
60+
cache := opts.CacheRepo
61+
if cache == "" {
62+
destination := opts.Destinations[0]
63+
destRef, err := name.NewTag(destination, name.WeakValidation)
64+
if err != nil {
65+
return "", errors.Wrap(err, "getting tag for destination")
66+
}
67+
return fmt.Sprintf("%s/cache:%s", destRef.Context(), cacheKey), nil
68+
}
69+
return fmt.Sprintf("%s:%s", cache, cacheKey), nil
70+
}

pkg/config/options.go

+2
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@ type KanikoOptions struct {
2424
Bucket string
2525
TarPath string
2626
Target string
27+
CacheRepo string
2728
Destinations multiArg
2829
BuildArgs multiArg
2930
InsecurePush bool
3031
SkipTLSVerify bool
3132
SingleSnapshot bool
3233
Reproducible bool
3334
NoPush bool
35+
Cache bool
3436
}

0 commit comments

Comments
 (0)