Skip to content

Commit 4ceba80

Browse files
authored
Merge pull request GoogleContainerTools#118 from nkubala/cache
add layer caching
2 parents 53c6341 + ae409d2 commit 4ceba80

12 files changed

+246
-6
lines changed

cmd/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ go_library(
1111
visibility = ["//visibility:public"],
1212
deps = [
1313
"//differs:go_default_library",
14+
"//pkg/cache:go_default_library",
1415
"//pkg/util:go_default_library",
1516
"//util:go_default_library",
1617
"//vendor/github.com/docker/docker/client:go_default_library",

cmd/root.go

+30
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@ import (
2020
goflag "flag"
2121
"fmt"
2222
"os"
23+
"os/user"
24+
"path/filepath"
2325
"sort"
2426
"strings"
2527

2628
"github.com/GoogleCloudPlatform/container-diff/differs"
29+
"github.com/GoogleCloudPlatform/container-diff/pkg/cache"
2730
pkgutil "github.com/GoogleCloudPlatform/container-diff/pkg/util"
2831
"github.com/GoogleCloudPlatform/container-diff/util"
2932
"github.com/docker/docker/client"
@@ -35,6 +38,7 @@ import (
3538
var json bool
3639
var save bool
3740
var types diffTypes
41+
var noCache bool
3842

3943
var LogLevel string
4044

@@ -120,25 +124,50 @@ func getPrepperForImage(image string) (pkgutil.Prepper, error) {
120124
if err != nil {
121125
return nil, err
122126
}
127+
128+
cacheDir, err := cacheDir()
129+
if err != nil {
130+
return nil, err
131+
}
132+
var fsCache cache.Cache
133+
if !noCache {
134+
fsCache, err = cache.NewFileCache(cacheDir)
135+
if err != nil {
136+
return nil, err
137+
}
138+
}
139+
123140
if pkgutil.IsTar(image) {
124141
return pkgutil.TarPrepper{
125142
Source: image,
126143
Client: cli,
144+
Cache: fsCache,
127145
}, nil
128146

129147
} else if strings.HasPrefix(image, DaemonPrefix) {
130148
return pkgutil.DaemonPrepper{
131149
Source: strings.Replace(image, DaemonPrefix, "", -1),
132150
Client: cli,
151+
Cache: fsCache,
133152
}, nil
134153
}
135154
// either has remote prefix or has no prefix, in which case we force remote
136155
return pkgutil.CloudPrepper{
137156
Source: strings.Replace(image, RemotePrefix, "", -1),
138157
Client: cli,
158+
Cache: fsCache,
139159
}, nil
140160
}
141161

162+
func cacheDir() (string, error) {
163+
user, err := user.Current()
164+
if err != nil {
165+
return "", err
166+
}
167+
rootDir := filepath.Join(user.HomeDir, ".container-diff")
168+
return filepath.Join(rootDir, "cache"), nil
169+
}
170+
142171
func init() {
143172
RootCmd.PersistentFlags().StringVarP(&LogLevel, "verbosity", "v", "warning", "This flag controls the verbosity of container-diff.")
144173
pflag.CommandLine.AddGoFlagSet(goflag.CommandLine)
@@ -175,4 +204,5 @@ func addSharedFlags(cmd *cobra.Command) {
175204
cmd.Flags().VarP(&types, "type", "t", "This flag sets the list of analyzer types to use. Set it repeatedly to use multiple analyzers.")
176205
cmd.Flags().BoolVarP(&save, "save", "s", false, "Set this flag to save rather than remove the final image filesystems on exit.")
177206
cmd.Flags().BoolVarP(&util.SortSize, "order", "o", false, "Set this flag to sort any file/package results by descending size. Otherwise, they will be sorted by name.")
207+
cmd.Flags().BoolVarP(&noCache, "no-cache", "n", false, "Set this to force retrieval of layers on each run.")
178208
}

pkg/cache/BUILD.bazel

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
load("@io_bazel_rules_go//go:def.bzl", "go_library")
2+
3+
go_library(
4+
name = "go_default_library",
5+
srcs = [
6+
"cache.go",
7+
"file_cache.go",
8+
],
9+
visibility = ["//visibility:public"],
10+
deps = ["//vendor/github.com/sirupsen/logrus:go_default_library"],
11+
)

pkg/cache/cache.go

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
Copyright 2017 Google, Inc. All rights reserved.
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 "io"
20+
21+
type Cache interface {
22+
HasLayer(string) bool
23+
GetLayer(string) (io.ReadCloser, error)
24+
SetLayer(string, io.Reader) (io.ReadCloser, error)
25+
Invalidate(string) error
26+
}

pkg/cache/file_cache.go

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
Copyright 2017 Google, Inc. All rights reserved.
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+
"io"
21+
"os"
22+
"path/filepath"
23+
24+
"github.com/sirupsen/logrus"
25+
)
26+
27+
type FileCache struct {
28+
RootDir string
29+
}
30+
31+
func NewFileCache(dir string) (*FileCache, error) {
32+
if err := os.MkdirAll(dir, 0700); err != nil {
33+
return nil, err
34+
}
35+
return &FileCache{RootDir: dir}, nil
36+
}
37+
38+
func (c *FileCache) HasLayer(layerId string) bool {
39+
_, err := os.Stat(filepath.Join(c.RootDir, layerId))
40+
return !os.IsNotExist(err)
41+
}
42+
43+
func (c *FileCache) SetLayer(layerId string, r io.Reader) (io.ReadCloser, error) {
44+
fullpath := filepath.Join(c.RootDir, layerId)
45+
entry, err := os.Create(fullpath)
46+
if err != nil {
47+
return nil, err
48+
}
49+
if _, err := io.Copy(entry, r); err != nil {
50+
return nil, err
51+
}
52+
return c.GetLayer(layerId)
53+
}
54+
55+
func (c *FileCache) GetLayer(layerId string) (io.ReadCloser, error) {
56+
logrus.Infof("retrieving layer %s from cache", layerId)
57+
return os.Open(filepath.Join(c.RootDir, layerId))
58+
}
59+
60+
func (c *FileCache) Invalidate(layerId string) error {
61+
return os.RemoveAll(filepath.Join(c.RootDir, layerId))
62+
}

pkg/util/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ go_library(
1414
],
1515
visibility = ["//visibility:public"],
1616
deps = [
17+
"//pkg/cache:go_default_library",
1718
"//vendor/github.com/containers/image/docker:go_default_library",
1819
"//vendor/github.com/containers/image/docker/daemon:go_default_library",
1920
"//vendor/github.com/containers/image/docker/tarfile:go_default_library",

pkg/util/cloud_prepper.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package util
1818

1919
import (
20+
"github.com/GoogleCloudPlatform/container-diff/pkg/cache"
2021
"github.com/containers/image/docker"
2122
"github.com/docker/docker/client"
2223
)
@@ -25,6 +26,7 @@ import (
2526
type CloudPrepper struct {
2627
Source string
2728
Client *client.Client
29+
Cache cache.Cache
2830
}
2931

3032
func (p CloudPrepper) Name() string {
@@ -45,7 +47,7 @@ func (p CloudPrepper) GetFileSystem() (string, error) {
4547
return "", err
4648
}
4749

48-
return getFileSystemFromReference(ref, p.Source)
50+
return getFileSystemFromReference(ref, p.Source, p.Cache)
4951
}
5052

5153
func (p CloudPrepper) GetConfig() (ConfigSchema, error) {

pkg/util/daemon_prepper.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package util
1919
import (
2020
"context"
2121

22+
"github.com/GoogleCloudPlatform/container-diff/pkg/cache"
2223
"github.com/containers/image/docker/daemon"
2324

2425
"github.com/docker/docker/client"
@@ -28,6 +29,7 @@ import (
2829
type DaemonPrepper struct {
2930
Source string
3031
Client *client.Client
32+
Cache cache.Cache
3133
}
3234

3335
func (p DaemonPrepper) Name() string {
@@ -47,7 +49,7 @@ func (p DaemonPrepper) GetFileSystem() (string, error) {
4749
if err != nil {
4850
return "", err
4951
}
50-
return getFileSystemFromReference(ref, p.Source)
52+
return getFileSystemFromReference(ref, p.Source, p.Cache)
5153
}
5254

5355
func (p DaemonPrepper) GetConfig() (ConfigSchema, error) {

pkg/util/image_prep_utils.go

+28-3
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@ import (
2121
"encoding/json"
2222
"errors"
2323
"fmt"
24+
"io"
2425
"io/ioutil"
2526
"os"
2627
"strings"
2728

29+
"github.com/GoogleCloudPlatform/container-diff/pkg/cache"
2830
"github.com/containers/image/pkg/compression"
2931
"github.com/containers/image/types"
3032
"github.com/sirupsen/logrus"
@@ -91,7 +93,7 @@ func getImageFromTar(tarPath string) (string, error) {
9193
return tempPath, unpackDockerSave(tarPath, tempPath)
9294
}
9395

94-
func getFileSystemFromReference(ref types.ImageReference, imageName string) (string, error) {
96+
func getFileSystemFromReference(ref types.ImageReference, imageName string, cache cache.Cache) (string, error) {
9597
sanitizedName := strings.Replace(imageName, ":", "", -1)
9698
sanitizedName = strings.Replace(sanitizedName, "/", "", -1)
9799

@@ -113,10 +115,33 @@ func getFileSystemFromReference(ref types.ImageReference, imageName string) (str
113115
return "", err
114116
}
115117

118+
var bi io.ReadCloser
116119
for _, b := range img.LayerInfos() {
117-
bi, _, err := imgSrc.GetBlob(b)
120+
layerId := b.Digest.String()
121+
if cache == nil {
122+
bi, _, err = imgSrc.GetBlob(b)
123+
if err != nil {
124+
logrus.Errorf("Failed to pull image layer: %s", err)
125+
return "", err
126+
}
127+
} else if cache.HasLayer(layerId) {
128+
logrus.Infof("cache hit for layer %s", layerId)
129+
bi, err = cache.GetLayer(layerId)
130+
} else {
131+
logrus.Infof("cache miss for layer %s", layerId)
132+
bi, _, err = imgSrc.GetBlob(b)
133+
if err != nil {
134+
logrus.Errorf("Failed to pull image layer: %s", err)
135+
return "", err
136+
}
137+
bi, err = cache.SetLayer(layerId, bi)
138+
if err != nil {
139+
logrus.Errorf("error when caching layer %s: %s", layerId, err)
140+
cache.Invalidate(layerId)
141+
}
142+
}
118143
if err != nil {
119-
logrus.Errorf("Failed to pull image layer: %s", err)
144+
logrus.Errorf("Failed to retrieve image layer: %s", err)
120145
return "", err
121146
}
122147
// try and detect layer compression

pkg/util/tar_prepper.go

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"os"
2424
"path/filepath"
2525

26+
"github.com/GoogleCloudPlatform/container-diff/pkg/cache"
2627
"github.com/containers/image/docker/tarfile"
2728
"github.com/docker/docker/client"
2829
"github.com/sirupsen/logrus"
@@ -31,6 +32,7 @@ import (
3132
type TarPrepper struct {
3233
Source string
3334
Client *client.Client
35+
Cache cache.Cache
3436
}
3537

3638
func (p TarPrepper) Name() string {

util/BUILD.bazel

+5-1
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,15 @@ go_library(
2424
go_test(
2525
name = "go_default_test",
2626
srcs = [
27+
"file_cache_test.go",
2728
"fs_utils_test.go",
2829
"output_sort_utils_test.go",
2930
"package_diff_utils_test.go",
3031
"tar_utils_test.go",
3132
],
3233
library = ":go_default_library",
33-
deps = ["//pkg/util:go_default_library"],
34+
deps = [
35+
"//pkg/cache:go_default_library",
36+
"//pkg/util:go_default_library",
37+
],
3438
)

0 commit comments

Comments
 (0)