diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 71926f63340..bbf84e69698 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -60,8 +60,8 @@ }, { "ImportPath": "github.com/fsouza/go-dockerclient", - "Comment": "0.2.1-241-g0dbb508", - "Rev": "0dbb508e94dd899a6743d035d8f249c7634d26da" + "Comment": "0.2.1-267-g15d2c6e", + "Rev": "15d2c6e3eb670c545d0af0604d7f9aff3871af04" }, { "ImportPath": "github.com/golang/glog", diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/.travis.yml b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/.travis.yml index 24bbadba936..ad1dff7a4f1 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/.travis.yml +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/.travis.yml @@ -1,7 +1,7 @@ language: go go: - 1.1.2 - - 1.2 + - 1.2.2 - 1.3.1 - tip env: diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/AUTHORS b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/AUTHORS index 0f08b9c30da..bb79881406d 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/AUTHORS +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/AUTHORS @@ -5,9 +5,11 @@ Andreas Jaekle Andrews Medina Andy Goldstein Ben McCann +Carlos Diaz-Padron Cezar Sa Espinola Cheah Chu Yeow cheneydeng +CMGS Daniel, Dao Quang Minh David Huie Ed @@ -29,6 +31,7 @@ Paul Morie Peter Jihoon Kim Philippe Lafoucrière Rafe Colton +Robert Williamson Salvador Gironès Simon Eskildsen Simon Menke diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/README.markdown b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/README.markdown index 0b7e83c63da..66cedca6299 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/README.markdown +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/README.markdown @@ -30,7 +30,6 @@ func main() { fmt.Println("Size: ", img.Size) fmt.Println("VirtualSize: ", img.VirtualSize) fmt.Println("ParentId: ", img.ParentId) - fmt.Println("Repository: ", img.Repository) } } ``` diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/change.go b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/change.go index 79260731356..cafffadd7bb 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/change.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/change.go @@ -16,7 +16,7 @@ const ( // Change represents a change in a container. // -// See http://goo.gl/DpGyzK for more details. +// See http://goo.gl/QkW9sH for more details. type Change struct { Path string Kind ChangeType diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/client.go b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/client.go index 436695d959d..e68a9efb4bc 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/client.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/client.go @@ -4,7 +4,7 @@ // Package docker provides a client for the Docker remote API. // -// See http://goo.gl/mxyql for more details on the remote API. +// See http://goo.gl/G3plxW for more details on the remote API. package docker import ( @@ -356,20 +356,30 @@ func (c *Client) stream(method, path string, setRawTerminal, rawJSONStream bool, return nil } -func (c *Client) hijack(method, path string, success chan struct{}, setRawTerminal bool, in io.Reader, stderr, stdout io.Writer) error { +func (c *Client) hijack(method, path string, success chan struct{}, setRawTerminal bool, in io.Reader, stderr, stdout io.Writer, data interface{}) error { if path != "/version" && !c.SkipServerVersionCheck && c.expectedApiVersion == nil { err := c.checkApiVersion() if err != nil { return err } } + + var params io.Reader + if data != nil { + buf, err := json.Marshal(data) + if err != nil { + return err + } + params = bytes.NewBuffer(buf) + } + if stdout == nil { stdout = ioutil.Discard } if stderr == nil { stderr = ioutil.Discard } - req, err := http.NewRequest(method, c.getURL(path), nil) + req, err := http.NewRequest(method, c.getURL(path), params) if err != nil { return err } diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/container.go b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/container.go index 3e3556b7823..c649b5a2e0b 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/container.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/container.go @@ -18,7 +18,7 @@ import ( // ListContainersOptions specify parameters to the ListContainers function. // -// See http://goo.gl/QpCnDN for more details. +// See http://goo.gl/XqtcyU for more details. type ListContainersOptions struct { All bool Size bool @@ -51,7 +51,7 @@ type APIContainers struct { // ListContainers returns a slice of containers matching the given criteria. // -// See http://goo.gl/QpCnDN for more details. +// See http://goo.gl/XqtcyU for more details. func (c *Client) ListContainers(opts ListContainersOptions) ([]APIContainers, error) { path := "/containers/json?" + queryString(opts) body, _, err := c.do("GET", path, nil) @@ -161,6 +161,7 @@ type Config struct { Memory int64 `json:"Memory,omitempty" yaml:"Memory,omitempty"` MemorySwap int64 `json:"MemorySwap,omitempty" yaml:"MemorySwap,omitempty"` CpuShares int64 `json:"CpuShares,omitempty" yaml:"CpuShares,omitempty"` + CpuSet string `json:"CpuSet,omitempty" yaml:"CpuSet,omitempty"` AttachStdin bool `json:"AttachStdin,omitempty" yaml:"AttachStdin,omitempty"` AttachStdout bool `json:"AttachStdout,omitempty" yaml:"AttachStdout,omitempty"` AttachStderr bool `json:"AttachStderr,omitempty" yaml:"AttachStderr,omitempty"` @@ -208,7 +209,7 @@ type Container struct { // InspectContainer returns information about a container by its ID. // -// See http://goo.gl/2o52Sx for more details. +// See http://goo.gl/CxVuJ5 for more details. func (c *Client) InspectContainer(id string) (*Container, error) { path := "/containers/" + id + "/json" body, status, err := c.do("GET", path, nil) @@ -228,7 +229,7 @@ func (c *Client) InspectContainer(id string) (*Container, error) { // ContainerChanges returns changes in the filesystem of the given container. // -// See http://goo.gl/DpGyzK for more details. +// See http://goo.gl/QkW9sH for more details. func (c *Client) ContainerChanges(id string) ([]Change, error) { path := "/containers/" + id + "/changes" body, status, err := c.do("GET", path, nil) @@ -248,7 +249,7 @@ func (c *Client) ContainerChanges(id string) ([]Change, error) { // CreateContainerOptions specify parameters to the CreateContainer function. // -// See http://goo.gl/WPPYtB for more details. +// See http://goo.gl/mErxNp for more details. type CreateContainerOptions struct { Name string Config *Config `qs:"-"` @@ -257,7 +258,7 @@ type CreateContainerOptions struct { // CreateContainer creates a new container, returning the container instance, // or an error in case of failure. // -// See http://goo.gl/tjihUc for more details. +// See http://goo.gl/mErxNp for more details. func (c *Client) CreateContainer(opts CreateContainerOptions) (*Container, error) { path := "/containers/create?" + queryString(opts) body, status, err := c.do("POST", path, opts.Config) @@ -333,7 +334,7 @@ type HostConfig struct { // StartContainer starts a container, returning an error in case of failure. // -// See http://goo.gl/y5GZlE for more details. +// See http://goo.gl/iM5GYs for more details. func (c *Client) StartContainer(id string, hostConfig *HostConfig) error { if hostConfig == nil { hostConfig = &HostConfig{} @@ -355,7 +356,7 @@ func (c *Client) StartContainer(id string, hostConfig *HostConfig) error { // StopContainer stops a container, killing it after the given timeout (in // seconds). // -// See http://goo.gl/X2mj8t for more details. +// See http://goo.gl/EbcpXt for more details. func (c *Client) StopContainer(id string, timeout uint) error { path := fmt.Sprintf("/containers/%s/stop?t=%d", id, timeout) _, status, err := c.do("POST", path, nil) @@ -374,7 +375,7 @@ func (c *Client) StopContainer(id string, timeout uint) error { // RestartContainer stops a container, killing it after the given timeout (in // seconds), during the stop process. // -// See http://goo.gl/zms73Z for more details. +// See http://goo.gl/VOzR2n for more details. func (c *Client) RestartContainer(id string, timeout uint) error { path := fmt.Sprintf("/containers/%s/restart?t=%d", id, timeout) _, status, err := c.do("POST", path, nil) @@ -417,8 +418,43 @@ func (c *Client) UnpauseContainer(id string) error { return nil } +// TopResult represents the list of processes running in a container, as +// returned by /containers//top. +// +// See http://goo.gl/qu4gse for more details. +type TopResult struct { + Titles []string + Processes [][]string +} + +// TopContainer returns processes running inside a container +// +// See http://goo.gl/qu4gse for more details. +func (c *Client) TopContainer(id string, psArgs string) (TopResult, error) { + var args string + var result TopResult + if psArgs != "" { + args = fmt.Sprintf("?ps_args=%s", psArgs) + } + path := fmt.Sprintf("/containers/%s/top%s", id, args) + body, status, err := c.do("GET", path, nil) + if status == http.StatusNotFound { + return result, &NoSuchContainer{ID: id} + } + if err != nil { + return result, err + } + err = json.Unmarshal(body, &result) + if err != nil { + return result, err + } + return result, nil +} + // KillContainerOptions represents the set of options that can be used in a // call to KillContainer. +// +// See http://goo.gl/TFkECx for more details. type KillContainerOptions struct { // The ID of the container. ID string `qs:"-"` @@ -430,7 +466,7 @@ type KillContainerOptions struct { // KillContainer kills a container, returning an error in case of failure. // -// See http://goo.gl/DPbbBy for more details. +// See http://goo.gl/TFkECx for more details. func (c *Client) KillContainer(opts KillContainerOptions) error { path := "/containers/" + opts.ID + "/kill" + "?" + queryString(opts) _, status, err := c.do("POST", path, nil) @@ -444,6 +480,8 @@ func (c *Client) KillContainer(opts KillContainerOptions) error { } // RemoveContainerOptions encapsulates options to remove a container. +// +// See http://goo.gl/ZB83ji for more details. type RemoveContainerOptions struct { // The ID of the container. ID string `qs:"-"` @@ -459,7 +497,7 @@ type RemoveContainerOptions struct { // RemoveContainer removes a container, returning an error in case of failure. // -// See http://goo.gl/PBvGdU for more details. +// See http://goo.gl/ZB83ji for more details. func (c *Client) RemoveContainer(opts RemoveContainerOptions) error { path := "/containers/" + opts.ID + "?" + queryString(opts) _, status, err := c.do("DELETE", path, nil) @@ -475,7 +513,7 @@ func (c *Client) RemoveContainer(opts RemoveContainerOptions) error { // CopyFromContainerOptions is the set of options that can be used when copying // files or folders from a container. // -// See http://goo.gl/mnxRMl for more details. +// See http://goo.gl/rINMlw for more details. type CopyFromContainerOptions struct { OutputStream io.Writer `json:"-"` Container string `json:"-"` @@ -485,7 +523,7 @@ type CopyFromContainerOptions struct { // CopyFromContainer copy files or folders from a container, using a given // resource. // -// See http://goo.gl/mnxRMl for more details. +// See http://goo.gl/rINMlw for more details. func (c *Client) CopyFromContainer(opts CopyFromContainerOptions) error { if opts.Container == "" { return &NoSuchContainer{ID: opts.Container} @@ -505,7 +543,7 @@ func (c *Client) CopyFromContainer(opts CopyFromContainerOptions) error { // WaitContainer blocks until the given container stops, return the exit code // of the container status. // -// See http://goo.gl/gnHJL2 for more details. +// See http://goo.gl/J88DHU for more details. func (c *Client) WaitContainer(id string) (int, error) { body, status, err := c.do("POST", "/containers/"+id+"/wait", nil) if status == http.StatusNotFound { @@ -524,7 +562,7 @@ func (c *Client) WaitContainer(id string) (int, error) { // CommitContainerOptions aggregates parameters to the CommitContainer method. // -// See http://goo.gl/628gxm for more details. +// See http://goo.gl/Jn8pe8 for more details. type CommitContainerOptions struct { Container string Repository string `qs:"repo"` @@ -536,7 +574,7 @@ type CommitContainerOptions struct { // CommitContainer creates a new image from a container's changes. // -// See http://goo.gl/628gxm for more details. +// See http://goo.gl/Jn8pe8 for more details. func (c *Client) CommitContainer(opts CommitContainerOptions) (*Image, error) { path := "/commit?" + queryString(opts) body, status, err := c.do("POST", path, opts.Run) @@ -557,7 +595,7 @@ func (c *Client) CommitContainer(opts CommitContainerOptions) (*Image, error) { // AttachToContainerOptions is the set of options that can be used when // attaching to a container. // -// See http://goo.gl/oPzcqH for more details. +// See http://goo.gl/RRAhws for more details. type AttachToContainerOptions struct { Container string `qs:"-"` InputStream io.Reader `qs:"-"` @@ -592,13 +630,13 @@ type AttachToContainerOptions struct { // AttachToContainer attaches to a container, using the given options. // -// See http://goo.gl/oPzcqH for more details. +// See http://goo.gl/RRAhws for more details. func (c *Client) AttachToContainer(opts AttachToContainerOptions) error { if opts.Container == "" { return &NoSuchContainer{ID: opts.Container} } path := "/containers/" + opts.Container + "/attach?" + queryString(opts) - return c.hijack("POST", path, opts.Success, opts.RawTerminal, opts.InputStream, opts.ErrorStream, opts.OutputStream) + return c.hijack("POST", path, opts.Success, opts.RawTerminal, opts.InputStream, opts.ErrorStream, opts.OutputStream, nil) } // LogsOptions represents the set of options used when getting logs from a @@ -645,7 +683,7 @@ func (c *Client) ResizeContainerTTY(id string, height, width int) error { // ExportContainerOptions is the set of parameters to the ExportContainer // method. // -// See http://goo.gl/Lqk0FZ for more details. +// See http://goo.gl/hnzE62 for more details. type ExportContainerOptions struct { ID string OutputStream io.Writer @@ -654,7 +692,7 @@ type ExportContainerOptions struct { // ExportContainer export the contents of container id as tar archive // and prints the exported contents to stdout. // -// See http://goo.gl/Lqk0FZ for more details. +// See http://goo.gl/hnzE62 for more details. func (c *Client) ExportContainer(opts ExportContainerOptions) error { if opts.ID == "" { return &NoSuchContainer{ID: opts.ID} diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/container_test.go b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/container_test.go index f3e19542a88..d80270ba024 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/container_test.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/container_test.go @@ -1414,3 +1414,82 @@ func TestNeverRestart(t *testing.T) { t.Errorf("NeverRestart(): wrong MaximumRetryCount. Want 0. Got %d", policy.MaximumRetryCount) } } + +func TestTopContainer(t *testing.T) { + jsonTop := `{ + "Processes": [ + [ + "ubuntu", + "3087", + "815", + "0", + "01:44", + "?", + "00:00:00", + "cmd1" + ], + [ + "root", + "3158", + "3087", + "0", + "01:44", + "?", + "00:00:01", + "cmd2" + ] + ], + "Titles": [ + "UID", + "PID", + "PPID", + "C", + "STIME", + "TTY", + "TIME", + "CMD" + ] +}` + var expected TopResult + err := json.Unmarshal([]byte(jsonTop), &expected) + if err != nil { + t.Fatal(err) + } + id := "4fa6e0f0" + fakeRT := &FakeRoundTripper{message: jsonTop, status: http.StatusOK} + client := newTestClient(fakeRT) + processes, err := client.TopContainer(id, "") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(processes, expected) { + t.Errorf("TopContainer: Expected %#v. Got %#v.", expected, processes) + } + if len(processes.Processes) != 2 || len(processes.Processes[0]) != 8 || + processes.Processes[0][7] != "cmd1" { + t.Errorf("TopContainer: Process list to include cmd1. Got %#v.", expected, processes) + } + expectedURI := "/containers/" + id + "/top" + if !strings.HasSuffix(fakeRT.requests[0].URL.String(), expectedURI) { + t.Errorf("TopContainer: Expected URI to have %q. Got %q.", expectedURI, fakeRT.requests[0].URL.String()) + } +} + +func TestTopContainerNotFound(t *testing.T) { + client := newTestClient(&FakeRoundTripper{message: "no such container", status: http.StatusNotFound}) + _, err := client.TopContainer("abef348", "") + expected := &NoSuchContainer{ID: "abef348"} + if !reflect.DeepEqual(err, expected) { + t.Errorf("StopContainer: Wrong error returned. Want %#v. Got %#v.", expected, err) + } +} + +func TestTopContainerWithPsArgs(t *testing.T) { + fakeRT := &FakeRoundTripper{message: "no such container", status: http.StatusNotFound} + client := newTestClient(fakeRT) + client.TopContainer("abef348", "aux") + expectedURI := "/containers/abef348/top?ps_args=aux" + if !strings.HasSuffix(fakeRT.requests[0].URL.String(), expectedURI) { + t.Errorf("TopContainer: Expected URI to have %q. Got %q.", expectedURI, fakeRT.requests[0].URL.String()) + } +} diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/exec.go b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/exec.go new file mode 100644 index 00000000000..62a0a80cd6e --- /dev/null +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/exec.go @@ -0,0 +1,126 @@ +// Copyright 2014 go-dockerclient authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Docs can currently be found at https://github.com/docker/docker/blob/master/docs/sources/reference/api/docker_remote_api_v1.15.md#exec-create + +package docker + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strconv" +) + +// CreateExecOptions specify parameters to the CreateExecContainer function. +// +// See http://goo.gl/8izrzI for more details +type CreateExecOptions struct { + AttachStdin bool `json:"AttachStdin,omitempty" yaml:"AttachStdin,omitempty"` + AttachStdout bool `json:"AttachStdout,omitempty" yaml:"AttachStdout,omitempty"` + AttachStderr bool `json:"AttachStderr,omitempty" yaml:"AttachStderr,omitempty"` + Tty bool `json:"Tty,omitempty" yaml:"Tty,omitempty"` + Cmd []string `json:"Cmd,omitempty" yaml:"Cmd,omitempty"` + Container string `json:"Container,omitempty" yaml:"Container,omitempty"` +} + +// StartExecOptions specify parameters to the StartExecContainer function. +// +// See http://goo.gl/JW8Lxl for more details +type StartExecOptions struct { + Detach bool `json:"Detach,omitempty" yaml:"Detach,omitempty"` + + Tty bool `json:"Tty,omitempty" yaml:"Tty,omitempty"` + + InputStream io.Reader `qs:"-"` + OutputStream io.Writer `qs:"-"` + ErrorStream io.Writer `qs:"-"` + + // Use raw terminal? Usually true when the container contains a TTY. + RawTerminal bool `qs:"-"` + + // If set, after a successful connect, a sentinel will be sent and then the + // client will block on receive before continuing. + // + // It must be an unbuffered channel. Using a buffered channel can lead + // to unexpected behavior. + Success chan struct{} `json:"-"` +} + +type Exec struct { + Id string `json:"Id,omitempty" yaml:"Id,omitempty"` +} + +// CreateExec sets up an exec instance in a running container `id`, returning the exec +// instance, or an error in case of failure. +// +// See http://goo.gl/8izrzI for more details +func (c *Client) CreateExec(opts CreateExecOptions) (*Exec, error) { + path := fmt.Sprintf("/containers/%s/exec", opts.Container) + body, status, err := c.do("POST", path, opts) + if status == http.StatusNotFound { + return nil, &NoSuchContainer{ID: opts.Container} + } + if err != nil { + return nil, err + } + var exec Exec + err = json.Unmarshal(body, &exec) + if err != nil { + return nil, err + } + + return &exec, nil +} + +// Starts a previously set up exec instance id. If opts.Detach is true, it returns +// after starting the exec command. Otherwise, it sets up an interactive session +// with the exec command. +// +// See http://goo.gl/JW8Lxl for more details +func (c *Client) StartExec(id string, opts StartExecOptions) error { + if id == "" { + return &NoSuchExec{ID: id} + } + + path := fmt.Sprintf("/exec/%s/start", id) + + if opts.Detach { + _, status, err := c.do("POST", path, opts) + if status == http.StatusNotFound { + return &NoSuchExec{ID: id} + } + if err != nil { + return err + } + return nil + } + + return c.hijack("POST", path, opts.Success, opts.RawTerminal, opts.InputStream, opts.ErrorStream, opts.OutputStream, opts) +} + +// Resizes the tty session used by the exec command id. This API is valid only +// if Tty was specified as part of creating and starting the exec command. +// +// See http://goo.gl/YDSx1f for more details +func (c *Client) ResizeExecTTY(id string, height, width int) error { + params := make(url.Values) + params.Set("h", strconv.Itoa(height)) + params.Set("w", strconv.Itoa(width)) + + path := fmt.Sprintf("/exec/%s/resize?%s", id, params.Encode()) + _, _, err := c.do("POST", path, nil) + return err +} + +// NoSuchExec is the error returned when a given exec instance does not exist. +type NoSuchExec struct { + ID string +} + +func (err *NoSuchExec) Error() string { + return "No such exec instance: " + err.ID +} diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/exec_test.go b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/exec_test.go new file mode 100644 index 00000000000..70fa64b4cee --- /dev/null +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/exec_test.go @@ -0,0 +1,128 @@ +// Copyright 2014 go-dockerclient authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package docker + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" +) + +func TestExecCreate(t *testing.T) { + jsonContainer := `{"Id": "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2"}` + var expected struct{ Id string } + err := json.Unmarshal([]byte(jsonContainer), &expected) + if err != nil { + t.Fatal(err) + } + fakeRT := &FakeRoundTripper{message: jsonContainer, status: http.StatusOK} + client := newTestClient(fakeRT) + config := CreateExecOptions{ + Container: "test", + AttachStdin: true, + AttachStdout: true, + AttachStderr: false, + Tty: false, + Cmd: []string{"touch", "/tmp/file"}, + } + execObj, err := client.CreateExec(config) + if err != nil { + t.Fatal(err) + } + expectedId := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2" + if execObj.Id != expectedId { + t.Errorf("ExecCreate: wrong ID. Want %q. Got %q.", expectedId, execObj.Id) + } + req := fakeRT.requests[0] + if req.Method != "POST" { + t.Errorf("ExecCreate: wrong HTTP method. Want %q. Got %q.", "POST", req.Method) + } + expectedURL, _ := url.Parse(client.getURL("/containers/test/exec")) + if gotPath := req.URL.Path; gotPath != expectedURL.Path { + t.Errorf("ExecCreate: Wrong path in request. Want %q. Got %q.", expectedURL.Path, gotPath) + } + var gotBody struct{ Id string } + err = json.NewDecoder(req.Body).Decode(&gotBody) + if err != nil { + t.Fatal(err) + } +} + +func TestExecStartDetached(t *testing.T) { + execId := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2" + fakeRT := &FakeRoundTripper{status: http.StatusOK} + client := newTestClient(fakeRT) + config := StartExecOptions{ + Detach: true, + } + err := client.StartExec(execId, config) + if err != nil { + t.Fatal(err) + } + req := fakeRT.requests[0] + if req.Method != "POST" { + t.Errorf("ExecStart: wrong HTTP method. Want %q. Got %q.", "POST", req.Method) + } + expectedURL, _ := url.Parse(client.getURL("/exec/" + execId + "/start")) + if gotPath := req.URL.Path; gotPath != expectedURL.Path { + t.Errorf("ExecCreate: Wrong path in request. Want %q. Got %q.", expectedURL.Path, gotPath) + } + t.Log(req.Body) + var gotBody struct{ Detach bool } + err = json.NewDecoder(req.Body).Decode(&gotBody) + if err != nil { + t.Fatal(err) + } + if !gotBody.Detach { + t.Fatal("Expected Detach in StartExecOptions to be true") + } +} + +func TestExecStartAndAttach(t *testing.T) { + var reader = strings.NewReader("send value") + var req http.Request + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte{1, 0, 0, 0, 0, 0, 0, 5}) + w.Write([]byte("hello")) + req = *r + })) + defer server.Close() + client, _ := NewClient(server.URL) + client.SkipServerVersionCheck = true + var stdout, stderr bytes.Buffer + success := make(chan struct{}) + execId := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2" + opts := StartExecOptions{ + OutputStream: &stdout, + ErrorStream: &stderr, + InputStream: reader, + RawTerminal: true, + Success: success, + } + go client.StartExec(execId, opts) + <-success +} + +func TestExecResize(t *testing.T) { + execId := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2" + fakeRT := &FakeRoundTripper{status: http.StatusOK} + client := newTestClient(fakeRT) + err := client.ResizeExecTTY(execId, 10, 20) + if err != nil { + t.Fatal(err) + } + req := fakeRT.requests[0] + if req.Method != "POST" { + t.Errorf("ExecStart: wrong HTTP method. Want %q. Got %q.", "POST", req.Method) + } + expectedURL, _ := url.Parse(client.getURL("/exec/" + execId + "/resize?h=10&w=20")) + if gotPath := req.URL.RequestURI(); gotPath != expectedURL.RequestURI() { + t.Errorf("ExecCreate: Wrong path in request. Want %q. Got %q.", expectedURL.Path, gotPath) + } +} diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/image.go b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/image.go index 4ce94c81c07..e1fbb234699 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/image.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/image.go @@ -42,6 +42,16 @@ type Image struct { Size int64 `json:"Size,omitempty" yaml:"Size,omitempty"` } +// ImageHistory represent a layer in an image's history returned by the +// ImageHistory call. +type ImageHistory struct { + ID string `json:"Id" yaml:"Id"` + Tags []string `json:"Tags,omitempty" yaml:"Tags,omitempty"` + Created int64 `json:"Created,omitempty" yaml:"Created,omitempty"` + CreatedBy string `json:"CreatedBy,omitempty" yaml:"CreatedBy,omitempty"` + Size int64 `json:"Size,omitempty" yaml:"Size,omitempty"` +} + type ImagePre012 struct { ID string `json:"id"` Parent string `json:"parent,omitempty"` @@ -71,7 +81,7 @@ var ( // ListImages returns the list of available images in the server. // -// See http://goo.gl/dkMrwP for more details. +// See http://goo.gl/VmcR6v for more details. func (c *Client) ListImages(all bool) ([]APIImages, error) { path := "/images/json?all=" if all { @@ -91,9 +101,28 @@ func (c *Client) ListImages(all bool) ([]APIImages, error) { return images, nil } +// ImageHistory returns the history of the image by its name or ID. +// +// See http://goo.gl/2oJmNs for more details. +func (c *Client) ImageHistory(name string) ([]ImageHistory, error) { + body, status, err := c.do("GET", "/images/"+name+"/history", nil) + if status == http.StatusNotFound { + return nil, ErrNoSuchImage + } + if err != nil { + return nil, err + } + var history []ImageHistory + err = json.Unmarshal(body, &history) + if err != nil { + return nil, err + } + return history, nil +} + // RemoveImage removes an image by its name or ID. // -// See http://goo.gl/7hjHHy for more details. +// See http://goo.gl/znj0wM for more details. func (c *Client) RemoveImage(name string) error { _, status, err := c.do("DELETE", "/images/"+name, nil) if status == http.StatusNotFound { @@ -104,7 +133,7 @@ func (c *Client) RemoveImage(name string) error { // InspectImage returns an image by its name or ID. // -// See http://goo.gl/pHEbma for more details. +// See http://goo.gl/Q112NY for more details. func (c *Client) InspectImage(name string) (*Image, error) { body, status, err := c.do("GET", "/images/"+name+"/json", nil) if status == http.StatusNotFound { @@ -147,7 +176,7 @@ func (c *Client) InspectImage(name string) (*Image, error) { // PushImageOptions represents options to use in the PushImage method. // -// See http://goo.gl/GBmyhc for more details. +// See http://goo.gl/pN8A3P for more details. type PushImageOptions struct { // Name of the image Name string @@ -158,7 +187,8 @@ type PushImageOptions struct { // Registry server to push the image Registry string - OutputStream io.Writer `qs:"-"` + OutputStream io.Writer `qs:"-"` + RawJSONStream bool `qs:"-"` } // AuthConfiguration represents authentication options to use in the PushImage @@ -174,7 +204,7 @@ type AuthConfiguration struct { // An empty instance of AuthConfiguration may be used for unauthenticated // pushes. // -// See http://goo.gl/GBmyhc for more details. +// See http://goo.gl/pN8A3P for more details. func (c *Client) PushImage(opts PushImageOptions, auth AuthConfiguration) error { if opts.Name == "" { return ErrNoSuchImage @@ -188,13 +218,13 @@ func (c *Client) PushImage(opts PushImageOptions, auth AuthConfiguration) error headers["X-Registry-Auth"] = base64.URLEncoding.EncodeToString(buf.Bytes()) - return c.stream("POST", path, true, false, headers, nil, opts.OutputStream, nil) + return c.stream("POST", path, true, opts.RawJSONStream, headers, nil, opts.OutputStream, nil) } // PullImageOptions present the set of options available for pulling an image // from a registry. // -// See http://goo.gl/PhBKnS for more details. +// See http://goo.gl/ACyYNS for more details. type PullImageOptions struct { Repository string `qs:"fromImage"` Registry string @@ -205,7 +235,7 @@ type PullImageOptions struct { // PullImage pulls an image from a remote registry, logging progress to w. // -// See http://goo.gl/PhBKnS for more details. +// See http://goo.gl/ACyYNS for more details. func (c *Client) PullImage(opts PullImageOptions, auth AuthConfiguration) error { if opts.Repository == "" { return ErrNoSuchImage @@ -288,9 +318,11 @@ func (c *Client) ImportImage(opts ImportImageOptions) error { return c.createImage(queryString(&opts), nil, opts.InputStream, opts.OutputStream, false) } -// BuildImageOptions present the set of informations available for building -// an image from a tarfile with a Dockerfile in it,the details about Dockerfile -// see http://docs.docker.io/en/latest/reference/builder/ +// BuildImageOptions present the set of informations available for building an +// image from a tarfile with a Dockerfile in it. +// +// For more details about the Docker building process, see +// http://goo.gl/tlPXPu. type BuildImageOptions struct { Name string `qs:"t"` NoCache bool `qs:"nocache"` @@ -299,11 +331,14 @@ type BuildImageOptions struct { ForceRmTmpContainer bool `qs:"forcerm"` InputStream io.Reader `qs:"-"` OutputStream io.Writer `qs:"-"` + RawJSONStream bool `qs:"-"` Remote string `qs:"remote"` } // BuildImage builds an image from a tarball's url or a Dockerfile in the input // stream. +// +// See http://goo.gl/wRsW76 for more details. func (c *Client) BuildImage(opts BuildImageOptions) error { if opts.OutputStream == nil { return ErrMissingOutputStream @@ -318,17 +353,21 @@ func (c *Client) BuildImage(opts BuildImageOptions) error { return ErrMissingRepo } return c.stream("POST", fmt.Sprintf("/build?%s", - queryString(&opts)), true, false, headers, opts.InputStream, opts.OutputStream, nil) + queryString(&opts)), true, opts.RawJSONStream, headers, opts.InputStream, opts.OutputStream, nil) } -// TagImageOptions present the set of options to tag an image +// TagImageOptions present the set of options to tag an image. +// +// See http://goo.gl/5g6qFy for more details. type TagImageOptions struct { Repo string Tag string Force bool } -// TagImage adds a tag to the image 'name' +// TagImage adds a tag to the image identified by the given name. +// +// See http://goo.gl/5g6qFy for more details. func (c *Client) TagImage(name string, opts TagImageOptions) error { if name == "" { return ErrNoSuchImage @@ -349,3 +388,30 @@ func isURL(u string) bool { } return p.Scheme == "http" || p.Scheme == "https" } + +// APIImageSearch reflect the result of a search on the dockerHub +// +// See http://goo.gl/xI5lLZ for more details. +type APIImageSearch struct { + Description string `json:"description,omitempty" yaml:"description,omitempty"` + IsOfficial bool `json:"is_official,omitempty" yaml:"is_official,omitempty"` + IsAutomated bool `json:"is_automated,omitempty" yaml:"is_automated,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + StarCount int `json:"star_count,omitempty" yaml:"star_count,omitempty"` +} + +// SearchImages search the docker hub with a specific given term. +// +// See http://goo.gl/xI5lLZ for more details. +func (c *Client) SearchImages(term string) ([]APIImageSearch, error) { + body, _, err := c.do("GET", "/images/search?term="+term, nil) + if err != nil { + return nil, err + } + var searchResult []APIImageSearch + err = json.Unmarshal(body, &searchResult) + if err != nil { + return nil, err + } + return searchResult, nil +} diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/image_test.go b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/image_test.go index 97612f25b03..5a7b2de7d40 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/image_test.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/image_test.go @@ -122,6 +122,48 @@ func TestListImagesParameters(t *testing.T) { } } +func TestImageHistory(t *testing.T) { + body := `[ + { + "Id": "25daec02219d2d852f7526137213a9b199926b4b24e732eab5b8bc6c49bd470e", + "Tags": [ + "debian:7.6", + "debian:latest", + "debian:7", + "debian:wheezy" + ], + "Created": 1409856216, + "CreatedBy": "/bin/sh -c #(nop) CMD [/bin/bash]" + }, + { + "Id": "41026a5347fb5be6ed16115bf22df8569697139f246186de9ae8d4f67c335dce", + "Created": 1409856213, + "CreatedBy": "/bin/sh -c #(nop) ADD file:1ee9e97209d00e3416a4543b23574cc7259684741a46bbcbc755909b8a053a38 in /", + "Size": 85178663 + }, + { + "Id": "511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158", + "Tags": [ + "scratch:latest" + ], + "Created": 1371157430 + } +]` + var expected []ImageHistory + err := json.Unmarshal([]byte(body), &expected) + if err != nil { + t.Fatal(err) + } + client := newTestClient(&FakeRoundTripper{message: body, status: http.StatusOK}) + history, err := client.ImageHistory("debian:latest") + if err != nil { + t.Error(err) + } + if !reflect.DeepEqual(history, expected) { + t.Errorf("ImageHistory: Wrong return value. Want %#v. Got %#v.", expected, history) + } +} + func TestRemoveImage(t *testing.T) { name := "test" fakeRT := &FakeRoundTripper{message: "", status: http.StatusNoContent} @@ -224,6 +266,35 @@ func TestPushImage(t *testing.T) { } } +func TestPushImageWithRawJSON(t *testing.T) { + body := ` + {"status":"Pushing..."} + {"status":"Pushing", "progress":"1/? (n/a)", "progressDetail":{"current":1}}} + {"status":"Image successfully pushed"} + ` + fakeRT := &FakeRoundTripper{ + message: body, + status: http.StatusOK, + header: map[string]string{ + "Content-Type": "application/json", + }, + } + client := newTestClient(fakeRT) + var buf bytes.Buffer + + err := client.PushImage(PushImageOptions{ + Name: "test", + OutputStream: &buf, + RawJSONStream: true, + }, AuthConfiguration{}) + if err != nil { + t.Fatal(err) + } + if buf.String() != body { + t.Errorf("PushImage: Wrong raw output. Want %q. Got %q.", body, buf.String()) + } +} + func TestPushImageWithAuthentication(t *testing.T) { fakeRT := &FakeRoundTripper{message: "Pushing 1/100", status: http.StatusOK} client := newTestClient(fakeRT) @@ -579,7 +650,7 @@ func TestBuildImageParametersForRemoteBuild(t *testing.T) { expected := map[string][]string{"t": {opts.Name}, "remote": {opts.Remote}, "q": {"1"}} got := map[string][]string(req.URL.Query()) if !reflect.DeepEqual(got, expected) { - t.Errorf("ImportImage: wrong query string. Want %#v. Got %#v.", expected, got) + t.Errorf("BuildImage: wrong query string. Want %#v. Got %#v.", expected, got) } } @@ -608,6 +679,44 @@ func TestBuildImageMissingOutputStream(t *testing.T) { } } +func TestBuildImageWithRawJSON(t *testing.T) { + body := ` + {"stream":"Step 0 : FROM ubuntu:latest\n"} + {"stream":" ---\u003e 4300eb9d3c8d\n"} + {"stream":"Step 1 : MAINTAINER docker \n"} + {"stream":" ---\u003e Using cache\n"} + {"stream":" ---\u003e 3a3ed758c370\n"} + {"stream":"Step 2 : CMD /usr/bin/top\n"} + {"stream":" ---\u003e Running in 36b1479cc2e4\n"} + {"stream":" ---\u003e 4b6188aebe39\n"} + {"stream":"Removing intermediate container 36b1479cc2e4\n"} + {"stream":"Successfully built 4b6188aebe39\n"} + ` + fakeRT := &FakeRoundTripper{ + message: body, + status: http.StatusOK, + header: map[string]string{ + "Content-Type": "application/json", + }, + } + client := newTestClient(fakeRT) + var buf bytes.Buffer + opts := BuildImageOptions{ + Name: "testImage", + RmTmpContainer: true, + InputStream: &buf, + OutputStream: &buf, + RawJSONStream: true, + } + err := client.BuildImage(opts) + if err != nil { + t.Fatal(err) + } + if buf.String() != body { + t.Errorf("BuildImage: Wrong raw output. Want %q. Got %q.", body, buf.String()) + } +} + func TestBuildImageRemoteWithoutName(t *testing.T) { fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK} client := newTestClient(fakeRT) @@ -710,3 +819,43 @@ func TestExportImage(t *testing.T) { t.Errorf("ExportIMage: wrong path. Expected %q. Got %q.", expectedPath, req.URL.Path) } } + +func TestSearchImages(t *testing.T) { + body := `[ + { + "description":"A container with Cassandra 2.0.3", + "is_official":true, + "is_automated":true, + "name":"poklet/cassandra", + "star_count":17 + }, + { + "description":"A container with Cassandra 2.0.3", + "is_official":true, + "is_automated":false, + "name":"poklet/cassandra", + "star_count":17 + } + , + { + "description":"A container with Cassandra 2.0.3", + "is_official":false, + "is_automated":true, + "name":"poklet/cassandra", + "star_count":17 + } +]` + var expected []APIImageSearch + err := json.Unmarshal([]byte(body), &expected) + if err != nil { + t.Fatal(err) + } + client := newTestClient(&FakeRoundTripper{message: body, status: http.StatusOK}) + result, err := client.SearchImages("cassandra") + if err != nil { + t.Error(err) + } + if !reflect.DeepEqual(result, expected) { + t.Errorf("SearchImages: Wrong return value. Want %#v. Got %#v.", expected, result) + } +} diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/misc.go b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/misc.go index 3f32bf23e7c..2678ab5cbf6 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/misc.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/misc.go @@ -11,7 +11,7 @@ import ( // Version returns version information about the docker server. // -// See http://goo.gl/IqKNRE for more details. +// See http://goo.gl/BOZrF5 for more details. func (c *Client) Version() (*Env, error) { body, _, err := c.do("GET", "/version", nil) if err != nil { @@ -24,9 +24,9 @@ func (c *Client) Version() (*Env, error) { return &env, nil } -// Info returns system-wide information, like the number of running containers. +// Info returns system-wide information about the Docker server. // -// See http://goo.gl/LOmySw for more details. +// See http://goo.gl/wmqZsW for more details. func (c *Client) Info() (*Env, error) { body, _, err := c.do("GET", "/info", nil) if err != nil { diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/server.go b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/server.go index 42c20e45fc6..e2ca1d97d3f 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/server.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/server.go @@ -31,7 +31,7 @@ import ( // It can used in standalone mode, listening for connections or as an arbitrary // HTTP handler. // -// For more details on the remote API, check http://goo.gl/yMI1S. +// For more details on the remote API, check http://goo.gl/G3plxW. type DockerServer struct { containers []*docker.Container cMut sync.RWMutex @@ -88,6 +88,7 @@ func (s *DockerServer) buildMuxer() { s.mux.Path("/containers/json").Methods("GET").HandlerFunc(s.handlerWrapper(s.listContainers)) s.mux.Path("/containers/create").Methods("POST").HandlerFunc(s.handlerWrapper(s.createContainer)) s.mux.Path("/containers/{id:.*}/json").Methods("GET").HandlerFunc(s.handlerWrapper(s.inspectContainer)) + s.mux.Path("/containers/{id:.*}/top").Methods("GET").HandlerFunc(s.handlerWrapper(s.topContainer)) s.mux.Path("/containers/{id:.*}/start").Methods("POST").HandlerFunc(s.handlerWrapper(s.startContainer)) s.mux.Path("/containers/{id:.*}/stop").Methods("POST").HandlerFunc(s.handlerWrapper(s.stopContainer)) s.mux.Path("/containers/{id:.*}/pause").Methods("POST").HandlerFunc(s.handlerWrapper(s.pauseContainer)) @@ -339,6 +340,29 @@ func (s *DockerServer) inspectContainer(w http.ResponseWriter, r *http.Request) json.NewEncoder(w).Encode(container) } +func (s *DockerServer) topContainer(w http.ResponseWriter, r *http.Request) { + id := mux.Vars(r)["id"] + container, _, err := s.findContainer(id) + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + if !container.State.Running { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "Container %s is not running", id) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + result := docker.TopResult{ + Titles: []string{"UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"}, + Processes: [][]string{ + {"root", "7535", "7516", "0", "03:20", "?", "00:00:00", container.Path + " " + strings.Join(container.Args, " ")}, + }, + } + json.NewEncoder(w).Encode(result) +} + func (s *DockerServer) startContainer(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] container, _, err := s.findContainer(id) diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/server_test.go b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/server_test.go index 520300438b2..ad5ce7dad5e 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/server_test.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/server_test.go @@ -343,6 +343,58 @@ func TestInspectContainerNotFound(t *testing.T) { } } +func TestTopContainer(t *testing.T) { + server := DockerServer{} + addContainers(&server, 1) + server.containers[0].State.Running = true + server.buildMuxer() + recorder := httptest.NewRecorder() + path := fmt.Sprintf("/containers/%s/top", server.containers[0].ID) + request, _ := http.NewRequest("GET", path, nil) + server.ServeHTTP(recorder, request) + if recorder.Code != http.StatusOK { + t.Errorf("TopContainer: wrong status. Want %d. Got %d.", http.StatusOK, recorder.Code) + } + var got docker.TopResult + err := json.NewDecoder(recorder.Body).Decode(&got) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got.Titles, []string{"UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"}) { + t.Fatalf("TopContainer: Unexpected titles, got: %#v", got.Titles) + } + if len(got.Processes) != 1 { + t.Fatalf("TopContainer: Unexpected process len, got: %d", len(got.Processes)) + } + if got.Processes[0][len(got.Processes[0])-1] != "ls -la .." { + t.Fatalf("TopContainer: Unexpected command name, got: %s", got.Processes[0][len(got.Processes[0])-1]) + } +} + +func TestTopContainerNotFound(t *testing.T) { + server := DockerServer{} + server.buildMuxer() + recorder := httptest.NewRecorder() + request, _ := http.NewRequest("GET", "/containers/xyz/top", nil) + server.ServeHTTP(recorder, request) + if recorder.Code != http.StatusNotFound { + t.Errorf("TopContainer: wrong status. Want %d. Got %d.", http.StatusNotFound, recorder.Code) + } +} + +func TestTopContainerStopped(t *testing.T) { + server := DockerServer{} + addContainers(&server, 1) + server.buildMuxer() + recorder := httptest.NewRecorder() + path := fmt.Sprintf("/containers/%s/top", server.containers[0].ID) + request, _ := http.NewRequest("GET", path, nil) + server.ServeHTTP(recorder, request) + if recorder.Code != http.StatusInternalServerError { + t.Errorf("TopContainer: wrong status. Want %d. Got %d.", http.StatusInternalServerError, recorder.Code) + } +} + func TestStartContainer(t *testing.T) { server := DockerServer{} addContainers(&server, 1)