From fb633704515e51fdd318adc79e64db56e3b07a98 Mon Sep 17 00:00:00 2001 From: Paul Weil Date: Fri, 10 Apr 2015 16:05:43 -0400 Subject: [PATCH] bump(github.com/fsouza/go-dockerclient): 17d39bcb22e8103ba6d1c0cb2530c6434cb870a3 --- Godeps/Godeps.json | 2 +- .../fsouza/go-dockerclient/.travis.yml | 1 - .../github.com/fsouza/go-dockerclient/AUTHORS | 17 +- .../github.com/fsouza/go-dockerclient/LICENSE | 2 +- .../fsouza/go-dockerclient/README.markdown | 60 +++- .../github.com/fsouza/go-dockerclient/auth.go | 83 +++++ .../fsouza/go-dockerclient/auth_test.go | 37 +++ .../fsouza/go-dockerclient/client.go | 32 +- .../fsouza/go-dockerclient/client_test.go | 13 +- .../fsouza/go-dockerclient/container.go | 104 ++++-- .../fsouza/go-dockerclient/container_test.go | 64 +++- .../fsouza/go-dockerclient/event.go | 6 +- .../github.com/fsouza/go-dockerclient/exec.go | 10 +- .../fsouza/go-dockerclient/exec_test.go | 2 +- .../fsouza/go-dockerclient/image.go | 57 ++-- .../fsouza/go-dockerclient/image_test.go | 27 +- .../github.com/fsouza/go-dockerclient/misc.go | 6 +- .../github.com/fsouza/go-dockerclient/tar.go | 2 +- .../fsouza/go-dockerclient/testing/server.go | 161 +++++++-- .../go-dockerclient/testing/server_test.go | 305 +++++++++++++++++- 20 files changed, 862 insertions(+), 129 deletions(-) create mode 100644 Godeps/_workspace/src/github.com/fsouza/go-dockerclient/auth.go create mode 100644 Godeps/_workspace/src/github.com/fsouza/go-dockerclient/auth_test.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 59e62915e5b6..c18e5c1f1383 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -179,7 +179,7 @@ }, { "ImportPath": "github.com/fsouza/go-dockerclient", - "Rev": "d19717788084716e4adff0515be6289aa04bec46" + "Rev": "17d39bcb22e8103ba6d1c0cb2530c6434cb870a3" }, { "ImportPath": "github.com/garyburd/redigo/internal", 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 592339865373..672f6ed38fbf 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/.travis.yml +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/.travis.yml @@ -1,6 +1,5 @@ language: go go: - - 1.2.2 - 1.3.1 - 1.4 - tip diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/AUTHORS b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/AUTHORS index 698901d9f4c6..0bd1e5c9169d 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/AUTHORS +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/AUTHORS @@ -1,25 +1,31 @@ # This is the official list of go-dockerclient authors for copyright purposes. +Adam Bell-Hanssen Aldrin Leal Andreas Jaekle Andrews Medina Artem Sidorenko Andy Goldstein +Ben Marini Ben McCann +Brian Lalor +Burke Libbey Carlos Diaz-Padron Cezar Sa Espinola Cheah Chu Yeow cheneydeng CMGS Daniel, Dao Quang Minh +Darren Shepherd David Huie Dawn Chen Ed Eric Anderson Fabio Rehm - Fatih Arslan +Fatih Arslan Flavia Missi Francisco Souza +Guillermo Álvarez Fernández Jari Kolehmainen Jason Wilder Jawher Moussa @@ -30,19 +36,26 @@ Johan Euphrosine Kamil Domanski Karan Misra Kim, Hirokuni +liron-l Lucas Clemente +Lucas Weiblen +Mantas Matelis Martin Sweeney Máximo Cuadros Ortiz +Michal Fojtik Mike Dillon Mrunal Patel +Nick Ethier Omeid Matten Paul Morie +Paul Weil Peter Jihoon Kim Philippe Lafoucrière Rafe Colton Rob Miller Robert Williamson Salvador Gironès +Sam Rijs Simon Eskildsen Simon Menke Skolos @@ -51,5 +64,7 @@ Sridhar Ratnakumar Summer Mousa Tarsis Azevedo Tim Schindler +Vincenzo Prignano Wiliam Souza Ye Yin +Yuriy Bogdanov diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/LICENSE b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/LICENSE index 7a6d8bb69d9c..4e11de1007ae 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/LICENSE +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014, go-dockerclient authors +Copyright (c) 2015, go-dockerclient authors All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/README.markdown b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/README.markdown index aa5d70ea6196..4019bca6e10b 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/README.markdown +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/README.markdown @@ -3,11 +3,12 @@ [![Build Status](https://drone.io/github.com/fsouza/go-dockerclient/status.png)](https://drone.io/github.com/fsouza/go-dockerclient/latest) [![Build Status](https://travis-ci.org/fsouza/go-dockerclient.png)](https://travis-ci.org/fsouza/go-dockerclient) -[![GoDoc](http://godoc.org/github.com/fsouza/go-dockerclient?status.png)](http://godoc.org/github.com/fsouza/go-dockerclient) +[![GoDoc](https://godoc.org/github.com/fsouza/go-dockerclient?status.png)](https://godoc.org/github.com/fsouza/go-dockerclient) -This package presents a client for the Docker remote API. +This package presents a client for the Docker remote API. It also provides +support for the extensions in the [Swarm API](https://docs.docker.com/swarm/API/). -For more details, check the [remote API documentation](http://docs.docker.io/en/latest/reference/api/docker_remote_api/). +For more details, check the [remote API documentation](http://docs.docker.com/en/latest/reference/api/docker_remote_api/). ## Example @@ -15,22 +16,49 @@ For more details, check the [remote API documentation](http://docs.docker.io/en/ package main import ( - "fmt" - "github.com/fsouza/go-dockerclient" + "fmt" + + "github.com/fsouza/go-dockerclient" +) + +func main() { + endpoint := "unix:///var/run/docker.sock" + client, _ := docker.NewClient(endpoint) + imgs, _ := client.ListImages(docker.ListImagesOptions{All: false}) + for _, img := range imgs { + fmt.Println("ID: ", img.ID) + fmt.Println("RepoTags: ", img.RepoTags) + fmt.Println("Created: ", img.Created) + fmt.Println("Size: ", img.Size) + fmt.Println("VirtualSize: ", img.VirtualSize) + fmt.Println("ParentId: ", img.ParentID) + } +} +``` + +## Using with Boot2Docker + +Boot2Docker runs Docker with TLS enabled. In order to instantiate the client you should use NewTLSClient, passing the endpoint and path for key and certificates as parameters. + +For more details about TLS support in Boot2Docker, please refer to [TLS support](https://github.com/boot2docker/boot2docker#tls-support) on Boot2Docker's readme. + +```go +package main + +import ( + "fmt" + + "github.com/fsouza/go-dockerclient" ) func main() { - endpoint := "unix:///var/run/docker.sock" - client, _ := docker.NewClient(endpoint) - imgs, _ := client.ListImages(docker.ListImagesOptions{All: false}) - for _, img := range imgs { - fmt.Println("ID: ", img.ID) - fmt.Println("RepoTags: ", img.RepoTags) - fmt.Println("Created: ", img.Created) - fmt.Println("Size: ", img.Size) - fmt.Println("VirtualSize: ", img.VirtualSize) - fmt.Println("ParentId: ", img.ParentID) - } + endpoint := "tcp://[ip]:[port]" + path := os.Getenv("DOCKER_CERT_PATH") + ca := fmt.Sprintf("%s/ca.pem", path) + cert := fmt.Sprintf("%s/cert.pem", path) + key := fmt.Sprintf("%s/key.pem", path) + client, _ := docker.NewTLSClient(endpoint, cert, key, ca) + // use client } ``` diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/auth.go b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/auth.go new file mode 100644 index 000000000000..5dbd2af528ad --- /dev/null +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/auth.go @@ -0,0 +1,83 @@ +// 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/base64" + "encoding/json" + "io" + "os" + "path" + "strings" +) + +// AuthConfiguration represents authentication options to use in the PushImage +// method. It represents the authentication in the Docker index server. +type AuthConfiguration struct { + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Email string `json:"email,omitempty"` + ServerAddress string `json:"serveraddress,omitempty"` +} + +// AuthConfigurations represents authentication options to use for the +// PushImage method accommodating the new X-Registry-Config header +type AuthConfigurations struct { + Configs map[string]AuthConfiguration `json:"configs"` +} + +// dockerConfig represents a registry authentation configuration from the +// .dockercfg file. +type dockerConfig struct { + Auth string `json:"auth"` + Email string `json:"email"` +} + +// NewAuthConfigurationsFromDockerCfg returns AuthConfigurations from the +// ~/.dockercfg file. +func NewAuthConfigurationsFromDockerCfg() (*AuthConfigurations, error) { + p := path.Join(os.Getenv("HOME"), ".dockercfg") + r, err := os.Open(p) + if err != nil { + return nil, err + } + return NewAuthConfigurations(r) +} + +// NewAuthConfigurations returns AuthConfigurations from a JSON encoded string in the +// same format as the .dockercfg file. +func NewAuthConfigurations(r io.Reader) (*AuthConfigurations, error) { + var auth *AuthConfigurations + var confs map[string]dockerConfig + if err := json.NewDecoder(r).Decode(&confs); err != nil { + return nil, err + } + auth, err := authConfigs(confs) + if err != nil { + return nil, err + } + return auth, nil +} + +// authConfigs converts a dockerConfigs map to a AuthConfigurations object. +func authConfigs(confs map[string]dockerConfig) (*AuthConfigurations, error) { + c := &AuthConfigurations{ + Configs: make(map[string]AuthConfiguration), + } + for reg, conf := range confs { + data, err := base64.StdEncoding.DecodeString(conf.Auth) + if err != nil { + return nil, err + } + userpass := strings.Split(string(data), ":") + c.Configs[reg] = AuthConfiguration{ + Email: conf.Email, + Username: userpass[0], + Password: userpass[1], + ServerAddress: reg, + } + } + return c, 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 new file mode 100644 index 000000000000..b9b123977053 --- /dev/null +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/auth_test.go @@ -0,0 +1,37 @@ +// 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/base64" + "fmt" + "strings" + "testing" +) + +func TestAuthConfig(t *testing.T) { + auth := base64.StdEncoding.EncodeToString([]byte("user:pass")) + read := strings.NewReader(fmt.Sprintf(`{"docker.io":{"auth":"%s","email":"user@example.com"}}`, auth)) + ac, err := NewAuthConfigurations(read) + if err != nil { + t.Error(err) + } + c, ok := ac.Configs["docker.io"] + if !ok { + t.Error("NewAuthConfigurations: Expected Configs to contain docker.io") + } + if got, want := c.Email, "user@example.com"; got != want { + t.Errorf(`AuthConfigurations.Configs["docker.io"].Email: wrong result. Want %q. Got %q`, want, got) + } + if got, want := c.Username, "user"; got != want { + t.Errorf(`AuthConfigurations.Configs["docker.io"].Username: wrong result. Want %q. Got %q`, want, got) + } + if got, want := c.Password, "pass"; got != want { + t.Errorf(`AuthConfigurations.Configs["docker.io"].Password: wrong result. Want %q. Got %q`, want, got) + } + if got, want := c.ServerAddress, "docker.io"; got != want { + t.Errorf(`AuthConfigurations.Configs["docker.io"].ServerAddress: wrong result. Want %q. Got %q`, want, got) + } +} 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 3b76a4a8239a..d64f8c15bc66 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/client.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/client.go @@ -1,4 +1,4 @@ -// Copyright 2014 go-dockerclient authors. All rights reserved. +// 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. @@ -34,7 +34,7 @@ var ( // ErrConnectionRefused is returned when the client cannot connect to the given endpoint. ErrConnectionRefused = errors.New("cannot connect to Docker endpoint") - apiVersion1_12, _ = NewAPIVersion("1.12") + apiVersion112, _ = NewAPIVersion("1.12") ) // APIVersion is an internal representation of a version of the Remote API. @@ -143,7 +143,7 @@ func NewClient(endpoint string) (*Client, error) { // server endpoint, key and certificates . It will use the latest remote API version // available in the server. func NewTLSClient(endpoint string, cert, key, ca string) (*Client, error) { - client, err := NewVersionnedTLSClient(endpoint, cert, key, ca, "") + client, err := NewVersionedTLSClient(endpoint, cert, key, ca, "") if err != nil { return nil, err } @@ -154,7 +154,7 @@ func NewTLSClient(endpoint string, cert, key, ca string) (*Client, error) { // NewVersionedClient returns a Client instance ready for communication with // the given server endpoint, using a specific remote API version. func NewVersionedClient(endpoint string, apiVersionString string) (*Client, error) { - u, err := parseEndpoint(endpoint) + u, err := parseEndpoint(endpoint, false) if err != nil { return nil, err } @@ -174,10 +174,15 @@ func NewVersionedClient(endpoint string, apiVersionString string) (*Client, erro }, nil } -// NewVersionnedTLSClient returns a Client instance ready for TLS communications with the givens -// server endpoint, key and certificates, using a specific remote API version. +// NewVersionnedTLSClient has been DEPRECATED, please use NewVersionedTLSClient. func NewVersionnedTLSClient(endpoint string, cert, key, ca, apiVersionString string) (*Client, error) { - u, err := parseEndpoint(endpoint) + return NewVersionedTLSClient(endpoint, cert, key, ca, apiVersionString) +} + +// NewVersionedTLSClient returns a Client instance ready for TLS communications with the givens +// server endpoint, key and certificates, using a specific remote API version. +func NewVersionedTLSClient(endpoint string, cert, key, ca, apiVersionString string) (*Client, error) { + u, err := parseEndpoint(endpoint, true) if err != nil { return nil, err } @@ -247,7 +252,7 @@ func (c *Client) checkAPIVersion() error { // See http://goo.gl/stJENm for more details. func (c *Client) Ping() error { path := "/_ping" - body, status, err := c.do("GET", path, nil) + body, status, err := c.do("GET", path, nil, false) if err != nil { return err } @@ -258,7 +263,7 @@ func (c *Client) Ping() error { } func (c *Client) getServerAPIVersionString() (version string, err error) { - body, status, err := c.do("GET", "/version", nil) + body, status, err := c.do("GET", "/version", nil, false) if err != nil { return "", err } @@ -274,9 +279,9 @@ func (c *Client) getServerAPIVersionString() (version string, err error) { return version, nil } -func (c *Client) do(method, path string, data interface{}) ([]byte, int, error) { +func (c *Client) do(method, path string, data interface{}, forceJSON bool) ([]byte, int, error) { var params io.Reader - if data != nil { + if data != nil || forceJSON { buf, err := json.Marshal(data) if err != nil { return nil, -1, err @@ -599,11 +604,14 @@ func (e *Error) Error() string { return fmt.Sprintf("API error (%d): %s", e.Status, e.Message) } -func parseEndpoint(endpoint string) (*url.URL, error) { +func parseEndpoint(endpoint string, tls bool) (*url.URL, error) { u, err := url.Parse(endpoint) if err != nil { return nil, ErrInvalidEndpoint } + if tls { + u.Scheme = "https" + } if u.Scheme == "tcp" { _, port, err := net.SplitHostPort(u.Host) if err != nil { 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 34543b410bed..e09e5e0b8618 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 @@ -1,4 +1,4 @@ -// Copyright 2014 go-dockerclient authors. All rights reserved. +// 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. @@ -93,7 +93,7 @@ func TestNewTLSVersionedClient(t *testing.T) { keyPath := "testing/data/key.pem" caPath := "testing/data/ca.pem" endpoint := "https://localhost:4243" - client, err := NewVersionnedTLSClient(endpoint, certPath, keyPath, caPath, "1.14") + client, err := NewVersionedTLSClient(endpoint, certPath, keyPath, caPath, "1.14") if err != nil { t.Fatal(err) } @@ -113,7 +113,7 @@ func TestNewTLSVersionedClientInvalidCA(t *testing.T) { keyPath := "testing/data/key.pem" caPath := "testing/data/key.pem" endpoint := "https://localhost:4243" - _, err := NewVersionnedTLSClient(endpoint, certPath, keyPath, caPath, "1.14") + _, err := NewVersionedTLSClient(endpoint, certPath, keyPath, caPath, "1.14") if err == nil { t.Errorf("Expected invalid ca at %s", caPath) } @@ -136,14 +136,15 @@ func TestNewClientInvalidEndpoint(t *testing.T) { } } -func TestNewTLSClient2376(t *testing.T) { +func TestNewTLSClient(t *testing.T) { var tests = []struct { endpoint string expected string }{ {"tcp://localhost:2376", "https"}, - {"tcp://localhost:2375", "http"}, - {"tcp://localhost:4000", "http"}, + {"tcp://localhost:2375", "https"}, + {"tcp://localhost:4000", "https"}, + {"http://localhost:4000", "https"}, } for _, tt := range tests { 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 40d6cf3cf406..ab34a9919721 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/container.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/container.go @@ -1,4 +1,4 @@ -// Copyright 2014 go-dockerclient authors. All rights reserved. +// 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. @@ -56,7 +56,7 @@ type APIContainers struct { // See http://goo.gl/6Y4Gz7 for more details. func (c *Client) ListContainers(opts ListContainersOptions) ([]APIContainers, error) { path := "/containers/json?" + queryString(opts) - body, _, err := c.do("GET", path, nil) + body, _, err := c.do("GET", path, nil, false) if err != nil { return nil, err } @@ -166,7 +166,7 @@ func parsePort(rawPort string) (int, error) { } // Config is the list of configuration options used when creating a container. -// Config does not the options that are specific to starting a container on a +// Config does not contain the options that are specific to starting a container on a // given host. Those are contained in HostConfig type Config struct { Hostname string `json:"Hostname,omitempty" yaml:"Hostname,omitempty"` @@ -193,6 +193,26 @@ type Config struct { WorkingDir string `json:"WorkingDir,omitempty" yaml:"WorkingDir,omitempty"` Entrypoint []string `json:"Entrypoint,omitempty" yaml:"Entrypoint,omitempty"` NetworkDisabled bool `json:"NetworkDisabled,omitempty" yaml:"NetworkDisabled,omitempty"` + SecurityOpts []string `json:"SecurityOpts,omitempty" yaml:"SecurityOpts,omitempty"` + OnBuild []string `json:"OnBuild,omitempty" yaml:"OnBuild,omitempty"` + Labels map[string]string `json:"Labels,omitempty" yaml:"Labels,omitempty"` +} + +// LogConfig defines the log driver type and the configuration for it. +type LogConfig struct { + Type string `json:"Type,omitempty" yaml:"Type,omitempty"` + Config map[string]string `json:"Config,omitempty" yaml:"Config,omitempty"` +} + +// SwarmNode containers information about which Swarm node the container is on +type SwarmNode struct { + ID string `json:"ID,omitempty" yaml:"ID,omitempty"` + IP string `json:"IP,omitempty" yaml:"IP,omitempty"` + Addr string `json:"Addr,omitempty" yaml:"Addr,omitempty"` + Name string `json:"Name,omitempty" yaml:"Name,omitempty"` + CPUs int64 `json:"CPUs,omitempty" yaml:"CPUs,omitempty"` + Memory int64 `json:"Memory,omitempty" yaml:"Memory,omitempty"` + Labels map[string]string `json:"Labels,omitempty" yaml:"Labels,omitempty"` } // Container is the type encompasing everything about a container - its config, @@ -209,6 +229,8 @@ type Container struct { State State `json:"State,omitempty" yaml:"State,omitempty"` Image string `json:"Image,omitempty" yaml:"Image,omitempty"` + Node *SwarmNode `json:"Node,omitempty" yaml:"Node,omitempty"` + NetworkSettings *NetworkSettings `json:"NetworkSettings,omitempty" yaml:"NetworkSettings,omitempty"` SysInitPath string `json:"SysInitPath,omitempty" yaml:"SysInitPath,omitempty"` @@ -221,6 +243,28 @@ type Container struct { Volumes map[string]string `json:"Volumes,omitempty" yaml:"Volumes,omitempty"` VolumesRW map[string]bool `json:"VolumesRW,omitempty" yaml:"VolumesRW,omitempty"` HostConfig *HostConfig `json:"HostConfig,omitempty" yaml:"HostConfig,omitempty"` + ExecIDs []string `json:"ExecIDs,omitempty" yaml:"ExecIDs,omitempty"` + + AppArmorProfile string `json:"AppArmorProfile,omitempty" yaml:"AppArmorProfile,omitempty"` +} + +// RenameContainerOptions specify parameters to the RenameContainer function. +// +// See http://goo.gl/L00hoj for more details. +type RenameContainerOptions struct { + // ID of container to rename + ID string `qs:"-"` + + // New name + Name string `json:"name,omitempty" yaml:"name,omitempty"` +} + +// RenameContainer updates and existing containers name +// +// See http://goo.gl/L00hoj for more details. +func (c *Client) RenameContainer(opts RenameContainerOptions) error { + _, _, err := c.do("POST", fmt.Sprintf("/containers/"+opts.ID+"/rename?%s", queryString(opts)), nil, false) + return err } // InspectContainer returns information about a container by its ID. @@ -228,7 +272,7 @@ type Container struct { // See http://goo.gl/CxVuJ5 for more details. func (c *Client) InspectContainer(id string) (*Container, error) { path := "/containers/" + id + "/json" - body, status, err := c.do("GET", path, nil) + body, status, err := c.do("GET", path, nil, false) if status == http.StatusNotFound { return nil, &NoSuchContainer{ID: id} } @@ -248,7 +292,7 @@ func (c *Client) InspectContainer(id string) (*Container, error) { // See http://goo.gl/QkW9sH for more details. func (c *Client) ContainerChanges(id string) ([]Change, error) { path := "/containers/" + id + "/changes" - body, status, err := c.do("GET", path, nil) + body, status, err := c.do("GET", path, nil, false) if status == http.StatusNotFound { return nil, &NoSuchContainer{ID: id} } @@ -284,7 +328,7 @@ func (c *Client) CreateContainer(opts CreateContainerOptions) (*Container, error }{ opts.Config, opts.HostConfig, - }) + }, false) if status == http.StatusNotFound { return nil, ErrNoSuchImage @@ -341,6 +385,14 @@ func NeverRestart() RestartPolicy { return RestartPolicy{Name: "no"} } +// Device represents a device mapping between the Docker host and the +// container. +type Device struct { + PathOnHost string `json:"PathOnHost,omitempty" yaml:"PathOnHost,omitempty"` + PathInContainer string `json:"PathInContainer,omitempty" yaml:"PathInContainer,omitempty"` + CgroupPermissions string `json:"CgroupPermissions,omitempty" yaml:"CgroupPermissions,omitempty"` +} + // HostConfig contains the container options related to starting a container on // a given host type HostConfig struct { @@ -359,20 +411,22 @@ type HostConfig struct { 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"` 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"` } // StartContainer starts a container, returning an error in case of failure. // // See http://goo.gl/iM5GYs for more details. func (c *Client) StartContainer(id string, hostConfig *HostConfig) error { - if hostConfig == nil { - hostConfig = &HostConfig{} - } path := "/containers/" + id + "/start" - _, status, err := c.do("POST", path, hostConfig) + _, status, err := c.do("POST", path, hostConfig, true) if status == http.StatusNotFound { - return &NoSuchContainer{ID: id} + return &NoSuchContainer{ID: id, Err: err} } if status == http.StatusNotModified { return &ContainerAlreadyRunning{ID: id} @@ -389,7 +443,7 @@ func (c *Client) StartContainer(id string, hostConfig *HostConfig) error { // See http://goo.gl/EbcpXt for more details. func (c *Client) StopContainer(id string, timeout uint) error { path := fmt.Sprintf("/containers/%s/stop?t=%d", id, timeout) - _, status, err := c.do("POST", path, nil) + _, status, err := c.do("POST", path, nil, false) if status == http.StatusNotFound { return &NoSuchContainer{ID: id} } @@ -408,7 +462,7 @@ func (c *Client) StopContainer(id string, timeout uint) error { // See http://goo.gl/VOzR2n for more details. func (c *Client) RestartContainer(id string, timeout uint) error { path := fmt.Sprintf("/containers/%s/restart?t=%d", id, timeout) - _, status, err := c.do("POST", path, nil) + _, status, err := c.do("POST", path, nil, false) if status == http.StatusNotFound { return &NoSuchContainer{ID: id} } @@ -423,7 +477,7 @@ func (c *Client) RestartContainer(id string, timeout uint) error { // See http://goo.gl/AM5t42 for more details. func (c *Client) PauseContainer(id string) error { path := fmt.Sprintf("/containers/%s/pause", id) - _, status, err := c.do("POST", path, nil) + _, status, err := c.do("POST", path, nil, false) if status == http.StatusNotFound { return &NoSuchContainer{ID: id} } @@ -438,7 +492,7 @@ func (c *Client) PauseContainer(id string) error { // See http://goo.gl/eBrNSL for more details. func (c *Client) UnpauseContainer(id string) error { path := fmt.Sprintf("/containers/%s/unpause", id) - _, status, err := c.do("POST", path, nil) + _, status, err := c.do("POST", path, nil, false) if status == http.StatusNotFound { return &NoSuchContainer{ID: id} } @@ -467,7 +521,7 @@ 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, nil) + body, status, err := c.do("GET", path, nil, false) if status == http.StatusNotFound { return result, &NoSuchContainer{ID: id} } @@ -499,7 +553,7 @@ type KillContainerOptions struct { // See http://goo.gl/TFkECx for more details. func (c *Client) KillContainer(opts KillContainerOptions) error { path := "/containers/" + opts.ID + "/kill" + "?" + queryString(opts) - _, status, err := c.do("POST", path, nil) + _, status, err := c.do("POST", path, nil, false) if status == http.StatusNotFound { return &NoSuchContainer{ID: opts.ID} } @@ -530,7 +584,7 @@ type RemoveContainerOptions struct { // See http://goo.gl/ZB83ji for more details. func (c *Client) RemoveContainer(opts RemoveContainerOptions) error { path := "/containers/" + opts.ID + "?" + queryString(opts) - _, status, err := c.do("DELETE", path, nil) + _, status, err := c.do("DELETE", path, nil, false) if status == http.StatusNotFound { return &NoSuchContainer{ID: opts.ID} } @@ -559,7 +613,7 @@ func (c *Client) CopyFromContainer(opts CopyFromContainerOptions) error { return &NoSuchContainer{ID: opts.Container} } url := fmt.Sprintf("/containers/%s/copy", opts.Container) - body, status, err := c.do("POST", url, opts) + body, status, err := c.do("POST", url, opts, false) if status == http.StatusNotFound { return &NoSuchContainer{ID: opts.Container} } @@ -575,7 +629,7 @@ func (c *Client) CopyFromContainer(opts CopyFromContainerOptions) error { // // See http://goo.gl/J88DHU for more details. func (c *Client) WaitContainer(id string) (int, error) { - body, status, err := c.do("POST", "/containers/"+id+"/wait", nil) + body, status, err := c.do("POST", "/containers/"+id+"/wait", nil, false) if status == http.StatusNotFound { return 0, &NoSuchContainer{ID: id} } @@ -607,7 +661,7 @@ type CommitContainerOptions struct { // See http://goo.gl/Jn8pe8 for more details. func (c *Client) CommitContainer(opts CommitContainerOptions) (*Image, error) { path := "/commit?" + queryString(opts) - body, status, err := c.do("POST", path, opts.Run) + body, status, err := c.do("POST", path, opts.Run, false) if status == http.StatusNotFound { return nil, &NoSuchContainer{ID: opts.Container} } @@ -706,7 +760,7 @@ 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(), nil) + _, _, err := c.do("POST", "/containers/"+id+"/resize?"+params.Encode(), nil, false) return err } @@ -733,10 +787,14 @@ func (c *Client) ExportContainer(opts ExportContainerOptions) error { // NoSuchContainer is the error returned when a given container does not exist. type NoSuchContainer struct { - ID string + ID string + Err error } func (err *NoSuchContainer) Error() string { + if err.Err != nil { + return err.Err.Error() + } return "No such container: " + err.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 bfb1119205fa..b43447363fa3 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 @@ -1,4 +1,4 @@ -// Copyright 2014 go-dockerclient authors. All rights reserved. +// 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. @@ -7,6 +7,7 @@ package docker import ( "bytes" "encoding/json" + "errors" "io/ioutil" "net" "net/http" @@ -154,6 +155,7 @@ func TestListContainersFailure(t *testing.T) { func TestInspectContainer(t *testing.T) { jsonContainer := `{ "Id": "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2", + "AppArmorProfile": "Profile", "Created": "2013-05-07T14:51:42.087658+02:00", "Path": "date", "Args": [], @@ -175,7 +177,10 @@ func TestInspectContainer(t *testing.T) { ], "Image": "base", "Volumes": {}, - "VolumesFrom": "" + "VolumesFrom": "", + "SecurityOpt": [ + "label:user:USER" + ] }, "State": { "Running": false, @@ -184,6 +189,21 @@ func TestInspectContainer(t *testing.T) { "StartedAt": "2013-05-07T14:51:42.087658+02:00", "Ghost": false }, + "Node": { + "ID": "4I4E:QR4I:Z733:QEZK:5X44:Q4T7:W2DD:JRDY:KB2O:PODO:Z5SR:XRB6", + "IP": "192.168.99.105", + "Addra": "192.168.99.105:2376", + "Name": "node-01", + "Cpus": 4, + "Memory": 1048436736, + "Labels": { + "executiondriver": "native-0.2", + "kernelversion": "3.18.5-tinycore64", + "operatingsystem": "Boot2Docker 1.5.0 (TCL 5.4); master : a66bce5 - Tue Feb 10 23:31:27 UTC 2015", + "provider": "virtualbox", + "storagedriver": "aufs" + } + }, "Image": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", "NetworkSettings": { "IpAddress": "", @@ -511,12 +531,17 @@ func TestStartContainerNilHostConfig(t *testing.T) { if contentType := req.Header.Get("Content-Type"); contentType != expectedContentType { t.Errorf("StartContainer(%q): Wrong content-type in request. Want %q. Got %q.", id, expectedContentType, contentType) } + var buf [4]byte + req.Body.Read(buf[:]) + if string(buf[:]) != "null" { + t.Errorf("Startcontainer(%q): Wrong body. Want null. Got %s", buf[:]) + } } func TestStartContainerNotFound(t *testing.T) { client := newTestClient(&FakeRoundTripper{message: "no such container", status: http.StatusNotFound}) err := client.StartContainer("a2344", &HostConfig{}) - expected := &NoSuchContainer{ID: "a2344"} + expected := &NoSuchContainer{ID: "a2344", Err: err.(*NoSuchContainer).Err} if !reflect.DeepEqual(err, expected) { t.Errorf("StartContainer: Wrong error returned. Want %#v. Got %#v.", expected, err) } @@ -1287,6 +1312,14 @@ func TestNoSuchContainerError(t *testing.T) { } } +func TestNoSuchContainerErrorMessage(t *testing.T) { + var err = &NoSuchContainer{ID: "i345", Err: errors.New("some advanced error info")} + expected := "some advanced error info" + if got := err.Error(); got != expected { + t.Errorf("NoSuchContainer: wrong message. Want %q. Got %q.", expected, got) + } +} + func TestExportContainer(t *testing.T) { content := "exported container tar content" out := stdoutMock{bytes.NewBufferString(content)} @@ -1311,7 +1344,7 @@ func TestExportContainerViaUnixSocket(t *testing.T) { tempSocket := tempfile("export_socket") defer os.Remove(tempSocket) endpoint := "unix://" + tempSocket - u, _ := parseEndpoint(endpoint) + u, _ := parseEndpoint(endpoint, false) client := Client{ HTTPClient: http.DefaultClient, endpoint: endpoint, @@ -1522,3 +1555,26 @@ func TestTopContainerWithPsArgs(t *testing.T) { t.Errorf("TopContainer: Expected URI to have %q. Got %q.", expectedURI, fakeRT.requests[0].URL.String()) } } + +func TestRenameContainer(t *testing.T) { + fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK} + client := newTestClient(fakeRT) + opts := RenameContainerOptions{ID: "something_old", Name: "something_new"} + err := client.RenameContainer(opts) + if err != nil { + t.Fatal(err) + } + req := fakeRT.requests[0] + if req.Method != "POST" { + t.Errorf("RenameContainer: wrong HTTP method. Want %q. Got %q.", "POST", req.Method) + } + expectedURL, _ := url.Parse(client.getURL("/containers/something_old/rename?name=something_new")) + if gotPath := req.URL.Path; gotPath != expectedURL.Path { + t.Errorf("RenameContainer: Wrong path in request. Want %q. Got %q.", expectedURL.Path, gotPath) + } + expectedValues := expectedURL.Query()["name"] + actualValues := req.URL.Query()["name"] + if len(actualValues) != 1 || expectedValues[0] != actualValues[0] { + t.Errorf("RenameContainer: Wrong params in request. Want %q. Got %q.", expectedValues, actualValues) + } +} 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 7c055c5ebd95..8468a320661b 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/event.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/event.go @@ -1,4 +1,4 @@ -// Copyright 2014 go-dockerclient authors. All rights reserved. +// 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. @@ -182,8 +182,8 @@ func (eventState *eventMonitoringState) monitorEvents(c *Client) { eventState.terminate() return } + eventState.updateLastSeen(ev) go eventState.sendEvent(ev) - go eventState.updateLastSeen(ev) case err = <-eventState.errC: if err == ErrNoListeners { eventState.terminate() @@ -227,7 +227,7 @@ func (eventState *eventMonitoringState) sendEvent(event *APIEvents) { eventState.Add(1) defer eventState.Done() if eventState.isEnabled() { - if eventState.noListeners() { + if len(eventState.listeners) == 0 { eventState.errC <- ErrNoListeners return } 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 0659ebd0f82f..a2fec1e69b26 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/exec.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/exec.go @@ -1,4 +1,4 @@ -// Copyright 2014 go-dockerclient authors. All rights reserved. +// 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. @@ -90,7 +90,7 @@ type ExecInspect struct { // See http://goo.gl/8izrzI for more details func (c *Client) CreateExec(opts CreateExecOptions) (*Exec, error) { path := fmt.Sprintf("/containers/%s/exec", opts.Container) - body, status, err := c.do("POST", path, opts) + body, status, err := c.do("POST", path, opts, false) if status == http.StatusNotFound { return nil, &NoSuchContainer{ID: opts.Container} } @@ -119,7 +119,7 @@ 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, opts) + _, status, err := c.do("POST", path, opts, false) if status == http.StatusNotFound { return &NoSuchExec{ID: id} } @@ -143,7 +143,7 @@ func (c *Client) ResizeExecTTY(id string, height, width int) error { params.Set("w", strconv.Itoa(width)) path := fmt.Sprintf("/exec/%s/resize?%s", id, params.Encode()) - _, _, err := c.do("POST", path, nil) + _, _, err := c.do("POST", path, nil, false) return err } @@ -152,7 +152,7 @@ func (c *Client) ResizeExecTTY(id string, height, width int) error { // See http://goo.gl/ypQULN 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, nil) + body, status, err := c.do("GET", path, nil, false) if status == http.StatusNotFound { return nil, &NoSuchExec{ID: id} } diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/exec_test.go b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/exec_test.go index 22cced52a0bc..bc53ed1a4197 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/exec_test.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/exec_test.go @@ -1,4 +1,4 @@ -// Copyright 2014 go-dockerclient authors. All rights reserved. +// 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. 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 3d55155ff78a..ceac53ee3e68 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/image.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/image.go @@ -1,4 +1,4 @@ -// Copyright 2014 go-dockerclient authors. All rights reserved. +// 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. @@ -99,7 +99,7 @@ var ( // See http://goo.gl/2rOLFF for more details. func (c *Client) ListImages(opts ListImagesOptions) ([]APIImages, error) { path := "/images/json?" + queryString(opts) - body, _, err := c.do("GET", path, nil) + body, _, err := c.do("GET", path, nil, false) if err != nil { return nil, err } @@ -115,7 +115,7 @@ func (c *Client) ListImages(opts ListImagesOptions) ([]APIImages, error) { // // See http://goo.gl/2oJmNs for more details. func (c *Client) ImageHistory(name string) ([]ImageHistory, error) { - body, status, err := c.do("GET", "/images/"+name+"/history", nil) + body, status, err := c.do("GET", "/images/"+name+"/history", nil, false) if status == http.StatusNotFound { return nil, ErrNoSuchImage } @@ -134,7 +134,29 @@ func (c *Client) ImageHistory(name string) ([]ImageHistory, error) { // // See http://goo.gl/znj0wM for more details. func (c *Client) RemoveImage(name string) error { - _, status, err := c.do("DELETE", "/images/"+name, nil) + _, status, err := c.do("DELETE", "/images/"+name, nil, false) + if status == http.StatusNotFound { + return ErrNoSuchImage + } + return err +} + +// RemoveImageOptions present the set of options available for removing an image +// from a registry. +// +// See http://goo.gl/6V48bF for more details. +type RemoveImageOptions struct { + Force bool `qs:"force"` + NoPrune bool `qs:"noprune"` +} + +// RemoveImageExtended removes an image by its name or ID. +// Extra params can be passed, see RemoveImageOptions +// +// See http://goo.gl/znj0wM 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, nil, false) if status == http.StatusNotFound { return ErrNoSuchImage } @@ -145,7 +167,7 @@ func (c *Client) RemoveImage(name string) error { // // See http://goo.gl/Q112NY for more details. func (c *Client) InspectImage(name string) (*Image, error) { - body, status, err := c.do("GET", "/images/"+name+"/json", nil) + body, status, err := c.do("GET", "/images/"+name+"/json", nil, false) if status == http.StatusNotFound { return nil, ErrNoSuchImage } @@ -156,7 +178,7 @@ func (c *Client) InspectImage(name string) (*Image, error) { 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(apiVersion1_12) { + if c.SkipServerVersionCheck || c.expectedAPIVersion.GreaterThanOrEqualTo(apiVersion112) { err = json.Unmarshal(body, &image) if err != nil { return nil, err @@ -201,21 +223,6 @@ type PushImageOptions struct { RawJSONStream bool `qs:"-"` } -// AuthConfiguration represents authentication options to use in the PushImage -// method. It represents the authentication in the Docker index server. -type AuthConfiguration struct { - Username string `json:"username,omitempty"` - Password string `json:"password,omitempty"` - Email string `json:"email,omitempty"` - ServerAddress string `json:"serveraddress,omitempty"` -} - -// AuthConfigurations represents authentication options to use for the -// PushImage method accommodating the new X-Registry-Config header -type AuthConfigurations struct { - Configs map[string]AuthConfiguration `json:"configs"` -} - // PushImage pushes an image to a remote registry, logging progress to w. // // An empty instance of AuthConfiguration may be used for unauthenticated @@ -245,7 +252,7 @@ type PullImageOptions struct { RawJSONStream bool `qs:"-"` } -// PullImage pulls an image from a remote registry, logging progress to w. +// PullImage pulls an image from a remote registry, logging progress to opts.OutputStream. // // See http://goo.gl/ACyYNS for more details. func (c *Client) PullImage(opts PullImageOptions, auth AuthConfiguration) error { @@ -333,6 +340,7 @@ func (c *Client) ImportImage(opts ImportImageOptions) error { // http://goo.gl/tlPXPu. type BuildImageOptions struct { Name string `qs:"t"` + Dockerfile string `qs:"dockerfile"` NoCache bool `qs:"nocache"` SuppressOutput bool `qs:"q"` RmTmpContainer bool `qs:"rm"` @@ -395,7 +403,8 @@ func (c *Client) TagImage(name string, opts TagImageOptions) error { return ErrNoSuchImage } _, status, err := c.do("POST", fmt.Sprintf("/images/"+name+"/tag?%s", - queryString(&opts)), nil) + queryString(&opts)), nil, false) + if status == http.StatusNotFound { return ErrNoSuchImage } @@ -445,7 +454,7 @@ type APIImageSearch struct { // // See http://goo.gl/xI5lLZ for more details. func (c *Client) SearchImages(term string) ([]APIImageSearch, error) { - body, _, err := c.do("GET", "/images/search?term="+term, nil) + body, _, err := c.do("GET", "/images/search?term="+term, nil, false) if err != nil { return nil, err } 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 11776e8c4d1c..539ea978c319 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 @@ -1,4 +1,4 @@ -// Copyright 2014 go-dockerclient authors. All rights reserved. +// 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. @@ -19,7 +19,7 @@ import ( func newTestClient(rt *FakeRoundTripper) Client { endpoint := "http://localhost:4243" - u, _ := parseEndpoint("http://localhost:4243") + u, _ := parseEndpoint("http://localhost:4243", false) client := Client{ HTTPClient: &http.Client{Transport: rt}, endpoint: endpoint, @@ -208,6 +208,29 @@ func TestRemoveImageNotFound(t *testing.T) { } } +func TestRemoveImageExtended(t *testing.T) { + name := "test" + fakeRT := &FakeRoundTripper{message: "", status: http.StatusNoContent} + client := newTestClient(fakeRT) + err := client.RemoveImageExtended(name, RemoveImageOptions{Force: true, NoPrune: true}) + if err != nil { + t.Fatal(err) + } + req := fakeRT.requests[0] + expectedMethod := "DELETE" + if req.Method != expectedMethod { + t.Errorf("RemoveImage(%q): Wrong HTTP method. Want %s. Got %s.", name, expectedMethod, req.Method) + } + u, _ := url.Parse(client.getURL("/images/" + name)) + if req.URL.Path != u.Path { + t.Errorf("RemoveImage(%q): Wrong request path. Want %q. Got %q.", name, u.Path, req.URL.Path) + } + expectedQuery := "force=1&noprune=1" + if query := req.URL.Query().Encode(); query != expectedQuery { + t.Errorf("PushImage: Wrong query string. Want %q. Got %q.", expectedQuery, query) + } +} + func TestInspectImage(t *testing.T) { body := `{ "id":"b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", 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 2678ab5cbf6f..689036cc5edd 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/misc.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/misc.go @@ -1,4 +1,4 @@ -// Copyright 2014 go-dockerclient authors. All rights reserved. +// 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. @@ -13,7 +13,7 @@ import ( // // See http://goo.gl/BOZrF5 for more details. func (c *Client) Version() (*Env, error) { - body, _, err := c.do("GET", "/version", nil) + body, _, err := c.do("GET", "/version", nil, false) if err != nil { return nil, err } @@ -28,7 +28,7 @@ func (c *Client) Version() (*Env, error) { // // See http://goo.gl/wmqZsW for more details. func (c *Client) Info() (*Env, error) { - body, _, err := c.do("GET", "/info", nil) + body, _, err := c.do("GET", "/info", nil, false) if err != nil { return nil, err } diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/tar.go b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/tar.go index 200519385262..aa56be204bc3 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/tar.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/tar.go @@ -1,4 +1,4 @@ -// Copyright 2014 go-dockerclient authors. All rights reserved. +// 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. 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 4f8c72b4bf7b..2114261750f2 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 @@ -1,4 +1,4 @@ -// Copyright 2014 go-dockerclient authors. All rights reserved. +// 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. @@ -35,6 +35,7 @@ import ( type DockerServer struct { containers []*docker.Container execs []*docker.ExecInspect + execMut sync.RWMutex cMut sync.RWMutex images []docker.Image iMut sync.RWMutex @@ -43,6 +44,8 @@ type DockerServer struct { mux *mux.Router hook func(*http.Request) failures map[string]string + multiFailures []map[string]string + execCallbacks map[string]func() customHandlers map[string]http.Handler handlerMutex sync.RWMutex cChan chan<- *docker.Container @@ -69,6 +72,7 @@ func NewServer(bind string, containerChan chan<- *docker.Container, hook func(*h imgIDs: make(map[string]string), hook: hook, failures: make(map[string]string), + execCallbacks: make(map[string]func()), customHandlers: make(map[string]http.Handler), cChan: containerChan, } @@ -89,6 +93,7 @@ func (s *DockerServer) buildMuxer() { s.mux.Path("/containers/json").Methods("GET").HandlerFunc(s.handlerWrapper(s.listContainers)) s.mux.Path("/containers/create").Methods("POST").HandlerFunc(s.handlerWrapper(s.createContainer)) s.mux.Path("/containers/{id:.*}/json").Methods("GET").HandlerFunc(s.handlerWrapper(s.inspectContainer)) + s.mux.Path("/containers/{id:.*}/rename").Methods("POST").HandlerFunc(s.handlerWrapper(s.renameContainer)) s.mux.Path("/containers/{id:.*}/top").Methods("GET").HandlerFunc(s.handlerWrapper(s.topContainer)) s.mux.Path("/containers/{id:.*}/start").Methods("POST").HandlerFunc(s.handlerWrapper(s.startContainer)) s.mux.Path("/containers/{id:.*}/kill").Methods("POST").HandlerFunc(s.handlerWrapper(s.stopContainer)) @@ -115,17 +120,59 @@ func (s *DockerServer) buildMuxer() { s.mux.Path("/images/{id:.*}/get").Methods("GET").HandlerFunc(s.handlerWrapper(s.getImage)) } +// SetHook changes the hook function used by the server. +// +// The hook function is a function called on every request. +func (s *DockerServer) SetHook(hook func(*http.Request)) { + s.hook = hook +} + +// PrepareExec adds a callback to a container exec in the fake server. +// +// This function will be called whenever the given exec id is started, and the +// given exec id will remain in the "Running" start while the function is +// running, so it's useful for emulating an exec that runs for two seconds, for +// example: +// +// opts := docker.CreateExecOptions{ +// AttachStdin: true, +// AttachStdout: true, +// AttachStderr: true, +// Tty: true, +// Cmd: []string{"/bin/bash", "-l"}, +// } +// // Client points to a fake server. +// exec, err := client.CreateExec(opts) +// // handle error +// server.PrepareExec(exec.ID, func() {time.Sleep(2 * time.Second)}) +// err = client.StartExec(exec.ID, docker.StartExecOptions{Tty: true}) // will block for 2 seconds +// // handle error +func (s *DockerServer) PrepareExec(id string, callback func()) { + s.execCallbacks[id] = callback +} + // PrepareFailure adds a new expected failure based on a URL regexp it receives // an id for the failure. func (s *DockerServer) PrepareFailure(id string, urlRegexp string) { s.failures[id] = urlRegexp } +// PrepareMultiFailures enqueues a new expected failure based on a URL regexp +// it receives an id for the failure. +func (s *DockerServer) PrepareMultiFailures(id string, urlRegexp string) { + s.multiFailures = append(s.multiFailures, map[string]string{"error": id, "url": urlRegexp}) +} + // ResetFailure removes an expected failure identified by the given id. func (s *DockerServer) ResetFailure(id string) { delete(s.failures, id) } +// ResetMultiFailures removes all enqueued failures. +func (s *DockerServer) ResetMultiFailures() { + s.multiFailures = []map[string]string{} +} + // CustomHandler registers a custom handler for a specific path. // // For example: @@ -170,9 +217,11 @@ func (s *DockerServer) URL() string { func (s *DockerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.handlerMutex.RLock() defer s.handlerMutex.RUnlock() - if handler, ok := s.customHandlers[r.URL.Path]; ok { - handler.ServeHTTP(w, r) - return + for re, handler := range s.customHandlers { + if m, _ := regexp.MatchString(re, r.URL.Path); m { + handler.ServeHTTP(w, r) + return + } } s.mux.ServeHTTP(w, r) if s.hook != nil { @@ -200,6 +249,19 @@ func (s *DockerServer) handlerWrapper(f func(http.ResponseWriter, *http.Request) http.Error(w, errorID, http.StatusBadRequest) return } + for i, failure := range s.multiFailures { + matched, err := regexp.MatchString(failure["url"], r.URL.Path) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if !matched { + continue + } + http.Error(w, failure["error"], http.StatusBadRequest) + s.multiFailures = append(s.multiFailures[:i], s.multiFailures[i+1:]...) + return + } f(w, r) } } @@ -336,6 +398,23 @@ func (s *DockerServer) generateID() string { return fmt.Sprintf("%x", buf) } +func (s *DockerServer) renameContainer(w http.ResponseWriter, r *http.Request) { + id := mux.Vars(r)["id"] + container, index, err := s.findContainer(id) + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + copy := *container + copy.Name = r.URL.Query().Get("name") + s.cMut.Lock() + defer s.cMut.Unlock() + if s.containers[index].ID == copy.ID { + s.containers[index] = © + } + w.WriteHeader(http.StatusNoContent) +} + func (s *DockerServer) inspectContainer(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] container, _, err := s.findContainer(id) @@ -524,9 +603,13 @@ func (s *DockerServer) commitContainer(w http.ResponseWriter, r *http.Request) { Config: config, } repository := r.URL.Query().Get("repo") + tag := r.URL.Query().Get("tag") s.iMut.Lock() s.images = append(s.images, image) if repository != "" { + if tag != "" { + repository += ":" + tag + } s.imgIDs[repository] = image.ID } s.iMut.Unlock() @@ -582,20 +665,28 @@ func (s *DockerServer) buildImage(w http.ResponseWriter, r *http.Request) { } func (s *DockerServer) pullImage(w http.ResponseWriter, r *http.Request) { - repository := r.URL.Query().Get("fromImage") + fromImageName := r.URL.Query().Get("fromImage") + tag := r.URL.Query().Get("tag") image := docker.Image{ ID: s.generateID(), } s.iMut.Lock() s.images = append(s.images, image) - if repository != "" { - s.imgIDs[repository] = image.ID + if fromImageName != "" { + if tag != "" { + fromImageName = fmt.Sprintf("%s:%s", fromImageName, tag) + } + s.imgIDs[fromImageName] = image.ID } s.iMut.Unlock() } func (s *DockerServer) pushImage(w http.ResponseWriter, r *http.Request) { name := mux.Vars(r)["name"] + tag := r.URL.Query().Get("tag") + if tag != "" { + name += ":" + tag + } s.iMut.RLock() if _, ok := s.imgIDs[name]; !ok { s.iMut.RUnlock() @@ -619,6 +710,10 @@ func (s *DockerServer) tagImage(w http.ResponseWriter, r *http.Request) { s.iMut.Lock() defer s.iMut.Unlock() newRepo := r.URL.Query().Get("repo") + newTag := r.URL.Query().Get("tag") + if newTag != "" { + newRepo += ":" + newTag + } s.imgIDs[newRepo] = s.imgIDs[name] w.WriteHeader(http.StatusCreated) } @@ -722,7 +817,6 @@ func (s *DockerServer) loadImage(w http.ResponseWriter, r *http.Request) { func (s *DockerServer) getImage(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/tar") - } func (s *DockerServer) createExecContainer(w http.ResponseWriter, r *http.Request) { @@ -733,7 +827,7 @@ func (s *DockerServer) createExecContainer(w http.ResponseWriter, r *http.Reques return } exec := docker.ExecInspect{ - ID: "id-exec-created-by-test", + ID: s.generateID(), Container: *container, } var params docker.CreateExecOptions @@ -748,44 +842,63 @@ func (s *DockerServer) createExecContainer(w http.ResponseWriter, r *http.Reques exec.ProcessConfig.Arguments = params.Cmd[1:] } } + s.execMut.Lock() s.execs = append(s.execs, &exec) + s.execMut.Unlock() w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"Id": exec.ID}) - } func (s *DockerServer) startExecContainer(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] - for _, exec := range s.execs { - if exec.ID == id { - w.WriteHeader(http.StatusOK) - return + if exec, err := s.getExec(id); err == nil { + s.execMut.Lock() + exec.Running = true + s.execMut.Unlock() + if callback, ok := s.execCallbacks[id]; ok { + callback() + delete(s.execCallbacks, id) + } else if callback, ok := s.execCallbacks["*"]; ok { + callback() + delete(s.execCallbacks, "*") } + s.execMut.Lock() + exec.Running = false + s.execMut.Unlock() + w.WriteHeader(http.StatusOK) + return } w.WriteHeader(http.StatusNotFound) } func (s *DockerServer) resizeExecContainer(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] - for _, exec := range s.execs { - if exec.ID == id { - w.WriteHeader(http.StatusOK) - return - } + if _, err := s.getExec(id); err == nil { + w.WriteHeader(http.StatusOK) + return } w.WriteHeader(http.StatusNotFound) } func (s *DockerServer) inspectExecContainer(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] + if exec, err := s.getExec(id); err == nil { + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(exec) + return + } + w.WriteHeader(http.StatusNotFound) +} + +func (s *DockerServer) getExec(id string) (*docker.ExecInspect, error) { + s.execMut.RLock() + defer s.execMut.RUnlock() for _, exec := range s.execs { if exec.ID == id { - w.WriteHeader(http.StatusOK) - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(exec) - return + return exec, nil } } - w.WriteHeader(http.StatusNotFound) + return nil, errors.New("exec not found") } 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 8217fb1d64fa..097158fbd18c 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 @@ -1,4 +1,4 @@ -// Copyright 2014 go-dockerclient authors. All rights reserved. +// 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. @@ -82,6 +82,19 @@ func TestHandleWithHook(t *testing.T) { } } +func TestSetHook(t *testing.T) { + var called bool + server, _ := NewServer("127.0.0.1:0", nil, nil) + defer server.Stop() + server.SetHook(func(*http.Request) { called = true }) + recorder := httptest.NewRecorder() + request, _ := http.NewRequest("GET", "/containers/json?all=1", nil) + server.ServeHTTP(recorder, request) + if !called { + t.Error("ServeHTTP did not call the hook function.") + } +} + func TestCustomHandler(t *testing.T) { var called bool server, _ := NewServer("127.0.0.1:0", nil, nil) @@ -101,6 +114,25 @@ func TestCustomHandler(t *testing.T) { } } +func TestCustomHandlerRegexp(t *testing.T) { + var called bool + server, _ := NewServer("127.0.0.1:0", nil, nil) + addContainers(server, 2) + server.CustomHandler("/containers/.*/json", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + called = true + fmt.Fprint(w, "Hello world") + })) + recorder := httptest.NewRecorder() + request, _ := http.NewRequest("GET", "/containers/.*/json?all=1", nil) + server.ServeHTTP(recorder, request) + if !called { + t.Error("Did not call the custom handler") + } + if got := recorder.Body.String(); got != "Hello world" { + t.Errorf("Wrong output for custom handler: want %q. Got %q.", "Hello world", got) + } +} + func TestListContainers(t *testing.T) { server := DockerServer{} addContainers(&server, 2) @@ -223,6 +255,35 @@ func TestCreateContainerImageNotFound(t *testing.T) { } } +func TestRenameContainer(t *testing.T) { + server := DockerServer{} + addContainers(&server, 2) + server.buildMuxer() + recorder := httptest.NewRecorder() + newName := server.containers[0].Name + "abc" + path := fmt.Sprintf("/containers/%s/rename?name=%s", server.containers[0].ID, newName) + request, _ := http.NewRequest("POST", path, nil) + server.ServeHTTP(recorder, request) + if recorder.Code != http.StatusNoContent { + t.Errorf("RenameContainer: wrong status. Want %d. Got %d.", http.StatusNoContent, recorder.Code) + } + container := server.containers[0] + if container.Name != newName { + t.Errorf("RenameContainer: did not rename the container. Want %q. Got %q.", newName, container.Name) + } +} + +func TestRenameContainerNotFound(t *testing.T) { + server := DockerServer{} + server.buildMuxer() + recorder := httptest.NewRecorder() + request, _ := http.NewRequest("POST", "/containers/blabla/rename?name=something", nil) + server.ServeHTTP(recorder, request) + if recorder.Code != http.StatusNotFound { + t.Errorf("RenameContainer: wrong status. Want %d. Got %d.", http.StatusNotFound, recorder.Code) + } +} + func TestCommitContainer(t *testing.T) { server := DockerServer{} addContainers(&server, 2) @@ -277,6 +338,27 @@ func TestCommitContainerComplete(t *testing.T) { } } +func TestCommitContainerWithTag(t *testing.T) { + server := DockerServer{} + server.imgIDs = make(map[string]string) + addContainers(&server, 2) + server.buildMuxer() + recorder := httptest.NewRecorder() + queryString := "container=" + server.containers[0].ID + "&repo=tsuru/python&tag=v1" + request, _ := http.NewRequest("POST", "/commit?"+queryString, nil) + server.ServeHTTP(recorder, request) + image := server.images[0] + if image.Parent != server.containers[0].Image { + t.Errorf("CommitContainer: wrong parent image. Want %q. Got %q.", server.containers[0].Image, image.Parent) + } + if image.Container != server.containers[0].ID { + t.Errorf("CommitContainer: wrong container. Want %q. Got %q.", server.containers[0].ID, image.Container) + } + if id := server.imgIDs["tsuru/python:v1"]; id != image.ID { + t.Errorf("CommitContainer: wrong ID saved for repository. Want %q. Got %q.", image.ID, id) + } +} + func TestCommitContainerInvalidRun(t *testing.T) { server := DockerServer{} addContainers(&server, 1) @@ -767,6 +849,23 @@ func TestPullImage(t *testing.T) { } } +func TestPullImageWithTag(t *testing.T) { + server := DockerServer{imgIDs: make(map[string]string)} + server.buildMuxer() + recorder := httptest.NewRecorder() + request, _ := http.NewRequest("POST", "/images/create?fromImage=base&tag=tag", nil) + server.ServeHTTP(recorder, request) + if recorder.Code != http.StatusOK { + t.Errorf("PullImage: wrong status. Want %d. Got %d.", http.StatusOK, recorder.Code) + } + if len(server.images) != 1 { + t.Errorf("PullImage: Want 1 image. Got %d.", len(server.images)) + } + if _, ok := server.imgIDs["base:tag"]; !ok { + t.Error("PullImage: Repository should not be empty.") + } +} + func TestPushImage(t *testing.T) { server := DockerServer{imgIDs: map[string]string{"tsuru/python": "a123"}} server.buildMuxer() @@ -778,6 +877,17 @@ func TestPushImage(t *testing.T) { } } +func TestPushImageWithTag(t *testing.T) { + server := DockerServer{imgIDs: map[string]string{"tsuru/python:v1": "a123"}} + server.buildMuxer() + recorder := httptest.NewRecorder() + request, _ := http.NewRequest("POST", "/images/tsuru/python/push?tag=v1", nil) + server.ServeHTTP(recorder, request) + if recorder.Code != http.StatusOK { + t.Errorf("PushImage: wrong status. Want %d. Got %d.", http.StatusOK, recorder.Code) + } +} + func TestPushImageNotFound(t *testing.T) { server := DockerServer{} server.buildMuxer() @@ -803,6 +913,20 @@ func TestTagImage(t *testing.T) { } } +func TestTagImageWithRepoAndTag(t *testing.T) { + server := DockerServer{imgIDs: map[string]string{"tsuru/python": "a123"}} + server.buildMuxer() + recorder := httptest.NewRecorder() + request, _ := http.NewRequest("POST", "/images/tsuru/python/tag?repo=tsuru/new-python&tag=v1", nil) + server.ServeHTTP(recorder, request) + if recorder.Code != http.StatusCreated { + t.Errorf("TagImage: wrong status. Want %d. Got %d.", http.StatusCreated, recorder.Code) + } + if server.imgIDs["tsuru/python"] != server.imgIDs["tsuru/new-python:v1"] { + t.Errorf("TagImage: did not tag the image") + } +} + func TestTagImageNotFound(t *testing.T) { server := DockerServer{} server.buildMuxer() @@ -986,6 +1110,41 @@ func TestPrepareFailure(t *testing.T) { } } +func TestPrepareMultiFailures(t *testing.T) { + server := DockerServer{multiFailures: []map[string]string{}} + server.buildMuxer() + errorID := "multi error" + server.PrepareMultiFailures(errorID, "containers/json") + server.PrepareMultiFailures(errorID, "containers/json") + recorder := httptest.NewRecorder() + request, _ := http.NewRequest("GET", "/containers/json?all=1", nil) + server.ServeHTTP(recorder, request) + if recorder.Code != http.StatusBadRequest { + t.Errorf("PrepareFailure: wrong status. Want %d. Got %d.", http.StatusBadRequest, recorder.Code) + } + if recorder.Body.String() != errorID+"\n" { + t.Errorf("PrepareFailure: wrong message. Want %s. Got %s.", errorID, recorder.Body.String()) + } + recorder = httptest.NewRecorder() + request, _ = http.NewRequest("GET", "/containers/json?all=1", nil) + server.ServeHTTP(recorder, request) + if recorder.Code != http.StatusBadRequest { + t.Errorf("PrepareFailure: wrong status. Want %d. Got %d.", http.StatusBadRequest, recorder.Code) + } + if recorder.Body.String() != errorID+"\n" { + t.Errorf("PrepareFailure: wrong message. Want %s. Got %s.", errorID, recorder.Body.String()) + } + recorder = httptest.NewRecorder() + request, _ = http.NewRequest("GET", "/containers/json?all=1", nil) + server.ServeHTTP(recorder, request) + if recorder.Code != http.StatusOK { + t.Errorf("PrepareFailure: wrong status. Want %d. Got %d.", http.StatusOK, recorder.Code) + } + if recorder.Body.String() == errorID+"\n" { + t.Errorf("PrepareFailure: wrong message. Want %s. Got %s.", errorID, recorder.Body.String()) + } +} + func TestRemoveFailure(t *testing.T) { server := DockerServer{failures: make(map[string]string)} server.buildMuxer() @@ -1006,6 +1165,21 @@ func TestRemoveFailure(t *testing.T) { } } +func TestResetMultiFailures(t *testing.T) { + server := DockerServer{multiFailures: []map[string]string{}} + server.buildMuxer() + errorID := "multi error" + server.PrepareMultiFailures(errorID, "containers/json") + server.PrepareMultiFailures(errorID, "containers/json") + if len(server.multiFailures) != 2 { + t.Errorf("PrepareMultiFailures: error adding multi failures.") + } + server.ResetMultiFailures() + if len(server.multiFailures) != 0 { + t.Errorf("ResetMultiFailures: error reseting multi failures.") + } +} + func TestMutateContainer(t *testing.T) { server := DockerServer{failures: make(map[string]string)} server.buildMuxer() @@ -1169,3 +1343,132 @@ func TestInspectExecContainer(t *testing.T) { t.Errorf("InspectContainer: wrong value. Want:\n%#v\nGot:\n%#v\n", expected, got2) } } + +func TestStartExecContainer(t *testing.T) { + server, _ := NewServer("127.0.0.1:0", nil, nil) + addContainers(server, 1) + server.buildMuxer() + recorder := httptest.NewRecorder() + body := `{"Cmd": ["bash", "-c", "ls"]}` + path := fmt.Sprintf("/containers/%s/exec", server.containers[0].ID) + request, _ := http.NewRequest("POST", path, strings.NewReader(body)) + server.ServeHTTP(recorder, request) + if recorder.Code != http.StatusOK { + t.Fatalf("CreateExec: wrong status. Want %d. Got %d.", http.StatusOK, recorder.Code) + } + var exec docker.Exec + err := json.NewDecoder(recorder.Body).Decode(&exec) + if err != nil { + t.Fatal(err) + } + unleash := make(chan bool) + server.PrepareExec(exec.ID, func() { + <-unleash + }) + codes := make(chan int, 1) + sent := make(chan bool) + go func() { + recorder := httptest.NewRecorder() + path := fmt.Sprintf("/exec/%s/start", exec.ID) + body := `{"Tty":true}` + request, _ := http.NewRequest("POST", path, strings.NewReader(body)) + close(sent) + server.ServeHTTP(recorder, request) + codes <- recorder.Code + }() + <-sent + execInfo, err := waitExec(server.URL(), exec.ID, true, 5) + if err != nil { + t.Fatal(err) + } + if !execInfo.Running { + t.Error("StartExec: expected exec to be running, but it's not running") + } + close(unleash) + if code := <-codes; code != http.StatusOK { + t.Errorf("StartExec: wrong status. Want %d. Got %d.", http.StatusOK, code) + } + execInfo, err = waitExec(server.URL(), exec.ID, false, 5) + if err != nil { + t.Fatal(err) + } + if execInfo.Running { + t.Error("StartExec: expected exec to be not running after start returns, but it's running") + } +} + +func TestStartExecContainerWildcardCallback(t *testing.T) { + server, _ := NewServer("127.0.0.1:0", nil, nil) + addContainers(server, 1) + server.buildMuxer() + recorder := httptest.NewRecorder() + body := `{"Cmd": ["bash", "-c", "ls"]}` + path := fmt.Sprintf("/containers/%s/exec", server.containers[0].ID) + request, _ := http.NewRequest("POST", path, strings.NewReader(body)) + server.ServeHTTP(recorder, request) + if recorder.Code != http.StatusOK { + t.Fatalf("CreateExec: wrong status. Want %d. Got %d.", http.StatusOK, recorder.Code) + } + unleash := make(chan bool) + server.PrepareExec("*", func() { + <-unleash + }) + var exec docker.Exec + err := json.NewDecoder(recorder.Body).Decode(&exec) + if err != nil { + t.Fatal(err) + } + codes := make(chan int, 1) + sent := make(chan bool) + go func() { + recorder := httptest.NewRecorder() + path := fmt.Sprintf("/exec/%s/start", exec.ID) + body := `{"Tty":true}` + request, _ := http.NewRequest("POST", path, strings.NewReader(body)) + close(sent) + server.ServeHTTP(recorder, request) + codes <- recorder.Code + }() + <-sent + execInfo, err := waitExec(server.URL(), exec.ID, true, 5) + if err != nil { + t.Fatal(err) + } + if !execInfo.Running { + t.Error("StartExec: expected exec to be running, but it's not running") + } + close(unleash) + if code := <-codes; code != http.StatusOK { + t.Errorf("StartExec: wrong status. Want %d. Got %d.", http.StatusOK, code) + } + execInfo, err = waitExec(server.URL(), exec.ID, false, 5) + if err != nil { + t.Fatal(err) + } + if execInfo.Running { + t.Error("StartExec: expected exec to be not running after start returns, but it's running") + } +} + +func TestStartExecContainerNotFound(t *testing.T) { + server, _ := NewServer("127.0.0.1:0", nil, nil) + addContainers(server, 1) + server.buildMuxer() + recorder := httptest.NewRecorder() + body := `{"Tty":true}` + request, _ := http.NewRequest("POST", "/exec/something-wat/start", strings.NewReader(body)) + server.ServeHTTP(recorder, request) +} + +func waitExec(url, execID string, running bool, maxTry int) (*docker.ExecInspect, error) { + client, err := docker.NewClient(url) + if err != nil { + return nil, err + } + exec, err := client.InspectExec(execID) + for i := 0; i < maxTry && exec.Running != running && err == nil; i++ { + time.Sleep(100e6) + exec, err = client.InspectExec(exec.ID) + } + return exec, err +}