Browse code

Prune digest references when deleting by tag

When pulling an image with content trust enabled, two references are
created: a digest reference and a tag reference. Deleting by tag
wouldn't actually remove the image, because the digest reference keeps
it alive.

This change modifies the rmi logic so that digest references don't keep
an image alive. If the last tag referencing a given image is deleted,
any digest references to it will be removed as well, so the image can
actually get deleted. This fixes the usability problem with deletions
when content trust is in use, so something like "docker pull busybox;
docker rmi busybox" will work as expected.

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>

Aaron Lehmann authored on 2016/01/07 10:57:21
Showing 4 changed files
... ...
@@ -90,8 +90,34 @@ func (daemon *Daemon) ImageDelete(imageRef string, force, prune bool) ([]types.I
90 90
 		daemon.LogImageEvent(imgID.String(), imgID.String(), "untag")
91 91
 		records = append(records, untaggedRecord)
92 92
 
93
-		// If has remaining references then untag finishes the remove
94
-		if len(repoRefs) > 1 {
93
+		repoRefs = daemon.referenceStore.References(imgID)
94
+
95
+		// If this is a tag reference and all the remaining references
96
+		// to this image are digest references, delete the remaining
97
+		// references so that they don't prevent removal of the image.
98
+		if _, isCanonical := parsedRef.(reference.Canonical); !isCanonical {
99
+			foundTagRef := false
100
+			for _, repoRef := range repoRefs {
101
+				if _, repoRefIsCanonical := repoRef.(reference.Canonical); !repoRefIsCanonical {
102
+					foundTagRef = true
103
+					break
104
+				}
105
+			}
106
+			if !foundTagRef {
107
+				for _, repoRef := range repoRefs {
108
+					if _, err := daemon.removeImageRef(repoRef); err != nil {
109
+						return records, err
110
+					}
111
+
112
+					untaggedRecord := types.ImageDelete{Untagged: repoRef.String()}
113
+					records = append(records, untaggedRecord)
114
+				}
115
+				repoRefs = []reference.Named{}
116
+			}
117
+		}
118
+
119
+		// If it has remaining references then the untag finished the remove
120
+		if len(repoRefs) > 0 {
95 121
 			return records, nil
96 122
 		}
97 123
 
... ...
@@ -19,8 +19,9 @@ parent = "smn_cli"
19 19
       --no-prune           Do not delete untagged parents
20 20
 
21 21
 You can remove an image using its short or long ID, its tag, or its digest. If
22
-an image has one or more tag or digest reference, you must remove all of them
23
-before the image is removed.
22
+an image has one or more tag referencing it, you must remove all of them before
23
+the image is removed. Digest references are removed automatically when an image
24
+is removed by tag.
24 25
 
25 26
     $ docker images
26 27
     REPOSITORY                TAG                 IMAGE ID            CREATED             SIZE
... ...
@@ -370,6 +370,35 @@ func (s *DockerRegistrySuite) TestDeleteImageByIDOnlyPulledByDigest(c *check.C)
370 370
 	dockerCmd(c, "rmi", imageID)
371 371
 }
372 372
 
373
+func (s *DockerRegistrySuite) TestDeleteImageWithDigestAndTag(c *check.C) {
374
+	pushDigest, err := setupImage(c)
375
+	c.Assert(err, checker.IsNil, check.Commentf("error setting up image"))
376
+
377
+	// pull from the registry using the <name>@<digest> reference
378
+	imageReference := fmt.Sprintf("%s@%s", repoName, pushDigest)
379
+	dockerCmd(c, "pull", imageReference)
380
+
381
+	imageID, err := inspectField(imageReference, "Id")
382
+	c.Assert(err, checker.IsNil, check.Commentf("error inspecting image id"))
383
+
384
+	repoTag := repoName + ":sometag"
385
+	repoTag2 := repoName + ":othertag"
386
+	dockerCmd(c, "tag", imageReference, repoTag)
387
+	dockerCmd(c, "tag", imageReference, repoTag2)
388
+
389
+	dockerCmd(c, "rmi", repoTag2)
390
+
391
+	// rmi should have deleted only repoTag2, because there's another tag
392
+	_, err = inspectField(repoTag, "Id")
393
+	c.Assert(err, checker.IsNil, check.Commentf("repoTag should not have been removed"))
394
+
395
+	dockerCmd(c, "rmi", repoTag)
396
+
397
+	// rmi should have deleted the tag, the digest reference, and the image itself
398
+	_, err = inspectField(imageID, "Id")
399
+	c.Assert(err, checker.NotNil, check.Commentf("image should have been deleted"))
400
+}
401
+
373 402
 // TestPullFailsWithAlteredManifest tests that a `docker pull` fails when
374 403
 // we have modified a manifest blob and its digest cannot be verified.
375 404
 func (s *DockerRegistrySuite) TestPullFailsWithAlteredManifest(c *check.C) {
... ...
@@ -4,6 +4,7 @@ import (
4 4
 	"fmt"
5 5
 	"io/ioutil"
6 6
 	"os/exec"
7
+	"strings"
7 8
 	"time"
8 9
 
9 10
 	"github.com/docker/docker/pkg/integration/checker"
... ...
@@ -200,3 +201,55 @@ func (s *DockerTrustSuite) TestTrustedOfflinePull(c *check.C) {
200 200
 	c.Assert(err, check.IsNil, check.Commentf(out))
201 201
 	c.Assert(string(out), checker.Contains, "Tagging", check.Commentf(out))
202 202
 }
203
+
204
+func (s *DockerTrustSuite) TestTrustedPullDelete(c *check.C) {
205
+	repoName := fmt.Sprintf("%v/dockercli/%s:latest", privateRegistryURL, "trusted-pull-delete")
206
+	// tag the image and upload it to the private registry
207
+	_, err := buildImage(repoName, `
208
+                    FROM busybox
209
+                    CMD echo trustedpulldelete
210
+                `, true)
211
+
212
+	pushCmd := exec.Command(dockerBinary, "push", repoName)
213
+	s.trustedCmd(pushCmd)
214
+	out, _, err := runCommandWithOutput(pushCmd)
215
+	if err != nil {
216
+		c.Fatalf("Error running trusted push: %s\n%s", err, out)
217
+	}
218
+	if !strings.Contains(string(out), "Signing and pushing trust metadata") {
219
+		c.Fatalf("Missing expected output on trusted push:\n%s", out)
220
+	}
221
+
222
+	if out, status := dockerCmd(c, "rmi", repoName); status != 0 {
223
+		c.Fatalf("Error removing image %q\n%s", repoName, out)
224
+	}
225
+
226
+	// Try pull
227
+	pullCmd := exec.Command(dockerBinary, "pull", repoName)
228
+	s.trustedCmd(pullCmd)
229
+	out, _, err = runCommandWithOutput(pullCmd)
230
+
231
+	c.Assert(err, check.IsNil, check.Commentf(out))
232
+
233
+	matches := digestRegex.FindStringSubmatch(out)
234
+	c.Assert(matches, checker.HasLen, 2, check.Commentf("unable to parse digest from pull output: %s", out))
235
+	pullDigest := matches[1]
236
+
237
+	imageID, err := inspectField(repoName, "Id")
238
+	c.Assert(err, checker.IsNil, check.Commentf("error inspecting image id"))
239
+
240
+	imageByDigest := repoName + "@" + pullDigest
241
+	byDigestID, err := inspectField(imageByDigest, "Id")
242
+	c.Assert(err, checker.IsNil, check.Commentf("error inspecting image id"))
243
+
244
+	c.Assert(byDigestID, checker.Equals, imageID)
245
+
246
+	// rmi of tag should also remove the digest reference
247
+	dockerCmd(c, "rmi", repoName)
248
+
249
+	_, err = inspectField(imageByDigest, "Id")
250
+	c.Assert(err, checker.NotNil, check.Commentf("digest reference should have been removed"))
251
+
252
+	_, err = inspectField(imageID, "Id")
253
+	c.Assert(err, checker.NotNil, check.Commentf("image should have been deleted"))
254
+}