Browse code

Add ability to refer to image by name + digest

Add ability to refer to an image by repository name and digest using the
format repository@digest. Works for pull, push, run, build, and rmi.

Signed-off-by: Andy Goldstein <agoldste@redhat.com>

Andy Goldstein authored on 2015/02/27 11:23:50
Showing 28 changed files
... ...
@@ -108,7 +108,7 @@ RUN go get golang.org/x/tools/cmd/cover
108 108
 RUN gem install --no-rdoc --no-ri fpm --version 1.3.2
109 109
 
110 110
 # Install registry
111
-ENV REGISTRY_COMMIT c448e0416925a9876d5576e412703c9b8b865e19
111
+ENV REGISTRY_COMMIT b4cc5e3ecc2e9f4fa0e95d94c389e1d79e902486
112 112
 RUN set -x \
113 113
 	&& git clone https://github.com/docker/distribution.git /go/src/github.com/docker/distribution \
114 114
 	&& (cd /go/src/github.com/docker/distribution && git checkout -q $REGISTRY_COMMIT) \
... ...
@@ -1312,7 +1312,7 @@ func (cli *DockerCli) CmdPush(args ...string) error {
1312 1312
 }
1313 1313
 
1314 1314
 func (cli *DockerCli) CmdPull(args ...string) error {
1315
-	cmd := cli.Subcmd("pull", "NAME[:TAG]", "Pull an image or a repository from the registry", true)
1315
+	cmd := cli.Subcmd("pull", "NAME[:TAG|@DIGEST]", "Pull an image or a repository from the registry", true)
1316 1316
 	allTags := cmd.Bool([]string{"a", "-all-tags"}, false, "Download all tagged images in the repository")
1317 1317
 	cmd.Require(flag.Exact, 1)
1318 1318
 
... ...
@@ -1325,7 +1325,7 @@ func (cli *DockerCli) CmdPull(args ...string) error {
1325 1325
 	)
1326 1326
 	taglessRemote, tag := parsers.ParseRepositoryTag(remote)
1327 1327
 	if tag == "" && !*allTags {
1328
-		newRemote = taglessRemote + ":" + graph.DEFAULTTAG
1328
+		newRemote = utils.ImageReference(taglessRemote, graph.DEFAULTTAG)
1329 1329
 	}
1330 1330
 	if tag != "" && *allTags {
1331 1331
 		return fmt.Errorf("tag can't be used with --all-tags/-a")
... ...
@@ -1378,6 +1378,7 @@ func (cli *DockerCli) CmdImages(args ...string) error {
1378 1378
 	quiet := cmd.Bool([]string{"q", "-quiet"}, false, "Only show numeric IDs")
1379 1379
 	all := cmd.Bool([]string{"a", "-all"}, false, "Show all images (default hides intermediate images)")
1380 1380
 	noTrunc := cmd.Bool([]string{"#notrunc", "-no-trunc"}, false, "Don't truncate output")
1381
+	showDigests := cmd.Bool([]string{"-digests"}, false, "Show digests")
1381 1382
 	// FIXME: --viz and --tree are deprecated. Remove them in a future version.
1382 1383
 	flViz := cmd.Bool([]string{"#v", "#viz", "#-viz"}, false, "Output graph in graphviz format")
1383 1384
 	flTree := cmd.Bool([]string{"#t", "#tree", "#-tree"}, false, "Output graph in tree format")
... ...
@@ -1504,20 +1505,43 @@ func (cli *DockerCli) CmdImages(args ...string) error {
1504 1504
 
1505 1505
 		w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0)
1506 1506
 		if !*quiet {
1507
-			fmt.Fprintln(w, "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tVIRTUAL SIZE")
1507
+			if *showDigests {
1508
+				fmt.Fprintln(w, "REPOSITORY\tTAG\tDIGEST\tIMAGE ID\tCREATED\tVIRTUAL SIZE")
1509
+			} else {
1510
+				fmt.Fprintln(w, "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tVIRTUAL SIZE")
1511
+			}
1508 1512
 		}
1509 1513
 
1510 1514
 		for _, out := range outs.Data {
1511
-			for _, repotag := range out.GetList("RepoTags") {
1515
+			outID := out.Get("Id")
1516
+			if !*noTrunc {
1517
+				outID = common.TruncateID(outID)
1518
+			}
1512 1519
 
1520
+			// Tags referring to this image ID.
1521
+			for _, repotag := range out.GetList("RepoTags") {
1513 1522
 				repo, tag := parsers.ParseRepositoryTag(repotag)
1514
-				outID := out.Get("Id")
1515
-				if !*noTrunc {
1516
-					outID = common.TruncateID(outID)
1523
+
1524
+				if !*quiet {
1525
+					if *showDigests {
1526
+						fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s ago\t%s\n", repo, tag, "<none>", outID, units.HumanDuration(time.Now().UTC().Sub(time.Unix(out.GetInt64("Created"), 0))), units.HumanSize(float64(out.GetInt64("VirtualSize"))))
1527
+					} else {
1528
+						fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\n", repo, tag, outID, units.HumanDuration(time.Now().UTC().Sub(time.Unix(out.GetInt64("Created"), 0))), units.HumanSize(float64(out.GetInt64("VirtualSize"))))
1529
+					}
1530
+				} else {
1531
+					fmt.Fprintln(w, outID)
1517 1532
 				}
1533
+			}
1518 1534
 
1535
+			// Digests referring to this image ID.
1536
+			for _, repoDigest := range out.GetList("RepoDigests") {
1537
+				repo, digest := parsers.ParseRepositoryTag(repoDigest)
1519 1538
 				if !*quiet {
1520
-					fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\n", repo, tag, outID, units.HumanDuration(time.Now().UTC().Sub(time.Unix(out.GetInt64("Created"), 0))), units.HumanSize(float64(out.GetInt64("VirtualSize"))))
1539
+					if *showDigests {
1540
+						fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s ago\t%s\n", repo, "<none>", digest, outID, units.HumanDuration(time.Now().UTC().Sub(time.Unix(out.GetInt64("Created"), 0))), units.HumanSize(float64(out.GetInt64("VirtualSize"))))
1541
+					} else {
1542
+						fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\n", repo, "<none>", outID, units.HumanDuration(time.Now().UTC().Sub(time.Unix(out.GetInt64("Created"), 0))), units.HumanSize(float64(out.GetInt64("VirtualSize"))))
1543
+					}
1521 1544
 				} else {
1522 1545
 					fmt.Fprintln(w, outID)
1523 1546
 				}
... ...
@@ -2208,7 +2232,7 @@ func (cli *DockerCli) createContainer(config *runconfig.Config, hostConfig *runc
2208 2208
 		if tag == "" {
2209 2209
 			tag = graph.DEFAULTTAG
2210 2210
 		}
2211
-		fmt.Fprintf(cli.err, "Unable to find image '%s:%s' locally\n", repo, tag)
2211
+		fmt.Fprintf(cli.err, "Unable to find image '%s' locally\n", utils.ImageReference(repo, tag))
2212 2212
 
2213 2213
 		// we don't want to write to stdout anything apart from container.ID
2214 2214
 		if err = cli.pullImageCustomOut(config.Image, cli.err); err != nil {
... ...
@@ -9,6 +9,7 @@ import (
9 9
 	"github.com/docker/docker/image"
10 10
 	"github.com/docker/docker/pkg/common"
11 11
 	"github.com/docker/docker/pkg/parsers"
12
+	"github.com/docker/docker/utils"
12 13
 )
13 14
 
14 15
 func (daemon *Daemon) ImageDelete(job *engine.Job) engine.Status {
... ...
@@ -48,7 +49,7 @@ func (daemon *Daemon) DeleteImage(eng *engine.Engine, name string, imgs *engine.
48 48
 	img, err := daemon.Repositories().LookupImage(name)
49 49
 	if err != nil {
50 50
 		if r, _ := daemon.Repositories().Get(repoName); r != nil {
51
-			return fmt.Errorf("No such image: %s:%s", repoName, tag)
51
+			return fmt.Errorf("No such image: %s", utils.ImageReference(repoName, tag))
52 52
 		}
53 53
 		return fmt.Errorf("No such image: %s", name)
54 54
 	}
... ...
@@ -102,7 +103,7 @@ func (daemon *Daemon) DeleteImage(eng *engine.Engine, name string, imgs *engine.
102 102
 		}
103 103
 		if tagDeleted {
104 104
 			out := &engine.Env{}
105
-			out.Set("Untagged", repoName+":"+tag)
105
+			out.Set("Untagged", utils.ImageReference(repoName, tag))
106 106
 			imgs.Add(out)
107 107
 			eng.Job("log", "untag", img.ID, "").Run()
108 108
 		}
... ...
@@ -8,6 +8,7 @@ import (
8 8
 
9 9
 	"github.com/docker/docker/graph"
10 10
 	"github.com/docker/docker/pkg/graphdb"
11
+	"github.com/docker/docker/utils"
11 12
 
12 13
 	"github.com/docker/docker/engine"
13 14
 	"github.com/docker/docker/pkg/parsers"
... ...
@@ -131,7 +132,7 @@ func (daemon *Daemon) Containers(job *engine.Job) engine.Status {
131 131
 		img := container.Config.Image
132 132
 		_, tag := parsers.ParseRepositoryTag(container.Config.Image)
133 133
 		if tag == "" {
134
-			img = img + ":" + graph.DEFAULTTAG
134
+			img = utils.ImageReference(img, graph.DEFAULTTAG)
135 135
 		}
136 136
 		out.SetJson("Image", img)
137 137
 		if len(container.Args) > 0 {
... ...
@@ -8,6 +8,7 @@ docker-images - List images
8 8
 **docker images**
9 9
 [**--help**]
10 10
 [**-a**|**--all**[=*false*]]
11
+[**--digests**[=*false*]]
11 12
 [**-f**|**--filter**[=*[]*]]
12 13
 [**--no-trunc**[=*false*]]
13 14
 [**-q**|**--quiet**[=*false*]]
... ...
@@ -33,6 +34,9 @@ versions.
33 33
 **-a**, **--all**=*true*|*false*
34 34
    Show all images (by default filter out the intermediate image layers). The default is *false*.
35 35
 
36
+**--digests**=*true*|*false*
37
+   Show image digests. The default is *false*.
38
+
36 39
 **-f**, **--filter**=[]
37 40
    Filters the output. The dangling=true filter finds unused images. While label=com.foo=amd64 filters for images with a com.foo value of amd64. The label=com.foo filter finds images with the label com.foo of any value.
38 41
 
... ...
@@ -62,6 +62,10 @@ You can set ulimit settings to be used within the container.
62 62
 **New!**
63 63
 This endpoint now returns `SystemTime`, `HttpProxy`,`HttpsProxy` and `NoProxy`. 
64 64
 
65
+`GET /images/json`
66
+
67
+**New!**
68
+Added a `RepoDigests` field to include image digest information.
65 69
 
66 70
 ## v1.17
67 71
 
... ...
@@ -1054,6 +1054,45 @@ Status Codes:
1054 1054
           }
1055 1055
         ]
1056 1056
 
1057
+**Example request, with digest information**:
1058
+
1059
+        GET /images/json?digests=1 HTTP/1.1
1060
+
1061
+**Example response, with digest information**:
1062
+
1063
+        HTTP/1.1 200 OK
1064
+        Content-Type: application/json
1065
+
1066
+        [
1067
+          {
1068
+            "Created": 1420064636,
1069
+            "Id": "4986bf8c15363d1c5d15512d5266f8777bfba4974ac56e3270e7760f6f0a8125",
1070
+            "ParentId": "ea13149945cb6b1e746bf28032f02e9b5a793523481a0a18645fc77ad53c4ea2",
1071
+            "RepoDigests": [
1072
+              "localhost:5000/test/busybox@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf"
1073
+            ],
1074
+            "RepoTags": [
1075
+              "localhost:5000/test/busybox:latest",
1076
+              "playdate:latest"
1077
+            ],
1078
+            "Size": 0,
1079
+            "VirtualSize": 2429728
1080
+          }
1081
+        ]
1082
+
1083
+The response shows a single image `Id` associated with two repositories
1084
+(`RepoTags`): `localhost:5000/test/busybox`: and `playdate`. A caller can use
1085
+either of the `RepoTags` values `localhost:5000/test/busybox:latest` or
1086
+`playdate:latest` to reference the image.
1087
+
1088
+You can also use `RepoDigests` values to reference an image. In this response,
1089
+the array has only one reference and that is to the
1090
+`localhost:5000/test/busybox` repository; the `playdate` repository has no
1091
+digest. You can reference this digest using the value:
1092
+`localhost:5000/test/busybox@sha256:cbbf2f9a99b47fc460d...`
1093
+
1094
+See the `docker run` and `docker build` commands for examples of digest and tag
1095
+references on the command line.
1057 1096
 
1058 1097
 Query Parameters:
1059 1098
 
... ...
@@ -192,6 +192,10 @@ Or
192 192
 
193 193
     FROM <image>:<tag>
194 194
 
195
+Or
196
+
197
+    FROM <image>@<digest>
198
+
195 199
 The `FROM` instruction sets the [*Base Image*](/terms/image/#base-image)
196 200
 for subsequent instructions. As such, a valid `Dockerfile` must have `FROM` as
197 201
 its first instruction. The image can be any valid image – it is especially easy
... ...
@@ -204,8 +208,9 @@ to start by **pulling an image** from the [*Public Repositories*](
204 204
 multiple images. Simply make a note of the last image ID output by the commit
205 205
 before each new `FROM` command.
206 206
 
207
-If no `tag` is given to the `FROM` instruction, `latest` is assumed. If the
208
-used tag does not exist, an error will be returned.
207
+The `tag` or `digest` values are optional. If you omit either of them, the builder
208
+assumes a `latest` by default. The builder returns an error if it cannot match
209
+the `tag` value.
209 210
 
210 211
 ## MAINTAINER
211 212
 
... ...
@@ -1112,7 +1112,9 @@ To see how the `docker:latest` image was built:
1112 1112
     List images
1113 1113
 
1114 1114
       -a, --all=false      Show all images (default hides intermediate images)
1115
+      --digests=false      Show digests
1115 1116
       -f, --filter=[]      Filter output based on conditions provided
1117
+      --help=false         Print usage
1116 1118
       --no-trunc=false     Don't truncate output
1117 1119
       -q, --quiet=false    Only show numeric IDs
1118 1120
 
... ...
@@ -1161,6 +1163,22 @@ uses up the `VIRTUAL SIZE` listed only once.
1161 1161
     tryout                        latest              2629d1fa0b81b222fca63371ca16cbf6a0772d07759ff80e8d1369b926940074   23 hours ago        131.5 MB
1162 1162
     <none>                        <none>              5ed6274db6ceb2397844896966ea239290555e74ef307030ebb01ff91b1914df   24 hours ago        1.089 GB
1163 1163
 
1164
+#### Listing image digests
1165
+
1166
+Images that use the v2 or later format have a content-addressable identifier
1167
+called a `digest`. As long as the input used to generate the image is
1168
+unchanged, the digest value is predictable. To list image digest values, use
1169
+the `--digests` flag:
1170
+
1171
+    $ sudo docker images --digests | head
1172
+    REPOSITORY                         TAG                 DIGEST                                                                    IMAGE ID            CREATED             VIRTUAL SIZE
1173
+    localhost:5000/test/busybox        <none>              sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf   4986bf8c1536        9 weeks ago         2.43 MB
1174
+
1175
+When pushing or pulling to a 2.0 registry, the `push` or `pull` command
1176
+output includes the image digest. You can `pull` using a digest value. You can
1177
+also reference by digest in `create`, `run`, and `rmi` commands, as well as the
1178
+`FROM` image reference in a Dockerfile.
1179
+
1164 1180
 #### Filtering
1165 1181
 
1166 1182
 The filtering flag (`-f` or `--filter`) format is of "key=value". If there is more
... ...
@@ -1563,6 +1581,10 @@ use `docker pull`:
1563 1563
     $ sudo docker pull debian:testing
1564 1564
     # will pull the image named debian:testing and any intermediate
1565 1565
     # layers it is based on.
1566
+    $ sudo docker pull debian@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf
1567
+    # will pull the image from the debian repository with the digest
1568
+    # sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf
1569
+    # and any intermediate layers it is based on.
1566 1570
     # (Typically the empty `scratch` image, a MAINTAINER layer,
1567 1571
     # and the un-tarred base).
1568 1572
     $ sudo docker pull --all-tags centos
... ...
@@ -1634,9 +1656,9 @@ deleted.
1634 1634
 
1635 1635
 #### Removing tagged images
1636 1636
 
1637
-Images can be removed either by their short or long IDs, or their image
1638
-names. If an image has more than one name, each of them needs to be
1639
-removed before the image is removed.
1637
+You can remove an image using its short or long ID, its tag, or its digest. If
1638
+an image has one or more tag or digest reference, you must remove all of them
1639
+before the image is removed.
1640 1640
 
1641 1641
     $ sudo docker images
1642 1642
     REPOSITORY                TAG                 IMAGE ID            CREATED             SIZE
... ...
@@ -1660,6 +1682,20 @@ removed before the image is removed.
1660 1660
     Untagged: test:latest
1661 1661
     Deleted: fd484f19954f4920da7ff372b5067f5b7ddb2fd3830cecd17b96ea9e286ba5b8
1662 1662
 
1663
+An image pulled by digest has no tag associated with it:
1664
+
1665
+    $ sudo docker images --digests
1666
+    REPOSITORY                     TAG       DIGEST                                                                    IMAGE ID        CREATED         VIRTUAL SIZE
1667
+    localhost:5000/test/busybox    <none>    sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf   4986bf8c1536    9 weeks ago     2.43 MB
1668
+
1669
+To remove an image using its digest:
1670
+
1671
+    $ sudo docker rmi localhost:5000/test/busybox@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf
1672
+    Untagged: localhost:5000/test/busybox@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf
1673
+    Deleted: 4986bf8c15363d1c5d15512d5266f8777bfba4974ac56e3270e7760f6f0a8125
1674
+    Deleted: ea13149945cb6b1e746bf28032f02e9b5a793523481a0a18645fc77ad53c4ea2
1675
+    Deleted: df7546f9f060a2268024c8a230d8639878585defcc1bc6f79d2728a13957871b
1676
+
1663 1677
 ## run
1664 1678
 
1665 1679
     Usage: docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
... ...
@@ -24,7 +24,7 @@ other `docker` command.
24 24
 
25 25
 The basic `docker run` command takes this form:
26 26
 
27
-    $ sudo docker run [OPTIONS] IMAGE[:TAG] [COMMAND] [ARG...]
27
+    $ sudo docker run [OPTIONS] IMAGE[:TAG|@DIGEST] [COMMAND] [ARG...]
28 28
 
29 29
 To learn how to interpret the types of `[OPTIONS]`,
30 30
 see [*Option types*](/reference/commandline/cli/#option-types).
... ...
@@ -140,6 +140,12 @@ While not strictly a means of identifying a container, you can specify a version
140 140
 image you'd like to run the container with by adding `image[:tag]` to the command. For
141 141
 example, `docker run ubuntu:14.04`.
142 142
 
143
+### Image[@digest]
144
+
145
+Images using the v2 or later image format have a content-addressable identifier
146
+called a digest. As long as the input used to generate the image is unchanged,
147
+the digest value is predictable and referenceable.
148
+
143 149
 ## PID Settings (--pid)
144 150
     --pid=""  : Set the PID (Process) Namespace mode for the container,
145 151
            'host': use the host's PID namespace inside the container
... ...
@@ -661,7 +667,7 @@ Dockerfile instruction and how the operator can override that setting.
661 661
 Recall the optional `COMMAND` in the Docker
662 662
 commandline:
663 663
 
664
-    $ sudo docker run [OPTIONS] IMAGE[:TAG] [COMMAND] [ARG...]
664
+    $ sudo docker run [OPTIONS] IMAGE[:TAG|@DIGEST] [COMMAND] [ARG...]
665 665
 
666 666
 This command is optional because the person who created the `IMAGE` may
667 667
 have already provided a default `COMMAND` using the Dockerfile `CMD`
... ...
@@ -5,6 +5,7 @@ import (
5 5
 
6 6
 	"github.com/docker/docker/engine"
7 7
 	"github.com/docker/docker/image"
8
+	"github.com/docker/docker/utils"
8 9
 )
9 10
 
10 11
 func (s *TagStore) CmdHistory(job *engine.Job) engine.Status {
... ...
@@ -24,7 +25,7 @@ func (s *TagStore) CmdHistory(job *engine.Job) engine.Status {
24 24
 			if _, exists := lookupMap[id]; !exists {
25 25
 				lookupMap[id] = []string{}
26 26
 			}
27
-			lookupMap[id] = append(lookupMap[id], name+":"+tag)
27
+			lookupMap[id] = append(lookupMap[id], utils.ImageReference(name, tag))
28 28
 		}
29 29
 	}
30 30
 
... ...
@@ -88,7 +88,7 @@ func (s *TagStore) CmdImport(job *engine.Job) engine.Status {
88 88
 	job.Stdout.Write(sf.FormatStatus("", img.ID))
89 89
 	logID := img.ID
90 90
 	if tag != "" {
91
-		logID += ":" + tag
91
+		logID = utils.ImageReference(logID, tag)
92 92
 	}
93 93
 	if err = job.Eng.Job("log", "import", logID, "").Run(); err != nil {
94 94
 		log.Errorf("Error logging event 'import' for %s: %s", logID, err)
... ...
@@ -1,7 +1,6 @@
1 1
 package graph
2 2
 
3 3
 import (
4
-	"fmt"
5 4
 	"log"
6 5
 	"path"
7 6
 	"strings"
... ...
@@ -9,6 +8,7 @@ import (
9 9
 	"github.com/docker/docker/engine"
10 10
 	"github.com/docker/docker/image"
11 11
 	"github.com/docker/docker/pkg/parsers/filters"
12
+	"github.com/docker/docker/utils"
12 13
 )
13 14
 
14 15
 var acceptedImageFilterTags = map[string]struct{}{
... ...
@@ -54,22 +54,27 @@ func (s *TagStore) CmdImages(job *engine.Job) engine.Status {
54 54
 	}
55 55
 	lookup := make(map[string]*engine.Env)
56 56
 	s.Lock()
57
-	for name, repository := range s.Repositories {
57
+	for repoName, repository := range s.Repositories {
58 58
 		if job.Getenv("filter") != "" {
59
-			if match, _ := path.Match(job.Getenv("filter"), name); !match {
59
+			if match, _ := path.Match(job.Getenv("filter"), repoName); !match {
60 60
 				continue
61 61
 			}
62 62
 		}
63
-		for tag, id := range repository {
63
+		for ref, id := range repository {
64
+			imgRef := utils.ImageReference(repoName, ref)
64 65
 			image, err := s.graph.Get(id)
65 66
 			if err != nil {
66
-				log.Printf("Warning: couldn't load %s from %s/%s: %s", id, name, tag, err)
67
+				log.Printf("Warning: couldn't load %s from %s: %s", id, imgRef, err)
67 68
 				continue
68 69
 			}
69 70
 
70 71
 			if out, exists := lookup[id]; exists {
71 72
 				if filt_tagged {
72
-					out.SetList("RepoTags", append(out.GetList("RepoTags"), fmt.Sprintf("%s:%s", name, tag)))
73
+					if utils.DigestReference(ref) {
74
+						out.SetList("RepoDigests", append(out.GetList("RepoDigests"), imgRef))
75
+					} else { // Tag Ref.
76
+						out.SetList("RepoTags", append(out.GetList("RepoTags"), imgRef))
77
+					}
73 78
 				}
74 79
 			} else {
75 80
 				// get the boolean list for if only the untagged images are requested
... ...
@@ -80,12 +85,20 @@ func (s *TagStore) CmdImages(job *engine.Job) engine.Status {
80 80
 				if filt_tagged {
81 81
 					out := &engine.Env{}
82 82
 					out.SetJson("ParentId", image.Parent)
83
-					out.SetList("RepoTags", []string{fmt.Sprintf("%s:%s", name, tag)})
84 83
 					out.SetJson("Id", image.ID)
85 84
 					out.SetInt64("Created", image.Created.Unix())
86 85
 					out.SetInt64("Size", image.Size)
87 86
 					out.SetInt64("VirtualSize", image.GetParentsSize(0)+image.Size)
88 87
 					out.SetJson("Labels", image.ContainerConfig.Labels)
88
+
89
+					if utils.DigestReference(ref) {
90
+						out.SetList("RepoTags", []string{})
91
+						out.SetList("RepoDigests", []string{imgRef})
92
+					} else {
93
+						out.SetList("RepoTags", []string{imgRef})
94
+						out.SetList("RepoDigests", []string{})
95
+					}
96
+
89 97
 					lookup[id] = out
90 98
 				}
91 99
 			}
... ...
@@ -108,6 +121,7 @@ func (s *TagStore) CmdImages(job *engine.Job) engine.Status {
108 108
 			out := &engine.Env{}
109 109
 			out.SetJson("ParentId", image.Parent)
110 110
 			out.SetList("RepoTags", []string{"<none>:<none>"})
111
+			out.SetList("RepoDigests", []string{"<none>@<none>"})
111 112
 			out.SetJson("Id", image.ID)
112 113
 			out.SetInt64("Created", image.Created.Unix())
113 114
 			out.SetInt64("Size", image.Size)
... ...
@@ -22,7 +22,7 @@ import (
22 22
 
23 23
 func (s *TagStore) CmdPull(job *engine.Job) engine.Status {
24 24
 	if n := len(job.Args); n != 1 && n != 2 {
25
-		return job.Errorf("Usage: %s IMAGE [TAG]", job.Name)
25
+		return job.Errorf("Usage: %s IMAGE [TAG|DIGEST]", job.Name)
26 26
 	}
27 27
 
28 28
 	var (
... ...
@@ -46,7 +46,7 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status {
46 46
 	job.GetenvJson("authConfig", authConfig)
47 47
 	job.GetenvJson("metaHeaders", &metaHeaders)
48 48
 
49
-	c, err := s.poolAdd("pull", repoInfo.LocalName+":"+tag)
49
+	c, err := s.poolAdd("pull", utils.ImageReference(repoInfo.LocalName, tag))
50 50
 	if err != nil {
51 51
 		if c != nil {
52 52
 			// Another pull of the same repository is already taking place; just wait for it to finish
... ...
@@ -56,7 +56,7 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status {
56 56
 		}
57 57
 		return job.Error(err)
58 58
 	}
59
-	defer s.poolRemove("pull", repoInfo.LocalName+":"+tag)
59
+	defer s.poolRemove("pull", utils.ImageReference(repoInfo.LocalName, tag))
60 60
 
61 61
 	log.Debugf("pulling image from host %q with remote name %q", repoInfo.Index.Name, repoInfo.RemoteName)
62 62
 	endpoint, err := repoInfo.GetEndpoint()
... ...
@@ -71,7 +71,7 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status {
71 71
 
72 72
 	logName := repoInfo.LocalName
73 73
 	if tag != "" {
74
-		logName += ":" + tag
74
+		logName = utils.ImageReference(logName, tag)
75 75
 	}
76 76
 
77 77
 	if len(repoInfo.Index.Mirrors) == 0 && ((repoInfo.Official && repoInfo.Index.Official) || endpoint.Version == registry.APIVersion2) {
... ...
@@ -113,7 +113,7 @@ func (s *TagStore) pullRepository(r *registry.Session, out io.Writer, repoInfo *
113 113
 	repoData, err := r.GetRepositoryData(repoInfo.RemoteName)
114 114
 	if err != nil {
115 115
 		if strings.Contains(err.Error(), "HTTP code: 404") {
116
-			return fmt.Errorf("Error: image %s:%s not found", repoInfo.RemoteName, askedTag)
116
+			return fmt.Errorf("Error: image %s not found", utils.ImageReference(repoInfo.RemoteName, askedTag))
117 117
 		}
118 118
 		// Unexpected HTTP error
119 119
 		return err
... ...
@@ -259,7 +259,7 @@ func (s *TagStore) pullRepository(r *registry.Session, out io.Writer, repoInfo *
259 259
 
260 260
 	requestedTag := repoInfo.CanonicalName
261 261
 	if len(askedTag) > 0 {
262
-		requestedTag = repoInfo.CanonicalName + ":" + askedTag
262
+		requestedTag = utils.ImageReference(repoInfo.CanonicalName, askedTag)
263 263
 	}
264 264
 	WriteStatus(requestedTag, out, sf, layers_downloaded)
265 265
 	return nil
... ...
@@ -421,7 +421,7 @@ func (s *TagStore) pullV2Repository(eng *engine.Engine, r *registry.Session, out
421 421
 
422 422
 	requestedTag := repoInfo.CanonicalName
423 423
 	if len(tag) > 0 {
424
-		requestedTag = repoInfo.CanonicalName + ":" + tag
424
+		requestedTag = utils.ImageReference(repoInfo.CanonicalName, tag)
425 425
 	}
426 426
 	WriteStatus(requestedTag, out, sf, layersDownloaded)
427 427
 	return nil
... ...
@@ -429,7 +429,7 @@ func (s *TagStore) pullV2Repository(eng *engine.Engine, r *registry.Session, out
429 429
 
430 430
 func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Writer, endpoint *registry.Endpoint, repoInfo *registry.RepositoryInfo, tag string, sf *utils.StreamFormatter, parallel bool, auth *registry.RequestAuthorization) (bool, error) {
431 431
 	log.Debugf("Pulling tag from V2 registry: %q", tag)
432
-	manifestBytes, err := r.GetV2ImageManifest(endpoint, repoInfo.RemoteName, tag, auth)
432
+	manifestBytes, digest, err := r.GetV2ImageManifest(endpoint, repoInfo.RemoteName, tag, auth)
433 433
 	if err != nil {
434 434
 		return false, err
435 435
 	}
... ...
@@ -444,7 +444,7 @@ func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Wri
444 444
 	}
445 445
 
446 446
 	if verified {
447
-		log.Printf("Image manifest for %s:%s has been verified", repoInfo.CanonicalName, tag)
447
+		log.Printf("Image manifest for %s has been verified", utils.ImageReference(repoInfo.CanonicalName, tag))
448 448
 	}
449 449
 	out.Write(sf.FormatStatus(tag, "Pulling from %s", repoInfo.CanonicalName))
450 450
 
... ...
@@ -601,11 +601,22 @@ func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Wri
601 601
 	}
602 602
 
603 603
 	if verified && tagUpdated {
604
-		out.Write(sf.FormatStatus(repoInfo.CanonicalName+":"+tag, "The image you are pulling has been verified. Important: image verification is a tech preview feature and should not be relied on to provide security."))
604
+		out.Write(sf.FormatStatus(utils.ImageReference(repoInfo.CanonicalName, tag), "The image you are pulling has been verified. Important: image verification is a tech preview feature and should not be relied on to provide security."))
605 605
 	}
606 606
 
607
-	if err = s.Set(repoInfo.LocalName, tag, downloads[0].img.ID, true); err != nil {
608
-		return false, err
607
+	if len(digest) > 0 {
608
+		out.Write(sf.FormatStatus("", "Digest: %s", digest))
609
+	}
610
+
611
+	if utils.DigestReference(tag) {
612
+		if err = s.SetDigest(repoInfo.LocalName, tag, downloads[0].img.ID); err != nil {
613
+			return false, err
614
+		}
615
+	} else {
616
+		// only set the repository/tag -> image ID mapping when pulling by tag (i.e. not by digest)
617
+		if err = s.Set(repoInfo.LocalName, tag, downloads[0].img.ID, true); err != nil {
618
+			return false, err
619
+		}
609 620
 	}
610 621
 
611 622
 	return tagUpdated, nil
... ...
@@ -36,8 +36,15 @@ func (s *TagStore) getImageList(localRepo map[string]string, requestedTag string
36 36
 
37 37
 	for tag, id := range localRepo {
38 38
 		if requestedTag != "" && requestedTag != tag {
39
+			// Include only the requested tag.
39 40
 			continue
40 41
 		}
42
+
43
+		if utils.DigestReference(tag) {
44
+			// Ignore digest references.
45
+			continue
46
+		}
47
+
41 48
 		var imageListForThisTag []string
42 49
 
43 50
 		tagsByImage[id] = append(tagsByImage[id], tag)
... ...
@@ -76,14 +83,16 @@ func (s *TagStore) getImageList(localRepo map[string]string, requestedTag string
76 76
 func (s *TagStore) getImageTags(localRepo map[string]string, askedTag string) ([]string, error) {
77 77
 	log.Debugf("Checking %s against %#v", askedTag, localRepo)
78 78
 	if len(askedTag) > 0 {
79
-		if _, ok := localRepo[askedTag]; !ok {
79
+		if _, ok := localRepo[askedTag]; !ok || utils.DigestReference(askedTag) {
80 80
 			return nil, fmt.Errorf("Tag does not exist: %s", askedTag)
81 81
 		}
82 82
 		return []string{askedTag}, nil
83 83
 	}
84 84
 	var tags []string
85 85
 	for tag := range localRepo {
86
-		tags = append(tags, tag)
86
+		if !utils.DigestReference(tag) {
87
+			tags = append(tags, tag)
88
+		}
87 89
 	}
88 90
 	return tags, nil
89 91
 }
... ...
@@ -422,9 +431,14 @@ func (s *TagStore) pushV2Repository(r *registry.Session, localRepo Repository, o
422 422
 		log.Infof("Signed manifest for %s:%s using daemon's key: %s", repoInfo.LocalName, tag, s.trustKey.KeyID())
423 423
 
424 424
 		// push the manifest
425
-		if err := r.PutV2ImageManifest(endpoint, repoInfo.RemoteName, tag, bytes.NewReader(signedBody), auth); err != nil {
425
+		digest, err := r.PutV2ImageManifest(endpoint, repoInfo.RemoteName, tag, bytes.NewReader(signedBody), auth)
426
+		if err != nil {
426 427
 			return err
427 428
 		}
429
+
430
+		if len(digest) > 0 {
431
+			out.Write(sf.FormatStatus("", "Digest: %s", digest))
432
+		}
428 433
 	}
429 434
 	return nil
430 435
 }
... ...
@@ -2,6 +2,7 @@ package graph
2 2
 
3 3
 import (
4 4
 	"encoding/json"
5
+	"errors"
5 6
 	"fmt"
6 7
 	"io/ioutil"
7 8
 	"os"
... ...
@@ -15,13 +16,16 @@ import (
15 15
 	"github.com/docker/docker/pkg/common"
16 16
 	"github.com/docker/docker/pkg/parsers"
17 17
 	"github.com/docker/docker/registry"
18
+	"github.com/docker/docker/utils"
18 19
 	"github.com/docker/libtrust"
19 20
 )
20 21
 
21 22
 const DEFAULTTAG = "latest"
22 23
 
23 24
 var (
25
+	//FIXME these 2 regexes also exist in registry/v2/regexp.go
24 26
 	validTagName = regexp.MustCompile(`^[\w][\w.-]{0,127}$`)
27
+	validDigest  = regexp.MustCompile(`[a-zA-Z0-9-_+.]+:[a-fA-F0-9]+`)
25 28
 )
26 29
 
27 30
 type TagStore struct {
... ...
@@ -107,20 +111,31 @@ func (store *TagStore) reload() error {
107 107
 func (store *TagStore) LookupImage(name string) (*image.Image, error) {
108 108
 	// FIXME: standardize on returning nil when the image doesn't exist, and err for everything else
109 109
 	// (so we can pass all errors here)
110
-	repos, tag := parsers.ParseRepositoryTag(name)
111
-	if tag == "" {
112
-		tag = DEFAULTTAG
110
+	repoName, ref := parsers.ParseRepositoryTag(name)
111
+	if ref == "" {
112
+		ref = DEFAULTTAG
113
+	}
114
+	var (
115
+		err error
116
+		img *image.Image
117
+	)
118
+
119
+	img, err = store.GetImage(repoName, ref)
120
+	if err != nil {
121
+		return nil, err
122
+	}
123
+
124
+	if img != nil {
125
+		return img, err
113 126
 	}
114
-	img, err := store.GetImage(repos, tag)
127
+
128
+	// name must be an image ID.
115 129
 	store.Lock()
116 130
 	defer store.Unlock()
117
-	if err != nil {
131
+	if img, err = store.graph.Get(name); err != nil {
118 132
 		return nil, err
119
-	} else if img == nil {
120
-		if img, err = store.graph.Get(name); err != nil {
121
-			return nil, err
122
-		}
123 133
 	}
134
+
124 135
 	return img, nil
125 136
 }
126 137
 
... ...
@@ -132,7 +147,7 @@ func (store *TagStore) ByID() map[string][]string {
132 132
 	byID := make(map[string][]string)
133 133
 	for repoName, repository := range store.Repositories {
134 134
 		for tag, id := range repository {
135
-			name := repoName + ":" + tag
135
+			name := utils.ImageReference(repoName, tag)
136 136
 			if _, exists := byID[id]; !exists {
137 137
 				byID[id] = []string{name}
138 138
 			} else {
... ...
@@ -171,32 +186,35 @@ func (store *TagStore) DeleteAll(id string) error {
171 171
 	return nil
172 172
 }
173 173
 
174
-func (store *TagStore) Delete(repoName, tag string) (bool, error) {
174
+func (store *TagStore) Delete(repoName, ref string) (bool, error) {
175 175
 	store.Lock()
176 176
 	defer store.Unlock()
177 177
 	deleted := false
178 178
 	if err := store.reload(); err != nil {
179 179
 		return false, err
180 180
 	}
181
+
181 182
 	repoName = registry.NormalizeLocalName(repoName)
182
-	if r, exists := store.Repositories[repoName]; exists {
183
-		if tag != "" {
184
-			if _, exists2 := r[tag]; exists2 {
185
-				delete(r, tag)
186
-				if len(r) == 0 {
187
-					delete(store.Repositories, repoName)
188
-				}
189
-				deleted = true
190
-			} else {
191
-				return false, fmt.Errorf("No such tag: %s:%s", repoName, tag)
192
-			}
193
-		} else {
183
+
184
+	if ref == "" {
185
+		// Delete the whole repository.
186
+		delete(store.Repositories, repoName)
187
+		return true, store.save()
188
+	}
189
+
190
+	repoRefs, exists := store.Repositories[repoName]
191
+	if !exists {
192
+		return false, fmt.Errorf("No such repository: %s", repoName)
193
+	}
194
+
195
+	if _, exists := repoRefs[ref]; exists {
196
+		delete(repoRefs, ref)
197
+		if len(repoRefs) == 0 {
194 198
 			delete(store.Repositories, repoName)
195
-			deleted = true
196 199
 		}
197
-	} else {
198
-		return false, fmt.Errorf("No such repository: %s", repoName)
200
+		deleted = true
199 201
 	}
202
+
200 203
 	return deleted, store.save()
201 204
 }
202 205
 
... ...
@@ -234,6 +252,40 @@ func (store *TagStore) Set(repoName, tag, imageName string, force bool) error {
234 234
 	return store.save()
235 235
 }
236 236
 
237
+// SetDigest creates a digest reference to an image ID.
238
+func (store *TagStore) SetDigest(repoName, digest, imageName string) error {
239
+	img, err := store.LookupImage(imageName)
240
+	if err != nil {
241
+		return err
242
+	}
243
+
244
+	if err := validateRepoName(repoName); err != nil {
245
+		return err
246
+	}
247
+
248
+	if err := validateDigest(digest); err != nil {
249
+		return err
250
+	}
251
+
252
+	store.Lock()
253
+	defer store.Unlock()
254
+	if err := store.reload(); err != nil {
255
+		return err
256
+	}
257
+
258
+	repoName = registry.NormalizeLocalName(repoName)
259
+	repoRefs, exists := store.Repositories[repoName]
260
+	if !exists {
261
+		repoRefs = Repository{}
262
+		store.Repositories[repoName] = repoRefs
263
+	} else if oldID, exists := repoRefs[digest]; exists && oldID != img.ID {
264
+		return fmt.Errorf("Conflict: Digest %s is already set to image %s", digest, oldID)
265
+	}
266
+
267
+	repoRefs[digest] = img.ID
268
+	return store.save()
269
+}
270
+
237 271
 func (store *TagStore) Get(repoName string) (Repository, error) {
238 272
 	store.Lock()
239 273
 	defer store.Unlock()
... ...
@@ -247,24 +299,29 @@ func (store *TagStore) Get(repoName string) (Repository, error) {
247 247
 	return nil, nil
248 248
 }
249 249
 
250
-func (store *TagStore) GetImage(repoName, tagOrID string) (*image.Image, error) {
250
+func (store *TagStore) GetImage(repoName, refOrID string) (*image.Image, error) {
251 251
 	repo, err := store.Get(repoName)
252
-	store.Lock()
253
-	defer store.Unlock()
252
+
254 253
 	if err != nil {
255 254
 		return nil, err
256
-	} else if repo == nil {
255
+	}
256
+	if repo == nil {
257 257
 		return nil, nil
258 258
 	}
259
-	if revision, exists := repo[tagOrID]; exists {
260
-		return store.graph.Get(revision)
259
+
260
+	store.Lock()
261
+	defer store.Unlock()
262
+	if imgID, exists := repo[refOrID]; exists {
263
+		return store.graph.Get(imgID)
261 264
 	}
265
+
262 266
 	// If no matching tag is found, search through images for a matching image id
263 267
 	for _, revision := range repo {
264
-		if strings.HasPrefix(revision, tagOrID) {
268
+		if strings.HasPrefix(revision, refOrID) {
265 269
 			return store.graph.Get(revision)
266 270
 		}
267 271
 	}
272
+
268 273
 	return nil, nil
269 274
 }
270 275
 
... ...
@@ -275,7 +332,7 @@ func (store *TagStore) GetRepoRefs() map[string][]string {
275 275
 	for name, repository := range store.Repositories {
276 276
 		for tag, id := range repository {
277 277
 			shortID := common.TruncateID(id)
278
-			reporefs[shortID] = append(reporefs[shortID], fmt.Sprintf("%s:%s", name, tag))
278
+			reporefs[shortID] = append(reporefs[shortID], utils.ImageReference(name, tag))
279 279
 		}
280 280
 	}
281 281
 	store.Unlock()
... ...
@@ -293,10 +350,10 @@ func validateRepoName(name string) error {
293 293
 	return nil
294 294
 }
295 295
 
296
-// Validate the name of a tag
296
+// ValidateTagName validates the name of a tag
297 297
 func ValidateTagName(name string) error {
298 298
 	if name == "" {
299
-		return fmt.Errorf("Tag name can't be empty")
299
+		return fmt.Errorf("tag name can't be empty")
300 300
 	}
301 301
 	if !validTagName.MatchString(name) {
302 302
 		return fmt.Errorf("Illegal tag name (%s): only [A-Za-z0-9_.-] are allowed, minimum 1, maximum 128 in length", name)
... ...
@@ -304,6 +361,16 @@ func ValidateTagName(name string) error {
304 304
 	return nil
305 305
 }
306 306
 
307
+func validateDigest(dgst string) error {
308
+	if dgst == "" {
309
+		return errors.New("digest can't be empty")
310
+	}
311
+	if !validDigest.MatchString(dgst) {
312
+		return fmt.Errorf("illegal digest (%s): must be of the form [a-zA-Z0-9-_+.]+:[a-fA-F0-9]+", dgst)
313
+	}
314
+	return nil
315
+}
316
+
307 317
 func (store *TagStore) poolAdd(kind, key string) (chan struct{}, error) {
308 318
 	store.Lock()
309 319
 	defer store.Unlock()
... ...
@@ -21,6 +21,8 @@ const (
21 21
 	testPrivateImageName     = "127.0.0.1:8000/privateapp"
22 22
 	testPrivateImageID       = "5bc255f8699e4ee89ac4469266c3d11515da88fdcbde45d7b069b636ff4efd81"
23 23
 	testPrivateImageIDShort  = "5bc255f8699e"
24
+	testPrivateImageDigest   = "sha256:bc8813ea7b3603864987522f02a76101c17ad122e1c46d790efc0fca78ca7bfb"
25
+	testPrivateImageTag      = "sometag"
24 26
 )
25 27
 
26 28
 func fakeTar() (io.Reader, error) {
... ...
@@ -83,6 +85,9 @@ func mkTestTagStore(root string, t *testing.T) *TagStore {
83 83
 	if err := store.Set(testPrivateImageName, "", testPrivateImageID, false); err != nil {
84 84
 		t.Fatal(err)
85 85
 	}
86
+	if err := store.SetDigest(testPrivateImageName, testPrivateImageDigest, testPrivateImageID); err != nil {
87
+		t.Fatal(err)
88
+	}
86 89
 	return store
87 90
 }
88 91
 
... ...
@@ -128,6 +133,10 @@ func TestLookupImage(t *testing.T) {
128 128
 		"fail:fail",
129 129
 	}
130 130
 
131
+	digestLookups := []string{
132
+		testPrivateImageName + "@" + testPrivateImageDigest,
133
+	}
134
+
131 135
 	for _, name := range officialLookups {
132 136
 		if img, err := store.LookupImage(name); err != nil {
133 137
 			t.Errorf("Error looking up %s: %s", name, err)
... ...
@@ -155,6 +164,16 @@ func TestLookupImage(t *testing.T) {
155 155
 			t.Errorf("Expected 0 image, 1 found: %s", name)
156 156
 		}
157 157
 	}
158
+
159
+	for _, name := range digestLookups {
160
+		if img, err := store.LookupImage(name); err != nil {
161
+			t.Errorf("Error looking up %s: %s", name, err)
162
+		} else if img == nil {
163
+			t.Errorf("Expected 1 image, none found: %s", name)
164
+		} else if img.ID != testPrivateImageID {
165
+			t.Errorf("Expected ID '%s' found '%s'", testPrivateImageID, img.ID)
166
+		}
167
+	}
158 168
 }
159 169
 
160 170
 func TestValidTagName(t *testing.T) {
... ...
@@ -174,3 +193,24 @@ func TestInvalidTagName(t *testing.T) {
174 174
 		}
175 175
 	}
176 176
 }
177
+
178
+func TestValidateDigest(t *testing.T) {
179
+	tests := []struct {
180
+		input       string
181
+		expectError bool
182
+	}{
183
+		{"", true},
184
+		{"latest", true},
185
+		{"a:b", false},
186
+		{"aZ0124-.+:bY852-_.+=", false},
187
+		{"#$%#$^:$%^#$%", true},
188
+	}
189
+
190
+	for i, test := range tests {
191
+		err := validateDigest(test.input)
192
+		gotError := err != nil
193
+		if e, a := test.expectError, gotError; e != a {
194
+			t.Errorf("%d: with input %s, expected error=%t, got %t: %s", i, test.input, test.expectError, gotError, err)
195
+		}
196
+	}
197
+}
177 198
new file mode 100644
... ...
@@ -0,0 +1,535 @@
0
+package main
1
+
2
+import (
3
+	"fmt"
4
+	"os/exec"
5
+	"regexp"
6
+	"strings"
7
+	"testing"
8
+
9
+	"github.com/docker/docker/utils"
10
+)
11
+
12
+var (
13
+	repoName    = fmt.Sprintf("%v/dockercli/busybox-by-dgst", privateRegistryURL)
14
+	digestRegex = regexp.MustCompile("Digest: ([^\n]+)")
15
+)
16
+
17
+func setupImage() (string, error) {
18
+	return setupImageWithTag("latest")
19
+}
20
+
21
+func setupImageWithTag(tag string) (string, error) {
22
+	containerName := "busyboxbydigest"
23
+
24
+	c := exec.Command(dockerBinary, "run", "-d", "-e", "digest=1", "--name", containerName, "busybox")
25
+	if _, err := runCommand(c); err != nil {
26
+		return "", err
27
+	}
28
+
29
+	// tag the image to upload it to the private registry
30
+	repoAndTag := utils.ImageReference(repoName, tag)
31
+	c = exec.Command(dockerBinary, "commit", containerName, repoAndTag)
32
+	if out, _, err := runCommandWithOutput(c); err != nil {
33
+		return "", fmt.Errorf("image tagging failed: %s, %v", out, err)
34
+	}
35
+	defer deleteImages(repoAndTag)
36
+
37
+	// delete the container as we don't need it any more
38
+	if err := deleteContainer(containerName); err != nil {
39
+		return "", err
40
+	}
41
+
42
+	// push the image
43
+	c = exec.Command(dockerBinary, "push", repoAndTag)
44
+	out, _, err := runCommandWithOutput(c)
45
+	if err != nil {
46
+		return "", fmt.Errorf("pushing the image to the private registry has failed: %s, %v", out, err)
47
+	}
48
+
49
+	// delete our local repo that we previously tagged
50
+	c = exec.Command(dockerBinary, "rmi", repoAndTag)
51
+	if out, _, err := runCommandWithOutput(c); err != nil {
52
+		return "", fmt.Errorf("error deleting images prior to real test: %s, %v", out, err)
53
+	}
54
+
55
+	// the push output includes "Digest: <digest>", so find that
56
+	matches := digestRegex.FindStringSubmatch(out)
57
+	if len(matches) != 2 {
58
+		return "", fmt.Errorf("unable to parse digest from push output: %s", out)
59
+	}
60
+	pushDigest := matches[1]
61
+
62
+	return pushDigest, nil
63
+}
64
+
65
+func TestPullByTagDisplaysDigest(t *testing.T) {
66
+	defer setupRegistry(t)()
67
+
68
+	pushDigest, err := setupImage()
69
+	if err != nil {
70
+		t.Fatalf("error setting up image: %v", err)
71
+	}
72
+
73
+	// pull from the registry using the tag
74
+	c := exec.Command(dockerBinary, "pull", repoName)
75
+	out, _, err := runCommandWithOutput(c)
76
+	if err != nil {
77
+		t.Fatalf("error pulling by tag: %s, %v", out, err)
78
+	}
79
+	defer deleteImages(repoName)
80
+
81
+	// the pull output includes "Digest: <digest>", so find that
82
+	matches := digestRegex.FindStringSubmatch(out)
83
+	if len(matches) != 2 {
84
+		t.Fatalf("unable to parse digest from pull output: %s", out)
85
+	}
86
+	pullDigest := matches[1]
87
+
88
+	// make sure the pushed and pull digests match
89
+	if pushDigest != pullDigest {
90
+		t.Fatalf("push digest %q didn't match pull digest %q", pushDigest, pullDigest)
91
+	}
92
+
93
+	logDone("by_digest - pull by tag displays digest")
94
+}
95
+
96
+func TestPullByDigest(t *testing.T) {
97
+	defer setupRegistry(t)()
98
+
99
+	pushDigest, err := setupImage()
100
+	if err != nil {
101
+		t.Fatalf("error setting up image: %v", err)
102
+	}
103
+
104
+	// pull from the registry using the <name>@<digest> reference
105
+	imageReference := fmt.Sprintf("%s@%s", repoName, pushDigest)
106
+	c := exec.Command(dockerBinary, "pull", imageReference)
107
+	out, _, err := runCommandWithOutput(c)
108
+	if err != nil {
109
+		t.Fatalf("error pulling by digest: %s, %v", out, err)
110
+	}
111
+	defer deleteImages(imageReference)
112
+
113
+	// the pull output includes "Digest: <digest>", so find that
114
+	matches := digestRegex.FindStringSubmatch(out)
115
+	if len(matches) != 2 {
116
+		t.Fatalf("unable to parse digest from pull output: %s", out)
117
+	}
118
+	pullDigest := matches[1]
119
+
120
+	// make sure the pushed and pull digests match
121
+	if pushDigest != pullDigest {
122
+		t.Fatalf("push digest %q didn't match pull digest %q", pushDigest, pullDigest)
123
+	}
124
+
125
+	logDone("by_digest - pull by digest")
126
+}
127
+
128
+func TestCreateByDigest(t *testing.T) {
129
+	defer setupRegistry(t)()
130
+
131
+	pushDigest, err := setupImage()
132
+	if err != nil {
133
+		t.Fatalf("error setting up image: %v", err)
134
+	}
135
+
136
+	imageReference := fmt.Sprintf("%s@%s", repoName, pushDigest)
137
+
138
+	containerName := "createByDigest"
139
+	c := exec.Command(dockerBinary, "create", "--name", containerName, imageReference)
140
+	out, _, err := runCommandWithOutput(c)
141
+	if err != nil {
142
+		t.Fatalf("error creating by digest: %s, %v", out, err)
143
+	}
144
+	defer deleteContainer(containerName)
145
+
146
+	res, err := inspectField(containerName, "Config.Image")
147
+	if err != nil {
148
+		t.Fatalf("failed to get Config.Image: %s, %v", out, err)
149
+	}
150
+	if res != imageReference {
151
+		t.Fatalf("unexpected Config.Image: %s (expected %s)", res, imageReference)
152
+	}
153
+
154
+	logDone("by_digest - create by digest")
155
+}
156
+
157
+func TestRunByDigest(t *testing.T) {
158
+	defer setupRegistry(t)()
159
+
160
+	pushDigest, err := setupImage()
161
+	if err != nil {
162
+		t.Fatalf("error setting up image: %v", err)
163
+	}
164
+
165
+	imageReference := fmt.Sprintf("%s@%s", repoName, pushDigest)
166
+
167
+	containerName := "runByDigest"
168
+	c := exec.Command(dockerBinary, "run", "--name", containerName, imageReference, "sh", "-c", "echo found=$digest")
169
+	out, _, err := runCommandWithOutput(c)
170
+	if err != nil {
171
+		t.Fatalf("error run by digest: %s, %v", out, err)
172
+	}
173
+	defer deleteContainer(containerName)
174
+
175
+	foundRegex := regexp.MustCompile("found=([^\n]+)")
176
+	matches := foundRegex.FindStringSubmatch(out)
177
+	if len(matches) != 2 {
178
+		t.Fatalf("error locating expected 'found=1' output: %s", out)
179
+	}
180
+	if matches[1] != "1" {
181
+		t.Fatalf("Expected %q, got %q", "1", matches[1])
182
+	}
183
+
184
+	res, err := inspectField(containerName, "Config.Image")
185
+	if err != nil {
186
+		t.Fatalf("failed to get Config.Image: %s, %v", out, err)
187
+	}
188
+	if res != imageReference {
189
+		t.Fatalf("unexpected Config.Image: %s (expected %s)", res, imageReference)
190
+	}
191
+
192
+	logDone("by_digest - run by digest")
193
+}
194
+
195
+func TestRemoveImageByDigest(t *testing.T) {
196
+	defer setupRegistry(t)()
197
+
198
+	digest, err := setupImage()
199
+	if err != nil {
200
+		t.Fatalf("error setting up image: %v", err)
201
+	}
202
+
203
+	imageReference := fmt.Sprintf("%s@%s", repoName, digest)
204
+
205
+	// pull from the registry using the <name>@<digest> reference
206
+	c := exec.Command(dockerBinary, "pull", imageReference)
207
+	out, _, err := runCommandWithOutput(c)
208
+	if err != nil {
209
+		t.Fatalf("error pulling by digest: %s, %v", out, err)
210
+	}
211
+
212
+	// make sure inspect runs ok
213
+	if _, err := inspectField(imageReference, "Id"); err != nil {
214
+		t.Fatalf("failed to inspect image: %v", err)
215
+	}
216
+
217
+	// do the delete
218
+	if err := deleteImages(imageReference); err != nil {
219
+		t.Fatalf("unexpected error deleting image: %v", err)
220
+	}
221
+
222
+	// try to inspect again - it should error this time
223
+	if _, err := inspectField(imageReference, "Id"); err == nil {
224
+		t.Fatalf("unexpected nil err trying to inspect what should be a non-existent image")
225
+	} else if !strings.Contains(err.Error(), "No such image") {
226
+		t.Fatalf("expected 'No such image' output, got %v", err)
227
+	}
228
+
229
+	logDone("by_digest - remove image by digest")
230
+}
231
+
232
+func TestBuildByDigest(t *testing.T) {
233
+	defer setupRegistry(t)()
234
+
235
+	digest, err := setupImage()
236
+	if err != nil {
237
+		t.Fatalf("error setting up image: %v", err)
238
+	}
239
+
240
+	imageReference := fmt.Sprintf("%s@%s", repoName, digest)
241
+
242
+	// pull from the registry using the <name>@<digest> reference
243
+	c := exec.Command(dockerBinary, "pull", imageReference)
244
+	out, _, err := runCommandWithOutput(c)
245
+	if err != nil {
246
+		t.Fatalf("error pulling by digest: %s, %v", out, err)
247
+	}
248
+
249
+	// get the image id
250
+	imageID, err := inspectField(imageReference, "Id")
251
+	if err != nil {
252
+		t.Fatalf("error getting image id: %v", err)
253
+	}
254
+
255
+	// do the build
256
+	name := "buildbydigest"
257
+	defer deleteImages(name)
258
+	_, err = buildImage(name, fmt.Sprintf(
259
+		`FROM %s
260
+     CMD ["/bin/echo", "Hello World"]`, imageReference),
261
+		true)
262
+	if err != nil {
263
+		t.Fatal(err)
264
+	}
265
+
266
+	// get the build's image id
267
+	res, err := inspectField(name, "Config.Image")
268
+	if err != nil {
269
+		t.Fatal(err)
270
+	}
271
+	// make sure they match
272
+	if res != imageID {
273
+		t.Fatalf("Image %s, expected %s", res, imageID)
274
+	}
275
+
276
+	logDone("by_digest - build by digest")
277
+}
278
+
279
+func TestTagByDigest(t *testing.T) {
280
+	defer setupRegistry(t)()
281
+
282
+	digest, err := setupImage()
283
+	if err != nil {
284
+		t.Fatalf("error setting up image: %v", err)
285
+	}
286
+
287
+	imageReference := fmt.Sprintf("%s@%s", repoName, digest)
288
+
289
+	// pull from the registry using the <name>@<digest> reference
290
+	c := exec.Command(dockerBinary, "pull", imageReference)
291
+	out, _, err := runCommandWithOutput(c)
292
+	if err != nil {
293
+		t.Fatalf("error pulling by digest: %s, %v", out, err)
294
+	}
295
+
296
+	// tag it
297
+	tag := "tagbydigest"
298
+	c = exec.Command(dockerBinary, "tag", imageReference, tag)
299
+	if _, err := runCommand(c); err != nil {
300
+		t.Fatalf("unexpected error tagging: %v", err)
301
+	}
302
+
303
+	expectedID, err := inspectField(imageReference, "Id")
304
+	if err != nil {
305
+		t.Fatalf("error getting original image id: %v", err)
306
+	}
307
+
308
+	tagID, err := inspectField(tag, "Id")
309
+	if err != nil {
310
+		t.Fatalf("error getting tagged image id: %v", err)
311
+	}
312
+
313
+	if tagID != expectedID {
314
+		t.Fatalf("expected image id %q, got %q", expectedID, tagID)
315
+	}
316
+
317
+	logDone("by_digest - tag by digest")
318
+}
319
+
320
+func TestListImagesWithoutDigests(t *testing.T) {
321
+	defer setupRegistry(t)()
322
+
323
+	digest, err := setupImage()
324
+	if err != nil {
325
+		t.Fatalf("error setting up image: %v", err)
326
+	}
327
+
328
+	imageReference := fmt.Sprintf("%s@%s", repoName, digest)
329
+
330
+	// pull from the registry using the <name>@<digest> reference
331
+	c := exec.Command(dockerBinary, "pull", imageReference)
332
+	out, _, err := runCommandWithOutput(c)
333
+	if err != nil {
334
+		t.Fatalf("error pulling by digest: %s, %v", out, err)
335
+	}
336
+
337
+	c = exec.Command(dockerBinary, "images")
338
+	out, _, err = runCommandWithOutput(c)
339
+	if err != nil {
340
+		t.Fatalf("error listing images: %s, %v", out, err)
341
+	}
342
+
343
+	if strings.Contains(out, "DIGEST") {
344
+		t.Fatalf("list output should not have contained DIGEST header: %s", out)
345
+	}
346
+
347
+	logDone("by_digest - list images - digest header not displayed by default")
348
+}
349
+
350
+func TestListImagesWithDigests(t *testing.T) {
351
+	defer setupRegistry(t)()
352
+	defer deleteImages(repoName+":tag1", repoName+":tag2")
353
+
354
+	// setup image1
355
+	digest1, err := setupImageWithTag("tag1")
356
+	if err != nil {
357
+		t.Fatalf("error setting up image: %v", err)
358
+	}
359
+	imageReference1 := fmt.Sprintf("%s@%s", repoName, digest1)
360
+	defer deleteImages(imageReference1)
361
+	t.Logf("imageReference1 = %s", imageReference1)
362
+
363
+	// pull image1 by digest
364
+	c := exec.Command(dockerBinary, "pull", imageReference1)
365
+	out, _, err := runCommandWithOutput(c)
366
+	if err != nil {
367
+		t.Fatalf("error pulling by digest: %s, %v", out, err)
368
+	}
369
+
370
+	// list images
371
+	c = exec.Command(dockerBinary, "images", "--digests")
372
+	out, _, err = runCommandWithOutput(c)
373
+	if err != nil {
374
+		t.Fatalf("error listing images: %s, %v", out, err)
375
+	}
376
+
377
+	// make sure repo shown, tag=<none>, digest = $digest1
378
+	re1 := regexp.MustCompile(`\s*` + repoName + `\s*<none>\s*` + digest1 + `\s`)
379
+	if !re1.MatchString(out) {
380
+		t.Fatalf("expected %q: %s", re1.String(), out)
381
+	}
382
+
383
+	// setup image2
384
+	digest2, err := setupImageWithTag("tag2")
385
+	if err != nil {
386
+		t.Fatalf("error setting up image: %v", err)
387
+	}
388
+	imageReference2 := fmt.Sprintf("%s@%s", repoName, digest2)
389
+	defer deleteImages(imageReference2)
390
+	t.Logf("imageReference2 = %s", imageReference2)
391
+
392
+	// pull image1 by digest
393
+	c = exec.Command(dockerBinary, "pull", imageReference1)
394
+	out, _, err = runCommandWithOutput(c)
395
+	if err != nil {
396
+		t.Fatalf("error pulling by digest: %s, %v", out, err)
397
+	}
398
+
399
+	// pull image2 by digest
400
+	c = exec.Command(dockerBinary, "pull", imageReference2)
401
+	out, _, err = runCommandWithOutput(c)
402
+	if err != nil {
403
+		t.Fatalf("error pulling by digest: %s, %v", out, err)
404
+	}
405
+
406
+	// list images
407
+	c = exec.Command(dockerBinary, "images", "--digests")
408
+	out, _, err = runCommandWithOutput(c)
409
+	if err != nil {
410
+		t.Fatalf("error listing images: %s, %v", out, err)
411
+	}
412
+
413
+	// make sure repo shown, tag=<none>, digest = $digest1
414
+	if !re1.MatchString(out) {
415
+		t.Fatalf("expected %q: %s", re1.String(), out)
416
+	}
417
+
418
+	// make sure repo shown, tag=<none>, digest = $digest2
419
+	re2 := regexp.MustCompile(`\s*` + repoName + `\s*<none>\s*` + digest2 + `\s`)
420
+	if !re2.MatchString(out) {
421
+		t.Fatalf("expected %q: %s", re2.String(), out)
422
+	}
423
+
424
+	// pull tag1
425
+	c = exec.Command(dockerBinary, "pull", repoName+":tag1")
426
+	out, _, err = runCommandWithOutput(c)
427
+	if err != nil {
428
+		t.Fatalf("error pulling tag1: %s, %v", out, err)
429
+	}
430
+
431
+	// list images
432
+	c = exec.Command(dockerBinary, "images", "--digests")
433
+	out, _, err = runCommandWithOutput(c)
434
+	if err != nil {
435
+		t.Fatalf("error listing images: %s, %v", out, err)
436
+	}
437
+
438
+	// make sure image 1 has repo, tag, <none> AND repo, <none>, digest
439
+	reWithTag1 := regexp.MustCompile(`\s*` + repoName + `\s*tag1\s*<none>\s`)
440
+	reWithDigest1 := regexp.MustCompile(`\s*` + repoName + `\s*<none>\s*` + digest1 + `\s`)
441
+	if !reWithTag1.MatchString(out) {
442
+		t.Fatalf("expected %q: %s", reWithTag1.String(), out)
443
+	}
444
+	if !reWithDigest1.MatchString(out) {
445
+		t.Fatalf("expected %q: %s", reWithDigest1.String(), out)
446
+	}
447
+	// make sure image 2 has repo, <none>, digest
448
+	if !re2.MatchString(out) {
449
+		t.Fatalf("expected %q: %s", re2.String(), out)
450
+	}
451
+
452
+	// pull tag 2
453
+	c = exec.Command(dockerBinary, "pull", repoName+":tag2")
454
+	out, _, err = runCommandWithOutput(c)
455
+	if err != nil {
456
+		t.Fatalf("error pulling tag2: %s, %v", out, err)
457
+	}
458
+
459
+	// list images
460
+	c = exec.Command(dockerBinary, "images", "--digests")
461
+	out, _, err = runCommandWithOutput(c)
462
+	if err != nil {
463
+		t.Fatalf("error listing images: %s, %v", out, err)
464
+	}
465
+
466
+	// make sure image 1 has repo, tag, digest
467
+	if !reWithTag1.MatchString(out) {
468
+		t.Fatalf("expected %q: %s", re1.String(), out)
469
+	}
470
+
471
+	// make sure image 2 has repo, tag, digest
472
+	reWithTag2 := regexp.MustCompile(`\s*` + repoName + `\s*tag2\s*<none>\s`)
473
+	reWithDigest2 := regexp.MustCompile(`\s*` + repoName + `\s*<none>\s*` + digest2 + `\s`)
474
+	if !reWithTag2.MatchString(out) {
475
+		t.Fatalf("expected %q: %s", reWithTag2.String(), out)
476
+	}
477
+	if !reWithDigest2.MatchString(out) {
478
+		t.Fatalf("expected %q: %s", reWithDigest2.String(), out)
479
+	}
480
+
481
+	// list images
482
+	c = exec.Command(dockerBinary, "images", "--digests")
483
+	out, _, err = runCommandWithOutput(c)
484
+	if err != nil {
485
+		t.Fatalf("error listing images: %s, %v", out, err)
486
+	}
487
+
488
+	// make sure image 1 has repo, tag, digest
489
+	if !reWithTag1.MatchString(out) {
490
+		t.Fatalf("expected %q: %s", re1.String(), out)
491
+	}
492
+	// make sure image 2 has repo, tag, digest
493
+	if !reWithTag2.MatchString(out) {
494
+		t.Fatalf("expected %q: %s", re2.String(), out)
495
+	}
496
+	// make sure busybox has tag, but not digest
497
+	busyboxRe := regexp.MustCompile(`\s*busybox\s*latest\s*<none>\s`)
498
+	if !busyboxRe.MatchString(out) {
499
+		t.Fatalf("expected %q: %s", busyboxRe.String(), out)
500
+	}
501
+
502
+	logDone("by_digest - list images with digests")
503
+}
504
+
505
+func TestDeleteImageByIDOnlyPulledByDigest(t *testing.T) {
506
+	defer setupRegistry(t)()
507
+
508
+	pushDigest, err := setupImage()
509
+	if err != nil {
510
+		t.Fatalf("error setting up image: %v", err)
511
+	}
512
+
513
+	// pull from the registry using the <name>@<digest> reference
514
+	imageReference := fmt.Sprintf("%s@%s", repoName, pushDigest)
515
+	c := exec.Command(dockerBinary, "pull", imageReference)
516
+	out, _, err := runCommandWithOutput(c)
517
+	if err != nil {
518
+		t.Fatalf("error pulling by digest: %s, %v", out, err)
519
+	}
520
+	// just in case...
521
+	defer deleteImages(imageReference)
522
+
523
+	imageID, err := inspectField(imageReference, ".Id")
524
+	if err != nil {
525
+		t.Fatalf("error inspecting image id: %v", err)
526
+	}
527
+
528
+	c = exec.Command(dockerBinary, "rmi", imageID)
529
+	if _, err := runCommand(c); err != nil {
530
+		t.Fatalf("error deleting image by id: %v", err)
531
+	}
532
+
533
+	logDone("by_digest - delete image by id only pulled by digest")
534
+}
... ...
@@ -17,7 +17,7 @@ func TestPushBusyboxImage(t *testing.T) {
17 17
 	defer setupRegistry(t)()
18 18
 
19 19
 	repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL)
20
-	// tag the image to upload it tot he private registry
20
+	// tag the image to upload it to the private registry
21 21
 	tagCmd := exec.Command(dockerBinary, "tag", "busybox", repoName)
22 22
 	if out, _, err := runCommandWithOutput(tagCmd); err != nil {
23 23
 		t.Fatalf("image tagging failed: %s, %v", out, err)
... ...
@@ -62,11 +62,17 @@ func ParseTCPAddr(addr string, defaultAddr string) (string, error) {
62 62
 	return fmt.Sprintf("tcp://%s:%d", host, p), nil
63 63
 }
64 64
 
65
-// Get a repos name and returns the right reposName + tag
65
+// Get a repos name and returns the right reposName + tag|digest
66 66
 // The tag can be confusing because of a port in a repository name.
67 67
 //     Ex: localhost.localdomain:5000/samalba/hipache:latest
68
+//     Digest ex: localhost:5000/foo/bar@sha256:bc8813ea7b3603864987522f02a76101c17ad122e1c46d790efc0fca78ca7bfb
68 69
 func ParseRepositoryTag(repos string) (string, string) {
69
-	n := strings.LastIndex(repos, ":")
70
+	n := strings.Index(repos, "@")
71
+	if n >= 0 {
72
+		parts := strings.Split(repos, "@")
73
+		return parts[0], parts[1]
74
+	}
75
+	n = strings.LastIndex(repos, ":")
70 76
 	if n < 0 {
71 77
 		return repos, ""
72 78
 	}
... ...
@@ -49,18 +49,27 @@ func TestParseRepositoryTag(t *testing.T) {
49 49
 	if repo, tag := ParseRepositoryTag("root:tag"); repo != "root" || tag != "tag" {
50 50
 		t.Errorf("Expected repo: '%s' and tag: '%s', got '%s' and '%s'", "root", "tag", repo, tag)
51 51
 	}
52
+	if repo, digest := ParseRepositoryTag("root@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); repo != "root" || digest != "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" {
53
+		t.Errorf("Expected repo: '%s' and digest: '%s', got '%s' and '%s'", "root", "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", repo, digest)
54
+	}
52 55
 	if repo, tag := ParseRepositoryTag("user/repo"); repo != "user/repo" || tag != "" {
53 56
 		t.Errorf("Expected repo: '%s' and tag: '%s', got '%s' and '%s'", "user/repo", "", repo, tag)
54 57
 	}
55 58
 	if repo, tag := ParseRepositoryTag("user/repo:tag"); repo != "user/repo" || tag != "tag" {
56 59
 		t.Errorf("Expected repo: '%s' and tag: '%s', got '%s' and '%s'", "user/repo", "tag", repo, tag)
57 60
 	}
61
+	if repo, digest := ParseRepositoryTag("user/repo@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); repo != "user/repo" || digest != "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" {
62
+		t.Errorf("Expected repo: '%s' and digest: '%s', got '%s' and '%s'", "user/repo", "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", repo, digest)
63
+	}
58 64
 	if repo, tag := ParseRepositoryTag("url:5000/repo"); repo != "url:5000/repo" || tag != "" {
59 65
 		t.Errorf("Expected repo: '%s' and tag: '%s', got '%s' and '%s'", "url:5000/repo", "", repo, tag)
60 66
 	}
61 67
 	if repo, tag := ParseRepositoryTag("url:5000/repo:tag"); repo != "url:5000/repo" || tag != "tag" {
62 68
 		t.Errorf("Expected repo: '%s' and tag: '%s', got '%s' and '%s'", "url:5000/repo", "tag", repo, tag)
63 69
 	}
70
+	if repo, digest := ParseRepositoryTag("url:5000/repo@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); repo != "url:5000/repo" || digest != "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" {
71
+		t.Errorf("Expected repo: '%s' and digest: '%s', got '%s' and '%s'", "url:5000/repo", "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", repo, digest)
72
+	}
64 73
 }
65 74
 
66 75
 func TestParsePortMapping(t *testing.T) {
... ...
@@ -12,6 +12,8 @@ import (
12 12
 	"github.com/docker/docker/utils"
13 13
 )
14 14
 
15
+const DockerDigestHeader = "Docker-Content-Digest"
16
+
15 17
 func getV2Builder(e *Endpoint) *v2.URLBuilder {
16 18
 	if e.URLBuilder == nil {
17 19
 		e.URLBuilder = v2.NewURLBuilder(e.URL)
... ...
@@ -63,10 +65,10 @@ func (r *Session) GetV2Authorization(ep *Endpoint, imageName string, readOnly bo
63 63
 //  1.c) if anything else, err
64 64
 // 2) PUT the created/signed manifest
65 65
 //
66
-func (r *Session) GetV2ImageManifest(ep *Endpoint, imageName, tagName string, auth *RequestAuthorization) ([]byte, error) {
66
+func (r *Session) GetV2ImageManifest(ep *Endpoint, imageName, tagName string, auth *RequestAuthorization) ([]byte, string, error) {
67 67
 	routeURL, err := getV2Builder(ep).BuildManifestURL(imageName, tagName)
68 68
 	if err != nil {
69
-		return nil, err
69
+		return nil, "", err
70 70
 	}
71 71
 
72 72
 	method := "GET"
... ...
@@ -74,30 +76,30 @@ func (r *Session) GetV2ImageManifest(ep *Endpoint, imageName, tagName string, au
74 74
 
75 75
 	req, err := r.reqFactory.NewRequest(method, routeURL, nil)
76 76
 	if err != nil {
77
-		return nil, err
77
+		return nil, "", err
78 78
 	}
79 79
 	if err := auth.Authorize(req); err != nil {
80
-		return nil, err
80
+		return nil, "", err
81 81
 	}
82 82
 	res, _, err := r.doRequest(req)
83 83
 	if err != nil {
84
-		return nil, err
84
+		return nil, "", err
85 85
 	}
86 86
 	defer res.Body.Close()
87 87
 	if res.StatusCode != 200 {
88 88
 		if res.StatusCode == 401 {
89
-			return nil, errLoginRequired
89
+			return nil, "", errLoginRequired
90 90
 		} else if res.StatusCode == 404 {
91
-			return nil, ErrDoesNotExist
91
+			return nil, "", ErrDoesNotExist
92 92
 		}
93
-		return nil, utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to fetch for %s:%s", res.StatusCode, imageName, tagName), res)
93
+		return nil, "", utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to fetch for %s:%s", res.StatusCode, imageName, tagName), res)
94 94
 	}
95 95
 
96 96
 	buf, err := ioutil.ReadAll(res.Body)
97 97
 	if err != nil {
98
-		return nil, fmt.Errorf("Error while reading the http response: %s", err)
98
+		return nil, "", fmt.Errorf("Error while reading the http response: %s", err)
99 99
 	}
100
-	return buf, nil
100
+	return buf, res.Header.Get(DockerDigestHeader), nil
101 101
 }
102 102
 
103 103
 // - Succeeded to head image blob (already exists)
... ...
@@ -261,41 +263,41 @@ func (r *Session) PutV2ImageBlob(ep *Endpoint, imageName, sumType, sumStr string
261 261
 }
262 262
 
263 263
 // Finally Push the (signed) manifest of the blobs we've just pushed
264
-func (r *Session) PutV2ImageManifest(ep *Endpoint, imageName, tagName string, manifestRdr io.Reader, auth *RequestAuthorization) error {
264
+func (r *Session) PutV2ImageManifest(ep *Endpoint, imageName, tagName string, manifestRdr io.Reader, auth *RequestAuthorization) (string, error) {
265 265
 	routeURL, err := getV2Builder(ep).BuildManifestURL(imageName, tagName)
266 266
 	if err != nil {
267
-		return err
267
+		return "", err
268 268
 	}
269 269
 
270 270
 	method := "PUT"
271 271
 	log.Debugf("[registry] Calling %q %s", method, routeURL)
272 272
 	req, err := r.reqFactory.NewRequest(method, routeURL, manifestRdr)
273 273
 	if err != nil {
274
-		return err
274
+		return "", err
275 275
 	}
276 276
 	if err := auth.Authorize(req); err != nil {
277
-		return err
277
+		return "", err
278 278
 	}
279 279
 	res, _, err := r.doRequest(req)
280 280
 	if err != nil {
281
-		return err
281
+		return "", err
282 282
 	}
283 283
 	defer res.Body.Close()
284 284
 
285 285
 	// All 2xx and 3xx responses can be accepted for a put.
286 286
 	if res.StatusCode >= 400 {
287 287
 		if res.StatusCode == 401 {
288
-			return errLoginRequired
288
+			return "", errLoginRequired
289 289
 		}
290 290
 		errBody, err := ioutil.ReadAll(res.Body)
291 291
 		if err != nil {
292
-			return err
292
+			return "", err
293 293
 		}
294 294
 		log.Debugf("Unexpected response from server: %q %#v", errBody, res.Header)
295
-		return utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to push %s:%s manifest", res.StatusCode, imageName, tagName), res)
295
+		return "", utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to push %s:%s manifest", res.StatusCode, imageName, tagName), res)
296 296
 	}
297 297
 
298
-	return nil
298
+	return res.Header.Get(DockerDigestHeader), nil
299 299
 }
300 300
 
301 301
 type remoteTags struct {
... ...
@@ -17,3 +17,6 @@ var RepositoryNameRegexp = regexp.MustCompile(`(?:` + RepositoryNameComponentReg
17 17
 
18 18
 // TagNameRegexp matches valid tag names. From docker/docker:graph/tags.go.
19 19
 var TagNameRegexp = regexp.MustCompile(`[\w][\w.-]{0,127}`)
20
+
21
+// DigestRegexp matches valid digest types.
22
+var DigestRegexp = regexp.MustCompile(`[a-zA-Z0-9-_+.]+:[a-zA-Z0-9-_+.=]+`)
... ...
@@ -33,11 +33,11 @@ func Router() *mux.Router {
33 33
 		Path("/v2/").
34 34
 		Name(RouteNameBase)
35 35
 
36
-	// GET      /v2/<name>/manifest/<tag>	Image Manifest	Fetch the image manifest identified by name and tag.
37
-	// PUT      /v2/<name>/manifest/<tag>	Image Manifest	Upload the image manifest identified by name and tag.
38
-	// DELETE   /v2/<name>/manifest/<tag>	Image Manifest	Delete the image identified by name and tag.
36
+	// GET      /v2/<name>/manifest/<reference>	Image Manifest	Fetch the image manifest identified by name and reference where reference can be a tag or digest.
37
+	// PUT      /v2/<name>/manifest/<reference>	Image Manifest	Upload the image manifest identified by name and reference where reference can be a tag or digest.
38
+	// DELETE   /v2/<name>/manifest/<reference>	Image Manifest	Delete the image identified by name and reference where reference can be a tag or digest.
39 39
 	router.
40
-		Path("/v2/{name:" + RepositoryNameRegexp.String() + "}/manifests/{tag:" + TagNameRegexp.String() + "}").
40
+		Path("/v2/{name:" + RepositoryNameRegexp.String() + "}/manifests/{reference:" + TagNameRegexp.String() + "|" + DigestRegexp.String() + "}").
41 41
 		Name(RouteNameManifest)
42 42
 
43 43
 	// GET	/v2/<name>/tags/list	Tags	Fetch the tags under the repository identified by name.
... ...
@@ -55,16 +55,16 @@ func TestRouter(t *testing.T) {
55 55
 			RouteName:  RouteNameManifest,
56 56
 			RequestURI: "/v2/foo/manifests/bar",
57 57
 			Vars: map[string]string{
58
-				"name": "foo",
59
-				"tag":  "bar",
58
+				"name":      "foo",
59
+				"reference": "bar",
60 60
 			},
61 61
 		},
62 62
 		{
63 63
 			RouteName:  RouteNameManifest,
64 64
 			RequestURI: "/v2/foo/bar/manifests/tag",
65 65
 			Vars: map[string]string{
66
-				"name": "foo/bar",
67
-				"tag":  "tag",
66
+				"name":      "foo/bar",
67
+				"reference": "tag",
68 68
 			},
69 69
 		},
70 70
 		{
... ...
@@ -128,8 +128,8 @@ func TestRouter(t *testing.T) {
128 128
 			RouteName:  RouteNameManifest,
129 129
 			RequestURI: "/v2/foo/bar/manifests/manifests/tags",
130 130
 			Vars: map[string]string{
131
-				"name": "foo/bar/manifests",
132
-				"tag":  "tags",
131
+				"name":      "foo/bar/manifests",
132
+				"reference": "tags",
133 133
 			},
134 134
 		},
135 135
 		{
... ...
@@ -74,11 +74,11 @@ func (ub *URLBuilder) BuildTagsURL(name string) (string, error) {
74 74
 	return tagsURL.String(), nil
75 75
 }
76 76
 
77
-// BuildManifestURL constructs a url for the manifest identified by name and tag.
78
-func (ub *URLBuilder) BuildManifestURL(name, tag string) (string, error) {
77
+// BuildManifestURL constructs a url for the manifest identified by name and reference.
78
+func (ub *URLBuilder) BuildManifestURL(name, reference string) (string, error) {
79 79
 	route := ub.cloneRoute(RouteNameManifest)
80 80
 
81
-	manifestURL, err := route.URL("name", name, "tag", tag)
81
+	manifestURL, err := route.URL("name", name, "reference", reference)
82 82
 	if err != nil {
83 83
 		return "", err
84 84
 	}
... ...
@@ -535,3 +535,20 @@ func (wc *WriteCounter) Write(p []byte) (count int, err error) {
535 535
 	wc.Count += int64(count)
536 536
 	return
537 537
 }
538
+
539
+// ImageReference combines `repo` and `ref` and returns a string representing
540
+// the combination. If `ref` is a digest (meaning it's of the form
541
+// <algorithm>:<digest>, the returned string is <repo>@<ref>. Otherwise,
542
+// ref is assumed to be a tag, and the returned string is <repo>:<tag>.
543
+func ImageReference(repo, ref string) string {
544
+	if DigestReference(ref) {
545
+		return repo + "@" + ref
546
+	}
547
+	return repo + ":" + ref
548
+}
549
+
550
+// DigestReference returns true if ref is a digest reference; i.e. if it
551
+// is of the form <algorithm>:<digest>.
552
+func DigestReference(ref string) bool {
553
+	return strings.Contains(ref, ":")
554
+}
... ...
@@ -122,3 +122,33 @@ func TestWriteCounter(t *testing.T) {
122 122
 		t.Error("Wrong message written")
123 123
 	}
124 124
 }
125
+
126
+func TestImageReference(t *testing.T) {
127
+	tests := []struct {
128
+		repo     string
129
+		ref      string
130
+		expected string
131
+	}{
132
+		{"repo", "tag", "repo:tag"},
133
+		{"repo", "sha256:c100b11b25d0cacd52c14e0e7bf525e1a4c0e6aec8827ae007055545909d1a64", "repo@sha256:c100b11b25d0cacd52c14e0e7bf525e1a4c0e6aec8827ae007055545909d1a64"},
134
+	}
135
+
136
+	for i, test := range tests {
137
+		actual := ImageReference(test.repo, test.ref)
138
+		if test.expected != actual {
139
+			t.Errorf("%d: expected %q, got %q", i, test.expected, actual)
140
+		}
141
+	}
142
+}
143
+
144
+func TestDigestReference(t *testing.T) {
145
+	input := "sha256:c100b11b25d0cacd52c14e0e7bf525e1a4c0e6aec8827ae007055545909d1a64"
146
+	if !DigestReference(input) {
147
+		t.Errorf("Expected DigestReference=true for input %q", input)
148
+	}
149
+
150
+	input = "latest"
151
+	if DigestReference(input) {
152
+		t.Errorf("Unexpected DigestReference=true for input %q", input)
153
+	}
154
+}