mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
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>
This commit is contained in:
parent
ad56b5c603
commit
a2b0c9778f
28 changed files with 987 additions and 118 deletions
|
@ -108,7 +108,7 @@ RUN go get golang.org/x/tools/cmd/cover
|
|||
RUN gem install --no-rdoc --no-ri fpm --version 1.3.2
|
||||
|
||||
# Install registry
|
||||
ENV REGISTRY_COMMIT c448e0416925a9876d5576e412703c9b8b865e19
|
||||
ENV REGISTRY_COMMIT b4cc5e3ecc2e9f4fa0e95d94c389e1d79e902486
|
||||
RUN set -x \
|
||||
&& git clone https://github.com/docker/distribution.git /go/src/github.com/docker/distribution \
|
||||
&& (cd /go/src/github.com/docker/distribution && git checkout -q $REGISTRY_COMMIT) \
|
||||
|
|
|
@ -1312,7 +1312,7 @@ func (cli *DockerCli) CmdPush(args ...string) error {
|
|||
}
|
||||
|
||||
func (cli *DockerCli) CmdPull(args ...string) error {
|
||||
cmd := cli.Subcmd("pull", "NAME[:TAG]", "Pull an image or a repository from the registry", true)
|
||||
cmd := cli.Subcmd("pull", "NAME[:TAG|@DIGEST]", "Pull an image or a repository from the registry", true)
|
||||
allTags := cmd.Bool([]string{"a", "-all-tags"}, false, "Download all tagged images in the repository")
|
||||
cmd.Require(flag.Exact, 1)
|
||||
|
||||
|
@ -1325,7 +1325,7 @@ func (cli *DockerCli) CmdPull(args ...string) error {
|
|||
)
|
||||
taglessRemote, tag := parsers.ParseRepositoryTag(remote)
|
||||
if tag == "" && !*allTags {
|
||||
newRemote = taglessRemote + ":" + graph.DEFAULTTAG
|
||||
newRemote = utils.ImageReference(taglessRemote, graph.DEFAULTTAG)
|
||||
}
|
||||
if tag != "" && *allTags {
|
||||
return fmt.Errorf("tag can't be used with --all-tags/-a")
|
||||
|
@ -1378,6 +1378,7 @@ func (cli *DockerCli) CmdImages(args ...string) error {
|
|||
quiet := cmd.Bool([]string{"q", "-quiet"}, false, "Only show numeric IDs")
|
||||
all := cmd.Bool([]string{"a", "-all"}, false, "Show all images (default hides intermediate images)")
|
||||
noTrunc := cmd.Bool([]string{"#notrunc", "-no-trunc"}, false, "Don't truncate output")
|
||||
showDigests := cmd.Bool([]string{"-digests"}, false, "Show digests")
|
||||
// FIXME: --viz and --tree are deprecated. Remove them in a future version.
|
||||
flViz := cmd.Bool([]string{"#v", "#viz", "#-viz"}, false, "Output graph in graphviz format")
|
||||
flTree := cmd.Bool([]string{"#t", "#tree", "#-tree"}, false, "Output graph in tree format")
|
||||
|
@ -1504,20 +1505,43 @@ func (cli *DockerCli) CmdImages(args ...string) error {
|
|||
|
||||
w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0)
|
||||
if !*quiet {
|
||||
fmt.Fprintln(w, "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tVIRTUAL SIZE")
|
||||
if *showDigests {
|
||||
fmt.Fprintln(w, "REPOSITORY\tTAG\tDIGEST\tIMAGE ID\tCREATED\tVIRTUAL SIZE")
|
||||
} else {
|
||||
fmt.Fprintln(w, "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tVIRTUAL SIZE")
|
||||
}
|
||||
}
|
||||
|
||||
for _, out := range outs.Data {
|
||||
for _, repotag := range out.GetList("RepoTags") {
|
||||
outID := out.Get("Id")
|
||||
if !*noTrunc {
|
||||
outID = common.TruncateID(outID)
|
||||
}
|
||||
|
||||
// Tags referring to this image ID.
|
||||
for _, repotag := range out.GetList("RepoTags") {
|
||||
repo, tag := parsers.ParseRepositoryTag(repotag)
|
||||
outID := out.Get("Id")
|
||||
if !*noTrunc {
|
||||
outID = common.TruncateID(outID)
|
||||
}
|
||||
|
||||
if !*quiet {
|
||||
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"))))
|
||||
if *showDigests {
|
||||
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"))))
|
||||
} else {
|
||||
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"))))
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintln(w, outID)
|
||||
}
|
||||
}
|
||||
|
||||
// Digests referring to this image ID.
|
||||
for _, repoDigest := range out.GetList("RepoDigests") {
|
||||
repo, digest := parsers.ParseRepositoryTag(repoDigest)
|
||||
if !*quiet {
|
||||
if *showDigests {
|
||||
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"))))
|
||||
} else {
|
||||
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"))))
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintln(w, outID)
|
||||
}
|
||||
|
@ -2208,7 +2232,7 @@ func (cli *DockerCli) createContainer(config *runconfig.Config, hostConfig *runc
|
|||
if tag == "" {
|
||||
tag = graph.DEFAULTTAG
|
||||
}
|
||||
fmt.Fprintf(cli.err, "Unable to find image '%s:%s' locally\n", repo, tag)
|
||||
fmt.Fprintf(cli.err, "Unable to find image '%s' locally\n", utils.ImageReference(repo, tag))
|
||||
|
||||
// we don't want to write to stdout anything apart from container.ID
|
||||
if err = cli.pullImageCustomOut(config.Image, cli.err); err != nil {
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/pkg/common"
|
||||
"github.com/docker/docker/pkg/parsers"
|
||||
"github.com/docker/docker/utils"
|
||||
)
|
||||
|
||||
func (daemon *Daemon) ImageDelete(job *engine.Job) engine.Status {
|
||||
|
@ -48,7 +49,7 @@ func (daemon *Daemon) DeleteImage(eng *engine.Engine, name string, imgs *engine.
|
|||
img, err := daemon.Repositories().LookupImage(name)
|
||||
if err != nil {
|
||||
if r, _ := daemon.Repositories().Get(repoName); r != nil {
|
||||
return fmt.Errorf("No such image: %s:%s", repoName, tag)
|
||||
return fmt.Errorf("No such image: %s", utils.ImageReference(repoName, tag))
|
||||
}
|
||||
return fmt.Errorf("No such image: %s", name)
|
||||
}
|
||||
|
@ -102,7 +103,7 @@ func (daemon *Daemon) DeleteImage(eng *engine.Engine, name string, imgs *engine.
|
|||
}
|
||||
if tagDeleted {
|
||||
out := &engine.Env{}
|
||||
out.Set("Untagged", repoName+":"+tag)
|
||||
out.Set("Untagged", utils.ImageReference(repoName, tag))
|
||||
imgs.Add(out)
|
||||
eng.Job("log", "untag", img.ID, "").Run()
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
"github.com/docker/docker/graph"
|
||||
"github.com/docker/docker/pkg/graphdb"
|
||||
"github.com/docker/docker/utils"
|
||||
|
||||
"github.com/docker/docker/engine"
|
||||
"github.com/docker/docker/pkg/parsers"
|
||||
|
@ -131,7 +132,7 @@ func (daemon *Daemon) Containers(job *engine.Job) engine.Status {
|
|||
img := container.Config.Image
|
||||
_, tag := parsers.ParseRepositoryTag(container.Config.Image)
|
||||
if tag == "" {
|
||||
img = img + ":" + graph.DEFAULTTAG
|
||||
img = utils.ImageReference(img, graph.DEFAULTTAG)
|
||||
}
|
||||
out.SetJson("Image", img)
|
||||
if len(container.Args) > 0 {
|
||||
|
|
|
@ -8,6 +8,7 @@ docker-images - List images
|
|||
**docker images**
|
||||
[**--help**]
|
||||
[**-a**|**--all**[=*false*]]
|
||||
[**--digests**[=*false*]]
|
||||
[**-f**|**--filter**[=*[]*]]
|
||||
[**--no-trunc**[=*false*]]
|
||||
[**-q**|**--quiet**[=*false*]]
|
||||
|
@ -33,6 +34,9 @@ versions.
|
|||
**-a**, **--all**=*true*|*false*
|
||||
Show all images (by default filter out the intermediate image layers). The default is *false*.
|
||||
|
||||
**--digests**=*true*|*false*
|
||||
Show image digests. The default is *false*.
|
||||
|
||||
**-f**, **--filter**=[]
|
||||
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.
|
||||
|
||||
|
|
|
@ -62,6 +62,10 @@ You can set ulimit settings to be used within the container.
|
|||
**New!**
|
||||
This endpoint now returns `SystemTime`, `HttpProxy`,`HttpsProxy` and `NoProxy`.
|
||||
|
||||
`GET /images/json`
|
||||
|
||||
**New!**
|
||||
Added a `RepoDigests` field to include image digest information.
|
||||
|
||||
## v1.17
|
||||
|
||||
|
|
|
@ -1054,6 +1054,45 @@ Status Codes:
|
|||
}
|
||||
]
|
||||
|
||||
**Example request, with digest information**:
|
||||
|
||||
GET /images/json?digests=1 HTTP/1.1
|
||||
|
||||
**Example response, with digest information**:
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
{
|
||||
"Created": 1420064636,
|
||||
"Id": "4986bf8c15363d1c5d15512d5266f8777bfba4974ac56e3270e7760f6f0a8125",
|
||||
"ParentId": "ea13149945cb6b1e746bf28032f02e9b5a793523481a0a18645fc77ad53c4ea2",
|
||||
"RepoDigests": [
|
||||
"localhost:5000/test/busybox@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf"
|
||||
],
|
||||
"RepoTags": [
|
||||
"localhost:5000/test/busybox:latest",
|
||||
"playdate:latest"
|
||||
],
|
||||
"Size": 0,
|
||||
"VirtualSize": 2429728
|
||||
}
|
||||
]
|
||||
|
||||
The response shows a single image `Id` associated with two repositories
|
||||
(`RepoTags`): `localhost:5000/test/busybox`: and `playdate`. A caller can use
|
||||
either of the `RepoTags` values `localhost:5000/test/busybox:latest` or
|
||||
`playdate:latest` to reference the image.
|
||||
|
||||
You can also use `RepoDigests` values to reference an image. In this response,
|
||||
the array has only one reference and that is to the
|
||||
`localhost:5000/test/busybox` repository; the `playdate` repository has no
|
||||
digest. You can reference this digest using the value:
|
||||
`localhost:5000/test/busybox@sha256:cbbf2f9a99b47fc460d...`
|
||||
|
||||
See the `docker run` and `docker build` commands for examples of digest and tag
|
||||
references on the command line.
|
||||
|
||||
Query Parameters:
|
||||
|
||||
|
|
|
@ -192,6 +192,10 @@ Or
|
|||
|
||||
FROM <image>:<tag>
|
||||
|
||||
Or
|
||||
|
||||
FROM <image>@<digest>
|
||||
|
||||
The `FROM` instruction sets the [*Base Image*](/terms/image/#base-image)
|
||||
for subsequent instructions. As such, a valid `Dockerfile` must have `FROM` as
|
||||
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*](
|
|||
multiple images. Simply make a note of the last image ID output by the commit
|
||||
before each new `FROM` command.
|
||||
|
||||
If no `tag` is given to the `FROM` instruction, `latest` is assumed. If the
|
||||
used tag does not exist, an error will be returned.
|
||||
The `tag` or `digest` values are optional. If you omit either of them, the builder
|
||||
assumes a `latest` by default. The builder returns an error if it cannot match
|
||||
the `tag` value.
|
||||
|
||||
## MAINTAINER
|
||||
|
||||
|
|
|
@ -1112,7 +1112,9 @@ To see how the `docker:latest` image was built:
|
|||
List images
|
||||
|
||||
-a, --all=false Show all images (default hides intermediate images)
|
||||
--digests=false Show digests
|
||||
-f, --filter=[] Filter output based on conditions provided
|
||||
--help=false Print usage
|
||||
--no-trunc=false Don't truncate output
|
||||
-q, --quiet=false Only show numeric IDs
|
||||
|
||||
|
@ -1161,6 +1163,22 @@ uses up the `VIRTUAL SIZE` listed only once.
|
|||
tryout latest 2629d1fa0b81b222fca63371ca16cbf6a0772d07759ff80e8d1369b926940074 23 hours ago 131.5 MB
|
||||
<none> <none> 5ed6274db6ceb2397844896966ea239290555e74ef307030ebb01ff91b1914df 24 hours ago 1.089 GB
|
||||
|
||||
#### Listing image digests
|
||||
|
||||
Images that use the v2 or later format have a content-addressable identifier
|
||||
called a `digest`. As long as the input used to generate the image is
|
||||
unchanged, the digest value is predictable. To list image digest values, use
|
||||
the `--digests` flag:
|
||||
|
||||
$ sudo docker images --digests | head
|
||||
REPOSITORY TAG DIGEST IMAGE ID CREATED VIRTUAL SIZE
|
||||
localhost:5000/test/busybox <none> sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf 4986bf8c1536 9 weeks ago 2.43 MB
|
||||
|
||||
When pushing or pulling to a 2.0 registry, the `push` or `pull` command
|
||||
output includes the image digest. You can `pull` using a digest value. You can
|
||||
also reference by digest in `create`, `run`, and `rmi` commands, as well as the
|
||||
`FROM` image reference in a Dockerfile.
|
||||
|
||||
#### Filtering
|
||||
|
||||
The filtering flag (`-f` or `--filter`) format is of "key=value". If there is more
|
||||
|
@ -1563,6 +1581,10 @@ use `docker pull`:
|
|||
$ sudo docker pull debian:testing
|
||||
# will pull the image named debian:testing and any intermediate
|
||||
# layers it is based on.
|
||||
$ sudo docker pull debian@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf
|
||||
# will pull the image from the debian repository with the digest
|
||||
# sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf
|
||||
# and any intermediate layers it is based on.
|
||||
# (Typically the empty `scratch` image, a MAINTAINER layer,
|
||||
# and the un-tarred base).
|
||||
$ sudo docker pull --all-tags centos
|
||||
|
@ -1634,9 +1656,9 @@ deleted.
|
|||
|
||||
#### Removing tagged images
|
||||
|
||||
Images can be removed either by their short or long IDs, or their image
|
||||
names. If an image has more than one name, each of them needs to be
|
||||
removed before the image is removed.
|
||||
You can remove an image using its short or long ID, its tag, or its digest. If
|
||||
an image has one or more tag or digest reference, you must remove all of them
|
||||
before the image is removed.
|
||||
|
||||
$ sudo docker images
|
||||
REPOSITORY TAG IMAGE ID CREATED SIZE
|
||||
|
@ -1660,6 +1682,20 @@ removed before the image is removed.
|
|||
Untagged: test:latest
|
||||
Deleted: fd484f19954f4920da7ff372b5067f5b7ddb2fd3830cecd17b96ea9e286ba5b8
|
||||
|
||||
An image pulled by digest has no tag associated with it:
|
||||
|
||||
$ sudo docker images --digests
|
||||
REPOSITORY TAG DIGEST IMAGE ID CREATED VIRTUAL SIZE
|
||||
localhost:5000/test/busybox <none> sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf 4986bf8c1536 9 weeks ago 2.43 MB
|
||||
|
||||
To remove an image using its digest:
|
||||
|
||||
$ sudo docker rmi localhost:5000/test/busybox@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf
|
||||
Untagged: localhost:5000/test/busybox@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf
|
||||
Deleted: 4986bf8c15363d1c5d15512d5266f8777bfba4974ac56e3270e7760f6f0a8125
|
||||
Deleted: ea13149945cb6b1e746bf28032f02e9b5a793523481a0a18645fc77ad53c4ea2
|
||||
Deleted: df7546f9f060a2268024c8a230d8639878585defcc1bc6f79d2728a13957871b
|
||||
|
||||
## run
|
||||
|
||||
Usage: docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
|
||||
|
|
|
@ -24,7 +24,7 @@ other `docker` command.
|
|||
|
||||
The basic `docker run` command takes this form:
|
||||
|
||||
$ sudo docker run [OPTIONS] IMAGE[:TAG] [COMMAND] [ARG...]
|
||||
$ sudo docker run [OPTIONS] IMAGE[:TAG|@DIGEST] [COMMAND] [ARG...]
|
||||
|
||||
To learn how to interpret the types of `[OPTIONS]`,
|
||||
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
|
|||
image you'd like to run the container with by adding `image[:tag]` to the command. For
|
||||
example, `docker run ubuntu:14.04`.
|
||||
|
||||
### Image[@digest]
|
||||
|
||||
Images using the v2 or later image format have a content-addressable identifier
|
||||
called a digest. As long as the input used to generate the image is unchanged,
|
||||
the digest value is predictable and referenceable.
|
||||
|
||||
## PID Settings (--pid)
|
||||
--pid="" : Set the PID (Process) Namespace mode for the container,
|
||||
'host': use the host's PID namespace inside the container
|
||||
|
@ -661,7 +667,7 @@ Dockerfile instruction and how the operator can override that setting.
|
|||
Recall the optional `COMMAND` in the Docker
|
||||
commandline:
|
||||
|
||||
$ sudo docker run [OPTIONS] IMAGE[:TAG] [COMMAND] [ARG...]
|
||||
$ sudo docker run [OPTIONS] IMAGE[:TAG|@DIGEST] [COMMAND] [ARG...]
|
||||
|
||||
This command is optional because the person who created the `IMAGE` may
|
||||
have already provided a default `COMMAND` using the Dockerfile `CMD`
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
"github.com/docker/docker/engine"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/utils"
|
||||
)
|
||||
|
||||
func (s *TagStore) CmdHistory(job *engine.Job) engine.Status {
|
||||
|
@ -24,7 +25,7 @@ func (s *TagStore) CmdHistory(job *engine.Job) engine.Status {
|
|||
if _, exists := lookupMap[id]; !exists {
|
||||
lookupMap[id] = []string{}
|
||||
}
|
||||
lookupMap[id] = append(lookupMap[id], name+":"+tag)
|
||||
lookupMap[id] = append(lookupMap[id], utils.ImageReference(name, tag))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -88,7 +88,7 @@ func (s *TagStore) CmdImport(job *engine.Job) engine.Status {
|
|||
job.Stdout.Write(sf.FormatStatus("", img.ID))
|
||||
logID := img.ID
|
||||
if tag != "" {
|
||||
logID += ":" + tag
|
||||
logID = utils.ImageReference(logID, tag)
|
||||
}
|
||||
if err = job.Eng.Job("log", "import", logID, "").Run(); err != nil {
|
||||
log.Errorf("Error logging event 'import' for %s: %s", logID, err)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"path"
|
||||
"strings"
|
||||
|
@ -9,6 +8,7 @@ import (
|
|||
"github.com/docker/docker/engine"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/pkg/parsers/filters"
|
||||
"github.com/docker/docker/utils"
|
||||
)
|
||||
|
||||
var acceptedImageFilterTags = map[string]struct{}{
|
||||
|
@ -54,22 +54,27 @@ func (s *TagStore) CmdImages(job *engine.Job) engine.Status {
|
|||
}
|
||||
lookup := make(map[string]*engine.Env)
|
||||
s.Lock()
|
||||
for name, repository := range s.Repositories {
|
||||
for repoName, repository := range s.Repositories {
|
||||
if job.Getenv("filter") != "" {
|
||||
if match, _ := path.Match(job.Getenv("filter"), name); !match {
|
||||
if match, _ := path.Match(job.Getenv("filter"), repoName); !match {
|
||||
continue
|
||||
}
|
||||
}
|
||||
for tag, id := range repository {
|
||||
for ref, id := range repository {
|
||||
imgRef := utils.ImageReference(repoName, ref)
|
||||
image, err := s.graph.Get(id)
|
||||
if err != nil {
|
||||
log.Printf("Warning: couldn't load %s from %s/%s: %s", id, name, tag, err)
|
||||
log.Printf("Warning: couldn't load %s from %s: %s", id, imgRef, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if out, exists := lookup[id]; exists {
|
||||
if filt_tagged {
|
||||
out.SetList("RepoTags", append(out.GetList("RepoTags"), fmt.Sprintf("%s:%s", name, tag)))
|
||||
if utils.DigestReference(ref) {
|
||||
out.SetList("RepoDigests", append(out.GetList("RepoDigests"), imgRef))
|
||||
} else { // Tag Ref.
|
||||
out.SetList("RepoTags", append(out.GetList("RepoTags"), imgRef))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 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 {
|
|||
if filt_tagged {
|
||||
out := &engine.Env{}
|
||||
out.SetJson("ParentId", image.Parent)
|
||||
out.SetList("RepoTags", []string{fmt.Sprintf("%s:%s", name, tag)})
|
||||
out.SetJson("Id", image.ID)
|
||||
out.SetInt64("Created", image.Created.Unix())
|
||||
out.SetInt64("Size", image.Size)
|
||||
out.SetInt64("VirtualSize", image.GetParentsSize(0)+image.Size)
|
||||
out.SetJson("Labels", image.ContainerConfig.Labels)
|
||||
|
||||
if utils.DigestReference(ref) {
|
||||
out.SetList("RepoTags", []string{})
|
||||
out.SetList("RepoDigests", []string{imgRef})
|
||||
} else {
|
||||
out.SetList("RepoTags", []string{imgRef})
|
||||
out.SetList("RepoDigests", []string{})
|
||||
}
|
||||
|
||||
lookup[id] = out
|
||||
}
|
||||
}
|
||||
|
@ -108,6 +121,7 @@ func (s *TagStore) CmdImages(job *engine.Job) engine.Status {
|
|||
out := &engine.Env{}
|
||||
out.SetJson("ParentId", image.Parent)
|
||||
out.SetList("RepoTags", []string{"<none>:<none>"})
|
||||
out.SetList("RepoDigests", []string{"<none>@<none>"})
|
||||
out.SetJson("Id", image.ID)
|
||||
out.SetInt64("Created", image.Created.Unix())
|
||||
out.SetInt64("Size", image.Size)
|
||||
|
|
|
@ -22,7 +22,7 @@ import (
|
|||
|
||||
func (s *TagStore) CmdPull(job *engine.Job) engine.Status {
|
||||
if n := len(job.Args); n != 1 && n != 2 {
|
||||
return job.Errorf("Usage: %s IMAGE [TAG]", job.Name)
|
||||
return job.Errorf("Usage: %s IMAGE [TAG|DIGEST]", job.Name)
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -46,7 +46,7 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status {
|
|||
job.GetenvJson("authConfig", authConfig)
|
||||
job.GetenvJson("metaHeaders", &metaHeaders)
|
||||
|
||||
c, err := s.poolAdd("pull", repoInfo.LocalName+":"+tag)
|
||||
c, err := s.poolAdd("pull", utils.ImageReference(repoInfo.LocalName, tag))
|
||||
if err != nil {
|
||||
if c != nil {
|
||||
// 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 {
|
|||
}
|
||||
return job.Error(err)
|
||||
}
|
||||
defer s.poolRemove("pull", repoInfo.LocalName+":"+tag)
|
||||
defer s.poolRemove("pull", utils.ImageReference(repoInfo.LocalName, tag))
|
||||
|
||||
log.Debugf("pulling image from host %q with remote name %q", repoInfo.Index.Name, repoInfo.RemoteName)
|
||||
endpoint, err := repoInfo.GetEndpoint()
|
||||
|
@ -71,7 +71,7 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status {
|
|||
|
||||
logName := repoInfo.LocalName
|
||||
if tag != "" {
|
||||
logName += ":" + tag
|
||||
logName = utils.ImageReference(logName, tag)
|
||||
}
|
||||
|
||||
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 *
|
|||
repoData, err := r.GetRepositoryData(repoInfo.RemoteName)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "HTTP code: 404") {
|
||||
return fmt.Errorf("Error: image %s:%s not found", repoInfo.RemoteName, askedTag)
|
||||
return fmt.Errorf("Error: image %s not found", utils.ImageReference(repoInfo.RemoteName, askedTag))
|
||||
}
|
||||
// Unexpected HTTP error
|
||||
return err
|
||||
|
@ -259,7 +259,7 @@ func (s *TagStore) pullRepository(r *registry.Session, out io.Writer, repoInfo *
|
|||
|
||||
requestedTag := repoInfo.CanonicalName
|
||||
if len(askedTag) > 0 {
|
||||
requestedTag = repoInfo.CanonicalName + ":" + askedTag
|
||||
requestedTag = utils.ImageReference(repoInfo.CanonicalName, askedTag)
|
||||
}
|
||||
WriteStatus(requestedTag, out, sf, layers_downloaded)
|
||||
return nil
|
||||
|
@ -421,7 +421,7 @@ func (s *TagStore) pullV2Repository(eng *engine.Engine, r *registry.Session, out
|
|||
|
||||
requestedTag := repoInfo.CanonicalName
|
||||
if len(tag) > 0 {
|
||||
requestedTag = repoInfo.CanonicalName + ":" + tag
|
||||
requestedTag = utils.ImageReference(repoInfo.CanonicalName, tag)
|
||||
}
|
||||
WriteStatus(requestedTag, out, sf, layersDownloaded)
|
||||
return nil
|
||||
|
@ -429,7 +429,7 @@ func (s *TagStore) pullV2Repository(eng *engine.Engine, r *registry.Session, out
|
|||
|
||||
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) {
|
||||
log.Debugf("Pulling tag from V2 registry: %q", tag)
|
||||
manifestBytes, err := r.GetV2ImageManifest(endpoint, repoInfo.RemoteName, tag, auth)
|
||||
manifestBytes, digest, err := r.GetV2ImageManifest(endpoint, repoInfo.RemoteName, tag, auth)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
@ -444,7 +444,7 @@ func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Wri
|
|||
}
|
||||
|
||||
if verified {
|
||||
log.Printf("Image manifest for %s:%s has been verified", repoInfo.CanonicalName, tag)
|
||||
log.Printf("Image manifest for %s has been verified", utils.ImageReference(repoInfo.CanonicalName, tag))
|
||||
}
|
||||
out.Write(sf.FormatStatus(tag, "Pulling from %s", repoInfo.CanonicalName))
|
||||
|
||||
|
@ -601,11 +601,22 @@ func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Wri
|
|||
}
|
||||
|
||||
if verified && tagUpdated {
|
||||
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."))
|
||||
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."))
|
||||
}
|
||||
|
||||
if err = s.Set(repoInfo.LocalName, tag, downloads[0].img.ID, true); err != nil {
|
||||
return false, err
|
||||
if len(digest) > 0 {
|
||||
out.Write(sf.FormatStatus("", "Digest: %s", digest))
|
||||
}
|
||||
|
||||
if utils.DigestReference(tag) {
|
||||
if err = s.SetDigest(repoInfo.LocalName, tag, downloads[0].img.ID); err != nil {
|
||||
return false, err
|
||||
}
|
||||
} else {
|
||||
// only set the repository/tag -> image ID mapping when pulling by tag (i.e. not by digest)
|
||||
if err = s.Set(repoInfo.LocalName, tag, downloads[0].img.ID, true); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return tagUpdated, nil
|
||||
|
|
|
@ -36,8 +36,15 @@ func (s *TagStore) getImageList(localRepo map[string]string, requestedTag string
|
|||
|
||||
for tag, id := range localRepo {
|
||||
if requestedTag != "" && requestedTag != tag {
|
||||
// Include only the requested tag.
|
||||
continue
|
||||
}
|
||||
|
||||
if utils.DigestReference(tag) {
|
||||
// Ignore digest references.
|
||||
continue
|
||||
}
|
||||
|
||||
var imageListForThisTag []string
|
||||
|
||||
tagsByImage[id] = append(tagsByImage[id], tag)
|
||||
|
@ -76,14 +83,16 @@ func (s *TagStore) getImageList(localRepo map[string]string, requestedTag string
|
|||
func (s *TagStore) getImageTags(localRepo map[string]string, askedTag string) ([]string, error) {
|
||||
log.Debugf("Checking %s against %#v", askedTag, localRepo)
|
||||
if len(askedTag) > 0 {
|
||||
if _, ok := localRepo[askedTag]; !ok {
|
||||
if _, ok := localRepo[askedTag]; !ok || utils.DigestReference(askedTag) {
|
||||
return nil, fmt.Errorf("Tag does not exist: %s", askedTag)
|
||||
}
|
||||
return []string{askedTag}, nil
|
||||
}
|
||||
var tags []string
|
||||
for tag := range localRepo {
|
||||
tags = append(tags, tag)
|
||||
if !utils.DigestReference(tag) {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
@ -422,9 +431,14 @@ func (s *TagStore) pushV2Repository(r *registry.Session, localRepo Repository, o
|
|||
log.Infof("Signed manifest for %s:%s using daemon's key: %s", repoInfo.LocalName, tag, s.trustKey.KeyID())
|
||||
|
||||
// push the manifest
|
||||
if err := r.PutV2ImageManifest(endpoint, repoInfo.RemoteName, tag, bytes.NewReader(signedBody), auth); err != nil {
|
||||
digest, err := r.PutV2ImageManifest(endpoint, repoInfo.RemoteName, tag, bytes.NewReader(signedBody), auth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(digest) > 0 {
|
||||
out.Write(sf.FormatStatus("", "Digest: %s", digest))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
143
graph/tags.go
143
graph/tags.go
|
@ -2,6 +2,7 @@ package graph
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
@ -15,13 +16,16 @@ import (
|
|||
"github.com/docker/docker/pkg/common"
|
||||
"github.com/docker/docker/pkg/parsers"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/docker/utils"
|
||||
"github.com/docker/libtrust"
|
||||
)
|
||||
|
||||
const DEFAULTTAG = "latest"
|
||||
|
||||
var (
|
||||
//FIXME these 2 regexes also exist in registry/v2/regexp.go
|
||||
validTagName = regexp.MustCompile(`^[\w][\w.-]{0,127}$`)
|
||||
validDigest = regexp.MustCompile(`[a-zA-Z0-9-_+.]+:[a-fA-F0-9]+`)
|
||||
)
|
||||
|
||||
type TagStore struct {
|
||||
|
@ -107,20 +111,31 @@ func (store *TagStore) reload() error {
|
|||
func (store *TagStore) LookupImage(name string) (*image.Image, error) {
|
||||
// FIXME: standardize on returning nil when the image doesn't exist, and err for everything else
|
||||
// (so we can pass all errors here)
|
||||
repos, tag := parsers.ParseRepositoryTag(name)
|
||||
if tag == "" {
|
||||
tag = DEFAULTTAG
|
||||
repoName, ref := parsers.ParseRepositoryTag(name)
|
||||
if ref == "" {
|
||||
ref = DEFAULTTAG
|
||||
}
|
||||
img, err := store.GetImage(repos, tag)
|
||||
store.Lock()
|
||||
defer store.Unlock()
|
||||
var (
|
||||
err error
|
||||
img *image.Image
|
||||
)
|
||||
|
||||
img, err = store.GetImage(repoName, ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if img == nil {
|
||||
if img, err = store.graph.Get(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if img != nil {
|
||||
return img, err
|
||||
}
|
||||
|
||||
// name must be an image ID.
|
||||
store.Lock()
|
||||
defer store.Unlock()
|
||||
if img, err = store.graph.Get(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return img, nil
|
||||
}
|
||||
|
||||
|
@ -132,7 +147,7 @@ func (store *TagStore) ByID() map[string][]string {
|
|||
byID := make(map[string][]string)
|
||||
for repoName, repository := range store.Repositories {
|
||||
for tag, id := range repository {
|
||||
name := repoName + ":" + tag
|
||||
name := utils.ImageReference(repoName, tag)
|
||||
if _, exists := byID[id]; !exists {
|
||||
byID[id] = []string{name}
|
||||
} else {
|
||||
|
@ -171,32 +186,35 @@ func (store *TagStore) DeleteAll(id string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (store *TagStore) Delete(repoName, tag string) (bool, error) {
|
||||
func (store *TagStore) Delete(repoName, ref string) (bool, error) {
|
||||
store.Lock()
|
||||
defer store.Unlock()
|
||||
deleted := false
|
||||
if err := store.reload(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
repoName = registry.NormalizeLocalName(repoName)
|
||||
if r, exists := store.Repositories[repoName]; exists {
|
||||
if tag != "" {
|
||||
if _, exists2 := r[tag]; exists2 {
|
||||
delete(r, tag)
|
||||
if len(r) == 0 {
|
||||
delete(store.Repositories, repoName)
|
||||
}
|
||||
deleted = true
|
||||
} else {
|
||||
return false, fmt.Errorf("No such tag: %s:%s", repoName, tag)
|
||||
}
|
||||
} else {
|
||||
delete(store.Repositories, repoName)
|
||||
deleted = true
|
||||
}
|
||||
} else {
|
||||
|
||||
if ref == "" {
|
||||
// Delete the whole repository.
|
||||
delete(store.Repositories, repoName)
|
||||
return true, store.save()
|
||||
}
|
||||
|
||||
repoRefs, exists := store.Repositories[repoName]
|
||||
if !exists {
|
||||
return false, fmt.Errorf("No such repository: %s", repoName)
|
||||
}
|
||||
|
||||
if _, exists := repoRefs[ref]; exists {
|
||||
delete(repoRefs, ref)
|
||||
if len(repoRefs) == 0 {
|
||||
delete(store.Repositories, repoName)
|
||||
}
|
||||
deleted = true
|
||||
}
|
||||
|
||||
return deleted, store.save()
|
||||
}
|
||||
|
||||
|
@ -234,6 +252,40 @@ func (store *TagStore) Set(repoName, tag, imageName string, force bool) error {
|
|||
return store.save()
|
||||
}
|
||||
|
||||
// SetDigest creates a digest reference to an image ID.
|
||||
func (store *TagStore) SetDigest(repoName, digest, imageName string) error {
|
||||
img, err := store.LookupImage(imageName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateRepoName(repoName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateDigest(digest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
store.Lock()
|
||||
defer store.Unlock()
|
||||
if err := store.reload(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repoName = registry.NormalizeLocalName(repoName)
|
||||
repoRefs, exists := store.Repositories[repoName]
|
||||
if !exists {
|
||||
repoRefs = Repository{}
|
||||
store.Repositories[repoName] = repoRefs
|
||||
} else if oldID, exists := repoRefs[digest]; exists && oldID != img.ID {
|
||||
return fmt.Errorf("Conflict: Digest %s is already set to image %s", digest, oldID)
|
||||
}
|
||||
|
||||
repoRefs[digest] = img.ID
|
||||
return store.save()
|
||||
}
|
||||
|
||||
func (store *TagStore) Get(repoName string) (Repository, error) {
|
||||
store.Lock()
|
||||
defer store.Unlock()
|
||||
|
@ -247,24 +299,29 @@ func (store *TagStore) Get(repoName string) (Repository, error) {
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
func (store *TagStore) GetImage(repoName, tagOrID string) (*image.Image, error) {
|
||||
func (store *TagStore) GetImage(repoName, refOrID string) (*image.Image, error) {
|
||||
repo, err := store.Get(repoName)
|
||||
store.Lock()
|
||||
defer store.Unlock()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if repo == nil {
|
||||
}
|
||||
if repo == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if revision, exists := repo[tagOrID]; exists {
|
||||
return store.graph.Get(revision)
|
||||
|
||||
store.Lock()
|
||||
defer store.Unlock()
|
||||
if imgID, exists := repo[refOrID]; exists {
|
||||
return store.graph.Get(imgID)
|
||||
}
|
||||
|
||||
// If no matching tag is found, search through images for a matching image id
|
||||
for _, revision := range repo {
|
||||
if strings.HasPrefix(revision, tagOrID) {
|
||||
if strings.HasPrefix(revision, refOrID) {
|
||||
return store.graph.Get(revision)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
@ -275,7 +332,7 @@ func (store *TagStore) GetRepoRefs() map[string][]string {
|
|||
for name, repository := range store.Repositories {
|
||||
for tag, id := range repository {
|
||||
shortID := common.TruncateID(id)
|
||||
reporefs[shortID] = append(reporefs[shortID], fmt.Sprintf("%s:%s", name, tag))
|
||||
reporefs[shortID] = append(reporefs[shortID], utils.ImageReference(name, tag))
|
||||
}
|
||||
}
|
||||
store.Unlock()
|
||||
|
@ -293,10 +350,10 @@ func validateRepoName(name string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Validate the name of a tag
|
||||
// ValidateTagName validates the name of a tag
|
||||
func ValidateTagName(name string) error {
|
||||
if name == "" {
|
||||
return fmt.Errorf("Tag name can't be empty")
|
||||
return fmt.Errorf("tag name can't be empty")
|
||||
}
|
||||
if !validTagName.MatchString(name) {
|
||||
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 {
|
|||
return nil
|
||||
}
|
||||
|
||||
func validateDigest(dgst string) error {
|
||||
if dgst == "" {
|
||||
return errors.New("digest can't be empty")
|
||||
}
|
||||
if !validDigest.MatchString(dgst) {
|
||||
return fmt.Errorf("illegal digest (%s): must be of the form [a-zA-Z0-9-_+.]+:[a-fA-F0-9]+", dgst)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *TagStore) poolAdd(kind, key string) (chan struct{}, error) {
|
||||
store.Lock()
|
||||
defer store.Unlock()
|
||||
|
|
|
@ -21,6 +21,8 @@ const (
|
|||
testPrivateImageName = "127.0.0.1:8000/privateapp"
|
||||
testPrivateImageID = "5bc255f8699e4ee89ac4469266c3d11515da88fdcbde45d7b069b636ff4efd81"
|
||||
testPrivateImageIDShort = "5bc255f8699e"
|
||||
testPrivateImageDigest = "sha256:bc8813ea7b3603864987522f02a76101c17ad122e1c46d790efc0fca78ca7bfb"
|
||||
testPrivateImageTag = "sometag"
|
||||
)
|
||||
|
||||
func fakeTar() (io.Reader, error) {
|
||||
|
@ -83,6 +85,9 @@ func mkTestTagStore(root string, t *testing.T) *TagStore {
|
|||
if err := store.Set(testPrivateImageName, "", testPrivateImageID, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store.SetDigest(testPrivateImageName, testPrivateImageDigest, testPrivateImageID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
|
@ -128,6 +133,10 @@ func TestLookupImage(t *testing.T) {
|
|||
"fail:fail",
|
||||
}
|
||||
|
||||
digestLookups := []string{
|
||||
testPrivateImageName + "@" + testPrivateImageDigest,
|
||||
}
|
||||
|
||||
for _, name := range officialLookups {
|
||||
if img, err := store.LookupImage(name); err != nil {
|
||||
t.Errorf("Error looking up %s: %s", name, err)
|
||||
|
@ -155,6 +164,16 @@ func TestLookupImage(t *testing.T) {
|
|||
t.Errorf("Expected 0 image, 1 found: %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
for _, name := range digestLookups {
|
||||
if img, err := store.LookupImage(name); err != nil {
|
||||
t.Errorf("Error looking up %s: %s", name, err)
|
||||
} else if img == nil {
|
||||
t.Errorf("Expected 1 image, none found: %s", name)
|
||||
} else if img.ID != testPrivateImageID {
|
||||
t.Errorf("Expected ID '%s' found '%s'", testPrivateImageID, img.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidTagName(t *testing.T) {
|
||||
|
@ -174,3 +193,24 @@ func TestInvalidTagName(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDigest(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expectError bool
|
||||
}{
|
||||
{"", true},
|
||||
{"latest", true},
|
||||
{"a:b", false},
|
||||
{"aZ0124-.+:bY852-_.+=", false},
|
||||
{"#$%#$^:$%^#$%", true},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
err := validateDigest(test.input)
|
||||
gotError := err != nil
|
||||
if e, a := test.expectError, gotError; e != a {
|
||||
t.Errorf("%d: with input %s, expected error=%t, got %t: %s", i, test.input, test.expectError, gotError, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
535
integration-cli/docker_cli_by_digest_test.go
Normal file
535
integration-cli/docker_cli_by_digest_test.go
Normal file
|
@ -0,0 +1,535 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
repoName = fmt.Sprintf("%v/dockercli/busybox-by-dgst", privateRegistryURL)
|
||||
digestRegex = regexp.MustCompile("Digest: ([^\n]+)")
|
||||
)
|
||||
|
||||
func setupImage() (string, error) {
|
||||
return setupImageWithTag("latest")
|
||||
}
|
||||
|
||||
func setupImageWithTag(tag string) (string, error) {
|
||||
containerName := "busyboxbydigest"
|
||||
|
||||
c := exec.Command(dockerBinary, "run", "-d", "-e", "digest=1", "--name", containerName, "busybox")
|
||||
if _, err := runCommand(c); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// tag the image to upload it to the private registry
|
||||
repoAndTag := utils.ImageReference(repoName, tag)
|
||||
c = exec.Command(dockerBinary, "commit", containerName, repoAndTag)
|
||||
if out, _, err := runCommandWithOutput(c); err != nil {
|
||||
return "", fmt.Errorf("image tagging failed: %s, %v", out, err)
|
||||
}
|
||||
defer deleteImages(repoAndTag)
|
||||
|
||||
// delete the container as we don't need it any more
|
||||
if err := deleteContainer(containerName); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// push the image
|
||||
c = exec.Command(dockerBinary, "push", repoAndTag)
|
||||
out, _, err := runCommandWithOutput(c)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("pushing the image to the private registry has failed: %s, %v", out, err)
|
||||
}
|
||||
|
||||
// delete our local repo that we previously tagged
|
||||
c = exec.Command(dockerBinary, "rmi", repoAndTag)
|
||||
if out, _, err := runCommandWithOutput(c); err != nil {
|
||||
return "", fmt.Errorf("error deleting images prior to real test: %s, %v", out, err)
|
||||
}
|
||||
|
||||
// the push output includes "Digest: <digest>", so find that
|
||||
matches := digestRegex.FindStringSubmatch(out)
|
||||
if len(matches) != 2 {
|
||||
return "", fmt.Errorf("unable to parse digest from push output: %s", out)
|
||||
}
|
||||
pushDigest := matches[1]
|
||||
|
||||
return pushDigest, nil
|
||||
}
|
||||
|
||||
func TestPullByTagDisplaysDigest(t *testing.T) {
|
||||
defer setupRegistry(t)()
|
||||
|
||||
pushDigest, err := setupImage()
|
||||
if err != nil {
|
||||
t.Fatalf("error setting up image: %v", err)
|
||||
}
|
||||
|
||||
// pull from the registry using the tag
|
||||
c := exec.Command(dockerBinary, "pull", repoName)
|
||||
out, _, err := runCommandWithOutput(c)
|
||||
if err != nil {
|
||||
t.Fatalf("error pulling by tag: %s, %v", out, err)
|
||||
}
|
||||
defer deleteImages(repoName)
|
||||
|
||||
// the pull output includes "Digest: <digest>", so find that
|
||||
matches := digestRegex.FindStringSubmatch(out)
|
||||
if len(matches) != 2 {
|
||||
t.Fatalf("unable to parse digest from pull output: %s", out)
|
||||
}
|
||||
pullDigest := matches[1]
|
||||
|
||||
// make sure the pushed and pull digests match
|
||||
if pushDigest != pullDigest {
|
||||
t.Fatalf("push digest %q didn't match pull digest %q", pushDigest, pullDigest)
|
||||
}
|
||||
|
||||
logDone("by_digest - pull by tag displays digest")
|
||||
}
|
||||
|
||||
func TestPullByDigest(t *testing.T) {
|
||||
defer setupRegistry(t)()
|
||||
|
||||
pushDigest, err := setupImage()
|
||||
if err != nil {
|
||||
t.Fatalf("error setting up image: %v", err)
|
||||
}
|
||||
|
||||
// pull from the registry using the <name>@<digest> reference
|
||||
imageReference := fmt.Sprintf("%s@%s", repoName, pushDigest)
|
||||
c := exec.Command(dockerBinary, "pull", imageReference)
|
||||
out, _, err := runCommandWithOutput(c)
|
||||
if err != nil {
|
||||
t.Fatalf("error pulling by digest: %s, %v", out, err)
|
||||
}
|
||||
defer deleteImages(imageReference)
|
||||
|
||||
// the pull output includes "Digest: <digest>", so find that
|
||||
matches := digestRegex.FindStringSubmatch(out)
|
||||
if len(matches) != 2 {
|
||||
t.Fatalf("unable to parse digest from pull output: %s", out)
|
||||
}
|
||||
pullDigest := matches[1]
|
||||
|
||||
// make sure the pushed and pull digests match
|
||||
if pushDigest != pullDigest {
|
||||
t.Fatalf("push digest %q didn't match pull digest %q", pushDigest, pullDigest)
|
||||
}
|
||||
|
||||
logDone("by_digest - pull by digest")
|
||||
}
|
||||
|
||||
func TestCreateByDigest(t *testing.T) {
|
||||
defer setupRegistry(t)()
|
||||
|
||||
pushDigest, err := setupImage()
|
||||
if err != nil {
|
||||
t.Fatalf("error setting up image: %v", err)
|
||||
}
|
||||
|
||||
imageReference := fmt.Sprintf("%s@%s", repoName, pushDigest)
|
||||
|
||||
containerName := "createByDigest"
|
||||
c := exec.Command(dockerBinary, "create", "--name", containerName, imageReference)
|
||||
out, _, err := runCommandWithOutput(c)
|
||||
if err != nil {
|
||||
t.Fatalf("error creating by digest: %s, %v", out, err)
|
||||
}
|
||||
defer deleteContainer(containerName)
|
||||
|
||||
res, err := inspectField(containerName, "Config.Image")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get Config.Image: %s, %v", out, err)
|
||||
}
|
||||
if res != imageReference {
|
||||
t.Fatalf("unexpected Config.Image: %s (expected %s)", res, imageReference)
|
||||
}
|
||||
|
||||
logDone("by_digest - create by digest")
|
||||
}
|
||||
|
||||
func TestRunByDigest(t *testing.T) {
|
||||
defer setupRegistry(t)()
|
||||
|
||||
pushDigest, err := setupImage()
|
||||
if err != nil {
|
||||
t.Fatalf("error setting up image: %v", err)
|
||||
}
|
||||
|
||||
imageReference := fmt.Sprintf("%s@%s", repoName, pushDigest)
|
||||
|
||||
containerName := "runByDigest"
|
||||
c := exec.Command(dockerBinary, "run", "--name", containerName, imageReference, "sh", "-c", "echo found=$digest")
|
||||
out, _, err := runCommandWithOutput(c)
|
||||
if err != nil {
|
||||
t.Fatalf("error run by digest: %s, %v", out, err)
|
||||
}
|
||||
defer deleteContainer(containerName)
|
||||
|
||||
foundRegex := regexp.MustCompile("found=([^\n]+)")
|
||||
matches := foundRegex.FindStringSubmatch(out)
|
||||
if len(matches) != 2 {
|
||||
t.Fatalf("error locating expected 'found=1' output: %s", out)
|
||||
}
|
||||
if matches[1] != "1" {
|
||||
t.Fatalf("Expected %q, got %q", "1", matches[1])
|
||||
}
|
||||
|
||||
res, err := inspectField(containerName, "Config.Image")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get Config.Image: %s, %v", out, err)
|
||||
}
|
||||
if res != imageReference {
|
||||
t.Fatalf("unexpected Config.Image: %s (expected %s)", res, imageReference)
|
||||
}
|
||||
|
||||
logDone("by_digest - run by digest")
|
||||
}
|
||||
|
||||
func TestRemoveImageByDigest(t *testing.T) {
|
||||
defer setupRegistry(t)()
|
||||
|
||||
digest, err := setupImage()
|
||||
if err != nil {
|
||||
t.Fatalf("error setting up image: %v", err)
|
||||
}
|
||||
|
||||
imageReference := fmt.Sprintf("%s@%s", repoName, digest)
|
||||
|
||||
// pull from the registry using the <name>@<digest> reference
|
||||
c := exec.Command(dockerBinary, "pull", imageReference)
|
||||
out, _, err := runCommandWithOutput(c)
|
||||
if err != nil {
|
||||
t.Fatalf("error pulling by digest: %s, %v", out, err)
|
||||
}
|
||||
|
||||
// make sure inspect runs ok
|
||||
if _, err := inspectField(imageReference, "Id"); err != nil {
|
||||
t.Fatalf("failed to inspect image: %v", err)
|
||||
}
|
||||
|
||||
// do the delete
|
||||
if err := deleteImages(imageReference); err != nil {
|
||||
t.Fatalf("unexpected error deleting image: %v", err)
|
||||
}
|
||||
|
||||
// try to inspect again - it should error this time
|
||||
if _, err := inspectField(imageReference, "Id"); err == nil {
|
||||
t.Fatalf("unexpected nil err trying to inspect what should be a non-existent image")
|
||||
} else if !strings.Contains(err.Error(), "No such image") {
|
||||
t.Fatalf("expected 'No such image' output, got %v", err)
|
||||
}
|
||||
|
||||
logDone("by_digest - remove image by digest")
|
||||
}
|
||||
|
||||
func TestBuildByDigest(t *testing.T) {
|
||||
defer setupRegistry(t)()
|
||||
|
||||
digest, err := setupImage()
|
||||
if err != nil {
|
||||
t.Fatalf("error setting up image: %v", err)
|
||||
}
|
||||
|
||||
imageReference := fmt.Sprintf("%s@%s", repoName, digest)
|
||||
|
||||
// pull from the registry using the <name>@<digest> reference
|
||||
c := exec.Command(dockerBinary, "pull", imageReference)
|
||||
out, _, err := runCommandWithOutput(c)
|
||||
if err != nil {
|
||||
t.Fatalf("error pulling by digest: %s, %v", out, err)
|
||||
}
|
||||
|
||||
// get the image id
|
||||
imageID, err := inspectField(imageReference, "Id")
|
||||
if err != nil {
|
||||
t.Fatalf("error getting image id: %v", err)
|
||||
}
|
||||
|
||||
// do the build
|
||||
name := "buildbydigest"
|
||||
defer deleteImages(name)
|
||||
_, err = buildImage(name, fmt.Sprintf(
|
||||
`FROM %s
|
||||
CMD ["/bin/echo", "Hello World"]`, imageReference),
|
||||
true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// get the build's image id
|
||||
res, err := inspectField(name, "Config.Image")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// make sure they match
|
||||
if res != imageID {
|
||||
t.Fatalf("Image %s, expected %s", res, imageID)
|
||||
}
|
||||
|
||||
logDone("by_digest - build by digest")
|
||||
}
|
||||
|
||||
func TestTagByDigest(t *testing.T) {
|
||||
defer setupRegistry(t)()
|
||||
|
||||
digest, err := setupImage()
|
||||
if err != nil {
|
||||
t.Fatalf("error setting up image: %v", err)
|
||||
}
|
||||
|
||||
imageReference := fmt.Sprintf("%s@%s", repoName, digest)
|
||||
|
||||
// pull from the registry using the <name>@<digest> reference
|
||||
c := exec.Command(dockerBinary, "pull", imageReference)
|
||||
out, _, err := runCommandWithOutput(c)
|
||||
if err != nil {
|
||||
t.Fatalf("error pulling by digest: %s, %v", out, err)
|
||||
}
|
||||
|
||||
// tag it
|
||||
tag := "tagbydigest"
|
||||
c = exec.Command(dockerBinary, "tag", imageReference, tag)
|
||||
if _, err := runCommand(c); err != nil {
|
||||
t.Fatalf("unexpected error tagging: %v", err)
|
||||
}
|
||||
|
||||
expectedID, err := inspectField(imageReference, "Id")
|
||||
if err != nil {
|
||||
t.Fatalf("error getting original image id: %v", err)
|
||||
}
|
||||
|
||||
tagID, err := inspectField(tag, "Id")
|
||||
if err != nil {
|
||||
t.Fatalf("error getting tagged image id: %v", err)
|
||||
}
|
||||
|
||||
if tagID != expectedID {
|
||||
t.Fatalf("expected image id %q, got %q", expectedID, tagID)
|
||||
}
|
||||
|
||||
logDone("by_digest - tag by digest")
|
||||
}
|
||||
|
||||
func TestListImagesWithoutDigests(t *testing.T) {
|
||||
defer setupRegistry(t)()
|
||||
|
||||
digest, err := setupImage()
|
||||
if err != nil {
|
||||
t.Fatalf("error setting up image: %v", err)
|
||||
}
|
||||
|
||||
imageReference := fmt.Sprintf("%s@%s", repoName, digest)
|
||||
|
||||
// pull from the registry using the <name>@<digest> reference
|
||||
c := exec.Command(dockerBinary, "pull", imageReference)
|
||||
out, _, err := runCommandWithOutput(c)
|
||||
if err != nil {
|
||||
t.Fatalf("error pulling by digest: %s, %v", out, err)
|
||||
}
|
||||
|
||||
c = exec.Command(dockerBinary, "images")
|
||||
out, _, err = runCommandWithOutput(c)
|
||||
if err != nil {
|
||||
t.Fatalf("error listing images: %s, %v", out, err)
|
||||
}
|
||||
|
||||
if strings.Contains(out, "DIGEST") {
|
||||
t.Fatalf("list output should not have contained DIGEST header: %s", out)
|
||||
}
|
||||
|
||||
logDone("by_digest - list images - digest header not displayed by default")
|
||||
}
|
||||
|
||||
func TestListImagesWithDigests(t *testing.T) {
|
||||
defer setupRegistry(t)()
|
||||
defer deleteImages(repoName+":tag1", repoName+":tag2")
|
||||
|
||||
// setup image1
|
||||
digest1, err := setupImageWithTag("tag1")
|
||||
if err != nil {
|
||||
t.Fatalf("error setting up image: %v", err)
|
||||
}
|
||||
imageReference1 := fmt.Sprintf("%s@%s", repoName, digest1)
|
||||
defer deleteImages(imageReference1)
|
||||
t.Logf("imageReference1 = %s", imageReference1)
|
||||
|
||||
// pull image1 by digest
|
||||
c := exec.Command(dockerBinary, "pull", imageReference1)
|
||||
out, _, err := runCommandWithOutput(c)
|
||||
if err != nil {
|
||||
t.Fatalf("error pulling by digest: %s, %v", out, err)
|
||||
}
|
||||
|
||||
// list images
|
||||
c = exec.Command(dockerBinary, "images", "--digests")
|
||||
out, _, err = runCommandWithOutput(c)
|
||||
if err != nil {
|
||||
t.Fatalf("error listing images: %s, %v", out, err)
|
||||
}
|
||||
|
||||
// make sure repo shown, tag=<none>, digest = $digest1
|
||||
re1 := regexp.MustCompile(`\s*` + repoName + `\s*<none>\s*` + digest1 + `\s`)
|
||||
if !re1.MatchString(out) {
|
||||
t.Fatalf("expected %q: %s", re1.String(), out)
|
||||
}
|
||||
|
||||
// setup image2
|
||||
digest2, err := setupImageWithTag("tag2")
|
||||
if err != nil {
|
||||
t.Fatalf("error setting up image: %v", err)
|
||||
}
|
||||
imageReference2 := fmt.Sprintf("%s@%s", repoName, digest2)
|
||||
defer deleteImages(imageReference2)
|
||||
t.Logf("imageReference2 = %s", imageReference2)
|
||||
|
||||
// pull image1 by digest
|
||||
c = exec.Command(dockerBinary, "pull", imageReference1)
|
||||
out, _, err = runCommandWithOutput(c)
|
||||
if err != nil {
|
||||
t.Fatalf("error pulling by digest: %s, %v", out, err)
|
||||
}
|
||||
|
||||
// pull image2 by digest
|
||||
c = exec.Command(dockerBinary, "pull", imageReference2)
|
||||
out, _, err = runCommandWithOutput(c)
|
||||
if err != nil {
|
||||
t.Fatalf("error pulling by digest: %s, %v", out, err)
|
||||
}
|
||||
|
||||
// list images
|
||||
c = exec.Command(dockerBinary, "images", "--digests")
|
||||
out, _, err = runCommandWithOutput(c)
|
||||
if err != nil {
|
||||
t.Fatalf("error listing images: %s, %v", out, err)
|
||||
}
|
||||
|
||||
// make sure repo shown, tag=<none>, digest = $digest1
|
||||
if !re1.MatchString(out) {
|
||||
t.Fatalf("expected %q: %s", re1.String(), out)
|
||||
}
|
||||
|
||||
// make sure repo shown, tag=<none>, digest = $digest2
|
||||
re2 := regexp.MustCompile(`\s*` + repoName + `\s*<none>\s*` + digest2 + `\s`)
|
||||
if !re2.MatchString(out) {
|
||||
t.Fatalf("expected %q: %s", re2.String(), out)
|
||||
}
|
||||
|
||||
// pull tag1
|
||||
c = exec.Command(dockerBinary, "pull", repoName+":tag1")
|
||||
out, _, err = runCommandWithOutput(c)
|
||||
if err != nil {
|
||||
t.Fatalf("error pulling tag1: %s, %v", out, err)
|
||||
}
|
||||
|
||||
// list images
|
||||
c = exec.Command(dockerBinary, "images", "--digests")
|
||||
out, _, err = runCommandWithOutput(c)
|
||||
if err != nil {
|
||||
t.Fatalf("error listing images: %s, %v", out, err)
|
||||
}
|
||||
|
||||
// make sure image 1 has repo, tag, <none> AND repo, <none>, digest
|
||||
reWithTag1 := regexp.MustCompile(`\s*` + repoName + `\s*tag1\s*<none>\s`)
|
||||
reWithDigest1 := regexp.MustCompile(`\s*` + repoName + `\s*<none>\s*` + digest1 + `\s`)
|
||||
if !reWithTag1.MatchString(out) {
|
||||
t.Fatalf("expected %q: %s", reWithTag1.String(), out)
|
||||
}
|
||||
if !reWithDigest1.MatchString(out) {
|
||||
t.Fatalf("expected %q: %s", reWithDigest1.String(), out)
|
||||
}
|
||||
// make sure image 2 has repo, <none>, digest
|
||||
if !re2.MatchString(out) {
|
||||
t.Fatalf("expected %q: %s", re2.String(), out)
|
||||
}
|
||||
|
||||
// pull tag 2
|
||||
c = exec.Command(dockerBinary, "pull", repoName+":tag2")
|
||||
out, _, err = runCommandWithOutput(c)
|
||||
if err != nil {
|
||||
t.Fatalf("error pulling tag2: %s, %v", out, err)
|
||||
}
|
||||
|
||||
// list images
|
||||
c = exec.Command(dockerBinary, "images", "--digests")
|
||||
out, _, err = runCommandWithOutput(c)
|
||||
if err != nil {
|
||||
t.Fatalf("error listing images: %s, %v", out, err)
|
||||
}
|
||||
|
||||
// make sure image 1 has repo, tag, digest
|
||||
if !reWithTag1.MatchString(out) {
|
||||
t.Fatalf("expected %q: %s", re1.String(), out)
|
||||
}
|
||||
|
||||
// make sure image 2 has repo, tag, digest
|
||||
reWithTag2 := regexp.MustCompile(`\s*` + repoName + `\s*tag2\s*<none>\s`)
|
||||
reWithDigest2 := regexp.MustCompile(`\s*` + repoName + `\s*<none>\s*` + digest2 + `\s`)
|
||||
if !reWithTag2.MatchString(out) {
|
||||
t.Fatalf("expected %q: %s", reWithTag2.String(), out)
|
||||
}
|
||||
if !reWithDigest2.MatchString(out) {
|
||||
t.Fatalf("expected %q: %s", reWithDigest2.String(), out)
|
||||
}
|
||||
|
||||
// list images
|
||||
c = exec.Command(dockerBinary, "images", "--digests")
|
||||
out, _, err = runCommandWithOutput(c)
|
||||
if err != nil {
|
||||
t.Fatalf("error listing images: %s, %v", out, err)
|
||||
}
|
||||
|
||||
// make sure image 1 has repo, tag, digest
|
||||
if !reWithTag1.MatchString(out) {
|
||||
t.Fatalf("expected %q: %s", re1.String(), out)
|
||||
}
|
||||
// make sure image 2 has repo, tag, digest
|
||||
if !reWithTag2.MatchString(out) {
|
||||
t.Fatalf("expected %q: %s", re2.String(), out)
|
||||
}
|
||||
// make sure busybox has tag, but not digest
|
||||
busyboxRe := regexp.MustCompile(`\s*busybox\s*latest\s*<none>\s`)
|
||||
if !busyboxRe.MatchString(out) {
|
||||
t.Fatalf("expected %q: %s", busyboxRe.String(), out)
|
||||
}
|
||||
|
||||
logDone("by_digest - list images with digests")
|
||||
}
|
||||
|
||||
func TestDeleteImageByIDOnlyPulledByDigest(t *testing.T) {
|
||||
defer setupRegistry(t)()
|
||||
|
||||
pushDigest, err := setupImage()
|
||||
if err != nil {
|
||||
t.Fatalf("error setting up image: %v", err)
|
||||
}
|
||||
|
||||
// pull from the registry using the <name>@<digest> reference
|
||||
imageReference := fmt.Sprintf("%s@%s", repoName, pushDigest)
|
||||
c := exec.Command(dockerBinary, "pull", imageReference)
|
||||
out, _, err := runCommandWithOutput(c)
|
||||
if err != nil {
|
||||
t.Fatalf("error pulling by digest: %s, %v", out, err)
|
||||
}
|
||||
// just in case...
|
||||
defer deleteImages(imageReference)
|
||||
|
||||
imageID, err := inspectField(imageReference, ".Id")
|
||||
if err != nil {
|
||||
t.Fatalf("error inspecting image id: %v", err)
|
||||
}
|
||||
|
||||
c = exec.Command(dockerBinary, "rmi", imageID)
|
||||
if _, err := runCommand(c); err != nil {
|
||||
t.Fatalf("error deleting image by id: %v", err)
|
||||
}
|
||||
|
||||
logDone("by_digest - delete image by id only pulled by digest")
|
||||
}
|
|
@ -17,7 +17,7 @@ func TestPushBusyboxImage(t *testing.T) {
|
|||
defer setupRegistry(t)()
|
||||
|
||||
repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL)
|
||||
// tag the image to upload it tot he private registry
|
||||
// tag the image to upload it to the private registry
|
||||
tagCmd := exec.Command(dockerBinary, "tag", "busybox", repoName)
|
||||
if out, _, err := runCommandWithOutput(tagCmd); err != nil {
|
||||
t.Fatalf("image tagging failed: %s, %v", out, err)
|
||||
|
|
|
@ -62,11 +62,17 @@ func ParseTCPAddr(addr string, defaultAddr string) (string, error) {
|
|||
return fmt.Sprintf("tcp://%s:%d", host, p), nil
|
||||
}
|
||||
|
||||
// Get a repos name and returns the right reposName + tag
|
||||
// Get a repos name and returns the right reposName + tag|digest
|
||||
// The tag can be confusing because of a port in a repository name.
|
||||
// Ex: localhost.localdomain:5000/samalba/hipache:latest
|
||||
// Digest ex: localhost:5000/foo/bar@sha256:bc8813ea7b3603864987522f02a76101c17ad122e1c46d790efc0fca78ca7bfb
|
||||
func ParseRepositoryTag(repos string) (string, string) {
|
||||
n := strings.LastIndex(repos, ":")
|
||||
n := strings.Index(repos, "@")
|
||||
if n >= 0 {
|
||||
parts := strings.Split(repos, "@")
|
||||
return parts[0], parts[1]
|
||||
}
|
||||
n = strings.LastIndex(repos, ":")
|
||||
if n < 0 {
|
||||
return repos, ""
|
||||
}
|
||||
|
|
|
@ -49,18 +49,27 @@ func TestParseRepositoryTag(t *testing.T) {
|
|||
if repo, tag := ParseRepositoryTag("root:tag"); repo != "root" || tag != "tag" {
|
||||
t.Errorf("Expected repo: '%s' and tag: '%s', got '%s' and '%s'", "root", "tag", repo, tag)
|
||||
}
|
||||
if repo, digest := ParseRepositoryTag("root@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); repo != "root" || digest != "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" {
|
||||
t.Errorf("Expected repo: '%s' and digest: '%s', got '%s' and '%s'", "root", "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", repo, digest)
|
||||
}
|
||||
if repo, tag := ParseRepositoryTag("user/repo"); repo != "user/repo" || tag != "" {
|
||||
t.Errorf("Expected repo: '%s' and tag: '%s', got '%s' and '%s'", "user/repo", "", repo, tag)
|
||||
}
|
||||
if repo, tag := ParseRepositoryTag("user/repo:tag"); repo != "user/repo" || tag != "tag" {
|
||||
t.Errorf("Expected repo: '%s' and tag: '%s', got '%s' and '%s'", "user/repo", "tag", repo, tag)
|
||||
}
|
||||
if repo, digest := ParseRepositoryTag("user/repo@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); repo != "user/repo" || digest != "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" {
|
||||
t.Errorf("Expected repo: '%s' and digest: '%s', got '%s' and '%s'", "user/repo", "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", repo, digest)
|
||||
}
|
||||
if repo, tag := ParseRepositoryTag("url:5000/repo"); repo != "url:5000/repo" || tag != "" {
|
||||
t.Errorf("Expected repo: '%s' and tag: '%s', got '%s' and '%s'", "url:5000/repo", "", repo, tag)
|
||||
}
|
||||
if repo, tag := ParseRepositoryTag("url:5000/repo:tag"); repo != "url:5000/repo" || tag != "tag" {
|
||||
t.Errorf("Expected repo: '%s' and tag: '%s', got '%s' and '%s'", "url:5000/repo", "tag", repo, tag)
|
||||
}
|
||||
if repo, digest := ParseRepositoryTag("url:5000/repo@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); repo != "url:5000/repo" || digest != "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" {
|
||||
t.Errorf("Expected repo: '%s' and digest: '%s', got '%s' and '%s'", "url:5000/repo", "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", repo, digest)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePortMapping(t *testing.T) {
|
||||
|
|
|
@ -12,6 +12,8 @@ import (
|
|||
"github.com/docker/docker/utils"
|
||||
)
|
||||
|
||||
const DockerDigestHeader = "Docker-Content-Digest"
|
||||
|
||||
func getV2Builder(e *Endpoint) *v2.URLBuilder {
|
||||
if e.URLBuilder == nil {
|
||||
e.URLBuilder = v2.NewURLBuilder(e.URL)
|
||||
|
@ -63,10 +65,10 @@ func (r *Session) GetV2Authorization(ep *Endpoint, imageName string, readOnly bo
|
|||
// 1.c) if anything else, err
|
||||
// 2) PUT the created/signed manifest
|
||||
//
|
||||
func (r *Session) GetV2ImageManifest(ep *Endpoint, imageName, tagName string, auth *RequestAuthorization) ([]byte, error) {
|
||||
func (r *Session) GetV2ImageManifest(ep *Endpoint, imageName, tagName string, auth *RequestAuthorization) ([]byte, string, error) {
|
||||
routeURL, err := getV2Builder(ep).BuildManifestURL(imageName, tagName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
method := "GET"
|
||||
|
@ -74,30 +76,30 @@ func (r *Session) GetV2ImageManifest(ep *Endpoint, imageName, tagName string, au
|
|||
|
||||
req, err := r.reqFactory.NewRequest(method, routeURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
if err := auth.Authorize(req); err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
res, _, err := r.doRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
if res.StatusCode == 401 {
|
||||
return nil, errLoginRequired
|
||||
return nil, "", errLoginRequired
|
||||
} else if res.StatusCode == 404 {
|
||||
return nil, ErrDoesNotExist
|
||||
return nil, "", ErrDoesNotExist
|
||||
}
|
||||
return nil, utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to fetch for %s:%s", res.StatusCode, imageName, tagName), res)
|
||||
return nil, "", utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to fetch for %s:%s", res.StatusCode, imageName, tagName), res)
|
||||
}
|
||||
|
||||
buf, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error while reading the http response: %s", err)
|
||||
return nil, "", fmt.Errorf("Error while reading the http response: %s", err)
|
||||
}
|
||||
return buf, nil
|
||||
return buf, res.Header.Get(DockerDigestHeader), nil
|
||||
}
|
||||
|
||||
// - Succeeded to head image blob (already exists)
|
||||
|
@ -261,41 +263,41 @@ func (r *Session) PutV2ImageBlob(ep *Endpoint, imageName, sumType, sumStr string
|
|||
}
|
||||
|
||||
// Finally Push the (signed) manifest of the blobs we've just pushed
|
||||
func (r *Session) PutV2ImageManifest(ep *Endpoint, imageName, tagName string, manifestRdr io.Reader, auth *RequestAuthorization) error {
|
||||
func (r *Session) PutV2ImageManifest(ep *Endpoint, imageName, tagName string, manifestRdr io.Reader, auth *RequestAuthorization) (string, error) {
|
||||
routeURL, err := getV2Builder(ep).BuildManifestURL(imageName, tagName)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
method := "PUT"
|
||||
log.Debugf("[registry] Calling %q %s", method, routeURL)
|
||||
req, err := r.reqFactory.NewRequest(method, routeURL, manifestRdr)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
if err := auth.Authorize(req); err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
res, _, err := r.doRequest(req)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
// All 2xx and 3xx responses can be accepted for a put.
|
||||
if res.StatusCode >= 400 {
|
||||
if res.StatusCode == 401 {
|
||||
return errLoginRequired
|
||||
return "", errLoginRequired
|
||||
}
|
||||
errBody, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
log.Debugf("Unexpected response from server: %q %#v", errBody, res.Header)
|
||||
return utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to push %s:%s manifest", res.StatusCode, imageName, tagName), res)
|
||||
return "", utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to push %s:%s manifest", res.StatusCode, imageName, tagName), res)
|
||||
}
|
||||
|
||||
return nil
|
||||
return res.Header.Get(DockerDigestHeader), nil
|
||||
}
|
||||
|
||||
type remoteTags struct {
|
||||
|
|
|
@ -17,3 +17,6 @@ var RepositoryNameRegexp = regexp.MustCompile(`(?:` + RepositoryNameComponentReg
|
|||
|
||||
// TagNameRegexp matches valid tag names. From docker/docker:graph/tags.go.
|
||||
var TagNameRegexp = regexp.MustCompile(`[\w][\w.-]{0,127}`)
|
||||
|
||||
// DigestRegexp matches valid digest types.
|
||||
var DigestRegexp = regexp.MustCompile(`[a-zA-Z0-9-_+.]+:[a-zA-Z0-9-_+.=]+`)
|
||||
|
|
|
@ -33,11 +33,11 @@ func Router() *mux.Router {
|
|||
Path("/v2/").
|
||||
Name(RouteNameBase)
|
||||
|
||||
// GET /v2/<name>/manifest/<tag> Image Manifest Fetch the image manifest identified by name and tag.
|
||||
// PUT /v2/<name>/manifest/<tag> Image Manifest Upload the image manifest identified by name and tag.
|
||||
// DELETE /v2/<name>/manifest/<tag> Image Manifest Delete the image identified by name and tag.
|
||||
// GET /v2/<name>/manifest/<reference> Image Manifest Fetch the image manifest identified by name and reference where reference can be a tag or digest.
|
||||
// PUT /v2/<name>/manifest/<reference> Image Manifest Upload the image manifest identified by name and reference where reference can be a tag or digest.
|
||||
// DELETE /v2/<name>/manifest/<reference> Image Manifest Delete the image identified by name and reference where reference can be a tag or digest.
|
||||
router.
|
||||
Path("/v2/{name:" + RepositoryNameRegexp.String() + "}/manifests/{tag:" + TagNameRegexp.String() + "}").
|
||||
Path("/v2/{name:" + RepositoryNameRegexp.String() + "}/manifests/{reference:" + TagNameRegexp.String() + "|" + DigestRegexp.String() + "}").
|
||||
Name(RouteNameManifest)
|
||||
|
||||
// GET /v2/<name>/tags/list Tags Fetch the tags under the repository identified by name.
|
||||
|
|
|
@ -55,16 +55,16 @@ func TestRouter(t *testing.T) {
|
|||
RouteName: RouteNameManifest,
|
||||
RequestURI: "/v2/foo/manifests/bar",
|
||||
Vars: map[string]string{
|
||||
"name": "foo",
|
||||
"tag": "bar",
|
||||
"name": "foo",
|
||||
"reference": "bar",
|
||||
},
|
||||
},
|
||||
{
|
||||
RouteName: RouteNameManifest,
|
||||
RequestURI: "/v2/foo/bar/manifests/tag",
|
||||
Vars: map[string]string{
|
||||
"name": "foo/bar",
|
||||
"tag": "tag",
|
||||
"name": "foo/bar",
|
||||
"reference": "tag",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -128,8 +128,8 @@ func TestRouter(t *testing.T) {
|
|||
RouteName: RouteNameManifest,
|
||||
RequestURI: "/v2/foo/bar/manifests/manifests/tags",
|
||||
Vars: map[string]string{
|
||||
"name": "foo/bar/manifests",
|
||||
"tag": "tags",
|
||||
"name": "foo/bar/manifests",
|
||||
"reference": "tags",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -74,11 +74,11 @@ func (ub *URLBuilder) BuildTagsURL(name string) (string, error) {
|
|||
return tagsURL.String(), nil
|
||||
}
|
||||
|
||||
// BuildManifestURL constructs a url for the manifest identified by name and tag.
|
||||
func (ub *URLBuilder) BuildManifestURL(name, tag string) (string, error) {
|
||||
// BuildManifestURL constructs a url for the manifest identified by name and reference.
|
||||
func (ub *URLBuilder) BuildManifestURL(name, reference string) (string, error) {
|
||||
route := ub.cloneRoute(RouteNameManifest)
|
||||
|
||||
manifestURL, err := route.URL("name", name, "tag", tag)
|
||||
manifestURL, err := route.URL("name", name, "reference", reference)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
|
@ -535,3 +535,20 @@ func (wc *WriteCounter) Write(p []byte) (count int, err error) {
|
|||
wc.Count += int64(count)
|
||||
return
|
||||
}
|
||||
|
||||
// ImageReference combines `repo` and `ref` and returns a string representing
|
||||
// the combination. If `ref` is a digest (meaning it's of the form
|
||||
// <algorithm>:<digest>, the returned string is <repo>@<ref>. Otherwise,
|
||||
// ref is assumed to be a tag, and the returned string is <repo>:<tag>.
|
||||
func ImageReference(repo, ref string) string {
|
||||
if DigestReference(ref) {
|
||||
return repo + "@" + ref
|
||||
}
|
||||
return repo + ":" + ref
|
||||
}
|
||||
|
||||
// DigestReference returns true if ref is a digest reference; i.e. if it
|
||||
// is of the form <algorithm>:<digest>.
|
||||
func DigestReference(ref string) bool {
|
||||
return strings.Contains(ref, ":")
|
||||
}
|
||||
|
|
|
@ -122,3 +122,33 @@ func TestWriteCounter(t *testing.T) {
|
|||
t.Error("Wrong message written")
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageReference(t *testing.T) {
|
||||
tests := []struct {
|
||||
repo string
|
||||
ref string
|
||||
expected string
|
||||
}{
|
||||
{"repo", "tag", "repo:tag"},
|
||||
{"repo", "sha256:c100b11b25d0cacd52c14e0e7bf525e1a4c0e6aec8827ae007055545909d1a64", "repo@sha256:c100b11b25d0cacd52c14e0e7bf525e1a4c0e6aec8827ae007055545909d1a64"},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
actual := ImageReference(test.repo, test.ref)
|
||||
if test.expected != actual {
|
||||
t.Errorf("%d: expected %q, got %q", i, test.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDigestReference(t *testing.T) {
|
||||
input := "sha256:c100b11b25d0cacd52c14e0e7bf525e1a4c0e6aec8827ae007055545909d1a64"
|
||||
if !DigestReference(input) {
|
||||
t.Errorf("Expected DigestReference=true for input %q", input)
|
||||
}
|
||||
|
||||
input = "latest"
|
||||
if DigestReference(input) {
|
||||
t.Errorf("Unexpected DigestReference=true for input %q", input)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue