diff --git a/api.go b/api.go index ea7a6f1461..167ed1ad07 100644 --- a/api.go +++ b/api.go @@ -129,9 +129,9 @@ func getImages(srv *Server, w http.ResponseWriter, r *http.Request) ([]byte, err all := r.Form.Get("all") == "1" filter := r.Form.Get("filter") - quiet := r.Form.Get("quiet") == "1" + only_ids := r.Form.Get("only_ids") == "1" - outs, err := srv.Images(all, quiet, filter) + outs, err := srv.Images(all, only_ids, filter) if err != nil { return nil, err } @@ -201,14 +201,14 @@ func getContainers(srv *Server, w http.ResponseWriter, r *http.Request) ([]byte, return nil, err } all := r.Form.Get("all") == "1" - notrunc := r.Form.Get("notrunc") == "1" - quiet := r.Form.Get("quiet") == "1" - n, err := strconv.Atoi(r.Form.Get("n")) + trunc_cmd := r.Form.Get("trunc_cmd") != "0" + only_ids := r.Form.Get("only_ids") == "1" + n, err := strconv.Atoi(r.Form.Get("limit")) if err != nil { n = -1 } - outs := srv.Containers(all, notrunc, quiet, n) + outs := srv.Containers(all, trunc_cmd, only_ids, n) b, err := json.Marshal(outs) if err != nil { return nil, err @@ -540,13 +540,13 @@ func ListenAndServe(addr string, srv *Server) error { "/containers/{name:.*}/export": getContainersExport, "/images": getImages, "/info": getInfo, + "/images/search": getImagesSearch, "/images/{name:.*}/history": getImagesHistory, "/containers/{name:.*}/changes": getContainersChanges, "/containers/{name:.*}/port": getContainersPort, "/containers": getContainers, - "/images/search": getImagesSearch, - "/containers/{name:.*}": getContainersByName, - "/images/{name:.*}": getImagesByName, + "/images/{name:.*}/json": getImagesByName, + "/containers/{name:.*}/json": getContainersByName, }, "POST": { "/auth": postAuth, diff --git a/api_test.go b/api_test.go new file mode 100644 index 0000000000..d92eacf519 --- /dev/null +++ b/api_test.go @@ -0,0 +1,240 @@ +package docker + +import ( + "encoding/json" + "github.com/dotcloud/docker/auth" + "testing" +) + +func init() { + // Make it our Store root + runtime, err := NewRuntimeFromDirectory(unitTestStoreBase, false) + if err != nil { + panic(err) + } + + // Create the "Server" + srv := &Server{ + runtime: runtime, + } + go ListenAndServe("0.0.0.0:4243", srv) + +} + +func TestAuth(t *testing.T) { + var out auth.AuthConfig + + out.Username = "utest" + out.Password = "utest" + out.Email = "utest@yopmail.com" + + _, _, err := call("POST", "/auth", out) + if err != nil { + t.Fatal(err) + } + + out.Username = "" + out.Password = "" + out.Email = "" + + body, _, err := call("GET", "/auth", nil) + if err != nil { + t.Fatal(err) + } + + err = json.Unmarshal(body, &out) + if err != nil { + t.Fatal(err) + } + + if out.Username != "utest" { + t.Errorf("Expected username to be utest, %s found", out.Username) + } +} + +func TestVersion(t *testing.T) { + body, _, err := call("GET", "/version", nil) + if err != nil { + t.Fatal(err) + } + var out ApiVersion + err = json.Unmarshal(body, &out) + if err != nil { + t.Fatal(err) + } + if out.Version != VERSION { + t.Errorf("Excepted version %s, %s found", VERSION, out.Version) + } +} + +func TestImages(t *testing.T) { + body, _, err := call("GET", "/images?quiet=0&all=0", nil) + if err != nil { + t.Fatal(err) + } + var outs []ApiImages + err = json.Unmarshal(body, &outs) + if err != nil { + t.Fatal(err) + } + + if len(outs) != 1 { + t.Errorf("Excepted 1 image, %d found", len(outs)) + } + + if outs[0].Repository != "docker-ut" { + t.Errorf("Excepted image docker-ut, %s found", outs[0].Repository) + } +} + +func TestInfo(t *testing.T) { + body, _, err := call("GET", "/info", nil) + if err != nil { + t.Fatal(err) + } + var out ApiInfo + err = json.Unmarshal(body, &out) + if err != nil { + t.Fatal(err) + } + if out.Version != VERSION { + t.Errorf("Excepted version %s, %s found", VERSION, out.Version) + } +} + +func TestHistory(t *testing.T) { + body, _, err := call("GET", "/images/"+unitTestImageName+"/history", nil) + if err != nil { + t.Fatal(err) + } + var outs []ApiHistory + err = json.Unmarshal(body, &outs) + if err != nil { + t.Fatal(err) + } + if len(outs) != 1 { + t.Errorf("Excepted 1 line, %d found", len(outs)) + } +} + +func TestImagesSearch(t *testing.T) { + body, _, err := call("GET", "/images/search?term=redis", nil) + if err != nil { + t.Fatal(err) + } + var outs []ApiSearch + err = json.Unmarshal(body, &outs) + if err != nil { + t.Fatal(err) + } + if len(outs) < 2 { + t.Errorf("Excepted at least 2 lines, %d found", len(outs)) + } +} + +func TestGetImage(t *testing.T) { + obj, _, err := call("GET", "/images/"+unitTestImageName+"/json", nil) + if err != nil { + t.Fatal(err) + } + var out Image + err = json.Unmarshal(obj, &out) + if err != nil { + t.Fatal(err) + } + if out.Comment != "Imported from http://get.docker.io/images/busybox" { + t.Errorf("Error inspecting image") + } +} + +func TestCreateListStartStopRestartKillWaitDelete(t *testing.T) { + containers := testListContainers(t, -1) + for _, container := range containers { + testDeleteContainer(t, container.Id) + } + testCreateContainer(t) + id := testListContainers(t, 1)[0].Id + testContainerStart(t, id) + testContainerStop(t, id) + testContainerRestart(t, id) + testContainerKill(t, id) + testContainerWait(t, id) + testDeleteContainer(t, id) + testListContainers(t, 0) +} + +func testCreateContainer(t *testing.T) { + config, _, err := ParseRun([]string{unitTestImageName, "touch test"}, nil) + if err != nil { + t.Fatal(err) + } + _, _, err = call("POST", "/containers", *config) + if err != nil { + t.Fatal(err) + } +} + +func testListContainers(t *testing.T, expected int) []ApiContainers { + body, _, err := call("GET", "/containers?quiet=1&all=1", nil) + if err != nil { + t.Fatal(err) + } + var outs []ApiContainers + err = json.Unmarshal(body, &outs) + if err != nil { + t.Fatal(err) + } + if expected >= 0 && len(outs) != expected { + t.Errorf("Excepted %d container, %d found", expected, len(outs)) + } + return outs +} + +func testContainerStart(t *testing.T, id string) { + _, _, err := call("POST", "/containers/"+id+"/start", nil) + if err != nil { + t.Fatal(err) + } +} + +func testContainerRestart(t *testing.T, id string) { + _, _, err := call("POST", "/containers/"+id+"/restart?t=1", nil) + if err != nil { + t.Fatal(err) + } +} + +func testContainerStop(t *testing.T, id string) { + _, _, err := call("POST", "/containers/"+id+"/stop?t=1", nil) + if err != nil { + t.Fatal(err) + } +} + +func testContainerKill(t *testing.T, id string) { + _, _, err := call("POST", "/containers/"+id+"/kill", nil) + if err != nil { + t.Fatal(err) + } +} + +func testContainerWait(t *testing.T, id string) { + _, _, err := call("POST", "/containers/"+id+"/wait", nil) + if err != nil { + t.Fatal(err) + } +} + +func testDeleteContainer(t *testing.T, id string) { + _, _, err := call("DELETE", "/containers/"+id, nil) + if err != nil { + t.Fatal(err) + } +} + +func testContainerChanges(t *testing.T, id string) { + _, _, err := call("GET", "/containers/"+id+"/changes", nil) + if err != nil { + t.Fatal(err) + } +} diff --git a/commands.go b/commands.go index a19a37d5ff..96960455de 100644 --- a/commands.go +++ b/commands.go @@ -449,9 +449,9 @@ func CmdInspect(args ...string) error { cmd.Usage() return nil } - obj, _, err := call("GET", "/containers/"+cmd.Arg(0), nil) + obj, _, err := call("GET", "/containers/"+cmd.Arg(0)+"/json", nil) if err != nil { - obj, _, err = call("GET", "/images/"+cmd.Arg(0), nil) + obj, _, err = call("GET", "/images/"+cmd.Arg(0)+"/json", nil) if err != nil { return err } @@ -720,7 +720,7 @@ func CmdImages(args ...string) error { v.Set("filter", cmd.Arg(0)) } if *quiet { - v.Set("quiet", "1") + v.Set("only_ids", "1") } if *all { v.Set("all", "1") @@ -773,16 +773,16 @@ func CmdPs(args ...string) error { *last = 1 } if *quiet { - v.Set("quiet", "1") + v.Set("only_ids", "1") } if *all { v.Set("all", "1") } if *noTrunc { - v.Set("notrunc", "1") + v.Set("trunc_cmd", "0") } if *last != -1 { - v.Set("n", strconv.Itoa(*last)) + v.Set("limit", strconv.Itoa(*last)) } body, _, err := call("GET", "/containers?"+v.Encode(), nil) @@ -888,13 +888,13 @@ func CmdDiff(args ...string) error { return err } - var changes []string + var changes []Change err = json.Unmarshal(body, &changes) if err != nil { return err } for _, change := range changes { - fmt.Println(change) + fmt.Println(change.String()) } return nil } diff --git a/docs/sources/remote-api/api.rst b/docs/sources/remote-api/api.rst index 210e289f34..1a430aa010 100644 --- a/docs/sources/remote-api/api.rst +++ b/docs/sources/remote-api/api.rst @@ -28,7 +28,7 @@ List containers .. sourcecode:: http - GET /containers?notrunc=1&all=1&quiet=0 HTTP/1.1 + GET /containers?trunc_cmd=0&all=1&only_ids=0 HTTP/1.1 **Example response**: @@ -47,30 +47,30 @@ List containers { "Id": "9cd87474be90", "Image": "base:latest", - "Command": "echo 2", + "Command": "echo 222222", "Created": 1367854155, "Status": "Exit 0" }, { "Id": "3176a2479c92", "Image": "base:latest", - "Command": "echo 3", + "Command": "echo 3333333333333333", "Created": 1367854154, "Status": "Exit 0" }, { "Id": "4cb07b47f9fb", "Image": "base:latest", - "Command": "echo 4", + "Command": "echo 444444444444444444444444444444444", "Created": 1367854152, "Status": "Exit 0" } ] - :query quiet: 1 or 0, Only display numeric IDs. Not quiet by default + :query only_ids: 1 or 0, Only display numeric IDs. Default 0 :query all: 1 or 0, Show all containers. Only running containers are shown by default - :query notrunc: 1 or 0, Don't truncate output. Output is truncated by default - :query n: limit number, Show n last created containers, include non-running ones. + :query trunc_cmd: 1 or 0, Truncate output. Output is truncated by default + :query limit: Show ``limit`` last created containers, include non-running ones. :statuscode 200: no error :statuscode 500: server error @@ -123,14 +123,14 @@ Create a container :jsonparam config: the container's configuration :statuscode 200: no error - :statuscode 400: no such container + :statuscode 404: no such container :statuscode 500: server error Inspect a container ******************* -.. http:get:: /containers/(id) +.. http:get:: /containers/(id)/json Return low-level information on the container ``id`` @@ -138,7 +138,7 @@ Inspect a container .. sourcecode:: http - GET /containers/4fa6e0f0c678 HTTP/1.1 + GET /containers/4fa6e0f0c678/json HTTP/1.1 **Example response**: @@ -193,7 +193,7 @@ Inspect a container } :statuscode 200: no error - :statuscode 400: no such container + :statuscode 404: no such container :statuscode 500: server error @@ -218,12 +218,22 @@ Inspect changes on a container's filesystem HTTP/1.1 200 OK [ - "C /dev", - "A /dev/kmsg" + { + "Path":"/dev", + "Kind":0 + }, + { + "Path":"/dev/kmsg", + "Kind":1 + }, + { + "Path":"/test", + "Kind":1 + } ] :statuscode 200: no error - :statuscode 400: no such container + :statuscode 404: no such container :statuscode 500: server error @@ -251,7 +261,7 @@ Export a container {{ STREAM }} :statuscode 200: no error - :statuscode 400: no such container + :statuscode 404: no such container :statuscode 500: server error @@ -279,7 +289,7 @@ Map container's private ports :query port: the container private port you want to get :statuscode 200: no error - :statuscode 400: no such container + :statuscode 404: no such container :statuscode 500: server error @@ -303,7 +313,7 @@ Start a container HTTP/1.1 200 OK :statuscode 200: no error - :statuscode 400: no such container + :statuscode 404: no such container :statuscode 500: server error @@ -328,7 +338,7 @@ Stop a contaier :query t: number of seconds to wait before killing the container :statuscode 200: no error - :statuscode 400: no such container + :statuscode 404: no such container :statuscode 500: server error @@ -353,7 +363,7 @@ Restart a container :query t: number of seconds to wait before killing the container :statuscode 200: no error - :statuscode 400: no such container + :statuscode 404: no such container :statuscode 500: server error @@ -377,7 +387,7 @@ Kill a container HTTP/1.1 200 OK :statuscode 200: no error - :statuscode 400: no such container + :statuscode 404: no such container :statuscode 500: server error @@ -409,7 +419,7 @@ Attach to a container :query stdout: 1 or 0, if logs=1, return stdout log, if stream=1, attach to stdout. Default 0 :query stderr: 1 or 0, if logs=1, return stderr log, if stream=1, attach to stderr. Default 0 :statuscode 200: no error - :statuscode 400: no such container + :statuscode 404: no such container :statuscode 500: server error @@ -435,7 +445,7 @@ Wait a container {"StatusCode":0} :statuscode 200: no error - :statuscode 400: no such container + :statuscode 404: no such container :statuscode 500: server error @@ -460,7 +470,7 @@ Remove a container :query v: 1 or 0, Remove the volumes associated to the container. Default 0 :statuscode 200: no error - :statuscode 400: no such container + :statuscode 404: no such container :statuscode 500: server error @@ -478,7 +488,7 @@ List Images .. sourcecode:: http - GET /images?all=0&quiet=0 HTTP/1.1 + GET /images?all=0&only_ids=0 HTTP/1.1 **Example response**: @@ -501,7 +511,7 @@ List Images } ] - :query quiet: 1 or 0, Only display numeric IDs. Not quiet by default + :query only_ids: 1 or 0, Only display numeric IDs. Default 0 :query all: 1 or 0, Show all containers. Only running containers are shown by default :statuscode 200: no error :statuscode 500: server error @@ -537,10 +547,36 @@ Create an image :statuscode 200: no error :statuscode 500: server error + +Insert a file in a 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 + + {{ STREAM }} + + :statuscode 200: no error + :statuscode 500: server error + + Inspect an image **************** -.. http:get:: /images/(name) +.. http:get:: /images/(name)/json Return low-level information on the image ``name`` @@ -548,7 +584,7 @@ Inspect an image .. sourcecode:: http - GET /images/base HTTP/1.1 + GET /images/base/json HTTP/1.1 **Example response**: @@ -703,9 +739,73 @@ Remove an image :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 + + [ + { + "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 }} + + :statuscode 200: no error + :statuscode 500: server error + + Get default username and email ****************************** @@ -792,6 +892,7 @@ Display system-wide information :statuscode 200: no error :statuscode 500: server error + Show the docker version information *********************************** @@ -870,7 +971,8 @@ Here are the steps of 'docker run' : * 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 - * Call /wait to get the exit code and exit with it +* If in detached mode or only stdin is attached: + * Display the container's id 3.2 Hijacking diff --git a/server.go b/server.go index 7f91bf0196..7ba3b68f83 100644 --- a/server.go +++ b/server.go @@ -136,7 +136,7 @@ func (srv *Server) ImagesViz(out io.Writer) error { return nil } -func (srv *Server) Images(all, quiet bool, filter string) ([]ApiImages, error) { +func (srv *Server) Images(all, only_ids bool, filter string) ([]ApiImages, error) { var allImages map[string]*Image var err error if all { @@ -160,7 +160,7 @@ func (srv *Server) Images(all, quiet bool, filter string) ([]ApiImages, error) { continue } delete(allImages, id) - if !quiet { + if !only_ids { out.Repository = name out.Tag = tag out.Id = TruncateId(id) @@ -175,7 +175,7 @@ func (srv *Server) Images(all, quiet bool, filter string) ([]ApiImages, error) { if filter == "" { for id, image := range allImages { var out ApiImages - if !quiet { + if !only_ids { out.Repository = "" out.Tag = "" out.Id = TruncateId(id) @@ -228,17 +228,9 @@ func (srv *Server) ImageHistory(name string) ([]ApiHistory, error) { } -func (srv *Server) ContainerChanges(name string) ([]string, error) { +func (srv *Server) ContainerChanges(name string) ([]Change, error) { if container := srv.runtime.Get(name); container != nil { - changes, err := container.Changes() - if err != nil { - return nil, err - } - var changesStr []string - for _, name := range changes { - changesStr = append(changesStr, name.String()) - } - return changesStr, nil + return container.Changes() } return nil, fmt.Errorf("No such container: %s", name) } @@ -253,7 +245,7 @@ func (srv *Server) ContainerPort(name, privatePort string) (string, error) { return "", fmt.Errorf("No such container: %s", name) } -func (srv *Server) Containers(all, notrunc, quiet bool, n int) []ApiContainers { +func (srv *Server) Containers(all, trunc_cmd, only_ids bool, n int) []ApiContainers { var outs []ApiContainers = []ApiContainers{} //produce [] when empty instead of 'null' for i, container := range srv.runtime.List() { if !container.State.Running && !all && n == -1 { @@ -264,9 +256,9 @@ func (srv *Server) Containers(all, notrunc, quiet bool, n int) []ApiContainers { } var out ApiContainers out.Id = container.ShortId() - if !quiet { + if !only_ids { command := fmt.Sprintf("%s %s", container.Path, strings.Join(container.Args, " ")) - if !notrunc { + if trunc_cmd { command = Trunc(command, 20) } out.Image = srv.runtime.repositories.ImageName(container.Image) @@ -461,7 +453,7 @@ func (srv *Server) ImageDelete(name string) error { return fmt.Errorf("No such image: %s", name) } else { if err := srv.runtime.graph.Delete(img.Id); err != nil { - return fmt.Errorf("Error deleteing image %s: %s", name, err.Error()) + return fmt.Errorf("Error deleting image %s: %s", name, err.Error()) } } return nil