Browse code

[daemon] Refactor image_delete.go

This file was not well documented and had very high cyclomatic complexity.
This patch completely rearranges this file and the ImageDelete method to
be easier to follow and more maintainable in the future.

Docker-DCO-1.1-Signed-off-by: Josh Hawn <josh.hawn@docker.com> (github: jlhawn)

Josh Hawn authored on 2015/08/15 16:30:25
Showing 5 changed files
... ...
@@ -233,10 +233,15 @@ func (s *Server) deleteImages(version version.Version, w http.ResponseWriter, r
233 233
 	}
234 234
 
235 235
 	name := vars["name"]
236
+
237
+	if name == "" {
238
+		return fmt.Errorf("image name cannot be blank")
239
+	}
240
+
236 241
 	force := boolValue(r, "force")
237
-	noprune := boolValue(r, "noprune")
242
+	prune := !boolValue(r, "noprune")
238 243
 
239
-	list, err := s.daemon.ImageDelete(name, force, noprune)
244
+	list, err := s.daemon.ImageDelete(name, force, prune)
240 245
 	if err != nil {
241 246
 		return err
242 247
 	}
... ...
@@ -4,7 +4,6 @@ import (
4 4
 	"fmt"
5 5
 	"strings"
6 6
 
7
-	"github.com/Sirupsen/logrus"
8 7
 	"github.com/docker/docker/api/types"
9 8
 	"github.com/docker/docker/graph/tags"
10 9
 	"github.com/docker/docker/image"
... ...
@@ -13,165 +12,329 @@ import (
13 13
 	"github.com/docker/docker/utils"
14 14
 )
15 15
 
16
-// ImageDelete removes the image from the filesystem.
17
-// FIXME: remove ImageDelete's dependency on Daemon, then move to graph/
18
-func (daemon *Daemon) ImageDelete(name string, force, noprune bool) ([]types.ImageDelete, error) {
19
-	list := []types.ImageDelete{}
20
-	if err := daemon.imgDeleteHelper(name, &list, true, force, noprune); err != nil {
16
+// ImageDelete deletes the image referenced by the given imageRef from this
17
+// daemon. The given imageRef can be an image ID, ID prefix, or a repository
18
+// reference (with an optional tag or digest, defaulting to the tag name
19
+// "latest"). There is differing behavior depending on whether the given
20
+// imageRef is a repository reference or not.
21
+//
22
+// If the given imageRef is a repository reference then that repository
23
+// reference will be removed. However, if there exists any containers which
24
+// were created using the same image reference then the repository reference
25
+// cannot be removed unless either there are other repository references to the
26
+// same image or force is true. Following removal of the repository reference,
27
+// the referenced image itself will attempt to be deleted as described below
28
+// but quietly, meaning any image delete conflicts will cause the image to not
29
+// be deleted and the conflict will not be reported.
30
+//
31
+// There may be conflicts preventing deletion of an image and these conflicts
32
+// are divided into two categories grouped by their severity:
33
+//
34
+// Hard Conflict:
35
+// 	- a pull or build using the image.
36
+// 	- any descendent image.
37
+// 	- any running container using the image.
38
+//
39
+// Soft Conflict:
40
+// 	- any stopped container using the image.
41
+// 	- any repository tag or digest references to the image.
42
+//
43
+// The image cannot be removed if there are any hard conflicts and can be
44
+// removed if there are soft conflicts only if force is true.
45
+//
46
+// If prune is true, ancestor images will each attempt to be deleted quietly,
47
+// meaning any delete conflicts will cause the image to not be deleted and the
48
+// conflict will not be reported.
49
+//
50
+// FIXME: remove ImageDelete's dependency on Daemon, then move to the graph
51
+// package. This would require that we no longer need the daemon to determine
52
+// whether images are being used by a stopped or running container.
53
+func (daemon *Daemon) ImageDelete(imageRef string, force, prune bool) ([]types.ImageDelete, error) {
54
+	records := []types.ImageDelete{}
55
+
56
+	img, err := daemon.Repositories().LookupImage(imageRef)
57
+	if err != nil {
21 58
 		return nil, err
22 59
 	}
23
-	if len(list) == 0 {
24
-		return nil, fmt.Errorf("Conflict, %s wasn't deleted", name)
60
+
61
+	var removedRepositoryRef bool
62
+	if !isImageIDPrefix(img.ID, imageRef) {
63
+		// A repository reference was given and should be removed
64
+		// first. We can only remove this reference if either force is
65
+		// true, there are multiple repository references to this
66
+		// image, or there are no containers using the given reference.
67
+		if !(force || daemon.imageHasMultipleRepositoryReferences(img.ID)) {
68
+			if container := daemon.getContainerUsingImage(img.ID); container != nil {
69
+				// If we removed the repository reference then
70
+				// this image would remain "dangling" and since
71
+				// we really want to avoid that the client must
72
+				// explicitly force its removal.
73
+				return nil, fmt.Errorf("conflict: unable to remove repository reference %q (must force) - container %s is using its referenced image %s", imageRef, stringid.TruncateID(container.ID), stringid.TruncateID(img.ID))
74
+			}
75
+		}
76
+
77
+		parsedRef, err := daemon.removeImageRef(imageRef)
78
+		if err != nil {
79
+			return nil, err
80
+		}
81
+
82
+		untaggedRecord := types.ImageDelete{Untagged: parsedRef}
83
+
84
+		daemon.EventsService.Log("untag", img.ID, "")
85
+		records = append(records, untaggedRecord)
86
+
87
+		removedRepositoryRef = true
88
+	} else {
89
+		// If an ID reference was given AND there is exactly one
90
+		// repository reference to the image then we will want to
91
+		// remove that reference.
92
+		// FIXME: Is this the behavior we want?
93
+		repoRefs := daemon.Repositories().ByID()[img.ID]
94
+		if len(repoRefs) == 1 {
95
+			parsedRef, err := daemon.removeImageRef(repoRefs[0])
96
+			if err != nil {
97
+				return nil, err
98
+			}
99
+
100
+			untaggedRecord := types.ImageDelete{Untagged: parsedRef}
101
+
102
+			daemon.EventsService.Log("untag", img.ID, "")
103
+			records = append(records, untaggedRecord)
104
+		}
25 105
 	}
26 106
 
27
-	return list, nil
107
+	return records, daemon.imageDeleteHelper(img, &records, force, prune, removedRepositoryRef)
28 108
 }
29 109
 
30
-func (daemon *Daemon) imgDeleteHelper(name string, list *[]types.ImageDelete, first, force, noprune bool) error {
31
-	if name == "" {
32
-		return fmt.Errorf("Image name can not be blank")
110
+// isImageIDPrefix returns whether the given possiblePrefix is a prefix of the
111
+// given imageID.
112
+func isImageIDPrefix(imageID, possiblePrefix string) bool {
113
+	return strings.HasPrefix(imageID, possiblePrefix)
114
+}
115
+
116
+// imageHasMultipleRepositoryReferences returns whether there are multiple
117
+// repository references to the given imageID.
118
+func (daemon *Daemon) imageHasMultipleRepositoryReferences(imageID string) bool {
119
+	return len(daemon.Repositories().ByID()[imageID]) > 1
120
+}
121
+
122
+// getContainerUsingImage returns a container that was created using the given
123
+// imageID. Returns nil if there is no such container.
124
+func (daemon *Daemon) getContainerUsingImage(imageID string) *Container {
125
+	for _, container := range daemon.List() {
126
+		if container.ImageID == imageID {
127
+			return container
128
+		}
33 129
 	}
34 130
 
35
-	var repoName, tag string
36
-	repoAndTags := make(map[string][]string)
131
+	return nil
132
+}
37 133
 
38
-	// FIXME: please respect DRY and centralize repo+tag parsing in a single central place! -- shykes
39
-	repoName, tag = parsers.ParseRepositoryTag(name)
40
-	if tag == "" {
41
-		tag = tags.DefaultTag
134
+// removeImageRef attempts to parse and remove the given image reference from
135
+// this daemon's store of repository tag/digest references. The given
136
+// repositoryRef must not be an image ID but a repository name followed by an
137
+// optional tag or digest reference. If tag or digest is omitted, the default
138
+// tag is used. Returns the resolved image reference and an error.
139
+func (daemon *Daemon) removeImageRef(repositoryRef string) (string, error) {
140
+	repository, ref := parsers.ParseRepositoryTag(repositoryRef)
141
+	if ref == "" {
142
+		ref = tags.DefaultTag
42 143
 	}
43 144
 
44
-	img, err := daemon.Repositories().LookupImage(name)
45
-	if err != nil {
46
-		if r, _ := daemon.Repositories().Get(repoName); r != nil {
47
-			return fmt.Errorf("No such image: %s", utils.ImageReference(repoName, tag))
48
-		}
49
-		return fmt.Errorf("No such image: %s", name)
50
-	}
51
-
52
-	if strings.Contains(img.ID, name) {
53
-		repoName = ""
54
-		tag = ""
55
-	}
56
-
57
-	byParents := daemon.Graph().ByParent()
58
-	repos := daemon.Repositories().ByID()[img.ID]
59
-
60
-	//If delete by id, see if the id belong only to one repository
61
-	deleteByID := repoName == ""
62
-	if deleteByID {
63
-		for _, repoAndTag := range repos {
64
-			parsedRepo, parsedTag := parsers.ParseRepositoryTag(repoAndTag)
65
-			if repoName == "" || repoName == parsedRepo {
66
-				repoName = parsedRepo
67
-				if parsedTag != "" {
68
-					repoAndTags[repoName] = append(repoAndTags[repoName], parsedTag)
69
-				}
70
-			} else if repoName != parsedRepo && !force && first {
71
-				// the id belongs to multiple repos, like base:latest and user:test,
72
-				// in that case return conflict
73
-				return fmt.Errorf("Conflict, cannot delete image %s because it is tagged in multiple repositories, use -f to force", name)
74
-			} else {
75
-				//the id belongs to multiple repos, with -f just delete all
76
-				repoName = parsedRepo
77
-				if parsedTag != "" {
78
-					repoAndTags[repoName] = append(repoAndTags[repoName], parsedTag)
79
-				}
80
-			}
145
+	// Ignore the boolean value returned, as far as we're concerned, this
146
+	// is an idempotent operation and it's okay if the reference didn't
147
+	// exist in the first place.
148
+	_, err := daemon.Repositories().Delete(repository, ref)
149
+
150
+	return utils.ImageReference(repository, ref), err
151
+}
152
+
153
+// removeAllReferencesToImageID attempts to remove every reference to the given
154
+// imgID from this daemon's store of repository tag/digest references. Returns
155
+// on the first encountered error. Removed references are logged to this
156
+// daemon's event service. An "Untagged" types.ImageDelete is added to the
157
+// given list of records.
158
+func (daemon *Daemon) removeAllReferencesToImageID(imgID string, records *[]types.ImageDelete) error {
159
+	imageRefs := daemon.Repositories().ByID()[imgID]
160
+
161
+	for _, imageRef := range imageRefs {
162
+		parsedRef, err := daemon.removeImageRef(imageRef)
163
+		if err != nil {
164
+			return err
81 165
 		}
166
+
167
+		untaggedRecord := types.ImageDelete{Untagged: parsedRef}
168
+
169
+		daemon.EventsService.Log("untag", imgID, "")
170
+		*records = append(*records, untaggedRecord)
171
+	}
172
+
173
+	return nil
174
+}
175
+
176
+// ImageDeleteConflict holds a soft or hard conflict and an associated error.
177
+// Implements the error interface.
178
+type imageDeleteConflict struct {
179
+	hard    bool
180
+	imgID   string
181
+	message string
182
+}
183
+
184
+func (idc *imageDeleteConflict) Error() string {
185
+	var forceMsg string
186
+	if idc.hard {
187
+		forceMsg = "cannot be forced"
82 188
 	} else {
83
-		repoAndTags[repoName] = append(repoAndTags[repoName], tag)
189
+		forceMsg = "must be forced"
190
+	}
191
+
192
+	return fmt.Sprintf("conflict: unable to delete %s (%s) - %s", stringid.TruncateID(idc.imgID), forceMsg, idc.message)
193
+}
194
+
195
+// imageDeleteHelper attempts to delete the given image from this daemon. If
196
+// the image has any hard delete conflicts (child images or running containers
197
+// using the image) then it cannot be deleted. If the image has any soft delete
198
+// conflicts (any tags/digests referencing the image or any stopped container
199
+// using the image) then it can only be deleted if force is true. If the delete
200
+// succeeds and prune is true, the parent images are also deleted if they do
201
+// not have any soft or hard delete conflicts themselves. Any deleted images
202
+// and untagged references are appended to the given records. If any error or
203
+// conflict is encountered, it will be returned immediately without deleting
204
+// the image. If quiet is true, any encountered conflicts will be ignored and
205
+// the function will return nil immediately without deleting the image.
206
+func (daemon *Daemon) imageDeleteHelper(img *image.Image, records *[]types.ImageDelete, force, prune, quiet bool) error {
207
+	// First, determine if this image has any conflicts. Ignore soft conflicts
208
+	// if force is true.
209
+	if conflict := daemon.checkImageDeleteConflict(img, force); conflict != nil {
210
+		if quiet && !daemon.imageIsDangling(img) {
211
+			// Ignore conflicts UNLESS the image is "dangling" in
212
+			// which case we want the user to know.
213
+			return nil
214
+		}
215
+
216
+		// There was a conflict and it's either a hard conflict OR we are not
217
+		// forcing deletion on soft conflicts.
218
+		return conflict
219
+	}
220
+
221
+	// Delete all repository tag/digest references to this image.
222
+	if err := daemon.removeAllReferencesToImageID(img.ID, records); err != nil {
223
+		return err
224
+	}
225
+
226
+	if err := daemon.Graph().Delete(img.ID); err != nil {
227
+		return err
84 228
 	}
85 229
 
86
-	if !first && len(repoAndTags) > 0 {
230
+	daemon.EventsService.Log("delete", img.ID, "")
231
+	*records = append(*records, types.ImageDelete{Deleted: img.ID})
232
+
233
+	if !prune || img.Parent == "" {
87 234
 		return nil
88 235
 	}
89 236
 
90
-	if len(repos) <= 1 || deleteByID {
91
-		if err := daemon.canDeleteImage(img.ID, force); err != nil {
92
-			return err
237
+	// We need to prune the parent image. This means delete it if there are
238
+	// no tags/digests referencing it and there are no containers using it (
239
+	// either running or stopped).
240
+	parentImg, err := daemon.Graph().Get(img.Parent)
241
+	if err != nil {
242
+		return fmt.Errorf("unable to get parent image: %v", err)
243
+	}
244
+
245
+	// Do not force prunings, but do so quietly (stopping on any encountered
246
+	// conflicts).
247
+	return daemon.imageDeleteHelper(parentImg, records, false, true, true)
248
+}
249
+
250
+// checkImageDeleteConflict determines whether there are any conflicts
251
+// preventing deletion of the given image from this daemon. A hard conflict is
252
+// any image which has the given image as a parent or any running container
253
+// using the image. A soft conflict is any tags/digest referencing the given
254
+// image or any stopped container using the image. If ignoreSoftConflicts is
255
+// true, this function will not check for soft conflict conditions.
256
+func (daemon *Daemon) checkImageDeleteConflict(img *image.Image, ignoreSoftConflicts bool) *imageDeleteConflict {
257
+	// Check for hard conflicts first.
258
+	if conflict := daemon.checkImageDeleteHardConflict(img); conflict != nil {
259
+		return conflict
260
+	}
261
+
262
+	// Then check for soft conflicts.
263
+	if ignoreSoftConflicts {
264
+		// Don't bother checking for soft conflicts.
265
+		return nil
266
+	}
267
+
268
+	return daemon.checkImageDeleteSoftConflict(img)
269
+}
270
+
271
+func (daemon *Daemon) checkImageDeleteHardConflict(img *image.Image) *imageDeleteConflict {
272
+	// Check if the image ID is being used by a pull or build.
273
+	if daemon.Graph().IsHeld(img.ID) {
274
+		return &imageDeleteConflict{
275
+			hard:    true,
276
+			imgID:   img.ID,
277
+			message: "image is held by an ongoing pull or build",
93 278
 		}
94 279
 	}
95 280
 
96
-	// Untag the current image
97
-	for repoName, tags := range repoAndTags {
98
-		for _, tag := range tags {
99
-			tagDeleted, err := daemon.Repositories().Delete(repoName, tag)
100
-			if err != nil {
101
-				return err
102
-			}
103
-			if tagDeleted {
104
-				*list = append(*list, types.ImageDelete{
105
-					Untagged: utils.ImageReference(repoName, tag),
106
-				})
107
-				daemon.EventsService.Log("untag", img.ID, "")
108
-			}
281
+	// Check if the image has any descendent images.
282
+	if daemon.Graph().HasChildren(img) {
283
+		return &imageDeleteConflict{
284
+			hard:    true,
285
+			imgID:   img.ID,
286
+			message: "image has dependent child images",
109 287
 		}
110 288
 	}
111
-	tags := daemon.Repositories().ByID()[img.ID]
112
-	if (len(tags) <= 1 && repoName == "") || len(tags) == 0 {
113
-		if len(byParents[img.ID]) == 0 {
114
-			if err := daemon.Repositories().DeleteAll(img.ID); err != nil {
115
-				return err
116
-			}
117
-			if err := daemon.Graph().Delete(img.ID); err != nil {
118
-				return err
119
-			}
120
-			*list = append(*list, types.ImageDelete{
121
-				Deleted: img.ID,
122
-			})
123
-			daemon.EventsService.Log("delete", img.ID, "")
124
-			if img.Parent != "" && !noprune {
125
-				err := daemon.imgDeleteHelper(img.Parent, list, false, force, noprune)
126
-				if first {
127
-					return err
128
-				}
129 289
 
130
-			}
290
+	// Check if any running container is using the image.
291
+	for _, container := range daemon.List() {
292
+		if !container.IsRunning() {
293
+			// Skip this until we check for soft conflicts later.
294
+			continue
295
+		}
131 296
 
297
+		if container.ImageID == img.ID {
298
+			return &imageDeleteConflict{
299
+				imgID:   img.ID,
300
+				hard:    true,
301
+				message: fmt.Sprintf("image is being used by running container %s", stringid.TruncateID(container.ID)),
302
+			}
132 303
 		}
133 304
 	}
305
+
134 306
 	return nil
135 307
 }
136 308
 
137
-func (daemon *Daemon) canDeleteImage(imgID string, force bool) error {
138
-	if daemon.Graph().IsHeld(imgID) {
139
-		return fmt.Errorf("Conflict, cannot delete because %s is held by an ongoing pull or build", stringid.TruncateID(imgID))
309
+func (daemon *Daemon) checkImageDeleteSoftConflict(img *image.Image) *imageDeleteConflict {
310
+	// Check if any repository tags/digest reference this image.
311
+	if daemon.Repositories().HasReferences(img) {
312
+		return &imageDeleteConflict{
313
+			imgID:   img.ID,
314
+			message: "image is referenced in one or more repositories",
315
+		}
140 316
 	}
317
+
318
+	// Check if any stopped containers reference this image.
141 319
 	for _, container := range daemon.List() {
142
-		if container.ImageID == "" {
143
-			// This technically should never happen, but if the container
144
-			// has no ImageID then log the situation and move on.
145
-			// If we allowed processing to continue then the code later
146
-			// on would fail with a "Prefix can't be empty" error even
147
-			// though the bad container has nothing to do with the image
148
-			// we're trying to delete.
149
-			logrus.Errorf("Container %q has no image associated with it!", container.ID)
320
+		if container.IsRunning() {
321
+			// Skip this as it was checked above in hard conflict conditions.
150 322
 			continue
151 323
 		}
152
-		parent, err := daemon.Repositories().LookupImage(container.ImageID)
153
-		if err != nil {
154
-			if daemon.Graph().IsNotExist(err, container.ImageID) {
155
-				continue
156
-			}
157
-			return err
158
-		}
159 324
 
160
-		if err := daemon.graph.WalkHistory(parent, func(p image.Image) error {
161
-			if imgID == p.ID {
162
-				if container.IsRunning() {
163
-					if force {
164
-						return fmt.Errorf("Conflict, cannot force delete %s because the running container %s is using it, stop it and retry", stringid.TruncateID(imgID), stringid.TruncateID(container.ID))
165
-					}
166
-					return fmt.Errorf("Conflict, cannot delete %s because the running container %s is using it, stop it and use -f to force", stringid.TruncateID(imgID), stringid.TruncateID(container.ID))
167
-				} else if !force {
168
-					return fmt.Errorf("Conflict, cannot delete %s because the container %s is using it, use -f to force", stringid.TruncateID(imgID), stringid.TruncateID(container.ID))
169
-				}
325
+		if container.ImageID == img.ID {
326
+			return &imageDeleteConflict{
327
+				imgID:   img.ID,
328
+				message: fmt.Sprintf("image is being used by stopped container %s", stringid.TruncateID(container.ID)),
170 329
 			}
171
-			return nil
172
-		}); err != nil {
173
-			return err
174 330
 		}
175 331
 	}
332
+
176 333
 	return nil
177 334
 }
335
+
336
+// imageIsDangling returns whether the given image is "dangling" which means
337
+// that there are no repository references to the given image and it has no
338
+// child images.
339
+func (daemon *Daemon) imageIsDangling(img *image.Image) bool {
340
+	return !(daemon.Repositories().HasReferences(img) || daemon.Graph().HasChildren(img))
341
+}
... ...
@@ -404,6 +404,11 @@ func (graph *Graph) ByParent() map[string][]*image.Image {
404 404
 	return byParent
405 405
 }
406 406
 
407
+// HasChildren returns whether the given image has any child images.
408
+func (graph *Graph) HasChildren(img *image.Image) bool {
409
+	return len(graph.ByParent()[img.ID]) > 0
410
+}
411
+
407 412
 // Retain keeps the images and layers that are in the pulling chain so that
408 413
 // they are not deleted. If not retained, they may be deleted by rmi.
409 414
 func (graph *Graph) Retain(sessionID string, layerIDs ...string) {
... ...
@@ -188,6 +188,12 @@ func (store *TagStore) ByID() map[string][]string {
188 188
 	return byID
189 189
 }
190 190
 
191
+// HasReferences returns whether or not the given image is referenced in one or
192
+// more repositories.
193
+func (store *TagStore) HasReferences(img *image.Image) bool {
194
+	return len(store.ByID()[img.ID]) > 0
195
+}
196
+
191 197
 // ImageName returns name of an image, given the image's ID.
192 198
 func (store *TagStore) ImageName(id string) string {
193 199
 	if names, exists := store.ByID()[id]; exists && len(names) > 0 {
... ...
@@ -106,7 +106,7 @@ func (s *DockerSuite) TestRmiImgIDMultipleTag(c *check.C) {
106 106
 
107 107
 	// first checkout without force it fails
108 108
 	out, _, err = dockerCmdWithError("rmi", imgID)
109
-	expected := fmt.Sprintf("Conflict, cannot delete %s because the running container %s is using it, stop it and use -f to force", imgID[:12], containerID[:12])
109
+	expected := fmt.Sprintf("conflict: unable to delete %s (cannot be forced) - image is being used by running container %s", imgID[:12], containerID[:12])
110 110
 	if err == nil || !strings.Contains(out, expected) {
111 111
 		c.Fatalf("rmi tagged in multiple repos should have failed without force: %s, %v, expected: %s", out, err, expected)
112 112
 	}
... ...
@@ -148,7 +148,7 @@ func (s *DockerSuite) TestRmiImgIDForce(c *check.C) {
148 148
 
149 149
 	// first checkout without force it fails
150 150
 	out, _, err = dockerCmdWithError("rmi", imgID)
151
-	if err == nil || !strings.Contains(out, fmt.Sprintf("Conflict, cannot delete image %s because it is tagged in multiple repositories, use -f to force", imgID)) {
151
+	if err == nil || !strings.Contains(out, "(must be forced) - image is referenced in one or more repositories") {
152 152
 		c.Fatalf("rmi tagged in multiple repos should have failed without force:%s, %v", out, err)
153 153
 	}
154 154
 
... ...
@@ -172,7 +172,7 @@ func (s *DockerSuite) TestRmiImageIDForceWithRunningContainersAndMultipleTags(c
172 172
 	dockerCmd(c, "run", "-d", imgID, "top")
173 173
 
174 174
 	out, _, err := dockerCmdWithError("rmi", "-f", imgID)
175
-	if err == nil || !strings.Contains(out, "stop it and retry") {
175
+	if err == nil || !strings.Contains(out, "(cannot be forced) - image is being used by running container") {
176 176
 		c.Log(out)
177 177
 		c.Fatalf("rmi -f should not delete image with running containers")
178 178
 	}
... ...
@@ -251,10 +251,10 @@ func (s *DockerSuite) TestRmiBlank(c *check.C) {
251 251
 	if err == nil {
252 252
 		c.Fatal("Should have failed to delete '' image")
253 253
 	}
254
-	if strings.Contains(out, "No such image") {
254
+	if strings.Contains(out, "no such id") {
255 255
 		c.Fatalf("Wrong error message generated: %s", out)
256 256
 	}
257
-	if !strings.Contains(out, "Image name can not be blank") {
257
+	if !strings.Contains(out, "image name cannot be blank") {
258 258
 		c.Fatalf("Expected error message not generated: %s", out)
259 259
 	}
260 260
 
... ...
@@ -262,7 +262,7 @@ func (s *DockerSuite) TestRmiBlank(c *check.C) {
262 262
 	if err == nil {
263 263
 		c.Fatal("Should have failed to delete '' image")
264 264
 	}
265
-	if !strings.Contains(out, "No such image") {
265
+	if !strings.Contains(out, "no such id") {
266 266
 		c.Fatalf("Expected error message not generated: %s", out)
267 267
 	}
268 268
 }
... ...
@@ -287,8 +287,59 @@ func (s *DockerSuite) TestRmiContainerImageNotFound(c *check.C) {
287 287
 
288 288
 	// Try to remove the image of the running container and see if it fails as expected.
289 289
 	out, _, err := dockerCmdWithError("rmi", "-f", imageIds[0])
290
-	if err == nil || !strings.Contains(out, "is using it") {
290
+	if err == nil || !strings.Contains(out, "image is being used by running container") {
291 291
 		c.Log(out)
292 292
 		c.Fatal("The image of the running container should not be removed.")
293 293
 	}
294 294
 }
295
+
296
+// #13422
297
+func (s *DockerSuite) TestRmiUntagHistoryLayer(c *check.C) {
298
+	image := "tmp1"
299
+	// Build a image for testing.
300
+	dockerfile := `FROM busybox
301
+MAINTAINER foo
302
+RUN echo 0 #layer0
303
+RUN echo 1 #layer1
304
+RUN echo 2 #layer2
305
+`
306
+	_, err := buildImage(image, dockerfile, false)
307
+	c.Assert(err, check.IsNil)
308
+
309
+	out, _ := dockerCmd(c, "history", "-q", image)
310
+	ids := strings.Split(out, "\n")
311
+	idToTag := ids[2]
312
+
313
+	// Tag layer0 to "tmp2".
314
+	newTag := "tmp2"
315
+	dockerCmd(c, "tag", idToTag, newTag)
316
+	// Create a container based on "tmp1".
317
+	dockerCmd(c, "run", "-d", image, "true")
318
+
319
+	// See if the "tmp2" can be untagged.
320
+	out, _ = dockerCmd(c, "rmi", newTag)
321
+	if d := strings.Count(out, "Untagged: "); d != 1 {
322
+		c.Log(out)
323
+		c.Fatalf("Expected 1 untagged entry got %d: %q", d, out)
324
+	}
325
+
326
+	// Now let's add the tag again and create a container based on it.
327
+	dockerCmd(c, "tag", idToTag, newTag)
328
+	out, _ = dockerCmd(c, "run", "-d", newTag, "true")
329
+	cid := strings.TrimSpace(out)
330
+
331
+	// At this point we have 2 containers, one based on layer2 and another based on layer0.
332
+	// Try to untag "tmp2" without the -f flag.
333
+	out, _, err = dockerCmdWithError("rmi", newTag)
334
+	if err == nil || !strings.Contains(out, cid[:12]) || !strings.Contains(out, "(must force)") {
335
+		c.Log(out)
336
+		c.Fatalf("%q should not be untagged without the -f flag", newTag)
337
+	}
338
+
339
+	// Add the -f flag and test again.
340
+	out, _ = dockerCmd(c, "rmi", "-f", newTag)
341
+	if !strings.Contains(out, fmt.Sprintf("Untagged: %s:latest", newTag)) {
342
+		c.Log(out)
343
+		c.Fatalf("%q should be allowed to untag with the -f flag", newTag)
344
+	}
345
+}