diff --git a/api.go b/api.go index 0c06b2e6d1..61252ab9af 100644 --- a/api.go +++ b/api.go @@ -23,7 +23,7 @@ import ( ) const ( - APIVERSION = 1.6 + APIVERSION = 1.7 DEFAULTHTTPHOST = "127.0.0.1" DEFAULTHTTPPORT = 4243 DEFAULTUNIXSOCKET = "/var/run/docker.sock" @@ -191,10 +191,24 @@ func getImagesJSON(srv *Server, version float64, w http.ResponseWriter, r *http. return err } - return writeJSON(w, http.StatusOK, outs) + if version < 1.7 { + outs2 := []APIImagesOld{} + for _, ctnr := range outs { + outs2 = append(outs2, ctnr.ToLegacy()...) + } + + return writeJSON(w, http.StatusOK, outs2) + } else { + return writeJSON(w, http.StatusOK, outs) + } } func getImagesViz(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if version > 1.6 { + w.WriteHeader(http.StatusNotFound) + return fmt.Errorf("This is now implemented in the client.") + } + if err := srv.ImagesViz(w); err != nil { return err } diff --git a/api_params.go b/api_params.go index f666ac2393..e4508542ef 100644 --- a/api_params.go +++ b/api_params.go @@ -1,5 +1,7 @@ package docker +import "strings" + type APIHistory struct { ID string `json:"Id"` Tags []string `json:",omitempty"` @@ -9,6 +11,15 @@ type APIHistory struct { } type APIImages struct { + ID string `json:"Id"` + RepoTags []string `json:",omitempty"` + Created int64 + Size int64 + VirtualSize int64 + ParentId string `json:",omitempty"` +} + +type APIImagesOld struct { Repository string `json:",omitempty"` Tag string `json:",omitempty"` ID string `json:"Id"` @@ -17,6 +28,26 @@ type APIImages struct { VirtualSize int64 } +func (self *APIImages) ToLegacy() []APIImagesOld { + + outs := []APIImagesOld{} + for _, repotag := range self.RepoTags { + + components := strings.SplitN(repotag, ":", 2) + + outs = append(outs, APIImagesOld{ + ID: self.ID, + Repository: components[0], + Tag: components[1], + Created: self.Created, + Size: self.Size, + VirtualSize: self.VirtualSize, + }) + } + + return outs +} + type APIInfo struct { Debug bool Containers int diff --git a/api_test.go b/api_test.go index fcca8ce83f..0027362009 100644 --- a/api_test.go +++ b/api_test.go @@ -184,7 +184,7 @@ func TestGetImagesJSON(t *testing.T) { found := false for _, img := range images { - if img.Repository == unitTestImageName { + if strings.Contains(img.RepoTags[0], unitTestImageName) { found = true break } @@ -275,31 +275,6 @@ func TestGetImagesJSON(t *testing.T) { } } -func TestGetImagesViz(t *testing.T) { - runtime := mkRuntime(t) - defer nuke(runtime) - - srv := &Server{runtime: runtime} - - r := httptest.NewRecorder() - if err := getImagesViz(srv, APIVERSION, r, nil, nil); err != nil { - t.Fatal(err) - } - - if r.Code != http.StatusOK { - t.Fatalf("%d OK expected, received %d\n", http.StatusOK, r.Code) - } - - reader := bufio.NewReader(r.Body) - line, err := reader.ReadString('\n') - if err != nil { - t.Fatal(err) - } - if line != "digraph docker {\n" { - t.Errorf("Expected digraph docker {\n, %s found", line) - } -} - func TestGetImagesHistory(t *testing.T) { runtime := mkRuntime(t) defer nuke(runtime) @@ -1226,7 +1201,7 @@ func TestDeleteImages(t *testing.T) { t.Fatal(err) } - if len(images) != len(initialImages)+1 { + if len(images[0].RepoTags) != len(initialImages[0].RepoTags)+1 { t.Errorf("Expected %d images, %d found", len(initialImages)+1, len(images)) } @@ -1265,7 +1240,7 @@ func TestDeleteImages(t *testing.T) { t.Fatal(err) } - if len(images) != len(initialImages) { + if len(images[0].RepoTags) != len(initialImages[0].RepoTags) { t.Errorf("Expected %d image, %d found", len(initialImages), len(images)) } diff --git a/commands.go b/commands.go index 8a5ba8f0ed..d41f0f86b8 100644 --- a/commands.go +++ b/commands.go @@ -1057,6 +1057,7 @@ func (cli *DockerCli) CmdImages(args ...string) error { all := cmd.Bool("a", false, "show all images") noTrunc := cmd.Bool("notrunc", false, "Don't truncate output") flViz := cmd.Bool("viz", false, "output graph in graphviz format") + flTree := cmd.Bool("tree", false, "output graph in tree format") if err := cmd.Parse(args); err != nil { return nil @@ -1067,11 +1068,77 @@ func (cli *DockerCli) CmdImages(args ...string) error { } if *flViz { - body, _, err := cli.call("GET", "/images/viz", false) + body, _, err := cli.call("GET", "/images/json?all=1", nil) if err != nil { return err } - fmt.Fprintf(cli.out, "%s", body) + + var outs []APIImages + err = json.Unmarshal(body, &outs) + if err != nil { + return err + } + + fmt.Fprintf(cli.out, "digraph docker {\n") + + for _, image := range outs { + if image.ParentId == "" { + fmt.Fprintf(cli.out, " base -> \"%s\" [style=invis]\n", utils.TruncateID(image.ID)) + } else { + fmt.Fprintf(cli.out, " \"%s\" -> \"%s\"\n", utils.TruncateID(image.ParentId), utils.TruncateID(image.ID)) + } + if image.RepoTags[0] != ":" { + fmt.Fprintf(cli.out, " \"%s\" [label=\"%s\\n%s\",shape=box,fillcolor=\"paleturquoise\",style=\"filled,rounded\"];\n", utils.TruncateID(image.ID), utils.TruncateID(image.ID), strings.Join(image.RepoTags, "\\n")) + } + } + + fmt.Fprintf(cli.out, " base [style=invisible]\n}\n") + } else if *flTree { + body, _, err := cli.call("GET", "/images/json?all=1", nil) + if err != nil { + return err + } + + var outs []APIImages + err = json.Unmarshal(body, &outs) + if err != nil { + return err + } + + var startImageArg = cmd.Arg(0) + var startImage APIImages + + var roots []APIImages + var byParent = make(map[string][]APIImages) + for _, image := range outs { + if image.ParentId == "" { + roots = append(roots, image) + } else { + if children, exists := byParent[image.ParentId]; exists { + byParent[image.ParentId] = append(children, image) + } else { + byParent[image.ParentId] = []APIImages{image} + } + } + + if startImageArg != "" { + if startImageArg == image.ID || startImageArg == utils.TruncateID(image.ID) { + startImage = image + } + + for _, repotag := range image.RepoTags { + if repotag == startImageArg { + startImage = image + } + } + } + } + + if startImageArg != "" { + WalkTree(cli, noTrunc, []APIImages{startImage}, byParent, "") + } else { + WalkTree(cli, noTrunc, roots, byParent, "") + } } else { v := url.Values{} if cmd.NArg() == 1 { @@ -1097,27 +1164,29 @@ func (cli *DockerCli) CmdImages(args ...string) error { fmt.Fprintln(w, "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tSIZE") } + var repo string + var tag string for _, out := range outs { - if out.Repository == "" { - out.Repository = "" - } - if out.Tag == "" { - out.Tag = "" - } + for _, repotag := range out.RepoTags { - if !*noTrunc { - out.ID = utils.TruncateID(out.ID) - } + components := strings.SplitN(repotag, ":", 2) + repo = components[0] + tag = components[1] - if !*quiet { - fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t", out.Repository, out.Tag, out.ID, utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0)))) - if out.VirtualSize > 0 { - fmt.Fprintf(w, "%s (virtual %s)\n", utils.HumanSize(out.Size), utils.HumanSize(out.VirtualSize)) - } else { - fmt.Fprintf(w, "%s\n", utils.HumanSize(out.Size)) + if !*noTrunc { + out.ID = utils.TruncateID(out.ID) + } + + if !*quiet { + fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t", repo, tag, out.ID, utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0)))) + if out.VirtualSize > 0 { + fmt.Fprintf(w, "%s (virtual %s)\n", utils.HumanSize(out.Size), utils.HumanSize(out.VirtualSize)) + } else { + fmt.Fprintf(w, "%s\n", utils.HumanSize(out.Size)) + } + } else { + fmt.Fprintln(w, out.ID) } - } else { - fmt.Fprintln(w, out.ID) } } @@ -1128,6 +1197,48 @@ func (cli *DockerCli) CmdImages(args ...string) error { return nil } +func WalkTree(cli *DockerCli, noTrunc *bool, images []APIImages, byParent map[string][]APIImages, prefix string) { + if len(images) > 1 { + length := len(images) + for index, image := range images { + if index+1 == length { + PrintTreeNode(cli, noTrunc, image, prefix+"└─") + if subimages, exists := byParent[image.ID]; exists { + WalkTree(cli, noTrunc, subimages, byParent, prefix+" ") + } + } else { + PrintTreeNode(cli, noTrunc, image, prefix+"|─") + if subimages, exists := byParent[image.ID]; exists { + WalkTree(cli, noTrunc, subimages, byParent, prefix+"| ") + } + } + } + } else { + for _, image := range images { + PrintTreeNode(cli, noTrunc, image, prefix+"└─") + if subimages, exists := byParent[image.ID]; exists { + WalkTree(cli, noTrunc, subimages, byParent, prefix+" ") + } + } + } +} + +func PrintTreeNode(cli *DockerCli, noTrunc *bool, image APIImages, prefix string) { + var imageID string + if *noTrunc { + imageID = image.ID + } else { + imageID = utils.TruncateID(image.ID) + } + + fmt.Fprintf(cli.out, "%s%s Size: %s (virtual %s)", prefix, imageID, utils.HumanSize(image.Size), utils.HumanSize(image.VirtualSize)) + if image.RepoTags[0] != ":" { + fmt.Fprintf(cli.out, " Tags: %s\n", strings.Join(image.RepoTags, ",")) + } else { + fmt.Fprint(cli.out, "\n") + } +} + func displayablePorts(ports []APIPort) string { result := []string{} for _, port := range ports { diff --git a/commands_test.go b/commands_test.go index 1ec005bdd1..6c6a8e975b 100644 --- a/commands_test.go +++ b/commands_test.go @@ -6,6 +6,7 @@ import ( "github.com/dotcloud/docker/utils" "io" "io/ioutil" + "regexp" "strings" "testing" "time" @@ -699,3 +700,128 @@ func TestRunErrorBindNonExistingSource(t *testing.T) { <-c }) } + +func TestImagesViz(t *testing.T) { + stdout, stdoutPipe := io.Pipe() + + cli := NewDockerCli(nil, stdoutPipe, ioutil.Discard, testDaemonProto, testDaemonAddr) + defer cleanup(globalRuntime) + + srv := &Server{runtime: globalRuntime} + image := buildTestImages(t, srv) + + c := make(chan struct{}) + go func() { + defer close(c) + if err := cli.CmdImages("-viz"); err != nil { + t.Fatal(err) + } + stdoutPipe.Close() + }() + + setTimeout(t, "Reading command output time out", 2*time.Second, func() { + cmdOutputBytes, err := ioutil.ReadAll(bufio.NewReader(stdout)) + if err != nil { + t.Fatal(err) + } + cmdOutput := string(cmdOutputBytes) + + regexpStrings := []string{ + "digraph docker {", + fmt.Sprintf("base -> \"%s\" \\[style=invis]", unitTestImageIDShort), + fmt.Sprintf("label=\"%s\\\\n%s:latest\"", unitTestImageIDShort, unitTestImageName), + fmt.Sprintf("label=\"%s\\\\n%s:%s\"", utils.TruncateID(image.ID), "test", "latest"), + "base \\[style=invisible]", + } + + compiledRegexps := []*regexp.Regexp{} + for _, regexpString := range regexpStrings { + regexp, err := regexp.Compile(regexpString) + if err != nil { + fmt.Println("Error in regex string: ", err) + return + } + compiledRegexps = append(compiledRegexps, regexp) + } + + for _, regexp := range compiledRegexps { + if !regexp.MatchString(cmdOutput) { + t.Fatalf("images -viz content '%s' did not match regexp '%s'", cmdOutput, regexp) + } + } + }) +} + +func TestImagesTree(t *testing.T) { + stdout, stdoutPipe := io.Pipe() + + cli := NewDockerCli(nil, stdoutPipe, ioutil.Discard, testDaemonProto, testDaemonAddr) + defer cleanup(globalRuntime) + + srv := &Server{runtime: globalRuntime} + image := buildTestImages(t, srv) + + c := make(chan struct{}) + go func() { + defer close(c) + if err := cli.CmdImages("-tree"); err != nil { + t.Fatal(err) + } + stdoutPipe.Close() + }() + + setTimeout(t, "Reading command output time out", 2*time.Second, func() { + cmdOutputBytes, err := ioutil.ReadAll(bufio.NewReader(stdout)) + if err != nil { + t.Fatal(err) + } + cmdOutput := string(cmdOutputBytes) + + regexpStrings := []string{ + fmt.Sprintf("└─%s Size: (\\d+.\\d+ MB) \\(virtual \\d+.\\d+ MB\\) Tags: %s:latest", unitTestImageIDShort, unitTestImageName), + "(?m)^ └─[0-9a-f]+", + "(?m)^ └─[0-9a-f]+", + "(?m)^ └─[0-9a-f]+", + fmt.Sprintf(" └─%s Size: \\d+ B \\(virtual \\d+.\\d+ MB\\) Tags: test:latest", utils.TruncateID(image.ID)), + } + + compiledRegexps := []*regexp.Regexp{} + for _, regexpString := range regexpStrings { + regexp, err := regexp.Compile(regexpString) + if err != nil { + fmt.Println("Error in regex string: ", err) + return + } + compiledRegexps = append(compiledRegexps, regexp) + } + + for _, regexp := range compiledRegexps { + if !regexp.MatchString(cmdOutput) { + t.Fatalf("images -tree content '%s' did not match regexp '%s'", cmdOutput, regexp) + } + } + }) +} + +func buildTestImages(t *testing.T, srv *Server) *Image { + + var testBuilder = testContextTemplate{ + ` +from {IMAGE} +run sh -c 'echo root:testpass > /tmp/passwd' +run mkdir -p /var/run/sshd +run [ "$(cat /tmp/passwd)" = "root:testpass" ] +run [ "$(ls -d /var/run/sshd)" = "/var/run/sshd" ] +`, + nil, + nil, + } + image := buildImage(testBuilder, t, srv, true) + + err := srv.ContainerTag(image.ID, "test", "latest", false) + if err != nil { + t.Fatal(err) + } + + return image +} diff --git a/docs/sources/api/docker_remote_api.rst b/docs/sources/api/docker_remote_api.rst index 86cacd17d9..d5cb1a44a9 100644 --- a/docs/sources/api/docker_remote_api.rst +++ b/docs/sources/api/docker_remote_api.rst @@ -26,14 +26,118 @@ Docker Remote API 2. Versions =========== -The current version of the API is 1.6 +The current version of the API is 1.7 Calling /images//insert is the same as calling -/v1.6/images//insert +/v1.7/images//insert You can still call an old version of the api using /v1.0/images//insert +v1.7 +**** + +Full Documentation +------------------ + +:doc:`docker_remote_api_v1.7` + +What's new +---------- + +.. http:get:: /images/json + + The format of the json returned from this uri changed. Instead of an entry + for each repo/tag on an image, each image is only represented once, with a + nested attribute indicating the repo/tags that apply to that image. + + Instead of: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "VirtualSize": 131506275, + "Size": 131506275, + "Created": 1365714795, + "Id": "8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c", + "Tag": "12.04", + "Repository": "ubuntu" + }, + { + "VirtualSize": 131506275, + "Size": 131506275, + "Created": 1365714795, + "Id": "8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c", + "Tag": "latest", + "Repository": "ubuntu" + }, + { + "VirtualSize": 131506275, + "Size": 131506275, + "Created": 1365714795, + "Id": "8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c", + "Tag": "precise", + "Repository": "ubuntu" + }, + { + "VirtualSize": 180116135, + "Size": 24653, + "Created": 1364102658, + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Tag": "12.10", + "Repository": "ubuntu" + }, + { + "VirtualSize": 180116135, + "Size": 24653, + "Created": 1364102658, + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Tag": "quantal", + "Repository": "ubuntu" + } + ] + + The returned json looks like this: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "RepoTag": [ + "ubuntu:12.04", + "ubuntu:precise", + "ubuntu:latest" + ], + "Id": "8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c", + "Created": 1365714795, + "Size": 131506275, + "VirtualSize": 131506275 + }, + { + "RepoTag": [ + "ubuntu:12.10", + "ubuntu:quantal" + ], + "ParentId": "27cf784147099545", + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Created": 1364102658, + "Size": 24653, + "VirtualSize": 180116135 + } + ] + +.. http:get:: /images/viz + + This URI no longer exists. The ``images -viz`` output is now generated in + the client, using the ``/images/json`` data. + v1.6 **** diff --git a/docs/sources/api/docker_remote_api_v1.7.rst b/docs/sources/api/docker_remote_api_v1.7.rst new file mode 100644 index 0000000000..3985f7aa0a --- /dev/null +++ b/docs/sources/api/docker_remote_api_v1.7.rst @@ -0,0 +1,1185 @@ +:title: Remote API v1.7 +:description: API Documentation for Docker +:keywords: API, Docker, rcli, REST, documentation + +:orphan: + +====================== +Docker Remote API v1.7 +====================== + +.. contents:: Table of Contents + +1. Brief introduction +===================== + +- The Remote API is replacing rcli +- Default port in the docker daemon is 4243 +- The API tends to be REST, but for some complex commands, like attach or pull, the HTTP connection is hijacked to transport stdout stdin and stderr + +2. Endpoints +============ + +2.1 Containers +-------------- + +List containers +*************** + +.. http:get:: /containers/json + + List containers + + **Example request**: + + .. sourcecode:: http + + GET /containers/json?all=1&before=8dfafdbc3a40&size=1 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id": "8dfafdbc3a40", + "Image": "base:latest", + "Command": "echo 1", + "Created": 1367854155, + "Status": "Exit 0", + "Ports":[{"PrivatePort": 2222, "PublicPort": 3333, "Type": "tcp"}], + "SizeRw":12288, + "SizeRootFs":0 + }, + { + "Id": "9cd87474be90", + "Image": "base:latest", + "Command": "echo 222222", + "Created": 1367854155, + "Status": "Exit 0", + "Ports":[], + "SizeRw":12288, + "SizeRootFs":0 + }, + { + "Id": "3176a2479c92", + "Image": "base:latest", + "Command": "echo 3333333333333333", + "Created": 1367854154, + "Status": "Exit 0", + "Ports":[], + "SizeRw":12288, + "SizeRootFs":0 + }, + { + "Id": "4cb07b47f9fb", + "Image": "base:latest", + "Command": "echo 444444444444444444444444444444444", + "Created": 1367854152, + "Status": "Exit 0", + "Ports":[], + "SizeRw":12288, + "SizeRootFs":0 + } + ] + + :query all: 1/True/true or 0/False/false, Show all containers. Only running containers are shown by default + :query limit: Show ``limit`` last created containers, include non-running ones. + :query since: Show only containers created since Id, include non-running ones. + :query before: Show only containers created before Id, include non-running ones. + :query size: 1/True/true or 0/False/false, Show the containers sizes + :statuscode 200: no error + :statuscode 400: bad parameter + :statuscode 500: server error + + +Create a container +****************** + +.. http:post:: /containers/create + + Create a container + + **Example request**: + + .. sourcecode:: http + + POST /containers/create HTTP/1.1 + Content-Type: application/json + + { + "Hostname":"", + "User":"", + "Memory":0, + "MemorySwap":0, + "AttachStdin":false, + "AttachStdout":true, + "AttachStderr":true, + "PortSpecs":null, + "Privileged": false, + "Tty":false, + "OpenStdin":false, + "StdinOnce":false, + "Env":null, + "Cmd":[ + "date" + ], + "Dns":null, + "Image":"base", + "Volumes":{}, + "VolumesFrom":"", + "WorkingDir":"" + + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 201 OK + Content-Type: application/json + + { + "Id":"e90e34656806" + "Warnings":[] + } + + :jsonparam config: the container's configuration + :statuscode 201: no error + :statuscode 404: no such container + :statuscode 406: impossible to attach (container not running) + :statuscode 500: server error + + +Inspect a container +******************* + +.. http:get:: /containers/(id)/json + + Return low-level information on the container ``id`` + + **Example request**: + + .. sourcecode:: http + + GET /containers/4fa6e0f0c678/json HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Id": "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2", + "Created": "2013-05-07T14:51:42.041847+02:00", + "Path": "date", + "Args": [], + "Config": { + "Hostname": "4fa6e0f0c678", + "User": "", + "Memory": 0, + "MemorySwap": 0, + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "PortSpecs": null, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ + "date" + ], + "Dns": null, + "Image": "base", + "Volumes": {}, + "VolumesFrom": "", + "WorkingDir":"" + + }, + "State": { + "Running": false, + "Pid": 0, + "ExitCode": 0, + "StartedAt": "2013-05-07T14:51:42.087658+02:01360", + "Ghost": false + }, + "Image": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "NetworkSettings": { + "IpAddress": "", + "IpPrefixLen": 0, + "Gateway": "", + "Bridge": "", + "PortMapping": null + }, + "SysInitPath": "/home/kitty/go/src/github.com/dotcloud/docker/bin/docker", + "ResolvConfPath": "/etc/resolv.conf", + "Volumes": {} + } + + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error + + +List processes running inside a container +***************************************** + +.. http:get:: /containers/(id)/top + + List processes running inside the container ``id`` + + **Example request**: + + .. sourcecode:: http + + GET /containers/4fa6e0f0c678/top HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Titles":[ + "USER", + "PID", + "%CPU", + "%MEM", + "VSZ", + "RSS", + "TTY", + "STAT", + "START", + "TIME", + "COMMAND" + ], + "Processes":[ + ["root","20147","0.0","0.1","18060","1864","pts/4","S","10:06","0:00","bash"], + ["root","20271","0.0","0.0","4312","352","pts/4","S+","10:07","0:00","sleep","10"] + ] + } + + :query ps_args: ps arguments to use (eg. aux) + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Inspect changes on a container's filesystem +******************************************* + +.. http:get:: /containers/(id)/changes + + Inspect changes on container ``id`` 's filesystem + + **Example request**: + + .. sourcecode:: http + + GET /containers/4fa6e0f0c678/changes HTTP/1.1 + + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Path":"/dev", + "Kind":0 + }, + { + "Path":"/dev/kmsg", + "Kind":1 + }, + { + "Path":"/test", + "Kind":1 + } + ] + + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Export a container +****************** + +.. http:get:: /containers/(id)/export + + Export the contents of container ``id`` + + **Example request**: + + .. sourcecode:: http + + GET /containers/4fa6e0f0c678/export HTTP/1.1 + + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/octet-stream + + {{ STREAM }} + + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Start a container +***************** + +.. http:post:: /containers/(id)/start + + Start the container ``id`` + + **Example request**: + + .. sourcecode:: http + + POST /containers/(id)/start HTTP/1.1 + Content-Type: application/json + + { + "Binds":["/tmp:/tmp"], + "LxcConf":{"lxc.utsname":"docker"} + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 204 No Content + Content-Type: text/plain + + :jsonparam hostConfig: the container's host configuration (optional) + :statuscode 204: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Stop a container +**************** + +.. http:post:: /containers/(id)/stop + + Stop the container ``id`` + + **Example request**: + + .. sourcecode:: http + + POST /containers/e90e34656806/stop?t=5 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 204 OK + + :query t: number of seconds to wait before killing the container + :statuscode 204: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Restart a container +******************* + +.. http:post:: /containers/(id)/restart + + Restart the container ``id`` + + **Example request**: + + .. sourcecode:: http + + POST /containers/e90e34656806/restart?t=5 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 204 OK + + :query t: number of seconds to wait before killing the container + :statuscode 204: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Kill a container +**************** + +.. http:post:: /containers/(id)/kill + + Kill the container ``id`` + + **Example request**: + + .. sourcecode:: http + + POST /containers/e90e34656806/kill HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 204 OK + + :statuscode 204: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Attach to a container +********************* + +.. http:post:: /containers/(id)/attach + + Attach to the container ``id`` + + **Example request**: + + .. sourcecode:: http + + POST /containers/16253994b7c4/attach?logs=1&stream=0&stdout=1 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/vnd.docker.raw-stream + + {{ STREAM }} + + :query logs: 1/True/true or 0/False/false, return logs. Default false + :query stream: 1/True/true or 0/False/false, return stream. Default false + :query stdin: 1/True/true or 0/False/false, if stream=true, attach to stdin. Default false + :query stdout: 1/True/true or 0/False/false, if logs=true, return stdout log, if stream=true, attach to stdout. Default false + :query stderr: 1/True/true or 0/False/false, if logs=true, return stderr log, if stream=true, attach to stderr. Default false + :statuscode 200: no error + :statuscode 400: bad parameter + :statuscode 404: no such container + :statuscode 500: server error + + **Stream details**: + + When using the TTY setting is enabled in + :http:post:`/containers/create`, the stream is the raw data + from the process PTY and client's stdin. When the TTY is + disabled, then the stream is multiplexed to separate stdout + and stderr. + + The format is a **Header** and a **Payload** (frame). + + **HEADER** + + The header will contain the information on which stream write + the stream (stdout or stderr). It also contain the size of + the associated frame encoded on the last 4 bytes (uint32). + + It is encoded on the first 8 bytes like this:: + + header := [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4} + + ``STREAM_TYPE`` can be: + + - 0: stdin (will be writen on stdout) + - 1: stdout + - 2: stderr + + ``SIZE1, SIZE2, SIZE3, SIZE4`` are the 4 bytes of the uint32 size encoded as big endian. + + **PAYLOAD** + + The payload is the raw stream. + + **IMPLEMENTATION** + + The simplest way to implement the Attach protocol is the following: + + 1) Read 8 bytes + 2) chose stdout or stderr depending on the first byte + 3) Extract the frame size from the last 4 byets + 4) Read the extracted size and output it on the correct output + 5) Goto 1) + + + +Wait a container +**************** + +.. http:post:: /containers/(id)/wait + + Block until container ``id`` stops, then returns the exit code + + **Example request**: + + .. sourcecode:: http + + POST /containers/16253994b7c4/wait HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + {"StatusCode":0} + + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Remove a container +******************* + +.. http:delete:: /containers/(id) + + Remove the container ``id`` from the filesystem + + **Example request**: + + .. sourcecode:: http + + DELETE /containers/16253994b7c4?v=1 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 204 OK + + :query v: 1/True/true or 0/False/false, Remove the volumes associated to the container. Default false + :statuscode 204: no error + :statuscode 400: bad parameter + :statuscode 404: no such container + :statuscode 500: server error + + +Copy files or folders from a container +************************************** + +.. http:post:: /containers/(id)/copy + + Copy files or folders of container ``id`` + + **Example request**: + + .. sourcecode:: http + + POST /containers/4fa6e0f0c678/copy HTTP/1.1 + Content-Type: application/json + + { + "Resource":"test.txt" + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/octet-stream + + {{ STREAM }} + + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error + + +2.2 Images +---------- + +List Images +*********** + +.. http:get:: /images/json + + **Example request**: + + .. sourcecode:: http + + GET /images/json?all=0 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "RepoTag": [ + "ubuntu:12.04", + "ubuntu:precise", + "ubuntu:latest" + ], + "Id": "8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c", + "Created": 1365714795, + "Size": 131506275, + "VirtualSize": 131506275 + }, + { + "RepoTag": [ + "ubuntu:12.10", + "ubuntu:quantal" + ], + "ParentId": "27cf784147099545", + "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "Created": 1364102658, + "Size": 24653, + "VirtualSize": 180116135 + } + ] + + +Create an image +*************** + +.. http:post:: /images/create + + Create an image, either by pull it from the registry or by importing it + + **Example request**: + + .. sourcecode:: http + + POST /images/create?fromImage=base HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status":"Pulling..."} + {"status":"Pulling", "progress":"1/? (n/a)"} + {"error":"Invalid..."} + ... + + When using this endpoint to pull an image from the registry, + the ``X-Registry-Auth`` header can be used to include a + base64-encoded AuthConfig object. + + :query fromImage: name of the image to pull + :query fromSrc: source to import, - means stdin + :query repo: repository + :query tag: tag + :query registry: the registry to pull from + :statuscode 200: no error + :statuscode 500: server error + + +Insert a file in an image +************************* + +.. http:post:: /images/(name)/insert + + Insert a file from ``url`` in the image ``name`` at ``path`` + + **Example request**: + + .. sourcecode:: http + + POST /images/test/insert?path=/usr&url=myurl HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status":"Inserting..."} + {"status":"Inserting", "progress":"1/? (n/a)"} + {"error":"Invalid..."} + ... + + :statuscode 200: no error + :statuscode 500: server error + + +Inspect an image +**************** + +.. http:get:: /images/(name)/json + + Return low-level information on the image ``name`` + + **Example request**: + + .. sourcecode:: http + + GET /images/base/json HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "id":"b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "parent":"27cf784147099545", + "created":"2013-03-23T22:24:18.818426-07:00", + "container":"3d67245a8d72ecf13f33dffac9f79dcdf70f75acb84d308770391510e0c23ad0", + "container_config": + { + "Hostname":"", + "User":"", + "Memory":0, + "MemorySwap":0, + "AttachStdin":false, + "AttachStdout":false, + "AttachStderr":false, + "PortSpecs":null, + "Tty":true, + "OpenStdin":true, + "StdinOnce":false, + "Env":null, + "Cmd": ["/bin/bash"] + ,"Dns":null, + "Image":"base", + "Volumes":null, + "VolumesFrom":"", + "WorkingDir":"" + }, + "Size": 6824592 + } + + :statuscode 200: no error + :statuscode 404: no such image + :statuscode 500: server error + + +Get the history of an image +*************************** + +.. http:get:: /images/(name)/history + + Return the history of the image ``name`` + + **Example request**: + + .. sourcecode:: http + + GET /images/base/history HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id":"b750fe79269d", + "Created":1364102658, + "CreatedBy":"/bin/bash" + }, + { + "Id":"27cf78414709", + "Created":1364068391, + "CreatedBy":"" + } + ] + + :statuscode 200: no error + :statuscode 404: no such image + :statuscode 500: server error + + +Push an image on the registry +***************************** + +.. http:post:: /images/(name)/push + + Push the image ``name`` on the registry + + **Example request**: + + .. sourcecode:: http + + POST /images/test/push HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status":"Pushing..."} + {"status":"Pushing", "progress":"1/? (n/a)"} + {"error":"Invalid..."} + ... + + The ``X-Registry-Auth`` header can be used to include a + base64-encoded AuthConfig object. + + :query registry: the registry you wan to push, optional + :statuscode 200: no error + :statuscode 404: no such image + :statuscode 500: server error + + +Tag an image into a repository +****************************** + +.. http:post:: /images/(name)/tag + + Tag the image ``name`` into a repository + + **Example request**: + + .. sourcecode:: http + + POST /images/test/tag?repo=myrepo&force=0 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + + :query repo: The repository to tag in + :query force: 1/True/true or 0/False/false, default false + :statuscode 200: no error + :statuscode 400: bad parameter + :statuscode 404: no such image + :statuscode 409: conflict + :statuscode 500: server error + + +Remove an image +*************** + +.. http:delete:: /images/(name) + + Remove the image ``name`` from the filesystem + + **Example request**: + + .. sourcecode:: http + + DELETE /images/test HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-type: application/json + + [ + {"Untagged":"3e2f21a89f"}, + {"Deleted":"3e2f21a89f"}, + {"Deleted":"53b4f83ac9"} + ] + + :statuscode 200: no error + :statuscode 404: no such image + :statuscode 409: conflict + :statuscode 500: server error + + +Search images +************* + +.. http:get:: /images/search + + Search for an image in the docker index + + **Example request**: + + .. sourcecode:: http + + GET /images/search?term=sshd HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Name":"cespare/sshd", + "Description":"" + }, + { + "Name":"johnfuller/sshd", + "Description":"" + }, + { + "Name":"dhrp/mongodb-sshd", + "Description":"" + } + ] + + :query term: term to search + :statuscode 200: no error + :statuscode 500: server error + + +2.3 Misc +-------- + +Build an image from Dockerfile via stdin +**************************************** + +.. http:post:: /build + + Build an image from Dockerfile via stdin + + **Example request**: + + .. sourcecode:: http + + POST /build HTTP/1.1 + + {{ STREAM }} + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + + {{ STREAM }} + + + The stream must be a tar archive compressed with one of the following algorithms: + identity (no compression), gzip, bzip2, xz. The archive must include a file called + `Dockerfile` at its root. It may include any number of other files, which will be + accessible in the build context (See the ADD build command). + + The Content-type header should be set to "application/tar". + + :query t: repository name (and optionally a tag) to be applied to the resulting image in case of success + :query q: suppress verbose build output + :query nocache: do not use the cache when building the image + :statuscode 200: no error + :statuscode 500: server error + + +Check auth configuration +************************ + +.. http:post:: /auth + + Get the default username and email + + **Example request**: + + .. sourcecode:: http + + POST /auth HTTP/1.1 + Content-Type: application/json + + { + "username":"hannibal", + "password:"xxxx", + "email":"hannibal@a-team.com", + "serveraddress":"https://index.docker.io/v1/" + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + + :statuscode 200: no error + :statuscode 204: no error + :statuscode 500: server error + + +Display system-wide information +******************************* + +.. http:get:: /info + + Display system-wide information + + **Example request**: + + .. sourcecode:: http + + GET /info HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Containers":11, + "Images":16, + "Debug":false, + "NFd": 11, + "NGoroutines":21, + "MemoryLimit":true, + "SwapLimit":false, + "IPv4Forwarding":true + } + + :statuscode 200: no error + :statuscode 500: server error + + +Show the docker version information +*********************************** + +.. http:get:: /version + + Show the docker version information + + **Example request**: + + .. sourcecode:: http + + GET /version HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Version":"0.2.2", + "GitCommit":"5a2a5cc+CHANGES", + "GoVersion":"go1.0.3" + } + + :statuscode 200: no error + :statuscode 500: server error + + +Create a new image from a container's changes +********************************************* + +.. http:post:: /commit + + Create a new image from a container's changes + + **Example request**: + + .. sourcecode:: http + + POST /commit?container=44c004db4b17&m=message&repo=myrepo HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 201 OK + Content-Type: application/vnd.docker.raw-stream + + {"Id":"596069db4bf5"} + + :query container: source container + :query repo: repository + :query tag: tag + :query m: commit message + :query author: author (eg. "John Hannibal Smith ") + :query run: config automatically applied when the image is run. (ex: {"Cmd": ["cat", "/world"], "PortSpecs":["22"]}) + :statuscode 201: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Monitor Docker's events +*********************** + +.. http:get:: /events + + Get events from docker, either in real time via streaming, or via polling (using `since`) + + **Example request**: + + .. sourcecode:: http + + POST /events?since=1374067924 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + {"status":"create","id":"dfdf82bd3881","from":"base:latest","time":1374067924} + {"status":"start","id":"dfdf82bd3881","from":"base:latest","time":1374067924} + {"status":"stop","id":"dfdf82bd3881","from":"base:latest","time":1374067966} + {"status":"destroy","id":"dfdf82bd3881","from":"base:latest","time":1374067970} + + :query since: timestamp used for polling + :statuscode 200: no error + :statuscode 500: server error + + +3. Going further +================ + +3.1 Inside 'docker run' +----------------------- + +Here are the steps of 'docker run' : + +* Create the container +* If the status code is 404, it means the image doesn't exists: + * Try to pull it + * Then retry to create the container +* Start the container +* If you are not in detached mode: + * Attach to the container, using logs=1 (to have stdout and stderr from the container's start) and stream=1 +* If in detached mode or only stdin is attached: + * Display the container's id + + +3.2 Hijacking +------------- + +In this version of the API, /attach, uses hijacking to transport stdin, stdout and stderr on the same socket. This might change in the future. + +3.3 CORS Requests +----------------- + +To enable cross origin requests to the remote api add the flag "-api-enable-cors" when running docker in daemon mode. + +.. code-block:: bash + + docker -d -H="192.168.1.9:4243" -api-enable-cors + diff --git a/docs/sources/commandline/cli.rst b/docs/sources/commandline/cli.rst index 6c2826567b..6d56bccc38 100644 --- a/docs/sources/commandline/cli.rst +++ b/docs/sources/commandline/cli.rst @@ -315,8 +315,10 @@ Shell 1: (Again .. now showing events) List images -a=false: show all images + -notrunc=false: Don't truncate output -q=false: only show numeric IDs - -viz=false: output in graphviz format + -tree=false: output graph in tree format + -viz=false: output graph in graphviz format Displaying images visually ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -328,6 +330,36 @@ Displaying images visually .. image:: docker_images.gif :alt: Example inheritance graph of Docker images. + +Displaying image hierarchy +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + sudo docker images -tree + + |─8dbd9e392a96 Size: 131.5 MB (virtual 131.5 MB) Tags: ubuntu:12.04,ubuntu:latest,ubuntu:precise + └─27cf78414709 Size: 180.1 MB (virtual 180.1 MB) + └─b750fe79269d Size: 24.65 kB (virtual 180.1 MB) Tags: ubuntu:12.10,ubuntu:quantal + |─f98de3b610d5 Size: 12.29 kB (virtual 180.1 MB) + | └─7da80deb7dbf Size: 16.38 kB (virtual 180.1 MB) + | └─65ed2fee0a34 Size: 20.66 kB (virtual 180.2 MB) + | └─a2b9ea53dddc Size: 819.7 MB (virtual 999.8 MB) + | └─a29b932eaba8 Size: 28.67 kB (virtual 999.9 MB) + | └─e270a44f124d Size: 12.29 kB (virtual 999.9 MB) Tags: progrium/buildstep:latest + └─17e74ac162d8 Size: 53.93 kB (virtual 180.2 MB) + └─339a3f56b760 Size: 24.65 kB (virtual 180.2 MB) + └─904fcc40e34d Size: 96.7 MB (virtual 276.9 MB) + └─b1b0235328dd Size: 363.3 MB (virtual 640.2 MB) + └─7cb05d1acb3b Size: 20.48 kB (virtual 640.2 MB) + └─47bf6f34832d Size: 20.48 kB (virtual 640.2 MB) + └─f165104e82ed Size: 12.29 kB (virtual 640.2 MB) + └─d9cf85a47b7e Size: 1.911 MB (virtual 642.2 MB) + └─3ee562df86ca Size: 17.07 kB (virtual 642.2 MB) + └─b05fc2d00e4a Size: 24.96 kB (virtual 642.2 MB) + └─c96a99614930 Size: 12.29 kB (virtual 642.2 MB) + └─a6a357a48c49 Size: 12.29 kB (virtual 642.2 MB) Tags: ndj/mongodb:latest + .. _cli_import: ``import`` diff --git a/runtime_test.go b/runtime_test.go index 4e46b7b6d0..e61f63d495 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -22,6 +22,7 @@ import ( const ( unitTestImageName = "docker-test-image" unitTestImageID = "83599e29c455eb719f77d799bc7c51521b9551972f5a850d7ad265bc1b5292f6" // 1.0 + unitTestImageIDShort = "83599e29c455" unitTestNetworkBridge = "testdockbr0" unitTestStoreBase = "/var/lib/docker/unit-tests" testDaemonAddr = "127.0.0.1:4270" diff --git a/server.go b/server.go index cbe7c6435c..3441029027 100644 --- a/server.go +++ b/server.go @@ -247,7 +247,7 @@ func (srv *Server) ImagesViz(out io.Writer) error { for _, image := range images { parentImage, err = image.GetParent() if err != nil { - return err + return fmt.Errorf("Error while getting parent image: %v", err) } if parentImage != nil { out.Write([]byte(" \"" + parentImage.ShortID() + "\" -> \"" + image.ShortID() + "\"\n")) @@ -284,7 +284,7 @@ func (srv *Server) Images(all bool, filter string) ([]APIImages, error) { if err != nil { return nil, err } - outs := []APIImages{} //produce [] when empty instead of 'null' + lookup := make(map[string]APIImages) for name, repository := range srv.runtime.repositories.Repositories { if filter != "" { if match, _ := path.Match(filter, name); !match { @@ -292,27 +292,46 @@ func (srv *Server) Images(all bool, filter string) ([]APIImages, error) { } } for tag, id := range repository { - var out APIImages image, err := srv.runtime.graph.Get(id) if err != nil { log.Printf("Warning: couldn't load %s from %s/%s: %s", id, name, tag, err) continue } - delete(allImages, id) - out.Repository = name - out.Tag = tag - out.ID = image.ID - out.Created = image.Created.Unix() - out.Size = image.Size - out.VirtualSize = image.getParentsSize(0) + image.Size - outs = append(outs, out) + + if out, exists := lookup[id]; exists { + out.RepoTags = append(out.RepoTags, fmt.Sprintf("%s:%s", name, tag)) + + lookup[id] = out + } else { + var out APIImages + + delete(allImages, id) + + out.ParentId = image.Parent + out.RepoTags = []string{fmt.Sprintf("%s:%s", name, tag)} + out.ID = image.ID + out.Created = image.Created.Unix() + out.Size = image.Size + out.VirtualSize = image.getParentsSize(0) + image.Size + + lookup[id] = out + } + } } - // Display images which aren't part of a + + outs := make([]APIImages, 0, len(lookup)) + for _, value := range lookup { + outs = append(outs, value) + } + + // Display images which aren't part of a repository/tag if filter == "" { for _, image := range allImages { var out APIImages out.ID = image.ID + out.ParentId = image.Parent + out.RepoTags = []string{":"} out.Created = image.Created.Unix() out.Size = image.Size out.VirtualSize = image.getParentsSize(0) + image.Size diff --git a/server_test.go b/server_test.go index 7f9cbaadfd..4072344f35 100644 --- a/server_test.go +++ b/server_test.go @@ -34,8 +34,8 @@ func TestContainerTagImageDelete(t *testing.T) { t.Fatal(err) } - if len(images) != len(initialImages)+3 { - t.Errorf("Expected %d images, %d found", len(initialImages)+2, len(images)) + if len(images[0].RepoTags) != len(initialImages[0].RepoTags)+3 { + t.Errorf("Expected %d images, %d found", len(initialImages)+3, len(images)) } if _, err := srv.ImageDelete("utest/docker:tag2", true); err != nil { @@ -47,7 +47,7 @@ func TestContainerTagImageDelete(t *testing.T) { t.Fatal(err) } - if len(images) != len(initialImages)+2 { + if len(images[0].RepoTags) != len(initialImages[0].RepoTags)+2 { t.Errorf("Expected %d images, %d found", len(initialImages)+2, len(images)) } @@ -60,7 +60,7 @@ func TestContainerTagImageDelete(t *testing.T) { t.Fatal(err) } - if len(images) != len(initialImages)+1 { + if len(images[0].RepoTags) != len(initialImages[0].RepoTags)+1 { t.Errorf("Expected %d images, %d found", len(initialImages)+1, len(images)) } @@ -462,7 +462,7 @@ func TestRmi(t *testing.T) { if strings.Contains(unitTestImageID, image.ID) { continue } - if image.Repository == "" { + if image.RepoTags[0] == ":" { t.Fatalf("Expected tagged image, got untagged one.") } } @@ -490,7 +490,7 @@ func TestImagesFilter(t *testing.T) { t.Fatal(err) } - if len(images) != 2 { + if len(images[0].RepoTags) != 2 { t.Fatal("incorrect number of matches returned") } @@ -499,7 +499,7 @@ func TestImagesFilter(t *testing.T) { t.Fatal(err) } - if len(images) != 1 { + if len(images[0].RepoTags) != 1 { t.Fatal("incorrect number of matches returned") } @@ -508,7 +508,7 @@ func TestImagesFilter(t *testing.T) { t.Fatal(err) } - if len(images) != 1 { + if len(images[0].RepoTags) != 1 { t.Fatal("incorrect number of matches returned") } @@ -517,7 +517,7 @@ func TestImagesFilter(t *testing.T) { t.Fatal(err) } - if len(images) != 1 { + if len(images[0].RepoTags) != 1 { t.Fatal("incorrect number of matches returned") } } diff --git a/sorter.go b/sorter.go index d4331eaf1f..c9a86b45c0 100644 --- a/sorter.go +++ b/sorter.go @@ -25,7 +25,7 @@ func (s *imageSorter) Less(i, j int) bool { // Sort []ApiImages by most recent creation date and tag name. func sortImagesByCreationAndTag(images []APIImages) { creationAndTag := func(i1, i2 *APIImages) bool { - return i1.Created > i2.Created || (i1.Created == i2.Created && i2.Tag > i1.Tag) + return i1.Created > i2.Created } sorter := &imageSorter{ diff --git a/sorter_test.go b/sorter_test.go index d61b1a7112..54f647132f 100644 --- a/sorter_test.go +++ b/sorter_test.go @@ -3,6 +3,7 @@ package docker import ( "fmt" "testing" + "time" ) func TestServerListOrderedImagesByCreationDate(t *testing.T) { @@ -34,29 +35,46 @@ func TestServerListOrderedImagesByCreationDateAndTag(t *testing.T) { runtime := mkRuntime(t) defer nuke(runtime) - archive, err := fakeTar() + err := generateImage("bar", runtime) if err != nil { t.Fatal(err) } - image, err := runtime.graph.Create(archive, nil, "Testing", "", nil) + + time.Sleep(time.Second) + + err = generateImage("zed", runtime) if err != nil { t.Fatal(err) } srv := &Server{runtime: runtime} - srv.ContainerTag(image.ID, "repo", "foo", false) - srv.ContainerTag(image.ID, "repo", "bar", false) - images, err := srv.Images(true, "") if err != nil { t.Fatal(err) } - if images[0].Created != images[1].Created || images[0].Tag >= images[1].Tag { - t.Error("Expected []APIImges to be ordered by most recent creation date and tag name.") + if images[0].RepoTags[0] != "repo:zed" && images[0].RepoTags[0] != "repo:bar" { + t.Errorf("Expected []APIImges to be ordered by most recent creation date. %s", images) } } +func generateImage(name string, runtime *Runtime) error { + + archive, err := fakeTar() + if err != nil { + return err + } + image, err := runtime.graph.Create(archive, nil, "Testing", "", nil) + if err != nil { + return err + } + + srv := &Server{runtime: runtime} + srv.ContainerTag(image.ID, "repo", name, false) + + return nil +} + func TestSortUniquePorts(t *testing.T) { ports := []Port{ Port("6379/tcp"),