From 1d741cbfc54626a6f89e77c6ac8e85d821bc055d Mon Sep 17 00:00:00 2001 From: Cameron Sparr Date: Wed, 23 Sep 2015 14:25:26 -0700 Subject: [PATCH] Update godep of go-dockerclient for Label access --- Godeps/Godeps.json | 2 +- .../fsouza/go-dockerclient/.gitignore | 2 + .../fsouza/go-dockerclient/.travis.yml | 5 +- .../github.com/fsouza/go-dockerclient/AUTHORS | 8 + .../github.com/fsouza/go-dockerclient/auth.go | 17 +- .../fsouza/go-dockerclient/auth_test.go | 2 +- .../fsouza/go-dockerclient/build_test.go | 10 + .../fsouza/go-dockerclient/change.go | 2 +- .../fsouza/go-dockerclient/client.go | 171 ++++--- .../fsouza/go-dockerclient/client_test.go | 44 +- .../fsouza/go-dockerclient/container.go | 430 +++++++++++------- .../fsouza/go-dockerclient/container_test.go | 56 ++- .../fsouza/go-dockerclient/event.go | 5 +- .../github.com/fsouza/go-dockerclient/exec.go | 151 +++--- .../fsouza/go-dockerclient/image.go | 179 ++++---- .../fsouza/go-dockerclient/image_test.go | 4 + .../github.com/fsouza/go-dockerclient/misc.go | 20 +- .../fsouza/go-dockerclient/network.go | 33 +- .../go-dockerclient/testing/data/symlink | 1 - .../fsouza/go-dockerclient/testing/server.go | 39 +- .../go-dockerclient/testing/server_test.go | 77 +++- .../github.com/fsouza/go-dockerclient/tls.go | 4 - .../fsouza/go-dockerclient/volume.go | 127 ++++++ .../fsouza/go-dockerclient/volume_test.go | 142 ++++++ 24 files changed, 1065 insertions(+), 466 deletions(-) create mode 100644 Godeps/_workspace/src/github.com/fsouza/go-dockerclient/.gitignore delete mode 120000 Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/data/symlink create mode 100644 Godeps/_workspace/src/github.com/fsouza/go-dockerclient/volume.go create mode 100644 Godeps/_workspace/src/github.com/fsouza/go-dockerclient/volume_test.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index dd9f212ec..447d70320 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -60,7 +60,7 @@ }, { "ImportPath": "github.com/fsouza/go-dockerclient", - "Rev": "42d06e2b125654477366c320dcea99107a86e9c2" + "Rev": "af9789bbd78acf3e279274caa54682185eb7ed33" }, { "ImportPath": "github.com/go-sql-driver/mysql", diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/.gitignore b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/.gitignore new file mode 100644 index 000000000..5f6b48eae --- /dev/null +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/.gitignore @@ -0,0 +1,2 @@ +# temporary symlink for testing +testing/data/symlink 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 3926838ac..927a999f0 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/.travis.yml +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/.travis.yml @@ -1,8 +1,9 @@ language: go sudo: false go: - - 1.3.1 - - 1.4 + - 1.3.3 + - 1.4.2 + - 1.5.1 - tip env: - GOARCH=amd64 diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/AUTHORS b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/AUTHORS index 2febb1f03..418a808fb 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/AUTHORS +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/AUTHORS @@ -11,8 +11,10 @@ Ben McCann Brendan Fosberry Brian Lalor Brian Palmer +Bryan Boreham Burke Libbey Carlos Diaz-Padron +Cesar Wong Cezar Sa Espinola Cheah Chu Yeow cheneydeng @@ -27,12 +29,14 @@ David Huie Dawn Chen Dinesh Subhraveti Ed +Erez Horev Eric Anderson Ewout Prangsma Fabio Rehm Fatih Arslan Flavia Missi Francisco Souza +Grégoire Delattre Guillermo Álvarez Fernández He Simei Ivan Mikushin @@ -43,6 +47,7 @@ Jawher Moussa Jean-Baptiste Dalido Jeff Mitchell Jeffrey Hulten +Jen Andre Johan Euphrosine Kamil Domanski Karan Misra @@ -50,6 +55,7 @@ Kim, Hirokuni Kyle Allan Liron Levin Liu Peng +Lorenz Leutgeb Lucas Clemente Lucas Weiblen Mantas Matelis @@ -66,12 +72,14 @@ Paul Morie Paul Weil Peter Edge Peter Jihoon Kim +Phil Lu Philippe Lafoucrière Rafe Colton Rob Miller Robert Williamson Salvador Gironès Sam Rijs +Samuel Karp Simon Eskildsen Simon Menke Skolos diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/auth.go b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/auth.go index fccd55740..30e3af3eb 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/auth.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/auth.go @@ -16,7 +16,8 @@ import ( "strings" ) -var AuthParseError error = errors.New("Failed to read authentication from dockercfg") +// ErrCannotParseDockercfg is the error returned by NewAuthConfigurations when the dockercfg cannot be parsed. +var ErrCannotParseDockercfg = errors.New("Failed to read authentication from dockercfg") // AuthConfiguration represents authentication options to use in the PushImage // method. It represents the authentication in the Docker index server. @@ -33,6 +34,10 @@ type AuthConfigurations struct { Configs map[string]AuthConfiguration `json:"configs"` } +// AuthConfigurations119 is used to serialize a set of AuthConfigurations +// for Docker API >= 1.19. +type AuthConfigurations119 map[string]AuthConfiguration + // dockerConfig represents a registry authentation configuration from the // .dockercfg file. type dockerConfig struct { @@ -103,7 +108,7 @@ func authConfigs(confs map[string]dockerConfig) (*AuthConfigurations, error) { } userpass := strings.Split(string(data), ":") if len(userpass) != 2 { - return nil, AuthParseError + return nil, ErrCannotParseDockercfg } c.Configs[reg] = AuthConfiguration{ Email: conf.Email, @@ -117,17 +122,15 @@ func authConfigs(confs map[string]dockerConfig) (*AuthConfigurations, error) { // AuthCheck validates the given credentials. It returns nil if successful. // -// See https://goo.gl/vPoEfJ for more details. +// See https://goo.gl/m2SleN for more details. func (c *Client) AuthCheck(conf *AuthConfiguration) error { if conf == nil { return fmt.Errorf("conf is nil") } - body, statusCode, err := c.do("POST", "/auth", doOptions{data: conf}) + resp, err := c.do("POST", "/auth", doOptions{data: conf}) if err != nil { return err } - if statusCode > 400 { - return fmt.Errorf("auth error (%d): %s", statusCode, body) - } + resp.Body.Close() return nil } diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/auth_test.go b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/auth_test.go index fc0ffab84..b3d4f8fc8 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/auth_test.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/auth_test.go @@ -41,7 +41,7 @@ func TestAuthBadConfig(t *testing.T) { auth := base64.StdEncoding.EncodeToString([]byte("userpass")) read := strings.NewReader(fmt.Sprintf(`{"docker.io":{"auth":"%s","email":"user@example.com"}}`, auth)) ac, err := NewAuthConfigurations(read) - if err != AuthParseError { + if err != ErrCannotParseDockercfg { t.Errorf("Incorrect error returned %v\n", err) } if ac != nil { diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/build_test.go b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/build_test.go index a4864db83..c9640f205 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/build_test.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/build_test.go @@ -35,6 +35,16 @@ func TestBuildImageMultipleContextsError(t *testing.T) { func TestBuildImageContextDirDockerignoreParsing(t *testing.T) { fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK} client := newTestClient(fakeRT) + + if err := os.Symlink("doesnotexist", "testing/data/symlink"); err != nil { + t.Errorf("error creating symlink on demand: %s", err) + } + defer func() { + if err := os.Remove("testing/data/symlink"); err != nil { + t.Errorf("error removing symlink on demand: %s", err) + } + }() + var buf bytes.Buffer opts := BuildImageOptions{ Name: "testImage", 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 e7b056c3f..d133594d4 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/change.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/change.go @@ -23,7 +23,7 @@ const ( // Change represents a change in a container. // -// See http://goo.gl/QkW9sH for more details. +// See https://goo.gl/9GsTIF 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 986bbb3d2..3ba44e864 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/G3plxW for more details on the remote API. +// See https://goo.gl/G3plxW for more details on the remote API. package docker import ( @@ -45,6 +45,8 @@ var ( ErrConnectionRefused = errors.New("cannot connect to Docker endpoint") apiVersion112, _ = NewAPIVersion("1.12") + + apiVersion119, _ = NewAPIVersion("1.19") ) // APIVersion is an internal representation of a version of the Remote API. @@ -128,6 +130,7 @@ type Client struct { SkipServerVersionCheck bool HTTPClient *http.Client TLSConfig *tls.Config + Dialer *net.Dialer endpoint string endpointURL *url.URL @@ -135,6 +138,7 @@ type Client struct { requestedAPIVersion APIVersion serverAPIVersion APIVersion expectedAPIVersion APIVersion + unixHTTPClient *http.Client } // NewClient returns a Client instance ready for communication with the given @@ -189,6 +193,7 @@ func NewVersionedClient(endpoint string, apiVersionString string) (*Client, erro } return &Client{ HTTPClient: http.DefaultClient, + Dialer: &net.Dialer{}, endpoint: endpoint, endpointURL: u, eventMonitor: new(eventMonitoringState), @@ -300,6 +305,7 @@ func NewVersionedTLSClientFromBytes(endpoint string, certPEMBlock, keyPEMBlock, return &Client{ HTTPClient: &http.Client{Transport: tr}, TLSConfig: tlsConfig, + Dialer: &net.Dialer{}, endpoint: endpoint, endpointURL: u, eventMonitor: new(eventMonitoringState), @@ -324,32 +330,40 @@ func (c *Client) checkAPIVersion() error { return nil } +// Endpoint returns the current endpoint. It's useful for getting the endpoint +// when using functions that get this data from the environment (like +// NewClientFromEnv. +func (c *Client) Endpoint() string { + return c.endpoint +} + // Ping pings the docker server // -// See http://goo.gl/stJENm for more details. +// See https://goo.gl/kQCfJj for more details. func (c *Client) Ping() error { path := "/_ping" - body, status, err := c.do("GET", path, doOptions{}) + resp, err := c.do("GET", path, doOptions{}) if err != nil { return err } - if status != http.StatusOK { - return newError(status, body) + if resp.StatusCode != http.StatusOK { + return newError(resp) } + resp.Body.Close() return nil } func (c *Client) getServerAPIVersionString() (version string, err error) { - body, status, err := c.do("GET", "/version", doOptions{}) + resp, err := c.do("GET", "/version", doOptions{}) if err != nil { return "", err } - if status != http.StatusOK { - return "", fmt.Errorf("Received unexpected status %d while trying to retrieve the server version", status) + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("Received unexpected status %d while trying to retrieve the server version", resp.StatusCode) } var versionResponse map[string]interface{} - err = json.Unmarshal(body, &versionResponse) - if err != nil { + if err := json.NewDecoder(resp.Body).Decode(&versionResponse); err != nil { return "", err } if version, ok := (versionResponse["ApiVersion"]).(string); ok { @@ -363,24 +377,35 @@ type doOptions struct { forceJSON bool } -func (c *Client) do(method, path string, doOptions doOptions) ([]byte, int, error) { +func (c *Client) do(method, path string, doOptions doOptions) (*http.Response, error) { var params io.Reader if doOptions.data != nil || doOptions.forceJSON { buf, err := json.Marshal(doOptions.data) if err != nil { - return nil, -1, err + return nil, err } params = bytes.NewBuffer(buf) } if path != "/version" && !c.SkipServerVersionCheck && c.expectedAPIVersion == nil { err := c.checkAPIVersion() if err != nil { - return nil, -1, err + return nil, err } } - req, err := http.NewRequest(method, c.getURL(path), params) + + httpClient := c.HTTPClient + protocol := c.endpointURL.Scheme + var u string + if protocol == "unix" { + httpClient = c.unixClient() + u = c.getFakeUnixURL(path) + } else { + u = c.getURL(path) + } + + req, err := http.NewRequest(method, u, params) if err != nil { - return nil, -1, err + return nil, err } req.Header.Set("User-Agent", userAgent) if doOptions.data != nil { @@ -388,40 +413,19 @@ func (c *Client) do(method, path string, doOptions doOptions) ([]byte, int, erro } else if method == "POST" { req.Header.Set("Content-Type", "plain/text") } - var resp *http.Response - protocol := c.endpointURL.Scheme - address := c.endpointURL.Path - if protocol == "unix" { - var dial net.Conn - dial, err = net.Dial(protocol, address) - if err != nil { - return nil, -1, err - } - defer dial.Close() - breader := bufio.NewReader(dial) - err = req.Write(dial) - if err != nil { - return nil, -1, err - } - resp, err = http.ReadResponse(breader, req) - } else { - resp, err = c.HTTPClient.Do(req) - } + + resp, err := httpClient.Do(req) if err != nil { if strings.Contains(err.Error(), "connection refused") { - return nil, -1, ErrConnectionRefused + return nil, ErrConnectionRefused } - return nil, -1, err - } - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, -1, err + return nil, err } + if resp.StatusCode < 200 || resp.StatusCode >= 400 { - return nil, resp.StatusCode, newError(resp.StatusCode, body) + return nil, newError(resp) } - return body, resp.StatusCode, nil + return resp, nil } type streamOptions struct { @@ -462,12 +466,16 @@ func (c *Client) stream(method, path string, streamOptions streamOptions) error address := c.endpointURL.Path if streamOptions.stdout == nil { streamOptions.stdout = ioutil.Discard + } else if t, ok := streamOptions.stdout.(io.Closer); ok { + defer t.Close() } if streamOptions.stderr == nil { streamOptions.stderr = ioutil.Discard + } else if t, ok := streamOptions.stderr.(io.Closer); ok { + defer t.Close() } if protocol == "unix" { - dial, err := net.Dial(protocol, address) + dial, err := c.Dialer.Dial(protocol, address) if err != nil { return err } @@ -503,11 +511,7 @@ func (c *Client) stream(method, path string, streamOptions streamOptions) error } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 400 { - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - return newError(resp.StatusCode, body) + return newError(resp) } if streamOptions.useJSONDecoder || resp.Header.Get("Content-Type") == "application/json" { // if we want to get raw json stream, just copy it back to output @@ -583,6 +587,8 @@ func (c *Client) hijack(method, path string, hijackOptions hijackOptions) error return err } req.Header.Set("Content-Type", "plain/text") + req.Header.Set("Connection", "Upgrade") + req.Header.Set("Upgrade", "tcp") protocol := c.endpointURL.Scheme address := c.endpointURL.Path if protocol != "unix" { @@ -591,12 +597,12 @@ func (c *Client) hijack(method, path string, hijackOptions hijackOptions) error } var dial net.Conn if c.TLSConfig != nil && protocol != "unix" { - dial, err = tlsDial(protocol, address, c.TLSConfig) + dial, err = tlsDialWithDialer(c.Dialer, protocol, address, c.TLSConfig) if err != nil { return err } } else { - dial, err = net.Dial(protocol, address) + dial, err = c.Dialer.Dial(protocol, address) if err != nil { return err } @@ -612,13 +618,16 @@ func (c *Client) hijack(method, path string, hijackOptions hijackOptions) error defer rwc.Close() errChanOut := make(chan error, 1) errChanIn := make(chan error, 1) - exit := make(chan bool) go func() { - defer close(exit) - defer close(errChanOut) + defer func() { + if hijackOptions.in != nil { + if closer, ok := hijackOptions.in.(io.Closer); ok { + closer.Close() + } + } + }() var err error if hijackOptions.setRawTerminal { - // When TTY is ON, use regular copy _, err = io.Copy(hijackOptions.stdout, br) } else { _, err = stdcopy.StdCopy(hijackOptions.stdout, hijackOptions.stderr, br) @@ -626,17 +635,15 @@ func (c *Client) hijack(method, path string, hijackOptions hijackOptions) error errChanOut <- err }() go func() { + var err error if hijackOptions.in != nil { - _, err := io.Copy(rwc, hijackOptions.in) - errChanIn <- err - } else { - errChanIn <- nil + _, err = io.Copy(rwc, hijackOptions.in) } + errChanIn <- err rwc.(interface { CloseWrite() error }).CloseWrite() }() - <-exit errIn := <-errChanIn errOut := <-errChanOut if errIn != nil { @@ -657,6 +664,41 @@ func (c *Client) getURL(path string) string { return fmt.Sprintf("%s%s", urlStr, path) } +// getFakeUnixURL returns the URL needed to make an HTTP request over a UNIX +// domain socket to the given path. +func (c *Client) getFakeUnixURL(path string) string { + u := *c.endpointURL // Copy. + + // Override URL so that net/http will not complain. + u.Scheme = "http" + u.Host = "unix.sock" // Doesn't matter what this is - it's not used. + u.Path = "" + + urlStr := strings.TrimRight(u.String(), "/") + + if c.requestedAPIVersion != nil { + return fmt.Sprintf("%s/v%s%s", urlStr, c.requestedAPIVersion, path) + } + return fmt.Sprintf("%s%s", urlStr, path) +} + +func (c *Client) unixClient() *http.Client { + if c.unixHTTPClient != nil { + return c.unixHTTPClient + } + + socketPath := c.endpointURL.Path + c.unixHTTPClient = &http.Client{ + Transport: &http.Transport{ + Dial: func(network, addr string) (net.Conn, error) { + return c.Dialer.Dial("unix", socketPath) + }, + }, + } + + return c.unixHTTPClient +} + type jsonMessage struct { Status string `json:"status,omitempty"` Progress string `json:"progress,omitempty"` @@ -738,8 +780,13 @@ type Error struct { Message string } -func newError(status int, body []byte) *Error { - return &Error{Status: status, Message: string(body)} +func newError(resp *http.Response) *Error { + defer resp.Body.Close() + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return &Error{Status: resp.StatusCode, Message: fmt.Sprintf("cannot read body, err: %v", err)} + } + return &Error{Status: resp.StatusCode, Message: string(data)} } func (e *Error) Error() string { diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/client_test.go b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/client_test.go index c00c3d30c..67230a4e1 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/client_test.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/client_test.go @@ -5,6 +5,7 @@ package docker import ( + "bytes" "fmt" "io/ioutil" "net" @@ -161,6 +162,16 @@ func TestNewTLSClient(t *testing.T) { } } +func TestEndpoint(t *testing.T) { + client, err := NewVersionedClient("http://localhost:4243", "1.12") + if err != nil { + t.Fatal(err) + } + if endpoint := client.Endpoint(); endpoint != client.endpoint { + t.Errorf("Client.Endpoint(): want %q. Got %q", client.endpoint, endpoint) + } +} + func TestGetURL(t *testing.T) { var tests = []struct { endpoint string @@ -185,8 +196,34 @@ func TestGetURL(t *testing.T) { } } +func TestGetFakeUnixURL(t *testing.T) { + var tests = []struct { + endpoint string + path string + expected string + }{ + {"unix://var/run/docker.sock", "/", "http://unix.sock/"}, + {"unix://var/run/docker.socket", "/", "http://unix.sock/"}, + {"unix://var/run/docker.sock", "/containers/ps", "http://unix.sock/containers/ps"}, + } + for _, tt := range tests { + client, _ := NewClient(tt.endpoint) + client.endpoint = tt.endpoint + client.SkipServerVersionCheck = true + got := client.getFakeUnixURL(tt.path) + if got != tt.expected { + t.Errorf("getURL(%q): Got %s. Want %s.", tt.path, got, tt.expected) + } + } +} + func TestError(t *testing.T) { - err := newError(400, []byte("bad parameter")) + fakeBody := ioutil.NopCloser(bytes.NewBufferString("bad parameter")) + resp := &http.Response{ + StatusCode: 400, + Body: fakeBody, + } + err := newError(resp) expected := Error{Status: 400, Message: "bad parameter"} if !reflect.DeepEqual(expected, *err) { t.Errorf("Wrong error type. Want %#v. Got %#v.", expected, *err) @@ -334,7 +371,7 @@ func TestPingErrorWithUnixSocket(t *testing.T) { } defer li.Close() if err != nil { - t.Fatalf("Expected to get listner, but failed: %#v", err) + t.Fatalf("Expected to get listener, but failed: %#v", err) } fd, err := li.Accept() @@ -345,7 +382,7 @@ func TestPingErrorWithUnixSocket(t *testing.T) { buf := make([]byte, 512) nr, err := fd.Read(buf) - // Create invalid response message to occur error + // Create invalid response message to trigger error. data := buf[0:nr] for i := 0; i < 10; i++ { data[i] = 63 @@ -366,6 +403,7 @@ func TestPingErrorWithUnixSocket(t *testing.T) { u, _ := parseEndpoint(endpoint, false) client := Client{ HTTPClient: http.DefaultClient, + Dialer: &net.Dialer{}, endpoint: endpoint, endpointURL: u, SkipServerVersionCheck: true, 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 89430975b..497cfd00c 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/container.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/container.go @@ -5,7 +5,6 @@ package docker import ( - "bytes" "encoding/json" "errors" "fmt" @@ -23,7 +22,7 @@ var ErrContainerAlreadyExists = errors.New("container already exists") // ListContainersOptions specify parameters to the ListContainers function. // -// See http://goo.gl/6Y4Gz7 for more details. +// See https://goo.gl/47a6tO for more details. type ListContainersOptions struct { All bool Size bool @@ -41,33 +40,33 @@ type APIPort struct { IP string `json:"IP,omitempty" yaml:"IP,omitempty"` } -// APIContainers represents a container. -// -// See http://goo.gl/QeFH7U for more details. +// APIContainers represents each container in the list returned by +// ListContainers. type APIContainers struct { - ID string `json:"Id" yaml:"Id"` - Image string `json:"Image,omitempty" yaml:"Image,omitempty"` - Command string `json:"Command,omitempty" yaml:"Command,omitempty"` - Created int64 `json:"Created,omitempty" yaml:"Created,omitempty"` - Status string `json:"Status,omitempty" yaml:"Status,omitempty"` - Ports []APIPort `json:"Ports,omitempty" yaml:"Ports,omitempty"` - SizeRw int64 `json:"SizeRw,omitempty" yaml:"SizeRw,omitempty"` - SizeRootFs int64 `json:"SizeRootFs,omitempty" yaml:"SizeRootFs,omitempty"` - Names []string `json:"Names,omitempty" yaml:"Names,omitempty"` + ID string `json:"Id" yaml:"Id"` + Image string `json:"Image,omitempty" yaml:"Image,omitempty"` + Command string `json:"Command,omitempty" yaml:"Command,omitempty"` + Created int64 `json:"Created,omitempty" yaml:"Created,omitempty"` + Status string `json:"Status,omitempty" yaml:"Status,omitempty"` + Ports []APIPort `json:"Ports,omitempty" yaml:"Ports,omitempty"` + SizeRw int64 `json:"SizeRw,omitempty" yaml:"SizeRw,omitempty"` + SizeRootFs int64 `json:"SizeRootFs,omitempty" yaml:"SizeRootFs,omitempty"` + Names []string `json:"Names,omitempty" yaml:"Names,omitempty"` + Labels map[string]string `json:"Labels,omitempty" yaml:"Labels, omitempty"` } // ListContainers returns a slice of containers matching the given criteria. // -// See http://goo.gl/6Y4Gz7 for more details. +// See https://goo.gl/47a6tO for more details. func (c *Client) ListContainers(opts ListContainersOptions) ([]APIContainers, error) { path := "/containers/json?" + queryString(opts) - body, _, err := c.do("GET", path, doOptions{}) + resp, err := c.do("GET", path, doOptions{}) if err != nil { return nil, err } + defer resp.Body.Close() var containers []APIContainers - err = json.Unmarshal(body, &containers) - if err != nil { + if err := json.NewDecoder(resp.Body).Decode(&containers); err != nil { return nil, err } return containers, nil @@ -206,6 +205,7 @@ type Config struct { DNS []string `json:"Dns,omitempty" yaml:"Dns,omitempty"` // For Docker API v1.9 and below only Image string `json:"Image,omitempty" yaml:"Image,omitempty"` Volumes map[string]struct{} `json:"Volumes,omitempty" yaml:"Volumes,omitempty"` + VolumeDriver string `json:"VolumeDriver,omitempty" yaml:"VolumeDriver,omitempty"` VolumesFrom string `json:"VolumesFrom,omitempty" yaml:"VolumesFrom,omitempty"` WorkingDir string `json:"WorkingDir,omitempty" yaml:"WorkingDir,omitempty"` MacAddress string `json:"MacAddress,omitempty" yaml:"MacAddress,omitempty"` @@ -213,9 +213,21 @@ type Config struct { NetworkDisabled bool `json:"NetworkDisabled,omitempty" yaml:"NetworkDisabled,omitempty"` SecurityOpts []string `json:"SecurityOpts,omitempty" yaml:"SecurityOpts,omitempty"` OnBuild []string `json:"OnBuild,omitempty" yaml:"OnBuild,omitempty"` + Mounts []Mount `json:"Mounts,omitempty" yaml:"Mounts,omitempty"` Labels map[string]string `json:"Labels,omitempty" yaml:"Labels,omitempty"` } +// Mount represents a mount point in the container. +// +// It has been added in the version 1.20 of the Docker API, available since +// Docker 1.8. +type Mount struct { + Source string + Destination string + Mode string + RW bool +} + // LogConfig defines the log driver type and the configuration for it. type LogConfig struct { Type string `json:"Type,omitempty" yaml:"Type,omitempty"` @@ -259,13 +271,14 @@ type Container struct { NetworkSettings *NetworkSettings `json:"NetworkSettings,omitempty" yaml:"NetworkSettings,omitempty"` - SysInitPath string `json:"SysInitPath,omitempty" yaml:"SysInitPath,omitempty"` - ResolvConfPath string `json:"ResolvConfPath,omitempty" yaml:"ResolvConfPath,omitempty"` - HostnamePath string `json:"HostnamePath,omitempty" yaml:"HostnamePath,omitempty"` - HostsPath string `json:"HostsPath,omitempty" yaml:"HostsPath,omitempty"` - LogPath string `json:"LogPath,omitempty" yaml:"LogPath,omitempty"` - Name string `json:"Name,omitempty" yaml:"Name,omitempty"` - Driver string `json:"Driver,omitempty" yaml:"Driver,omitempty"` + SysInitPath string `json:"SysInitPath,omitempty" yaml:"SysInitPath,omitempty"` + ResolvConfPath string `json:"ResolvConfPath,omitempty" yaml:"ResolvConfPath,omitempty"` + HostnamePath string `json:"HostnamePath,omitempty" yaml:"HostnamePath,omitempty"` + HostsPath string `json:"HostsPath,omitempty" yaml:"HostsPath,omitempty"` + LogPath string `json:"LogPath,omitempty" yaml:"LogPath,omitempty"` + Name string `json:"Name,omitempty" yaml:"Name,omitempty"` + Driver string `json:"Driver,omitempty" yaml:"Driver,omitempty"` + Mounts []Mount `json:"Mounts,omitempty" yaml:"Mounts,omitempty"` Volumes map[string]string `json:"Volumes,omitempty" yaml:"Volumes,omitempty"` VolumesRW map[string]bool `json:"VolumesRW,omitempty" yaml:"VolumesRW,omitempty"` @@ -279,7 +292,7 @@ type Container struct { // RenameContainerOptions specify parameters to the RenameContainer function. // -// See http://goo.gl/L00hoj for more details. +// See https://goo.gl/laSOIy for more details. type RenameContainerOptions struct { // ID of container to rename ID string `qs:"-"` @@ -290,27 +303,31 @@ type RenameContainerOptions struct { // RenameContainer updates and existing containers name // -// See http://goo.gl/L00hoj for more details. +// See https://goo.gl/laSOIy for more details. func (c *Client) RenameContainer(opts RenameContainerOptions) error { - _, _, err := c.do("POST", fmt.Sprintf("/containers/"+opts.ID+"/rename?%s", queryString(opts)), doOptions{}) - return err + resp, err := c.do("POST", fmt.Sprintf("/containers/"+opts.ID+"/rename?%s", queryString(opts)), doOptions{}) + if err != nil { + return err + } + resp.Body.Close() + return nil } // InspectContainer returns information about a container by its ID. // -// See http://goo.gl/CxVuJ5 for more details. +// See https://goo.gl/RdIq0b for more details. func (c *Client) InspectContainer(id string) (*Container, error) { path := "/containers/" + id + "/json" - body, status, err := c.do("GET", path, doOptions{}) - if status == http.StatusNotFound { - return nil, &NoSuchContainer{ID: id} - } + resp, err := c.do("GET", path, doOptions{}) if err != nil { + if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { + return nil, &NoSuchContainer{ID: id} + } return nil, err } + defer resp.Body.Close() var container Container - err = json.Unmarshal(body, &container) - if err != nil { + if err := json.NewDecoder(resp.Body).Decode(&container); err != nil { return nil, err } return &container, nil @@ -318,19 +335,19 @@ func (c *Client) InspectContainer(id string) (*Container, error) { // ContainerChanges returns changes in the filesystem of the given container. // -// See http://goo.gl/QkW9sH for more details. +// See https://goo.gl/9GsTIF for more details. func (c *Client) ContainerChanges(id string) ([]Change, error) { path := "/containers/" + id + "/changes" - body, status, err := c.do("GET", path, doOptions{}) - if status == http.StatusNotFound { - return nil, &NoSuchContainer{ID: id} - } + resp, err := c.do("GET", path, doOptions{}) if err != nil { + if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { + return nil, &NoSuchContainer{ID: id} + } return nil, err } + defer resp.Body.Close() var changes []Change - err = json.Unmarshal(body, &changes) - if err != nil { + if err := json.NewDecoder(resp.Body).Decode(&changes); err != nil { return nil, err } return changes, nil @@ -338,7 +355,7 @@ func (c *Client) ContainerChanges(id string) ([]Change, error) { // CreateContainerOptions specify parameters to the CreateContainer function. // -// See http://goo.gl/2xxQQK for more details. +// See https://goo.gl/WxQzrr for more details. type CreateContainerOptions struct { Name string Config *Config `qs:"-"` @@ -348,10 +365,10 @@ type CreateContainerOptions struct { // CreateContainer creates a new container, returning the container instance, // or an error in case of failure. // -// See http://goo.gl/mErxNp for more details. +// See https://goo.gl/WxQzrr for more details. func (c *Client) CreateContainer(opts CreateContainerOptions) (*Container, error) { path := "/containers/create?" + queryString(opts) - body, status, err := c.do( + resp, err := c.do( "POST", path, doOptions{ @@ -365,18 +382,21 @@ func (c *Client) CreateContainer(opts CreateContainerOptions) (*Container, error }, ) - if status == http.StatusNotFound { - return nil, ErrNoSuchImage - } - if status == http.StatusConflict { - return nil, ErrContainerAlreadyExists + if e, ok := err.(*Error); ok { + if e.Status == http.StatusNotFound { + return nil, ErrNoSuchImage + } + if e.Status == http.StatusConflict { + return nil, ErrContainerAlreadyExists + } } + if err != nil { return nil, err } + defer resp.Body.Close() var container Container - err = json.Unmarshal(body, &container) - if err != nil { + if err := json.NewDecoder(resp.Body).Decode(&container); err != nil { return nil, err } @@ -434,125 +454,135 @@ type Device struct { // HostConfig contains the container options related to starting a container on // a given host type HostConfig struct { - Binds []string `json:"Binds,omitempty" yaml:"Binds,omitempty"` - CapAdd []string `json:"CapAdd,omitempty" yaml:"CapAdd,omitempty"` - CapDrop []string `json:"CapDrop,omitempty" yaml:"CapDrop,omitempty"` - ContainerIDFile string `json:"ContainerIDFile,omitempty" yaml:"ContainerIDFile,omitempty"` - LxcConf []KeyValuePair `json:"LxcConf,omitempty" yaml:"LxcConf,omitempty"` - Privileged bool `json:"Privileged,omitempty" yaml:"Privileged,omitempty"` - PortBindings map[Port][]PortBinding `json:"PortBindings,omitempty" yaml:"PortBindings,omitempty"` - Links []string `json:"Links,omitempty" yaml:"Links,omitempty"` - PublishAllPorts bool `json:"PublishAllPorts,omitempty" yaml:"PublishAllPorts,omitempty"` - DNS []string `json:"Dns,omitempty" yaml:"Dns,omitempty"` // For Docker API v1.10 and above only - DNSSearch []string `json:"DnsSearch,omitempty" yaml:"DnsSearch,omitempty"` - ExtraHosts []string `json:"ExtraHosts,omitempty" yaml:"ExtraHosts,omitempty"` - VolumesFrom []string `json:"VolumesFrom,omitempty" yaml:"VolumesFrom,omitempty"` - NetworkMode string `json:"NetworkMode,omitempty" yaml:"NetworkMode,omitempty"` - IpcMode string `json:"IpcMode,omitempty" yaml:"IpcMode,omitempty"` - PidMode string `json:"PidMode,omitempty" yaml:"PidMode,omitempty"` - UTSMode string `json:"UTSMode,omitempty" yaml:"UTSMode,omitempty"` - RestartPolicy RestartPolicy `json:"RestartPolicy,omitempty" yaml:"RestartPolicy,omitempty"` - Devices []Device `json:"Devices,omitempty" yaml:"Devices,omitempty"` - LogConfig LogConfig `json:"LogConfig,omitempty" yaml:"LogConfig,omitempty"` - ReadonlyRootfs bool `json:"ReadonlyRootfs,omitempty" yaml:"ReadonlyRootfs,omitempty"` - SecurityOpt []string `json:"SecurityOpt,omitempty" yaml:"SecurityOpt,omitempty"` - CgroupParent string `json:"CgroupParent,omitempty" yaml:"CgroupParent,omitempty"` - 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"` - CPUQuota int64 `json:"CpuQuota,omitempty" yaml:"CpuQuota,omitempty"` - CPUPeriod int64 `json:"CpuPeriod,omitempty" yaml:"CpuPeriod,omitempty"` - Ulimits []ULimit `json:"Ulimits,omitempty" yaml:"Ulimits,omitempty"` + Binds []string `json:"Binds,omitempty" yaml:"Binds,omitempty"` + CapAdd []string `json:"CapAdd,omitempty" yaml:"CapAdd,omitempty"` + CapDrop []string `json:"CapDrop,omitempty" yaml:"CapDrop,omitempty"` + ContainerIDFile string `json:"ContainerIDFile,omitempty" yaml:"ContainerIDFile,omitempty"` + LxcConf []KeyValuePair `json:"LxcConf,omitempty" yaml:"LxcConf,omitempty"` + Privileged bool `json:"Privileged,omitempty" yaml:"Privileged,omitempty"` + PortBindings map[Port][]PortBinding `json:"PortBindings,omitempty" yaml:"PortBindings,omitempty"` + Links []string `json:"Links,omitempty" yaml:"Links,omitempty"` + PublishAllPorts bool `json:"PublishAllPorts,omitempty" yaml:"PublishAllPorts,omitempty"` + DNS []string `json:"Dns,omitempty" yaml:"Dns,omitempty"` // For Docker API v1.10 and above only + DNSSearch []string `json:"DnsSearch,omitempty" yaml:"DnsSearch,omitempty"` + ExtraHosts []string `json:"ExtraHosts,omitempty" yaml:"ExtraHosts,omitempty"` + VolumesFrom []string `json:"VolumesFrom,omitempty" yaml:"VolumesFrom,omitempty"` + NetworkMode string `json:"NetworkMode,omitempty" yaml:"NetworkMode,omitempty"` + IpcMode string `json:"IpcMode,omitempty" yaml:"IpcMode,omitempty"` + PidMode string `json:"PidMode,omitempty" yaml:"PidMode,omitempty"` + UTSMode string `json:"UTSMode,omitempty" yaml:"UTSMode,omitempty"` + RestartPolicy RestartPolicy `json:"RestartPolicy,omitempty" yaml:"RestartPolicy,omitempty"` + Devices []Device `json:"Devices,omitempty" yaml:"Devices,omitempty"` + LogConfig LogConfig `json:"LogConfig,omitempty" yaml:"LogConfig,omitempty"` + ReadonlyRootfs bool `json:"ReadonlyRootfs,omitempty" yaml:"ReadonlyRootfs,omitempty"` + SecurityOpt []string `json:"SecurityOpt,omitempty" yaml:"SecurityOpt,omitempty"` + CgroupParent string `json:"CgroupParent,omitempty" yaml:"CgroupParent,omitempty"` + Memory int64 `json:"Memory,omitempty" yaml:"Memory,omitempty"` + MemorySwap int64 `json:"MemorySwap,omitempty" yaml:"MemorySwap,omitempty"` + MemorySwappiness int64 `json:"MemorySwappiness,omitempty" yaml:"MemorySwappiness,omitempty"` + OOMKillDisable bool `json:"OomKillDisable,omitempty" yaml:"OomKillDisable"` + CPUShares int64 `json:"CpuShares,omitempty" yaml:"CpuShares,omitempty"` + CPUSet string `json:"Cpuset,omitempty" yaml:"Cpuset,omitempty"` + CPUSetCPUs string `json:"CpusetCpus,omitempty" yaml:"CpusetCpus,omitempty"` + CPUSetMEMs string `json:"CpusetMems,omitempty" yaml:"CpusetMems,omitempty"` + CPUQuota int64 `json:"CpuQuota,omitempty" yaml:"CpuQuota,omitempty"` + CPUPeriod int64 `json:"CpuPeriod,omitempty" yaml:"CpuPeriod,omitempty"` + BlkioWeight int64 `json:"BlkioWeight,omitempty" yaml:"BlkioWeight"` + Ulimits []ULimit `json:"Ulimits,omitempty" yaml:"Ulimits,omitempty"` } // StartContainer starts a container, returning an error in case of failure. // -// See http://goo.gl/iM5GYs for more details. +// See https://goo.gl/MrBAJv for more details. func (c *Client) StartContainer(id string, hostConfig *HostConfig) error { path := "/containers/" + id + "/start" - _, status, err := c.do("POST", path, doOptions{data: hostConfig, forceJSON: true}) - if status == http.StatusNotFound { - return &NoSuchContainer{ID: id, Err: err} - } - if status == http.StatusNotModified { - return &ContainerAlreadyRunning{ID: id} - } + resp, err := c.do("POST", path, doOptions{data: hostConfig, forceJSON: true}) if err != nil { + if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { + return &NoSuchContainer{ID: id, Err: err} + } return err } + if resp.StatusCode == http.StatusNotModified { + return &ContainerAlreadyRunning{ID: id} + } + resp.Body.Close() return nil } // StopContainer stops a container, killing it after the given timeout (in // seconds). // -// See http://goo.gl/EbcpXt for more details. +// See https://goo.gl/USqsFt 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, doOptions{}) - if status == http.StatusNotFound { - return &NoSuchContainer{ID: id} - } - if status == http.StatusNotModified { - return &ContainerNotRunning{ID: id} - } + resp, err := c.do("POST", path, doOptions{}) if err != nil { + if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { + return &NoSuchContainer{ID: id} + } return err } + if resp.StatusCode == http.StatusNotModified { + return &ContainerNotRunning{ID: id} + } + resp.Body.Close() return nil } // RestartContainer stops a container, killing it after the given timeout (in // seconds), during the stop process. // -// See http://goo.gl/VOzR2n for more details. +// See https://goo.gl/QzsDnz 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, doOptions{}) - if status == http.StatusNotFound { - return &NoSuchContainer{ID: id} - } + resp, err := c.do("POST", path, doOptions{}) if err != nil { + if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { + return &NoSuchContainer{ID: id} + } return err } + resp.Body.Close() return nil } // PauseContainer pauses the given container. // -// See http://goo.gl/AM5t42 for more details. +// See https://goo.gl/OF7W9X for more details. func (c *Client) PauseContainer(id string) error { path := fmt.Sprintf("/containers/%s/pause", id) - _, status, err := c.do("POST", path, doOptions{}) - if status == http.StatusNotFound { - return &NoSuchContainer{ID: id} - } + resp, err := c.do("POST", path, doOptions{}) if err != nil { + if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { + return &NoSuchContainer{ID: id} + } return err } + resp.Body.Close() return nil } // UnpauseContainer unpauses the given container. // -// See http://goo.gl/eBrNSL for more details. +// See https://goo.gl/7dwyPA for more details. func (c *Client) UnpauseContainer(id string) error { path := fmt.Sprintf("/containers/%s/unpause", id) - _, status, err := c.do("POST", path, doOptions{}) - if status == http.StatusNotFound { - return &NoSuchContainer{ID: id} - } + resp, err := c.do("POST", path, doOptions{}) if err != nil { + if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { + return &NoSuchContainer{ID: id} + } return err } + resp.Body.Close() 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. +// See https://goo.gl/Rb46aY for more details. type TopResult struct { Titles []string Processes [][]string @@ -560,7 +590,7 @@ type TopResult struct { // TopContainer returns processes running inside a container // -// See http://goo.gl/qu4gse for more details. +// See https://goo.gl/Rb46aY for more details. func (c *Client) TopContainer(id string, psArgs string) (TopResult, error) { var args string var result TopResult @@ -568,15 +598,15 @@ func (c *Client) TopContainer(id string, psArgs string) (TopResult, error) { args = fmt.Sprintf("?ps_args=%s", psArgs) } path := fmt.Sprintf("/containers/%s/top%s", id, args) - body, status, err := c.do("GET", path, doOptions{}) - if status == http.StatusNotFound { - return result, &NoSuchContainer{ID: id} - } + resp, err := c.do("GET", path, doOptions{}) if err != nil { + if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { + return result, &NoSuchContainer{ID: id} + } return result, err } - err = json.Unmarshal(body, &result) - if err != nil { + defer resp.Body.Close() + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return result, err } return result, nil @@ -584,7 +614,7 @@ func (c *Client) TopContainer(id string, psArgs string) (TopResult, error) { // Stats represents container statistics, returned by /containers//stats. // -// See http://goo.gl/DFMiYD for more details. +// See https://goo.gl/GNmLHb for more details. type Stats struct { Read time.Time `json:"read,omitempty" yaml:"read,omitempty"` Network struct { @@ -674,7 +704,7 @@ type BlkioStatsEntry struct { // StatsOptions specify parameters to the Stats function. // -// See http://goo.gl/DFMiYD for more details. +// See https://goo.gl/GNmLHb for more details. type StatsOptions struct { ID string Stats chan<- *Stats @@ -690,9 +720,10 @@ type StatsOptions struct { // This function is blocking, similar to a streaming call for logs, and should be run // on a separate goroutine from the caller. Note that this function will block until // the given container is removed, not just exited. When finished, this function -// will close the given channel. Alternatively, function can be stopped by signaling on the Done channel +// will close the given channel. Alternatively, function can be stopped by +// signaling on the Done channel. // -// See http://goo.gl/DFMiYD for more details. +// See https://goo.gl/GNmLHb for more details. func (c *Client) Stats(opts StatsOptions) (retErr error) { errC := make(chan error, 1) readCloser, writeCloser := io.Pipe() @@ -750,7 +781,7 @@ func (c *Client) Stats(opts StatsOptions) (retErr error) { decoder := json.NewDecoder(readCloser) stats := new(Stats) - for err := decoder.Decode(&stats); err != io.EOF; err = decoder.Decode(stats) { + for err := decoder.Decode(stats); err != io.EOF; err = decoder.Decode(stats) { if err != nil { return err } @@ -763,7 +794,7 @@ func (c *Client) Stats(opts StatsOptions) (retErr error) { // KillContainerOptions represents the set of options that can be used in a // call to KillContainer. // -// See http://goo.gl/TFkECx for more details. +// See https://goo.gl/hkS9i8 for more details. type KillContainerOptions struct { // The ID of the container. ID string `qs:"-"` @@ -773,24 +804,26 @@ type KillContainerOptions struct { Signal Signal } -// KillContainer kills a container, returning an error in case of failure. +// KillContainer sends a signal to a container, returning an error in case of +// failure. // -// See http://goo.gl/TFkECx for more details. +// See https://goo.gl/hkS9i8 for more details. func (c *Client) KillContainer(opts KillContainerOptions) error { path := "/containers/" + opts.ID + "/kill" + "?" + queryString(opts) - _, status, err := c.do("POST", path, doOptions{}) - if status == http.StatusNotFound { - return &NoSuchContainer{ID: opts.ID} - } + resp, err := c.do("POST", path, doOptions{}) if err != nil { + if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { + return &NoSuchContainer{ID: opts.ID} + } return err } + resp.Body.Close() return nil } // RemoveContainerOptions encapsulates options to remove a container. // -// See http://goo.gl/ZB83ji for more details. +// See https://goo.gl/RQyX62 for more details. type RemoveContainerOptions struct { // The ID of the container. ID string `qs:"-"` @@ -806,64 +839,107 @@ type RemoveContainerOptions struct { // RemoveContainer removes a container, returning an error in case of failure. // -// See http://goo.gl/ZB83ji for more details. +// See https://goo.gl/RQyX62 for more details. func (c *Client) RemoveContainer(opts RemoveContainerOptions) error { path := "/containers/" + opts.ID + "?" + queryString(opts) - _, status, err := c.do("DELETE", path, doOptions{}) - if status == http.StatusNotFound { - return &NoSuchContainer{ID: opts.ID} - } + resp, err := c.do("DELETE", path, doOptions{}) if err != nil { + if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { + return &NoSuchContainer{ID: opts.ID} + } return err } + resp.Body.Close() return nil } -// CopyFromContainerOptions is the set of options that can be used when copying -// files or folders from a container. +// UploadToContainerOptions is the set of options that can be used when +// uploading an archive into a container. // -// See http://goo.gl/rINMlw for more details. +// See https://goo.gl/Ss97HW for more details. +type UploadToContainerOptions struct { + InputStream io.Reader `json:"-" qs:"-"` + Path string `qs:"path"` + NoOverwriteDirNonDir bool `qs:"noOverwriteDirNonDir"` +} + +// UploadToContainer uploads a tar archive to be extracted to a path in the +// filesystem of the container. +// +// See https://goo.gl/Ss97HW for more details. +func (c *Client) UploadToContainer(id string, opts UploadToContainerOptions) error { + url := fmt.Sprintf("/containers/%s/archive?", id) + queryString(opts) + + return c.stream("PUT", url, streamOptions{ + in: opts.InputStream, + }) +} + +// DownloadFromContainerOptions is the set of options that can be used when +// downloading resources from a container. +// +// See https://goo.gl/KnZJDX for more details. +type DownloadFromContainerOptions struct { + OutputStream io.Writer `json:"-" qs:"-"` + Path string `qs:"path"` +} + +// DownloadFromContainer downloads a tar archive of files or folders in a container. +// +// See https://goo.gl/KnZJDX for more details. +func (c *Client) DownloadFromContainer(id string, opts DownloadFromContainerOptions) error { + url := fmt.Sprintf("/containers/%s/archive?", id) + queryString(opts) + + return c.stream("GET", url, streamOptions{ + setRawTerminal: true, + stdout: opts.OutputStream, + }) +} + +// CopyFromContainerOptions has been DEPRECATED, please use DownloadFromContainerOptions along with DownloadFromContainer. +// +// See https://goo.gl/R2jevW for more details. type CopyFromContainerOptions struct { OutputStream io.Writer `json:"-"` Container string `json:"-"` Resource string } -// CopyFromContainer copy files or folders from a container, using a given -// resource. +// CopyFromContainer has been DEPRECATED, please use DownloadFromContainerOptions along with DownloadFromContainer. // -// See http://goo.gl/rINMlw for more details. +// See https://goo.gl/R2jevW for more details. func (c *Client) CopyFromContainer(opts CopyFromContainerOptions) error { if opts.Container == "" { return &NoSuchContainer{ID: opts.Container} } url := fmt.Sprintf("/containers/%s/copy", opts.Container) - body, status, err := c.do("POST", url, doOptions{data: opts}) - if status == http.StatusNotFound { - return &NoSuchContainer{ID: opts.Container} - } + resp, err := c.do("POST", url, doOptions{data: opts}) if err != nil { + if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { + return &NoSuchContainer{ID: opts.Container} + } return err } - _, err = io.Copy(opts.OutputStream, bytes.NewBuffer(body)) + defer resp.Body.Close() + _, err = io.Copy(opts.OutputStream, resp.Body) return err } // WaitContainer blocks until the given container stops, return the exit code // of the container status. // -// See http://goo.gl/J88DHU for more details. +// See https://goo.gl/Gc1rge for more details. func (c *Client) WaitContainer(id string) (int, error) { - body, status, err := c.do("POST", "/containers/"+id+"/wait", doOptions{}) - if status == http.StatusNotFound { - return 0, &NoSuchContainer{ID: id} - } + resp, err := c.do("POST", "/containers/"+id+"/wait", doOptions{}) if err != nil { + if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { + return 0, &NoSuchContainer{ID: id} + } return 0, err } + defer resp.Body.Close() var r struct{ StatusCode int } - err = json.Unmarshal(body, &r) - if err != nil { + if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { return 0, err } return r.StatusCode, nil @@ -871,7 +947,7 @@ func (c *Client) WaitContainer(id string) (int, error) { // CommitContainerOptions aggregates parameters to the CommitContainer method. // -// See http://goo.gl/Jn8pe8 for more details. +// See https://goo.gl/mqfoCw for more details. type CommitContainerOptions struct { Container string Repository string `qs:"repo"` @@ -883,19 +959,19 @@ type CommitContainerOptions struct { // CommitContainer creates a new image from a container's changes. // -// See http://goo.gl/Jn8pe8 for more details. +// See https://goo.gl/mqfoCw for more details. func (c *Client) CommitContainer(opts CommitContainerOptions) (*Image, error) { path := "/commit?" + queryString(opts) - body, status, err := c.do("POST", path, doOptions{data: opts.Run}) - if status == http.StatusNotFound { - return nil, &NoSuchContainer{ID: opts.Container} - } + resp, err := c.do("POST", path, doOptions{data: opts.Run}) if err != nil { + if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { + return nil, &NoSuchContainer{ID: opts.Container} + } return nil, err } + defer resp.Body.Close() var image Image - err = json.Unmarshal(body, &image) - if err != nil { + if err := json.NewDecoder(resp.Body).Decode(&image); err != nil { return nil, err } return &image, nil @@ -904,7 +980,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/RRAhws for more details. +// See https://goo.gl/NKpkFk for more details. type AttachToContainerOptions struct { Container string `qs:"-"` InputStream io.Reader `qs:"-"` @@ -939,7 +1015,7 @@ type AttachToContainerOptions struct { // AttachToContainer attaches to a container, using the given options. // -// See http://goo.gl/RRAhws for more details. +// See https://goo.gl/NKpkFk for more details. func (c *Client) AttachToContainer(opts AttachToContainerOptions) error { if opts.Container == "" { return &NoSuchContainer{ID: opts.Container} @@ -957,7 +1033,7 @@ func (c *Client) AttachToContainer(opts AttachToContainerOptions) error { // LogsOptions represents the set of options used when getting logs from a // container. // -// See http://goo.gl/rLhKSU for more details. +// See https://goo.gl/yl8PGm for more details. type LogsOptions struct { Container string `qs:"-"` OutputStream io.Writer `qs:"-"` @@ -975,7 +1051,7 @@ type LogsOptions struct { // Logs gets stdout and stderr logs from the specified container. // -// See http://goo.gl/rLhKSU for more details. +// See https://goo.gl/yl8PGm for more details. func (c *Client) Logs(opts LogsOptions) error { if opts.Container == "" { return &NoSuchContainer{ID: opts.Container} @@ -992,18 +1068,24 @@ func (c *Client) Logs(opts LogsOptions) error { } // ResizeContainerTTY resizes the terminal to the given height and width. +// +// See https://goo.gl/xERhCc for more details. func (c *Client) ResizeContainerTTY(id string, height, width int) error { params := make(url.Values) params.Set("h", strconv.Itoa(height)) params.Set("w", strconv.Itoa(width)) - _, _, err := c.do("POST", "/containers/"+id+"/resize?"+params.Encode(), doOptions{}) - return err + resp, err := c.do("POST", "/containers/"+id+"/resize?"+params.Encode(), doOptions{}) + if err != nil { + return err + } + resp.Body.Close() + return nil } // ExportContainerOptions is the set of parameters to the ExportContainer // method. // -// See http://goo.gl/hnzE62 for more details. +// See https://goo.gl/dOkTyk for more details. type ExportContainerOptions struct { ID string OutputStream io.Writer @@ -1012,7 +1094,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/hnzE62 for more details. +// See https://goo.gl/dOkTyk 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 00966aa19..d42c82fa0 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 @@ -1122,10 +1122,7 @@ func TestAttachToContainerRawTerminalFalse(t *testing.T) { Stream: true, RawTerminal: false, } - err := client.AttachToContainer(opts) - if err != nil { - t.Fatal(err) - } + client.AttachToContainer(opts) expected := map[string][]string{ "stdin": {"1"}, "stdout": {"1"}, @@ -1374,6 +1371,7 @@ func TestExportContainerViaUnixSocket(t *testing.T) { u, _ := parseEndpoint(endpoint, false) client := Client{ HTTPClient: http.DefaultClient, + Dialer: &net.Dialer{}, endpoint: endpoint, endpointURL: u, SkipServerVersionCheck: true, @@ -1429,17 +1427,60 @@ func TestExportContainerNoId(t *testing.T) { } } +func TestUploadToContainer(t *testing.T) { + content := "File content" + in := stdinMock{bytes.NewBufferString(content)} + fakeRT := &FakeRoundTripper{status: http.StatusOK} + client := newTestClient(fakeRT) + opts := UploadToContainerOptions{ + Path: "abc", + InputStream: in, + } + err := client.UploadToContainer("a123456", opts) + if err != nil { + t.Errorf("UploadToContainer: caught error %#v while uploading archive to container, expected nil", err) + } + + req := fakeRT.requests[0] + + if req.Method != "PUT" { + t.Errorf("UploadToContainer{Path:abc}: Wrong HTTP method. Want PUT. Got %s", req.Method) + } + + if pathParam := req.URL.Query().Get("path"); pathParam != "abc" { + t.Errorf("ListImages({Path:abc}): Wrong parameter. Want path=abc. Got path=%s", pathParam) + } + +} + +func TestDownloadFromContainer(t *testing.T) { + filecontent := "File content" + client := newTestClient(&FakeRoundTripper{message: filecontent, status: http.StatusOK}) + + var out bytes.Buffer + opts := DownloadFromContainerOptions{ + OutputStream: &out, + } + err := client.DownloadFromContainer("a123456", opts) + if err != nil { + t.Errorf("DownloadFromContainer: caught error %#v while downloading from container, expected nil", err.Error()) + } + if out.String() != filecontent { + t.Errorf("DownloadFromContainer: wrong stdout. Want %#v. Got %#v.", filecontent, out.String()) + } +} + func TestCopyFromContainer(t *testing.T) { content := "File content" out := stdoutMock{bytes.NewBufferString(content)} client := newTestClient(&FakeRoundTripper{status: http.StatusOK}) opts := CopyFromContainerOptions{ Container: "a123456", - OutputStream: out, + OutputStream: &out, } err := client.CopyFromContainer(opts) if err != nil { - t.Errorf("CopyFromContainer: caugh error %#v while copying from container, expected nil", err.Error()) + t.Errorf("CopyFromContainer: caught error %#v while copying from container, expected nil", err.Error()) } if out.String() != content { t.Errorf("CopyFromContainer: wrong stdout. Want %#v. Got %#v.", content, out.String()) @@ -1587,7 +1628,6 @@ func TestTopContainerWithPsArgs(t *testing.T) { } func TestStatsTimeout(t *testing.T) { - l, err := net.Listen("unix", "/tmp/docker_test.sock") if err != nil { t.Fatal(err) @@ -1597,7 +1637,7 @@ func TestStatsTimeout(t *testing.T) { go func() { l.Accept() received = true - time.Sleep(time.Millisecond * 250) + time.Sleep(time.Second) }() client, _ := NewClient("unix:///tmp/docker_test.sock") client.SkipServerVersionCheck = true diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/event.go b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/event.go index 5a85983cc..eaffddb82 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/event.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/event.go @@ -5,7 +5,6 @@ package docker import ( - "crypto/tls" "encoding/json" "errors" "fmt" @@ -260,9 +259,9 @@ func (c *Client) eventHijack(startTime int64, eventChan chan *APIEvents, errChan var dial net.Conn var err error if c.TLSConfig == nil { - dial, err = net.Dial(protocol, address) + dial, err = c.Dialer.Dial(protocol, address) } else { - dial, err = tls.Dial(protocol, address, c.TLSConfig) + dial, err = tlsDialWithDialer(c.Dialer, protocol, address, c.TLSConfig) } if err != nil { return err diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/exec.go b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/exec.go index bc7c5cfcd..f3b705fa0 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/exec.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/exec.go @@ -2,8 +2,6 @@ // 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 ( @@ -15,9 +13,15 @@ import ( "strconv" ) +// Exec is the type representing a `docker exec` instance and containing the +// instance ID +type Exec struct { + ID string `json:"Id,omitempty" yaml:"Id,omitempty"` +} + // CreateExecOptions specify parameters to the CreateExecContainer function. // -// See http://goo.gl/8izrzI for more details +// See https://goo.gl/1KSIb7 for more details type CreateExecOptions struct { AttachStdin bool `json:"AttachStdin,omitempty" yaml:"AttachStdin,omitempty"` AttachStdout bool `json:"AttachStdout,omitempty" yaml:"AttachStdout,omitempty"` @@ -28,9 +32,31 @@ type CreateExecOptions struct { User string `json:"User,omitempty" yaml:"User,omitempty"` } +// CreateExec sets up an exec instance in a running container `id`, returning the exec +// instance, or an error in case of failure. +// +// See https://goo.gl/1KSIb7 for more details +func (c *Client) CreateExec(opts CreateExecOptions) (*Exec, error) { + path := fmt.Sprintf("/containers/%s/exec", opts.Container) + resp, err := c.do("POST", path, doOptions{data: opts}) + if err != nil { + if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { + return nil, &NoSuchContainer{ID: opts.Container} + } + return nil, err + } + defer resp.Body.Close() + var exec Exec + if err := json.NewDecoder(resp.Body).Decode(&exec); err != nil { + return nil, err + } + + return &exec, nil +} + // StartExecOptions specify parameters to the StartExecContainer function. // -// See http://goo.gl/JW8Lxl for more details +// See https://goo.gl/iQCnto for more details type StartExecOptions struct { Detach bool `json:"Detach,omitempty" yaml:"Detach,omitempty"` @@ -51,67 +77,11 @@ type StartExecOptions struct { Success chan struct{} `json:"-"` } -// Exec is the type representing a `docker exec` instance and containing the -// instance ID -type Exec struct { - ID string `json:"Id,omitempty" yaml:"Id,omitempty"` -} - -// ExecProcessConfig is a type describing the command associated to a Exec -// instance. It's used in the ExecInspect type. -// -// See http://goo.gl/ypQULN for more details -type ExecProcessConfig struct { - Privileged bool `json:"privileged,omitempty" yaml:"privileged,omitempty"` - User string `json:"user,omitempty" yaml:"user,omitempty"` - Tty bool `json:"tty,omitempty" yaml:"tty,omitempty"` - EntryPoint string `json:"entrypoint,omitempty" yaml:"entrypoint,omitempty"` - Arguments []string `json:"arguments,omitempty" yaml:"arguments,omitempty"` -} - -// ExecInspect is a type with details about a exec instance, including the -// exit code if the command has finished running. It's returned by a api -// call to /exec/(id)/json -// -// See http://goo.gl/ypQULN for more details -type ExecInspect struct { - ID string `json:"ID,omitempty" yaml:"ID,omitempty"` - Running bool `json:"Running,omitempty" yaml:"Running,omitempty"` - ExitCode int `json:"ExitCode,omitempty" yaml:"ExitCode,omitempty"` - OpenStdin bool `json:"OpenStdin,omitempty" yaml:"OpenStdin,omitempty"` - OpenStderr bool `json:"OpenStderr,omitempty" yaml:"OpenStderr,omitempty"` - OpenStdout bool `json:"OpenStdout,omitempty" yaml:"OpenStdout,omitempty"` - ProcessConfig ExecProcessConfig `json:"ProcessConfig,omitempty" yaml:"ProcessConfig,omitempty"` - Container Container `json:"Container,omitempty" yaml:"Container,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, doOptions{data: 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 -} - // StartExec 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 +// See https://goo.gl/iQCnto for more details func (c *Client) StartExec(id string, opts StartExecOptions) error { if id == "" { return &NoSuchExec{ID: id} @@ -120,13 +90,14 @@ func (c *Client) StartExec(id string, opts StartExecOptions) error { path := fmt.Sprintf("/exec/%s/start", id) if opts.Detach { - _, status, err := c.do("POST", path, doOptions{data: opts}) - if status == http.StatusNotFound { - return &NoSuchExec{ID: id} - } + resp, err := c.do("POST", path, doOptions{data: opts}) if err != nil { + if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { + return &NoSuchExec{ID: id} + } return err } + defer resp.Body.Close() return nil } @@ -144,32 +115,62 @@ func (c *Client) StartExec(id string, opts StartExecOptions) error { // is valid only if Tty was specified as part of creating and starting the exec // command. // -// See http://goo.gl/YDSx1f for more details +// See https://goo.gl/e1JpsA 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, doOptions{}) - return err + resp, err := c.do("POST", path, doOptions{}) + if err != nil { + return err + } + resp.Body.Close() + return nil +} + +// ExecProcessConfig is a type describing the command associated to a Exec +// instance. It's used in the ExecInspect type. +type ExecProcessConfig struct { + Privileged bool `json:"privileged,omitempty" yaml:"privileged,omitempty"` + User string `json:"user,omitempty" yaml:"user,omitempty"` + Tty bool `json:"tty,omitempty" yaml:"tty,omitempty"` + EntryPoint string `json:"entrypoint,omitempty" yaml:"entrypoint,omitempty"` + Arguments []string `json:"arguments,omitempty" yaml:"arguments,omitempty"` +} + +// ExecInspect is a type with details about a exec instance, including the +// exit code if the command has finished running. It's returned by a api +// call to /exec/(id)/json +// +// See https://goo.gl/gPtX9R for more details +type ExecInspect struct { + ID string `json:"ID,omitempty" yaml:"ID,omitempty"` + Running bool `json:"Running,omitempty" yaml:"Running,omitempty"` + ExitCode int `json:"ExitCode,omitempty" yaml:"ExitCode,omitempty"` + OpenStdin bool `json:"OpenStdin,omitempty" yaml:"OpenStdin,omitempty"` + OpenStderr bool `json:"OpenStderr,omitempty" yaml:"OpenStderr,omitempty"` + OpenStdout bool `json:"OpenStdout,omitempty" yaml:"OpenStdout,omitempty"` + ProcessConfig ExecProcessConfig `json:"ProcessConfig,omitempty" yaml:"ProcessConfig,omitempty"` + Container Container `json:"Container,omitempty" yaml:"Container,omitempty"` } // InspectExec returns low-level information about the exec command id. // -// See http://goo.gl/ypQULN for more details +// See https://goo.gl/gPtX9R for more details func (c *Client) InspectExec(id string) (*ExecInspect, error) { path := fmt.Sprintf("/exec/%s/json", id) - body, status, err := c.do("GET", path, doOptions{}) - if status == http.StatusNotFound { - return nil, &NoSuchExec{ID: id} - } + resp, err := c.do("GET", path, doOptions{}) if err != nil { + if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { + return nil, &NoSuchExec{ID: id} + } return nil, err } + defer resp.Body.Close() var exec ExecInspect - err = json.Unmarshal(body, &exec) - if err != nil { + if err := json.NewDecoder(resp.Body).Decode(&exec); err != nil { return nil, err } return &exec, nil 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 4bceb0d3d..c54264616 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/image.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/image.go @@ -11,7 +11,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net/http" "net/url" "os" @@ -46,16 +45,6 @@ type Image struct { VirtualSize int64 `json:"VirtualSize,omitempty" yaml:"VirtualSize,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"` -} - // ImagePre012 serves the same purpose as the Image type except that it is for // earlier versions of the Docker API (pre-012 to be specific) type ImagePre012 struct { @@ -72,15 +61,6 @@ type ImagePre012 struct { Size int64 `json:"size,omitempty"` } -// ListImagesOptions specify parameters to the ListImages function. -// -// See http://goo.gl/HRVN1Z for more details. -type ListImagesOptions struct { - All bool - Filters map[string][]string - Digests bool -} - var ( // ErrNoSuchImage is the error returned when the image does not exist. ErrNoSuchImage = errors.New("no such image") @@ -102,37 +82,56 @@ var ( ErrMustSpecifyNames = errors.New("must specify at least one name to export") ) +// ListImagesOptions specify parameters to the ListImages function. +// +// See https://goo.gl/xBe1u3 for more details. +type ListImagesOptions struct { + All bool + Filters map[string][]string + Digests bool +} + // ListImages returns the list of available images in the server. // -// See http://goo.gl/HRVN1Z for more details. +// See https://goo.gl/xBe1u3 for more details. func (c *Client) ListImages(opts ListImagesOptions) ([]APIImages, error) { path := "/images/json?" + queryString(opts) - body, _, err := c.do("GET", path, doOptions{}) + resp, err := c.do("GET", path, doOptions{}) if err != nil { return nil, err } + defer resp.Body.Close() var images []APIImages - err = json.Unmarshal(body, &images) - if err != nil { + if err := json.NewDecoder(resp.Body).Decode(&images); err != nil { return nil, err } return images, nil } +// 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"` +} + // ImageHistory returns the history of the image by its name or ID. // -// See http://goo.gl/2oJmNs for more details. +// See https://goo.gl/8bnTId for more details. func (c *Client) ImageHistory(name string) ([]ImageHistory, error) { - body, status, err := c.do("GET", "/images/"+name+"/history", doOptions{}) - if status == http.StatusNotFound { - return nil, ErrNoSuchImage - } + resp, err := c.do("GET", "/images/"+name+"/history", doOptions{}) if err != nil { + if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { + return nil, ErrNoSuchImage + } return nil, err } + defer resp.Body.Close() var history []ImageHistory - err = json.Unmarshal(body, &history) - if err != nil { + if err := json.NewDecoder(resp.Body).Decode(&history); err != nil { return nil, err } return history, nil @@ -140,19 +139,23 @@ func (c *Client) ImageHistory(name string) ([]ImageHistory, error) { // RemoveImage removes an image by its name or ID. // -// See http://goo.gl/znj0wM for more details. +// See https://goo.gl/V3ZWnK for more details. func (c *Client) RemoveImage(name string) error { - _, status, err := c.do("DELETE", "/images/"+name, doOptions{}) - if status == http.StatusNotFound { - return ErrNoSuchImage + resp, err := c.do("DELETE", "/images/"+name, doOptions{}) + if err != nil { + if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { + return ErrNoSuchImage + } + return err } - return err + resp.Body.Close() + return nil } // RemoveImageOptions present the set of options available for removing an image // from a registry. // -// See http://goo.gl/6V48bF for more details. +// See https://goo.gl/V3ZWnK for more details. type RemoveImageOptions struct { Force bool `qs:"force"` NoPrune bool `qs:"noprune"` @@ -161,40 +164,43 @@ type RemoveImageOptions struct { // RemoveImageExtended removes an image by its name or ID. // Extra params can be passed, see RemoveImageOptions // -// See http://goo.gl/znj0wM for more details. +// See https://goo.gl/V3ZWnK for more details. func (c *Client) RemoveImageExtended(name string, opts RemoveImageOptions) error { uri := fmt.Sprintf("/images/%s?%s", name, queryString(&opts)) - _, status, err := c.do("DELETE", uri, doOptions{}) - if status == http.StatusNotFound { - return ErrNoSuchImage + resp, err := c.do("DELETE", uri, doOptions{}) + if err != nil { + if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { + return ErrNoSuchImage + } + return err } - return err + resp.Body.Close() + return nil } // InspectImage returns an image by its name or ID. // -// See http://goo.gl/Q112NY for more details. +// See https://goo.gl/jHPcg6 for more details. func (c *Client) InspectImage(name string) (*Image, error) { - body, status, err := c.do("GET", "/images/"+name+"/json", doOptions{}) - if status == http.StatusNotFound { - return nil, ErrNoSuchImage - } + resp, err := c.do("GET", "/images/"+name+"/json", doOptions{}) if err != nil { + if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { + return nil, ErrNoSuchImage + } return nil, err } + defer resp.Body.Close() var image Image // if the caller elected to skip checking the server's version, assume it's the latest if c.SkipServerVersionCheck || c.expectedAPIVersion.GreaterThanOrEqualTo(apiVersion112) { - err = json.Unmarshal(body, &image) - if err != nil { + if err := json.NewDecoder(resp.Body).Decode(&image); err != nil { return nil, err } } else { var imagePre012 ImagePre012 - err = json.Unmarshal(body, &imagePre012) - if err != nil { + if err := json.NewDecoder(resp.Body).Decode(&imagePre012); err != nil { return nil, err } @@ -216,7 +222,7 @@ func (c *Client) InspectImage(name string) (*Image, error) { // PushImageOptions represents options to use in the PushImage method. // -// See http://goo.gl/pN8A3P for more details. +// See https://goo.gl/zPtZaT for more details. type PushImageOptions struct { // Name of the image Name string @@ -236,7 +242,7 @@ type PushImageOptions struct { // An empty instance of AuthConfiguration may be used for unauthenticated // pushes. // -// See http://goo.gl/pN8A3P for more details. +// See https://goo.gl/zPtZaT for more details. func (c *Client) PushImage(opts PushImageOptions, auth AuthConfiguration) error { if opts.Name == "" { return ErrNoSuchImage @@ -259,7 +265,7 @@ func (c *Client) PushImage(opts PushImageOptions, auth AuthConfiguration) error // PullImageOptions present the set of options available for pulling an image // from a registry. // -// See http://goo.gl/ACyYNS for more details. +// See https://goo.gl/iJkZjD for more details. type PullImageOptions struct { Repository string `qs:"fromImage"` Registry string @@ -268,9 +274,10 @@ type PullImageOptions struct { RawJSONStream bool `qs:"-"` } -// PullImage pulls an image from a remote registry, logging progress to opts.OutputStream. +// PullImage pulls an image from a remote registry, logging progress to +// opts.OutputStream. // -// See http://goo.gl/ACyYNS for more details. +// See https://goo.gl/iJkZjD for more details. func (c *Client) PullImage(opts PullImageOptions, auth AuthConfiguration) error { if opts.Repository == "" { return ErrNoSuchImage @@ -296,14 +303,14 @@ func (c *Client) createImage(qs string, headers map[string]string, in io.Reader, // LoadImageOptions represents the options for LoadImage Docker API Call // -// See http://goo.gl/Y8NNCq for more details. +// See https://goo.gl/JyClMX for more details. type LoadImageOptions struct { InputStream io.Reader } // LoadImage imports a tarball docker image // -// See http://goo.gl/Y8NNCq for more details. +// See https://goo.gl/JyClMX for more details. func (c *Client) LoadImage(opts LoadImageOptions) error { return c.stream("POST", "/images/load", streamOptions{ setRawTerminal: true, @@ -311,17 +318,17 @@ func (c *Client) LoadImage(opts LoadImageOptions) error { }) } -// ExportImageOptions represent the options for ExportImage Docker API call +// ExportImageOptions represent the options for ExportImage Docker API call. // -// See http://goo.gl/mi6kvk for more details. +// See https://goo.gl/le7vK8 for more details. type ExportImageOptions struct { Name string OutputStream io.Writer } -// ExportImage exports an image (as a tar file) into the stream +// ExportImage exports an image (as a tar file) into the stream. // -// See http://goo.gl/mi6kvk for more details. +// See https://goo.gl/le7vK8 for more details. func (c *Client) ExportImage(opts ExportImageOptions) error { return c.stream("GET", fmt.Sprintf("/images/%s/get", opts.Name), streamOptions{ setRawTerminal: true, @@ -331,7 +338,7 @@ func (c *Client) ExportImage(opts ExportImageOptions) error { // ExportImagesOptions represent the options for ExportImages Docker API call // -// See http://goo.gl/YeZzQK for more details. +// See https://goo.gl/huC7HA for more details. type ExportImagesOptions struct { Names []string OutputStream io.Writer `qs:"-"` @@ -339,7 +346,7 @@ type ExportImagesOptions struct { // ExportImages exports one or more images (as a tar file) into the stream // -// See http://goo.gl/YeZzQK for more details. +// See https://goo.gl/huC7HA for more details. func (c *Client) ExportImages(opts ExportImagesOptions) error { if opts.Names == nil || len(opts.Names) == 0 { return ErrMustSpecifyNames @@ -353,7 +360,7 @@ func (c *Client) ExportImages(opts ExportImagesOptions) error { // ImportImageOptions present the set of informations available for importing // an image from a source file or the stdin. // -// See http://goo.gl/PhBKnS for more details. +// See https://goo.gl/iJkZjD for more details. type ImportImageOptions struct { Repository string `qs:"repo"` Source string `qs:"fromSrc"` @@ -366,7 +373,7 @@ type ImportImageOptions struct { // ImportImage imports an image from a url, a file or stdin // -// See http://goo.gl/PhBKnS for more details. +// See https://goo.gl/iJkZjD for more details. func (c *Client) ImportImage(opts ImportImageOptions) error { if opts.Repository == "" { return ErrNoSuchImage @@ -379,8 +386,7 @@ func (c *Client) ImportImage(opts ImportImageOptions) error { if err != nil { return err } - b, err := ioutil.ReadAll(f) - opts.InputStream = bytes.NewBuffer(b) + opts.InputStream = f opts.Source = "-" } return c.createImage(queryString(&opts), nil, opts.InputStream, opts.OutputStream, opts.RawJSONStream) @@ -415,12 +421,12 @@ type BuildImageOptions struct { // BuildImage builds an image from a tarball's url or a Dockerfile in the input // stream. // -// See http://goo.gl/7nuGXa for more details. +// See https://goo.gl/xySxCe for more details. func (c *Client) BuildImage(opts BuildImageOptions) error { if opts.OutputStream == nil { return ErrMissingOutputStream } - headers, err := headersWithAuth(opts.Auth, opts.AuthConfigs) + headers, err := headersWithAuth(opts.Auth, c.versionedAuthConfigs(opts.AuthConfigs)) if err != nil { return err } @@ -452,9 +458,19 @@ func (c *Client) BuildImage(opts BuildImageOptions) error { }) } +func (c *Client) versionedAuthConfigs(authConfigs AuthConfigurations) interface{} { + if c.serverAPIVersion == nil { + c.checkAPIVersion() + } + if c.serverAPIVersion != nil && c.serverAPIVersion.GreaterThanOrEqualTo(apiVersion119) { + return AuthConfigurations119(authConfigs.Configs) + } + return authConfigs +} + // TagImageOptions present the set of options to tag an image. // -// See http://goo.gl/5g6qFy for more details. +// See https://goo.gl/98ZzkU for more details. type TagImageOptions struct { Repo string Tag string @@ -463,15 +479,16 @@ type TagImageOptions struct { // TagImage adds a tag to the image identified by the given name. // -// See http://goo.gl/5g6qFy for more details. +// See https://goo.gl/98ZzkU for more details. func (c *Client) TagImage(name string, opts TagImageOptions) error { if name == "" { return ErrNoSuchImage } - _, status, err := c.do("POST", fmt.Sprintf("/images/"+name+"/tag?%s", + resp, err := c.do("POST", fmt.Sprintf("/images/"+name+"/tag?%s", queryString(&opts)), doOptions{}) + defer resp.Body.Close() - if status == http.StatusNotFound { + if resp.StatusCode == http.StatusNotFound { return ErrNoSuchImage } @@ -497,7 +514,7 @@ func headersWithAuth(auths ...interface{}) (map[string]string, error) { return nil, err } headers["X-Registry-Auth"] = base64.URLEncoding.EncodeToString(buf.Bytes()) - case AuthConfigurations: + case AuthConfigurations, AuthConfigurations119: var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(auth); err != nil { return nil, err @@ -509,9 +526,9 @@ func headersWithAuth(auths ...interface{}) (map[string]string, error) { return headers, nil } -// APIImageSearch reflect the result of a search on the dockerHub +// APIImageSearch reflect the result of a search on the Docker Hub. // -// See http://goo.gl/xI5lLZ for more details. +// See https://goo.gl/AYjyrF for more details. type APIImageSearch struct { Description string `json:"description,omitempty" yaml:"description,omitempty"` IsOfficial bool `json:"is_official,omitempty" yaml:"is_official,omitempty"` @@ -522,15 +539,15 @@ type APIImageSearch struct { // SearchImages search the docker hub with a specific given term. // -// See http://goo.gl/xI5lLZ for more details. +// See https://goo.gl/AYjyrF for more details. func (c *Client) SearchImages(term string) ([]APIImageSearch, error) { - body, _, err := c.do("GET", "/images/search?term="+term, doOptions{}) + resp, err := c.do("GET", "/images/search?term="+term, doOptions{}) + defer resp.Body.Close() if err != nil { return nil, err } var searchResult []APIImageSearch - err = json.Unmarshal(body, &searchResult) - if err != nil { + if err := json.NewDecoder(resp.Body).Decode(&searchResult); 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 d6bce64cf..11d45e72a 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 @@ -9,6 +9,7 @@ import ( "encoding/base64" "encoding/json" "io/ioutil" + "net" "net/http" "net/url" "os" @@ -21,11 +22,14 @@ import ( func newTestClient(rt *FakeRoundTripper) Client { endpoint := "http://localhost:4243" u, _ := parseEndpoint("http://localhost:4243", false) + testAPIVersion, _ := NewAPIVersion("1.17") client := Client{ HTTPClient: &http.Client{Transport: rt}, + Dialer: &net.Dialer{}, endpoint: endpoint, endpointURL: u, SkipServerVersionCheck: true, + serverAPIVersion: testAPIVersion, } return client } 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 42d1c7e48..34c96531a 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/misc.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/misc.go @@ -4,21 +4,19 @@ package docker -import ( - "bytes" - "strings" -) +import "strings" // Version returns version information about the docker server. // -// See http://goo.gl/BOZrF5 for more details. +// See https://goo.gl/ND9R8L for more details. func (c *Client) Version() (*Env, error) { - body, _, err := c.do("GET", "/version", doOptions{}) + resp, err := c.do("GET", "/version", doOptions{}) if err != nil { return nil, err } + defer resp.Body.Close() var env Env - if err := env.Decode(bytes.NewReader(body)); err != nil { + if err := env.Decode(resp.Body); err != nil { return nil, err } return &env, nil @@ -26,15 +24,15 @@ func (c *Client) Version() (*Env, error) { // Info returns system-wide information about the Docker server. // -// See http://goo.gl/wmqZsW for more details. +// See https://goo.gl/ElTHi2 for more details. func (c *Client) Info() (*Env, error) { - body, _, err := c.do("GET", "/info", doOptions{}) + resp, err := c.do("GET", "/info", doOptions{}) if err != nil { return nil, err } + defer resp.Body.Close() var info Env - err = info.Decode(bytes.NewReader(body)) - if err != nil { + if err := info.Decode(resp.Body); err != nil { return nil, err } return &info, nil diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/network.go b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/network.go index 0d3e2d43f..e21e7dfea 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/network.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/network.go @@ -38,12 +38,13 @@ type Endpoint struct { // // See https://goo.gl/4hCNtZ for more details. func (c *Client) ListNetworks() ([]Network, error) { - body, _, err := c.do("GET", "/networks", doOptions{}) + resp, err := c.do("GET", "/networks", doOptions{}) if err != nil { return nil, err } + defer resp.Body.Close() var networks []Network - if err := json.Unmarshal(body, &networks); err != nil { + if err := json.NewDecoder(resp.Body).Decode(&networks); err != nil { return nil, err } return networks, nil @@ -54,15 +55,16 @@ func (c *Client) ListNetworks() ([]Network, error) { // See https://goo.gl/4hCNtZ for more details. func (c *Client) NetworkInfo(id string) (*Network, error) { path := "/networks/" + id - body, status, err := c.do("GET", path, doOptions{}) - if status == http.StatusNotFound { - return nil, &NoSuchNetwork{ID: id} - } + resp, err := c.do("GET", path, doOptions{}) if err != nil { + if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { + return nil, &NoSuchNetwork{ID: id} + } return nil, err } + defer resp.Body.Close() var network Network - if err := json.Unmarshal(body, &network); err != nil { + if err := json.NewDecoder(resp.Body).Decode(&network); err != nil { return nil, err } return &network, nil @@ -83,35 +85,34 @@ type CreateNetworkOptions struct { // // See http://goo.gl/mErxNp for more details. func (c *Client) CreateNetwork(opts CreateNetworkOptions) (*Network, error) { - body, status, err := c.do( + resp, err := c.do( "POST", "/networks", doOptions{ data: opts, }, ) - - if status == http.StatusConflict { - return nil, ErrNetworkAlreadyExists - } if err != nil { + if e, ok := err.(*Error); ok && e.Status == http.StatusConflict { + return nil, ErrNetworkAlreadyExists + } return nil, err } + defer resp.Body.Close() type createNetworkResponse struct { ID string } var ( network Network - resp createNetworkResponse + cnr createNetworkResponse ) - err = json.Unmarshal(body, &resp) - if err != nil { + if err := json.NewDecoder(resp.Body).Decode(&cnr); err != nil { return nil, err } network.Name = opts.Name - network.ID = resp.ID + network.ID = cnr.ID network.Type = opts.NetworkType return &network, nil diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/data/symlink b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/data/symlink deleted file mode 120000 index 3ddf86a35..000000000 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/data/symlink +++ /dev/null @@ -1 +0,0 @@ -doesnotexist \ No newline at end of file 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 41d872771..46f6b6721 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 @@ -12,6 +12,7 @@ import ( "encoding/json" "errors" "fmt" + "io/ioutil" mathrand "math/rand" "net" "net/http" @@ -532,7 +533,7 @@ func (s *DockerServer) startContainer(w http.ResponseWriter, r *http.Request) { } container.HostConfig = &hostConfig if container.State.Running { - http.Error(w, "Container already running", http.StatusBadRequest) + http.Error(w, "", http.StatusNotModified) return } container.State.Running = true @@ -610,14 +611,34 @@ func (s *DockerServer) attachContainer(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } + wg := sync.WaitGroup{} + if r.URL.Query().Get("stdin") == "1" { + wg.Add(1) + go func() { + ioutil.ReadAll(conn) + wg.Done() + }() + } outStream := stdcopy.NewStdWriter(conn, stdcopy.Stdout) if container.State.Running { - fmt.Fprintf(outStream, "Container %q is running\n", container.ID) + fmt.Fprintf(outStream, "Container is running\n") } else { - fmt.Fprintf(outStream, "Container %q is not running\n", container.ID) + fmt.Fprintf(outStream, "Container is not running\n") } fmt.Fprintln(outStream, "What happened?") fmt.Fprintln(outStream, "Something happened") + wg.Wait() + if r.URL.Query().Get("stream") == "1" { + for { + time.Sleep(1e6) + s.cMut.RLock() + if !container.State.Running { + s.cMut.RUnlock() + break + } + s.cMut.RUnlock() + } + } conn.Close() } @@ -936,7 +957,7 @@ func (s *DockerServer) createExecContainer(w http.ResponseWriter, r *http.Reques func (s *DockerServer) startExecContainer(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] - if exec, err := s.getExec(id); err == nil { + if exec, err := s.getExec(id, false); err == nil { s.execMut.Lock() exec.Running = true s.execMut.Unlock() @@ -958,7 +979,7 @@ func (s *DockerServer) startExecContainer(w http.ResponseWriter, r *http.Request func (s *DockerServer) resizeExecContainer(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] - if _, err := s.getExec(id); err == nil { + if _, err := s.getExec(id, false); err == nil { w.WriteHeader(http.StatusOK) return } @@ -967,7 +988,7 @@ func (s *DockerServer) resizeExecContainer(w http.ResponseWriter, r *http.Reques func (s *DockerServer) inspectExecContainer(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] - if exec, err := s.getExec(id); err == nil { + if exec, err := s.getExec(id, true); err == nil { w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(exec) @@ -976,11 +997,15 @@ func (s *DockerServer) inspectExecContainer(w http.ResponseWriter, r *http.Reque w.WriteHeader(http.StatusNotFound) } -func (s *DockerServer) getExec(id string) (*docker.ExecInspect, error) { +func (s *DockerServer) getExec(id string, copy bool) (*docker.ExecInspect, error) { s.execMut.RLock() defer s.execMut.RUnlock() for _, exec := range s.execs { if exec.ID == id { + if copy { + cp := *exec + exec = &cp + } return exec, nil } } 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 36789abb3..bb24ce560 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 @@ -5,9 +5,11 @@ package testing import ( + "bufio" "bytes" "encoding/json" "fmt" + "io/ioutil" "math/rand" "net" "net/http" @@ -624,8 +626,8 @@ func TestStartContainerAlreadyRunning(t *testing.T) { path := fmt.Sprintf("/containers/%s/start", server.containers[0].ID) request, _ := http.NewRequest("POST", path, bytes.NewBuffer([]byte("null"))) server.ServeHTTP(recorder, request) - if recorder.Code != http.StatusBadRequest { - t.Errorf("StartContainer: wrong status code. Want %d. Got %d.", http.StatusBadRequest, recorder.Code) + if recorder.Code != http.StatusNotModified { + t.Errorf("StartContainer: wrong status code. Want %d. Got %d.", http.StatusNotModified, recorder.Code) } } @@ -845,22 +847,41 @@ func TestWaitContainerNotFound(t *testing.T) { } } +type HijackableResponseRecorder struct { + httptest.ResponseRecorder + readCh chan []byte +} + +func (r *HijackableResponseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) { + myConn, otherConn := net.Pipe() + r.readCh = make(chan []byte) + go func() { + data, _ := ioutil.ReadAll(myConn) + r.readCh <- data + }() + return otherConn, nil, nil +} + +func (r *HijackableResponseRecorder) HijackBuffer() string { + return string(<-r.readCh) +} + func TestAttachContainer(t *testing.T) { server := DockerServer{} addContainers(&server, 1) server.containers[0].State.Running = true server.buildMuxer() - recorder := httptest.NewRecorder() + recorder := &HijackableResponseRecorder{} path := fmt.Sprintf("/containers/%s/attach?logs=1", server.containers[0].ID) request, _ := http.NewRequest("POST", path, nil) server.ServeHTTP(recorder, request) lines := []string{ - fmt.Sprintf("\x01\x00\x00\x00\x03\x00\x00\x00Container %q is running", server.containers[0].ID), - "What happened?", - "Something happened", + "\x01\x00\x00\x00\x00\x00\x00\x15Container is running", + "\x01\x00\x00\x00\x00\x00\x00\x0fWhat happened?", + "\x01\x00\x00\x00\x00\x00\x00\x13Something happened", } expected := strings.Join(lines, "\n") + "\n" - if body := recorder.Body.String(); body == expected { + if body := recorder.HijackBuffer(); body != expected { t.Errorf("AttachContainer: wrong body. Want %q. Got %q.", expected, body) } } @@ -868,7 +889,7 @@ func TestAttachContainer(t *testing.T) { func TestAttachContainerNotFound(t *testing.T) { server := DockerServer{} server.buildMuxer() - recorder := httptest.NewRecorder() + recorder := &HijackableResponseRecorder{} path := "/containers/abc123/attach?logs=1" request, _ := http.NewRequest("POST", path, nil) server.ServeHTTP(recorder, request) @@ -877,6 +898,44 @@ func TestAttachContainerNotFound(t *testing.T) { } } +func TestAttachContainerWithStreamBlocks(t *testing.T) { + server := DockerServer{} + addContainers(&server, 1) + server.containers[0].State.Running = true + server.buildMuxer() + path := fmt.Sprintf("/containers/%s/attach?logs=1&stdout=1&stream=1", server.containers[0].ID) + request, _ := http.NewRequest("POST", path, nil) + done := make(chan string) + go func() { + recorder := &HijackableResponseRecorder{} + server.ServeHTTP(recorder, request) + done <- recorder.HijackBuffer() + }() + select { + case <-done: + t.Fatalf("attach stream returned before container is stopped") + case <-time.After(500 * time.Millisecond): + } + server.cMut.Lock() + server.containers[0].State.Running = false + server.cMut.Unlock() + var body string + select { + case body = <-done: + case <-time.After(5 * time.Second): + t.Fatalf("timed out waiting for attach to finish") + } + lines := []string{ + "\x01\x00\x00\x00\x00\x00\x00\x15Container is running", + "\x01\x00\x00\x00\x00\x00\x00\x0fWhat happened?", + "\x01\x00\x00\x00\x00\x00\x00\x13Something happened", + } + expected := strings.Join(lines, "\n") + "\n" + if body != expected { + t.Errorf("AttachContainer: wrong body. Want %q. Got %q.", expected, body) + } +} + func TestRemoveContainer(t *testing.T) { server := DockerServer{} addContainers(&server, 1) @@ -1690,7 +1749,7 @@ func addNetworks(server *DockerServer, n int) { ID: fmt.Sprintf("%x", rand.Int()%10000), Type: "bridge", Endpoints: []*docker.Endpoint{ - &docker.Endpoint{ + { Name: "blah", ID: fmt.Sprintf("%x", rand.Int()%10000), Network: netid, diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/tls.go b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/tls.go index 11d571761..55f43174b 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/tls.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/tls.go @@ -94,7 +94,3 @@ func tlsDialWithDialer(dialer *net.Dialer, network, addr string, config *tls.Con // wrapper which holds both the TLS and raw connections. return &tlsClientCon{conn, rawConn}, nil } - -func tlsDial(network, addr string, config *tls.Config) (net.Conn, error) { - return tlsDialWithDialer(new(net.Dialer), network, addr, config) -} diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/volume.go b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/volume.go new file mode 100644 index 000000000..a989a6eee --- /dev/null +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/volume.go @@ -0,0 +1,127 @@ +// Copyright 2015 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 ( + "encoding/json" + "errors" + "net/http" +) + +var ( + // ErrNoSuchVolume is the error returned when the volume does not exist. + ErrNoSuchVolume = errors.New("no such volume") + + // ErrVolumeInUse is the error returned when the volume requested to be removed is still in use. + ErrVolumeInUse = errors.New("volume in use and cannot be removed") +) + +// Volume represents a volume. +// +// See https://goo.gl/FZA4BK for more details. +type Volume struct { + Name string `json:"Name" yaml:"Name"` + Driver string `json:"Driver,omitempty" yaml:"Driver,omitempty"` + Mountpoint string `json:"Mountpoint,omitempty" yaml:"Mountpoint,omitempty"` +} + +// ListVolumesOptions specify parameters to the ListVolumes function. +// +// See https://goo.gl/FZA4BK for more details. +type ListVolumesOptions struct { + Filters map[string][]string +} + +// ListVolumes returns a list of available volumes in the server. +// +// See https://goo.gl/FZA4BK for more details. +func (c *Client) ListVolumes(opts ListVolumesOptions) ([]Volume, error) { + resp, err := c.do("GET", "/volumes?"+queryString(opts), doOptions{}) + if err != nil { + return nil, err + } + defer resp.Body.Close() + m := make(map[string]interface{}) + if err := json.NewDecoder(resp.Body).Decode(&m); err != nil { + return nil, err + } + var volumes []Volume + volumesJSON, ok := m["Volumes"] + if !ok { + return volumes, nil + } + data, err := json.Marshal(volumesJSON) + if err != nil { + return nil, err + } + if err := json.Unmarshal(data, &volumes); err != nil { + return nil, err + } + return volumes, nil +} + +// CreateVolumeOptions specify parameters to the CreateVolume function. +// +// See https://goo.gl/pBUbZ9 for more details. +type CreateVolumeOptions struct { + Name string + Driver string + DriverOpts map[string]string +} + +// CreateVolume creates a volume on the server. +// +// See https://goo.gl/pBUbZ9 for more details. +func (c *Client) CreateVolume(opts CreateVolumeOptions) (*Volume, error) { + resp, err := c.do("POST", "/volumes", doOptions{data: opts}) + if err != nil { + return nil, err + } + defer resp.Body.Close() + var volume Volume + if err := json.NewDecoder(resp.Body).Decode(&volume); err != nil { + return nil, err + } + return &volume, nil +} + +// InspectVolume returns a volume by its name. +// +// See https://goo.gl/0g9A6i for more details. +func (c *Client) InspectVolume(name string) (*Volume, error) { + resp, err := c.do("GET", "/volumes/"+name, doOptions{}) + if err != nil { + if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { + return nil, ErrNoSuchVolume + } + return nil, err + } + defer resp.Body.Close() + var volume Volume + if err := json.NewDecoder(resp.Body).Decode(&volume); err != nil { + return nil, err + } + return &volume, nil +} + +// RemoveVolume removes a volume by its name. +// +// See https://goo.gl/79GNQz for more details. +func (c *Client) RemoveVolume(name string) error { + resp, err := c.do("DELETE", "/volumes/"+name, doOptions{}) + if err != nil { + if e, ok := err.(*Error); ok { + if e.Status == http.StatusNotFound { + return ErrNoSuchVolume + } + if e.Status == http.StatusConflict { + return ErrVolumeInUse + } + } + return nil + } + defer resp.Body.Close() + return nil +} diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/volume_test.go b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/volume_test.go new file mode 100644 index 000000000..9707c09cd --- /dev/null +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/volume_test.go @@ -0,0 +1,142 @@ +// Copyright 2015 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 ( + "encoding/json" + "net/http" + "net/url" + "reflect" + "testing" +) + +func TestListVolumes(t *testing.T) { + volumesData := `[ + { + "Name": "tardis", + "Driver": "local", + "Mountpoint": "/var/lib/docker/volumes/tardis" + }, + { + "Name": "foo", + "Driver": "bar", + "Mountpoint": "/var/lib/docker/volumes/bar" + } +]` + body := `{ "Volumes": ` + volumesData + ` }` + var expected []Volume + if err := json.Unmarshal([]byte(volumesData), &expected); err != nil { + t.Fatal(err) + } + client := newTestClient(&FakeRoundTripper{message: body, status: http.StatusOK}) + volumes, err := client.ListVolumes(ListVolumesOptions{}) + if err != nil { + t.Error(err) + } + if !reflect.DeepEqual(volumes, expected) { + t.Errorf("ListVolumes: Wrong return value. Want %#v. Got %#v.", expected, volumes) + } +} + +func TestCreateVolume(t *testing.T) { + body := `{ + "Name": "tardis", + "Driver": "local", + "Mountpoint": "/var/lib/docker/volumes/tardis" + }` + var expected Volume + if err := json.Unmarshal([]byte(body), &expected); err != nil { + t.Fatal(err) + } + fakeRT := &FakeRoundTripper{message: body, status: http.StatusOK} + client := newTestClient(fakeRT) + volume, err := client.CreateVolume( + CreateVolumeOptions{ + Name: "tardis", + Driver: "local", + DriverOpts: map[string]string{ + "foo": "bar", + }, + }, + ) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(volume, &expected) { + t.Errorf("CreateVolume: Wrong return value. Want %#v. Got %#v.", expected, volume) + } + req := fakeRT.requests[0] + expectedMethod := "POST" + if req.Method != expectedMethod { + t.Errorf("CreateVolume(): Wrong HTTP method. Want %s. Got %s.", expectedMethod, req.Method) + } + u, _ := url.Parse(client.getURL("/volumes")) + if req.URL.Path != u.Path { + t.Errorf("CreateVolume(): Wrong request path. Want %q. Got %q.", u.Path, req.URL.Path) + } +} + +func TestInspectVolume(t *testing.T) { + body := `{ + "Name": "tardis", + "Driver": "local", + "Mountpoint": "/var/lib/docker/volumes/tardis" + }` + var expected Volume + if err := json.Unmarshal([]byte(body), &expected); err != nil { + t.Fatal(err) + } + fakeRT := &FakeRoundTripper{message: body, status: http.StatusOK} + client := newTestClient(fakeRT) + name := "tardis" + volume, err := client.InspectVolume(name) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(volume, &expected) { + t.Errorf("InspectVolume: Wrong return value. Want %#v. Got %#v.", expected, volume) + } + req := fakeRT.requests[0] + expectedMethod := "GET" + if req.Method != expectedMethod { + t.Errorf("InspectVolume(%q): Wrong HTTP method. Want %s. Got %s.", name, expectedMethod, req.Method) + } + u, _ := url.Parse(client.getURL("/volumes/" + name)) + if req.URL.Path != u.Path { + t.Errorf("CreateVolume(%q): Wrong request path. Want %q. Got %q.", name, u.Path, req.URL.Path) + } +} + +func TestRemoveVolume(t *testing.T) { + name := "test" + fakeRT := &FakeRoundTripper{message: "", status: http.StatusNoContent} + client := newTestClient(fakeRT) + if err := client.RemoveVolume(name); err != nil { + t.Fatal(err) + } + req := fakeRT.requests[0] + expectedMethod := "DELETE" + if req.Method != expectedMethod { + t.Errorf("RemoveVolume(%q): Wrong HTTP method. Want %s. Got %s.", name, expectedMethod, req.Method) + } + u, _ := url.Parse(client.getURL("/volumes/" + name)) + if req.URL.Path != u.Path { + t.Errorf("RemoveVolume(%q): Wrong request path. Want %q. Got %q.", name, u.Path, req.URL.Path) + } +} + +func TestRemoveVolumeNotFound(t *testing.T) { + client := newTestClient(&FakeRoundTripper{message: "no such volume", status: http.StatusNotFound}) + if err := client.RemoveVolume("test:"); err != ErrNoSuchVolume { + t.Errorf("RemoveVolume: wrong error. Want %#v. Got %#v.", ErrNoSuchVolume, err) + } +} + +func TestRemoveVolumeInUse(t *testing.T) { + client := newTestClient(&FakeRoundTripper{message: "volume in use and cannot be removed", status: http.StatusConflict}) + if err := client.RemoveVolume("test:"); err != ErrVolumeInUse { + t.Errorf("RemoveVolume: wrong error. Want %#v. Got %#v.", ErrVolumeInUse, err) + } +}