Browse code

Implement build cache based on history array

Based on work by KJ Tsanaktsidis

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
Signed-off-by: KJ Tsanaktsidis <kjtsanaktsidis@gmail.com>

Tõnis Tiigi authored on 2016/09/23 06:38:00
Showing 13 changed files
... ...
@@ -70,7 +70,7 @@ func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBui
70 70
 	var buildUlimits = []*units.Ulimit{}
71 71
 	ulimitsJSON := r.FormValue("ulimits")
72 72
 	if ulimitsJSON != "" {
73
-		if err := json.NewDecoder(strings.NewReader(ulimitsJSON)).Decode(&buildUlimits); err != nil {
73
+		if err := json.Unmarshal([]byte(ulimitsJSON), &buildUlimits); err != nil {
74 74
 			return nil, err
75 75
 		}
76 76
 		options.Ulimits = buildUlimits
... ...
@@ -79,7 +79,7 @@ func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBui
79 79
 	var buildArgs = map[string]string{}
80 80
 	buildArgsJSON := r.FormValue("buildargs")
81 81
 	if buildArgsJSON != "" {
82
-		if err := json.NewDecoder(strings.NewReader(buildArgsJSON)).Decode(&buildArgs); err != nil {
82
+		if err := json.Unmarshal([]byte(buildArgsJSON), &buildArgs); err != nil {
83 83
 			return nil, err
84 84
 		}
85 85
 		options.BuildArgs = buildArgs
... ...
@@ -87,12 +87,21 @@ func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBui
87 87
 	var labels = map[string]string{}
88 88
 	labelsJSON := r.FormValue("labels")
89 89
 	if labelsJSON != "" {
90
-		if err := json.NewDecoder(strings.NewReader(labelsJSON)).Decode(&labels); err != nil {
90
+		if err := json.Unmarshal([]byte(labelsJSON), &labels); err != nil {
91 91
 			return nil, err
92 92
 		}
93 93
 		options.Labels = labels
94 94
 	}
95 95
 
96
+	var cacheFrom = []string{}
97
+	cacheFromJSON := r.FormValue("cachefrom")
98
+	if cacheFromJSON != "" {
99
+		if err := json.Unmarshal([]byte(cacheFromJSON), &cacheFrom); err != nil {
100
+			return nil, err
101
+		}
102
+		options.CacheFrom = cacheFrom
103
+	}
104
+
96 105
 	return options, nil
97 106
 }
98 107
 
... ...
@@ -151,6 +151,9 @@ type ImageBuildOptions struct {
151 151
 	// preserves the original image and creates a new one from the parent with all
152 152
 	// the changes applied to a single layer
153 153
 	Squash bool
154
+	// CacheFrom specifies images that are used for matching cache. Images
155
+	// specified here do not need to have a valid parent chain to match cache.
156
+	CacheFrom []string
154 157
 }
155 158
 
156 159
 // ImageBuildResponse holds information
... ...
@@ -153,10 +153,16 @@ type Image interface {
153 153
 	RunConfig() *container.Config
154 154
 }
155 155
 
156
-// ImageCache abstracts an image cache store.
156
+// ImageCacheBuilder represents a generator for stateful image cache.
157
+type ImageCacheBuilder interface {
158
+	// MakeImageCache creates a stateful image cache.
159
+	MakeImageCache(cacheFrom []string) ImageCache
160
+}
161
+
162
+// ImageCache abstracts an image cache.
157 163
 // (parent image, child runconfig) -> child image
158 164
 type ImageCache interface {
159 165
 	// GetCachedImageOnBuild returns a reference to a cached image whose parent equals `parent`
160 166
 	// and runconfig equals `cfg`. A cache miss is expected to return an empty ID and a nil error.
161
-	GetCachedImageOnBuild(parentID string, cfg *container.Config) (imageID string, err error)
167
+	GetCache(parentID string, cfg *container.Config) (imageID string, err error)
162 168
 }
... ...
@@ -75,6 +75,8 @@ type Builder struct {
75 75
 
76 76
 	// TODO: remove once docker.Commit can receive a tag
77 77
 	id string
78
+
79
+	imageCache builder.ImageCache
78 80
 }
79 81
 
80 82
 // BuildManager implements builder.Backend and is shared across all Builder objects.
... ...
@@ -136,6 +138,10 @@ func NewBuilder(clientCtx context.Context, config *types.ImageBuildOptions, back
136 136
 			LookingForDirectives: true,
137 137
 		},
138 138
 	}
139
+	if icb, ok := backend.(builder.ImageCacheBuilder); ok {
140
+		b.imageCache = icb.MakeImageCache(config.CacheFrom)
141
+	}
142
+
139 143
 	parser.SetEscapeToken(parser.DefaultEscapeToken, &b.directive) // Assume the default token for escape
140 144
 
141 145
 	if dockerfile != nil {
... ...
@@ -438,18 +438,16 @@ func (b *Builder) processImageFrom(img builder.Image) error {
438 438
 	return nil
439 439
 }
440 440
 
441
-// probeCache checks if `b.docker` implements builder.ImageCache and image-caching
442
-// is enabled (`b.UseCache`).
443
-// If so attempts to look up the current `b.image` and `b.runConfig` pair with `b.docker`.
441
+// probeCache checks if cache match can be found for current build instruction.
444 442
 // If an image is found, probeCache returns `(true, nil)`.
445 443
 // If no image is found, it returns `(false, nil)`.
446 444
 // If there is any error, it returns `(false, err)`.
447 445
 func (b *Builder) probeCache() (bool, error) {
448
-	c, ok := b.docker.(builder.ImageCache)
449
-	if !ok || b.options.NoCache || b.cacheBusted {
446
+	c := b.imageCache
447
+	if c == nil || b.options.NoCache || b.cacheBusted {
450 448
 		return false, nil
451 449
 	}
452
-	cache, err := c.GetCachedImageOnBuild(b.image, b.runConfig)
450
+	cache, err := c.GetCache(b.image, b.runConfig)
453 451
 	if err != nil {
454 452
 		return false, err
455 453
 	}
... ...
@@ -55,6 +55,7 @@ type buildOptions struct {
55 55
 	rm             bool
56 56
 	forceRm        bool
57 57
 	pull           bool
58
+	cacheFrom      []string
58 59
 }
59 60
 
60 61
 // NewBuildCommand creates a new `docker build` command
... ...
@@ -98,6 +99,7 @@ func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command {
98 98
 	flags.BoolVar(&options.forceRm, "force-rm", false, "Always remove intermediate containers")
99 99
 	flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the build output and print image ID on success")
100 100
 	flags.BoolVar(&options.pull, "pull", false, "Always attempt to pull a newer version of the image")
101
+	flags.StringSliceVar(&options.cacheFrom, "cache-from", []string{}, "Images to consider as cache sources")
101 102
 
102 103
 	command.AddTrustedFlags(flags, true)
103 104
 
... ...
@@ -289,6 +291,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
289 289
 		BuildArgs:      runconfigopts.ConvertKVStringsToMap(options.buildArgs.GetAll()),
290 290
 		AuthConfigs:    authConfig,
291 291
 		Labels:         runconfigopts.ConvertKVStringsToMap(options.labels),
292
+		CacheFrom:      options.cacheFrom,
292 293
 	}
293 294
 
294 295
 	response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions)
... ...
@@ -110,6 +110,13 @@ func imageBuildOptionsToQuery(options types.ImageBuildOptions) (url.Values, erro
110 110
 		return query, err
111 111
 	}
112 112
 	query.Set("labels", string(labelsJSON))
113
+
114
+	cacheFromJSON, err := json.Marshal(options.CacheFrom)
115
+	if err != nil {
116
+		return query, err
117
+	}
118
+	query.Set("cachefrom", string(cacheFromJSON))
119
+
113 120
 	return query, nil
114 121
 }
115 122
 
116 123
new file mode 100644
... ...
@@ -0,0 +1,254 @@
0
+package daemon
1
+
2
+import (
3
+	"encoding/json"
4
+	"fmt"
5
+	"reflect"
6
+	"strings"
7
+
8
+	"github.com/Sirupsen/logrus"
9
+	containertypes "github.com/docker/docker/api/types/container"
10
+	"github.com/docker/docker/builder"
11
+	"github.com/docker/docker/dockerversion"
12
+	"github.com/docker/docker/image"
13
+	"github.com/docker/docker/layer"
14
+	"github.com/docker/docker/runconfig"
15
+	"github.com/pkg/errors"
16
+)
17
+
18
+// getLocalCachedImage returns the most recent created image that is a child
19
+// of the image with imgID, that had the same config when it was
20
+// created. nil is returned if a child cannot be found. An error is
21
+// returned if the parent image cannot be found.
22
+func (daemon *Daemon) getLocalCachedImage(imgID image.ID, config *containertypes.Config) (*image.Image, error) {
23
+	// Loop on the children of the given image and check the config
24
+	getMatch := func(siblings []image.ID) (*image.Image, error) {
25
+		var match *image.Image
26
+		for _, id := range siblings {
27
+			img, err := daemon.imageStore.Get(id)
28
+			if err != nil {
29
+				return nil, fmt.Errorf("unable to find image %q", id)
30
+			}
31
+
32
+			if runconfig.Compare(&img.ContainerConfig, config) {
33
+				// check for the most up to date match
34
+				if match == nil || match.Created.Before(img.Created) {
35
+					match = img
36
+				}
37
+			}
38
+		}
39
+		return match, nil
40
+	}
41
+
42
+	// In this case, this is `FROM scratch`, which isn't an actual image.
43
+	if imgID == "" {
44
+		images := daemon.imageStore.Map()
45
+		var siblings []image.ID
46
+		for id, img := range images {
47
+			if img.Parent == imgID {
48
+				siblings = append(siblings, id)
49
+			}
50
+		}
51
+		return getMatch(siblings)
52
+	}
53
+
54
+	// find match from child images
55
+	siblings := daemon.imageStore.Children(imgID)
56
+	return getMatch(siblings)
57
+}
58
+
59
+// MakeImageCache creates a stateful image cache.
60
+func (daemon *Daemon) MakeImageCache(sourceRefs []string) builder.ImageCache {
61
+	if len(sourceRefs) == 0 {
62
+		return &localImageCache{daemon}
63
+	}
64
+
65
+	cache := &imageCache{daemon: daemon, localImageCache: &localImageCache{daemon}}
66
+
67
+	for _, ref := range sourceRefs {
68
+		img, err := daemon.GetImage(ref)
69
+		if err != nil {
70
+			logrus.Warnf("Could not look up %s for cache resolution, skipping: %+v", ref, err)
71
+			continue
72
+		}
73
+		cache.sources = append(cache.sources, img)
74
+	}
75
+
76
+	return cache
77
+}
78
+
79
+// localImageCache is cache based on parent chain.
80
+type localImageCache struct {
81
+	daemon *Daemon
82
+}
83
+
84
+func (lic *localImageCache) GetCache(imgID string, config *containertypes.Config) (string, error) {
85
+	return getImageIDAndError(lic.daemon.getLocalCachedImage(image.ID(imgID), config))
86
+}
87
+
88
+// imageCache is cache based on history objects. Requires initial set of images.
89
+type imageCache struct {
90
+	sources         []*image.Image
91
+	daemon          *Daemon
92
+	localImageCache *localImageCache
93
+}
94
+
95
+func (ic *imageCache) restoreCachedImage(parent, target *image.Image, cfg *containertypes.Config) (image.ID, error) {
96
+	var history []image.History
97
+	rootFS := image.NewRootFS()
98
+	lenHistory := 0
99
+	if parent != nil {
100
+		history = parent.History
101
+		rootFS = parent.RootFS
102
+		lenHistory = len(parent.History)
103
+	}
104
+	history = append(history, target.History[lenHistory])
105
+	if layer := getLayerForHistoryIndex(target, lenHistory); layer != "" {
106
+		rootFS.Append(layer)
107
+	}
108
+
109
+	config, err := json.Marshal(&image.Image{
110
+		V1Image: image.V1Image{
111
+			DockerVersion: dockerversion.Version,
112
+			Config:        cfg,
113
+			Architecture:  target.Architecture,
114
+			OS:            target.OS,
115
+			Author:        target.Author,
116
+			Created:       history[len(history)-1].Created,
117
+		},
118
+		RootFS:     rootFS,
119
+		History:    history,
120
+		OSFeatures: target.OSFeatures,
121
+		OSVersion:  target.OSVersion,
122
+	})
123
+	if err != nil {
124
+		return "", errors.Wrap(err, "failed to marshal image config")
125
+	}
126
+
127
+	imgID, err := ic.daemon.imageStore.Create(config)
128
+	if err != nil {
129
+		return "", errors.Wrap(err, "failed to create cache image")
130
+	}
131
+
132
+	if parent != nil {
133
+		if err := ic.daemon.imageStore.SetParent(imgID, parent.ID()); err != nil {
134
+			return "", errors.Wrapf(err, "failed to set parent for %v to %v", target.ID(), parent.ID())
135
+		}
136
+	}
137
+	return imgID, nil
138
+}
139
+
140
+func (ic *imageCache) isParent(imgID, parentID image.ID) bool {
141
+	nextParent, err := ic.daemon.imageStore.GetParent(imgID)
142
+	if err != nil {
143
+		return false
144
+	}
145
+	if nextParent == parentID {
146
+		return true
147
+	}
148
+	return ic.isParent(nextParent, parentID)
149
+}
150
+
151
+func (ic *imageCache) GetCache(parentID string, cfg *containertypes.Config) (string, error) {
152
+	imgID, err := ic.localImageCache.GetCache(parentID, cfg)
153
+	if err != nil {
154
+		return "", err
155
+	}
156
+	if imgID != "" {
157
+		for _, s := range ic.sources {
158
+			if ic.isParent(s.ID(), image.ID(imgID)) {
159
+				return imgID, nil
160
+			}
161
+		}
162
+	}
163
+
164
+	var parent *image.Image
165
+	lenHistory := 0
166
+	if parentID != "" {
167
+		parent, err = ic.daemon.imageStore.Get(image.ID(parentID))
168
+		if err != nil {
169
+			return "", errors.Wrapf(err, "unable to find image %v", parentID)
170
+		}
171
+		lenHistory = len(parent.History)
172
+	}
173
+
174
+	for _, target := range ic.sources {
175
+		if !isValidParent(target, parent) || !isValidConfig(cfg, target.History[lenHistory]) {
176
+			continue
177
+		}
178
+
179
+		if len(target.History)-1 == lenHistory { // last
180
+			if parent != nil {
181
+				if err := ic.daemon.imageStore.SetParent(target.ID(), parent.ID()); err != nil {
182
+					return "", errors.Wrapf(err, "failed to set parent for %v to %v", target.ID(), parent.ID())
183
+				}
184
+			}
185
+			return target.ID().String(), nil
186
+		}
187
+
188
+		imgID, err := ic.restoreCachedImage(parent, target, cfg)
189
+		if err != nil {
190
+			return "", errors.Wrapf(err, "failed to restore cached image from %q to %v", parentID, target.ID())
191
+		}
192
+
193
+		ic.sources = []*image.Image{target} // avoid jumping to different target, tuned for safety atm
194
+		return imgID.String(), nil
195
+	}
196
+
197
+	return "", nil
198
+}
199
+
200
+func getImageIDAndError(img *image.Image, err error) (string, error) {
201
+	if img == nil || err != nil {
202
+		return "", err
203
+	}
204
+	return img.ID().String(), nil
205
+}
206
+
207
+func isValidParent(img, parent *image.Image) bool {
208
+	if len(img.History) == 0 {
209
+		return false
210
+	}
211
+	if parent == nil || len(parent.History) == 0 && len(parent.RootFS.DiffIDs) == 0 {
212
+		return true
213
+	}
214
+	if len(parent.History) >= len(img.History) {
215
+		return false
216
+	}
217
+	if len(parent.RootFS.DiffIDs) >= len(img.RootFS.DiffIDs) {
218
+		return false
219
+	}
220
+
221
+	for i, h := range parent.History {
222
+		if !reflect.DeepEqual(h, img.History[i]) {
223
+			return false
224
+		}
225
+	}
226
+	for i, d := range parent.RootFS.DiffIDs {
227
+		if d != img.RootFS.DiffIDs[i] {
228
+			return false
229
+		}
230
+	}
231
+	return true
232
+}
233
+
234
+func getLayerForHistoryIndex(image *image.Image, index int) layer.DiffID {
235
+	layerIndex := 0
236
+	for i, h := range image.History {
237
+		if i == index {
238
+			if h.EmptyLayer {
239
+				return ""
240
+			}
241
+			break
242
+		}
243
+		if !h.EmptyLayer {
244
+			layerIndex++
245
+		}
246
+	}
247
+	return image.RootFS.DiffIDs[layerIndex] // validate?
248
+}
249
+
250
+func isValidConfig(cfg *containertypes.Config, h image.History) bool {
251
+	// todo: make this format better than join that loses data
252
+	return strings.Join(cfg.Cmd, " ") == h.CreatedBy
253
+}
... ...
@@ -3,11 +3,9 @@ package daemon
3 3
 import (
4 4
 	"fmt"
5 5
 
6
-	containertypes "github.com/docker/docker/api/types/container"
7 6
 	"github.com/docker/docker/builder"
8 7
 	"github.com/docker/docker/image"
9 8
 	"github.com/docker/docker/reference"
10
-	"github.com/docker/docker/runconfig"
11 9
 )
12 10
 
13 11
 // ErrImageDoesNotExist is error returned when no image can be found for a reference.
... ...
@@ -71,54 +69,3 @@ func (daemon *Daemon) GetImageOnBuild(name string) (builder.Image, error) {
71 71
 	}
72 72
 	return img, nil
73 73
 }
74
-
75
-// GetCachedImage returns the most recent created image that is a child
76
-// of the image with imgID, that had the same config when it was
77
-// created. nil is returned if a child cannot be found. An error is
78
-// returned if the parent image cannot be found.
79
-func (daemon *Daemon) GetCachedImage(imgID image.ID, config *containertypes.Config) (*image.Image, error) {
80
-	// Loop on the children of the given image and check the config
81
-	getMatch := func(siblings []image.ID) (*image.Image, error) {
82
-		var match *image.Image
83
-		for _, id := range siblings {
84
-			img, err := daemon.imageStore.Get(id)
85
-			if err != nil {
86
-				return nil, fmt.Errorf("unable to find image %q", id)
87
-			}
88
-
89
-			if runconfig.Compare(&img.ContainerConfig, config) {
90
-				// check for the most up to date match
91
-				if match == nil || match.Created.Before(img.Created) {
92
-					match = img
93
-				}
94
-			}
95
-		}
96
-		return match, nil
97
-	}
98
-
99
-	// In this case, this is `FROM scratch`, which isn't an actual image.
100
-	if imgID == "" {
101
-		images := daemon.imageStore.Map()
102
-		var siblings []image.ID
103
-		for id, img := range images {
104
-			if img.Parent == imgID {
105
-				siblings = append(siblings, id)
106
-			}
107
-		}
108
-		return getMatch(siblings)
109
-	}
110
-
111
-	// find match from child images
112
-	siblings := daemon.imageStore.Children(imgID)
113
-	return getMatch(siblings)
114
-}
115
-
116
-// GetCachedImageOnBuild returns a reference to a cached image whose parent equals `parent`
117
-// and runconfig equals `cfg`. A cache miss is expected to return an empty ID and a nil error.
118
-func (daemon *Daemon) GetCachedImageOnBuild(imgID string, cfg *containertypes.Config) (string, error) {
119
-	cache, err := daemon.GetCachedImage(image.ID(imgID), cfg)
120
-	if cache == nil || err != nil {
121
-		return "", err
122
-	}
123
-	return cache.ID().String(), nil
124
-}
... ...
@@ -124,6 +124,7 @@ This section lists each version from latest to oldest.  Each listing includes a
124 124
 * `POST /containers/create` now validates IPAMConfig in NetworkingConfig, and returns error for invalid IPv4 and IPv6 addresses (`--ip` and `--ip6` in `docker create/run`).
125 125
 * `POST /containers/create` now takes a `Mounts` field in `HostConfig` which replaces `Binds` and `Volumes`. *note*: `Binds` and `Volumes` are still available but are exclusive with `Mounts`
126 126
 * `POST /build` now performs a preliminary validation of the `Dockerfile` before starting the build, and returns an error if the syntax is incorrect. Note that this change is _unversioned_ and applied to all API versions.
127
+* `POST /build` accepts `cachefrom` parameter to specify images used for build cache.
127 128
 
128 129
 ### v1.24 API changes
129 130
 
... ...
@@ -1715,6 +1715,7 @@ or being killed.
1715 1715
         there must be a file with the corresponding path inside the tarball.
1716 1716
 -   **q** – Suppress verbose build output.
1717 1717
 -   **nocache** – Do not use the cache when building the image.
1718
+-   **cachefrom** - JSON array of images used for build cache resolution.
1718 1719
 -   **pull** - Attempt to pull the image even if an older image exists locally.
1719 1720
 -   **rm** - Remove intermediate containers after a successful build (default behavior).
1720 1721
 -   **forcerm** - Always remove intermediate containers (includes `rm`).
... ...
@@ -106,6 +106,13 @@ the `Using cache` message in the console output.
106 106
      ---> 7ea8aef582cc
107 107
     Successfully built 7ea8aef582cc
108 108
 
109
+Build cache is only used from images that have a local parent chain. This means
110
+that these images were created by previous builds or the whole chain of images
111
+was loaded with `docker load`. If you wish to use build cache of a specific
112
+image you can specify it with `--cache-from` option. Images specified with
113
+`--cache-from` do not need to have a parent chain and may be pulled from other
114
+registries.
115
+
109 116
 When you're done with your build, you're ready to look into [*Pushing a
110 117
 repository to its registry*](../tutorials/dockerrepos.md#contributing-to-docker-hub).
111 118
 
... ...
@@ -17,6 +17,7 @@ Build an image from a Dockerfile
17 17
 
18 18
 Options:
19 19
       --build-arg value         Set build-time variables (default [])
20
+      --cache-from value        Images to consider as cache sources (default [])
20 21
       --cgroup-parent string    Optional parent cgroup for the container
21 22
       --cpu-period int          Limit the CPU CFS (Completely Fair Scheduler) period
22 23
       --cpu-quota int           Limit the CPU CFS (Completely Fair Scheduler) quota