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>
| ... | ... |
@@ -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 |
+} |