diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 56547d81860..6de24af7789 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -104,8 +104,8 @@ }, { "ImportPath": "github.com/fsouza/go-dockerclient", - "Comment": "0.2.1-267-g15d2c6e", - "Rev": "15d2c6e3eb670c545d0af0604d7f9aff3871af04" + "Comment": "0.2.1-334-g9c377ff", + "Rev": "9c377ffd9aed48a012adf1c3fd517fe98394120b" }, { "ImportPath": "github.com/ghodss/yaml", 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 ad1dff7a4f1..5a19fae36fb 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.1.2 - 1.2.2 - 1.3.1 - tip @@ -11,3 +10,4 @@ install: - go get -d ./... script: - go test ./... + - ./testing/bin/fmtpolice diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/AUTHORS b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/AUTHORS index bb79881406d..1470c64dfae 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/AUTHORS +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/AUTHORS @@ -12,6 +12,7 @@ cheneydeng CMGS Daniel, Dao Quang Minh David Huie +Dawn Chen Ed Eric Anderson Fabio Rehm @@ -19,6 +20,7 @@ Flavia Missi Francisco Souza Jari Kolehmainen Jason Wilder +Jawher Moussa Jean-Baptiste Dalido Jeff Mitchell Jeffrey Hulten @@ -26,11 +28,14 @@ Johan Euphrosine Karan Misra Kim, Hirokuni Lucas Clemente +Máximo Cuadros Ortiz +Mike Dillon Omeid Matten Paul Morie Peter Jihoon Kim Philippe Lafoucrière -Rafe Colton +Rafe Colton +Rob Miller Robert Williamson Salvador Gironès Simon Eskildsen @@ -42,3 +47,4 @@ Summer Mousa Tarsis Azevedo Tim Schindler Wiliam Souza +Ye Yin 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 66cedca6299..0f95d1fe28b 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/README.markdown +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/README.markdown @@ -29,7 +29,7 @@ func main() { fmt.Println("Created: ", img.Created) fmt.Println("Size: ", img.Size) fmt.Println("VirtualSize: ", img.VirtualSize) - fmt.Println("ParentId: ", img.ParentId) + fmt.Println("ParentId: ", img.ParentID) } } ``` 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 new file mode 100644 index 00000000000..ecd9885ff7d --- /dev/null +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/build_test.go @@ -0,0 +1,144 @@ +package docker + +import ( + "bytes" + "io" + "io/ioutil" + "net/http" + "os" + "reflect" + "testing" + + "github.com/docker/docker/pkg/archive" +) + +func TestBuildImageMultipleContextsError(t *testing.T) { + fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK} + client := newTestClient(fakeRT) + var buf bytes.Buffer + opts := BuildImageOptions{ + Name: "testImage", + NoCache: true, + SuppressOutput: true, + RmTmpContainer: true, + ForceRmTmpContainer: true, + InputStream: &buf, + OutputStream: &buf, + ContextDir: "testing/data", + } + err := client.BuildImage(opts) + if err != ErrMultipleContexts { + t.Errorf("BuildImage: providing both InputStream and ContextDir should produce an error") + } +} + +func TestBuildImageContextDirDockerignoreParsing(t *testing.T) { + fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK} + client := newTestClient(fakeRT) + var buf bytes.Buffer + opts := BuildImageOptions{ + Name: "testImage", + NoCache: true, + SuppressOutput: true, + RmTmpContainer: true, + ForceRmTmpContainer: true, + OutputStream: &buf, + ContextDir: "testing/data", + } + err := client.BuildImage(opts) + if err != nil { + t.Fatal(err) + } + reqBody := fakeRT.requests[0].Body + tmpdir, err := unpackBodyTarball(reqBody) + if err != nil { + t.Fatal(err) + } + + defer func() { + if err := os.RemoveAll(tmpdir); err != nil { + t.Fatal(err) + } + }() + + files, err := ioutil.ReadDir(tmpdir) + if err != nil { + t.Fatal(err) + } + + foundFiles := []string{} + for _, file := range files { + foundFiles = append(foundFiles, file.Name()) + } + + expectedFiles := []string{ + ".dockerignore", + "Dockerfile", + "barfile", + "ca.pem", + "cert.pem", + "key.pem", + "server.pem", + "serverkey.pem", + "symlink", + } + + if !reflect.DeepEqual(expectedFiles, foundFiles) { + t.Errorf( + "BuildImage: incorrect files sent in tarball to docker server\nexpected %+v, found %+v", + expectedFiles, foundFiles, + ) + } +} + +func TestBuildImageSendXRegistryConfig(t *testing.T) { + fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK} + client := newTestClient(fakeRT) + var buf bytes.Buffer + opts := BuildImageOptions{ + Name: "testImage", + NoCache: true, + SuppressOutput: true, + RmTmpContainer: true, + ForceRmTmpContainer: true, + OutputStream: &buf, + ContextDir: "testing/data", + AuthConfigs: AuthConfigurations{ + Configs: map[string]AuthConfiguration{ + "quay.io": { + Username: "foo", + Password: "bar", + Email: "baz", + ServerAddress: "quay.io", + }, + }, + }, + } + + encodedConfig := "eyJjb25maWdzIjp7InF1YXkuaW8iOnsidXNlcm5hbWUiOiJmb28iLCJwYXNzd29yZCI6ImJhciIsImVtYWlsIjoiYmF6Iiwic2VydmVyYWRkcmVzcyI6InF1YXkuaW8ifX19Cg==" + + if err := client.BuildImage(opts); err != nil { + t.Fatal(err) + } + + xRegistryConfig := fakeRT.requests[0].Header["X-Registry-Config"][0] + if xRegistryConfig != encodedConfig { + t.Errorf( + "BuildImage: X-Registry-Config not set currectly: expected %q, got %q", + encodedConfig, + xRegistryConfig, + ) + } +} + +func unpackBodyTarball(req io.ReadCloser) (tmpdir string, err error) { + tmpdir, err = ioutil.TempDir("", "go-dockerclient-test") + if err != nil { + return + } + err = archive.Untar(req, tmpdir, &archive.TarOptions{ + Compression: archive.Uncompressed, + NoLchown: true, + }) + return +} 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 cafffadd7bb..e7b056c3f88 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/change.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/change.go @@ -6,11 +6,18 @@ package docker import "fmt" +// ChangeType is a type for constants indicating the type of change +// in a container type ChangeType int const ( + // ChangeModify is the ChangeType for container modifications ChangeModify ChangeType = iota + + // ChangeAdd is the ChangeType for additions to a container ChangeAdd + + // ChangeDelete is the ChangeType for deletions from a container ChangeDelete ) 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 e68a9efb4bc..3d86ff26fe1 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/client.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/client.go @@ -9,6 +9,8 @@ package docker import ( "bytes" + "crypto/tls" + "crypto/x509" "encoding/json" "errors" "fmt" @@ -32,22 +34,22 @@ var ( // ErrConnectionRefused is returned when the client cannot connect to the given endpoint. ErrConnectionRefused = errors.New("cannot connect to Docker endpoint") - apiVersion_1_12, _ = NewApiVersion("1.12") + apiVersion1_12, _ = NewAPIVersion("1.12") ) -// ApiVersion is an internal representation of a version of the Remote API. -type ApiVersion []int +// APIVersion is an internal representation of a version of the Remote API. +type APIVersion []int -// NewApiVersion returns an instance of ApiVersion for the given string. +// NewAPIVersion returns an instance of APIVersion for the given string. // // The given string must be in the form .., where , // and are integer numbers. -func NewApiVersion(input string) (ApiVersion, error) { +func NewAPIVersion(input string) (APIVersion, error) { if !strings.Contains(input, ".") { return nil, fmt.Errorf("Unable to parse version %q", input) } arr := strings.Split(input, ".") - ret := make(ApiVersion, len(arr)) + ret := make(APIVersion, len(arr)) var err error for i, val := range arr { ret[i], err = strconv.Atoi(val) @@ -58,7 +60,7 @@ func NewApiVersion(input string) (ApiVersion, error) { return ret, nil } -func (version ApiVersion) String() string { +func (version APIVersion) String() string { var str string for i, val := range version { str += strconv.Itoa(val) @@ -69,23 +71,27 @@ func (version ApiVersion) String() string { return str } -func (version ApiVersion) LessThan(other ApiVersion) bool { +// LessThan is a function for comparing APIVersion structs +func (version APIVersion) LessThan(other APIVersion) bool { return version.compare(other) < 0 } -func (version ApiVersion) LessThanOrEqualTo(other ApiVersion) bool { +// LessThanOrEqualTo is a function for comparing APIVersion structs +func (version APIVersion) LessThanOrEqualTo(other APIVersion) bool { return version.compare(other) <= 0 } -func (version ApiVersion) GreaterThan(other ApiVersion) bool { +// GreaterThan is a function for comparing APIVersion structs +func (version APIVersion) GreaterThan(other APIVersion) bool { return version.compare(other) > 0 } -func (version ApiVersion) GreaterThanOrEqualTo(other ApiVersion) bool { +// GreaterThanOrEqualTo is a function for comparing APIVersion structs +func (version APIVersion) GreaterThanOrEqualTo(other APIVersion) bool { return version.compare(other) >= 0 } -func (version ApiVersion) compare(other ApiVersion) int { +func (version APIVersion) compare(other APIVersion) int { for i, v := range version { if i <= len(other)-1 { otherVersion := other[i] @@ -111,13 +117,14 @@ func (version ApiVersion) compare(other ApiVersion) int { type Client struct { SkipServerVersionCheck bool HTTPClient *http.Client + TLSConfig *tls.Config endpoint string endpointURL *url.URL eventMonitor *eventMonitoringState - requestedApiVersion ApiVersion - serverApiVersion ApiVersion - expectedApiVersion ApiVersion + requestedAPIVersion APIVersion + serverAPIVersion APIVersion + expectedAPIVersion APIVersion } // NewClient returns a Client instance ready for communication with the given @@ -132,6 +139,18 @@ func NewClient(endpoint string) (*Client, error) { return client, nil } +// NewTLSClient returns a Client instance ready for TLS communications with the givens +// 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, "") + if err != nil { + return nil, err + } + client.SkipServerVersionCheck = true + return client, nil +} + // 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) { @@ -139,9 +158,9 @@ func NewVersionedClient(endpoint string, apiVersionString string) (*Client, erro if err != nil { return nil, err } - var requestedApiVersion ApiVersion + var requestedAPIVersion APIVersion if strings.Contains(apiVersionString, ".") { - requestedApiVersion, err = NewApiVersion(apiVersionString) + requestedAPIVersion, err = NewAPIVersion(apiVersionString) if err != nil { return nil, err } @@ -151,23 +170,74 @@ func NewVersionedClient(endpoint string, apiVersionString string) (*Client, erro endpoint: endpoint, endpointURL: u, eventMonitor: new(eventMonitoringState), - requestedApiVersion: requestedApiVersion, + requestedAPIVersion: requestedAPIVersion, }, nil } -func (c *Client) checkApiVersion() error { - serverApiVersionString, err := c.getServerApiVersionString() +// NewVersionnedTLSClient returns a Client instance ready for TLS communications with the givens +// server endpoint, key and certificates, using a specific remote API version. +func NewVersionnedTLSClient(endpoint string, cert, key, ca, apiVersionString string) (*Client, error) { + u, err := parseEndpoint(endpoint) if err != nil { - return err + return nil, err } - c.serverApiVersion, err = NewApiVersion(serverApiVersionString) + var requestedAPIVersion APIVersion + if strings.Contains(apiVersionString, ".") { + requestedAPIVersion, err = NewAPIVersion(apiVersionString) + if err != nil { + return nil, err + } + } + if cert == "" || key == "" { + return nil, errors.New("Both cert and key path are required") + } + tlsCert, err := tls.LoadX509KeyPair(cert, key) if err != nil { - return err + return nil, err } - if c.requestedApiVersion == nil { - c.expectedApiVersion = c.serverApiVersion + tlsConfig := &tls.Config{Certificates: []tls.Certificate{tlsCert}} + if ca == "" { + tlsConfig.InsecureSkipVerify = true } else { - c.expectedApiVersion = c.requestedApiVersion + cert, err := ioutil.ReadFile(ca) + if err != nil { + return nil, err + } + caPool := x509.NewCertPool() + if !caPool.AppendCertsFromPEM(cert) { + return nil, errors.New("Could not add RootCA pem") + } + tlsConfig.RootCAs = caPool + } + tr := &http.Transport{ + TLSClientConfig: tlsConfig, + } + if err != nil { + return nil, err + } + return &Client{ + HTTPClient: &http.Client{Transport: tr}, + TLSConfig: tlsConfig, + endpoint: endpoint, + endpointURL: u, + eventMonitor: new(eventMonitoringState), + requestedAPIVersion: requestedAPIVersion, + }, nil +} + +func (c *Client) checkAPIVersion() error { + serverAPIVersionString, err := c.getServerAPIVersionString() + if err != nil { + return err + } + c.serverAPIVersion, err = NewAPIVersion(serverAPIVersionString) + if err != nil { + return err + } + if c.requestedAPIVersion == nil { + c.expectedAPIVersion = c.serverAPIVersion + } else { + c.expectedAPIVersion = c.requestedAPIVersion } return nil } @@ -187,7 +257,7 @@ func (c *Client) Ping() error { return nil } -func (c *Client) getServerApiVersionString() (version string, err error) { +func (c *Client) getServerAPIVersionString() (version string, err error) { body, status, err := c.do("GET", "/version", nil) if err != nil { return "", err @@ -213,8 +283,8 @@ func (c *Client) do(method, path string, data interface{}) ([]byte, int, error) } params = bytes.NewBuffer(buf) } - if path != "/version" && !c.SkipServerVersionCheck && c.expectedApiVersion == nil { - err := c.checkApiVersion() + if path != "/version" && !c.SkipServerVersionCheck && c.expectedAPIVersion == nil { + err := c.checkAPIVersion() if err != nil { return nil, -1, err } @@ -268,8 +338,8 @@ func (c *Client) stream(method, path string, setRawTerminal, rawJSONStream bool, if (method == "POST" || method == "PUT") && in == nil { in = bytes.NewReader(nil) } - if path != "/version" && !c.SkipServerVersionCheck && c.expectedApiVersion == nil { - err := c.checkApiVersion() + if path != "/version" && !c.SkipServerVersionCheck && c.expectedAPIVersion == nil { + err := c.checkAPIVersion() if err != nil { return err } @@ -357,8 +427,8 @@ func (c *Client) stream(method, path string, setRawTerminal, rawJSONStream bool, } func (c *Client) hijack(method, path string, success chan struct{}, setRawTerminal bool, in io.Reader, stderr, stdout io.Writer, data interface{}) error { - if path != "/version" && !c.SkipServerVersionCheck && c.expectedApiVersion == nil { - err := c.checkApiVersion() + if path != "/version" && !c.SkipServerVersionCheck && c.expectedAPIVersion == nil { + err := c.checkAPIVersion() if err != nil { return err } @@ -434,11 +504,10 @@ func (c *Client) getURL(path string) string { urlStr = "" } - if c.requestedApiVersion != nil { - return fmt.Sprintf("%s/v%s%s", urlStr, c.requestedApiVersion, path) - } else { - return fmt.Sprintf("%s%s", urlStr, path) + if c.requestedAPIVersion != nil { + return fmt.Sprintf("%s/v%s%s", urlStr, c.requestedAPIVersion, path) } + return fmt.Sprintf("%s%s", urlStr, path) } type jsonMessage struct { @@ -495,6 +564,12 @@ func queryString(opts interface{}) string { items.Add(key, string(b)) } } + case reflect.Map: + if len(v.MapKeys()) > 0 { + if b, err := json.Marshal(v.Interface()); err == nil { + items.Add(key, string(b)) + } + } } } return items.Encode() @@ -520,7 +595,22 @@ func parseEndpoint(endpoint string) (*url.URL, error) { return nil, ErrInvalidEndpoint } if u.Scheme == "tcp" { - u.Scheme = "http" + _, port, err := net.SplitHostPort(u.Host) + if err != nil { + if e, ok := err.(*net.AddrError); ok { + if e.Err == "missing port in address" { + return u, nil + } + } + return nil, ErrInvalidEndpoint + } + + number, err := strconv.ParseInt(port, 10, 64) + if err == nil && number == 2376 { + u.Scheme = "https" + } else { + u.Scheme = "http" + } } if u.Scheme != "http" && u.Scheme != "https" && u.Scheme != "unix" { return nil, ErrInvalidEndpoint 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 9def1716aab..34543b410be 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 @@ -39,8 +39,32 @@ func TestNewAPIClient(t *testing.T) { if !client.SkipServerVersionCheck { t.Error("Expected SkipServerVersionCheck to be true, got false") } - if client.requestedApiVersion != nil { - t.Errorf("Expected requestedApiVersion to be nil, got %#v.", client.requestedApiVersion) + if client.requestedAPIVersion != nil { + t.Errorf("Expected requestedAPIVersion to be nil, got %#v.", client.requestedAPIVersion) + } +} + +func newTLSClient(endpoint string) (*Client, error) { + return NewTLSClient(endpoint, + "testing/data/cert.pem", + "testing/data/key.pem", + "testing/data/ca.pem") +} + +func TestNewTSLAPIClient(t *testing.T) { + endpoint := "https://localhost:4243" + client, err := newTLSClient(endpoint) + if err != nil { + t.Fatal(err) + } + if client.endpoint != endpoint { + t.Errorf("Expected endpoint %s. Got %s.", endpoint, client.endpoint) + } + if !client.SkipServerVersionCheck { + t.Error("Expected SkipServerVersionCheck to be true, got false") + } + if client.requestedAPIVersion != nil { + t.Errorf("Expected requestedAPIVersion to be nil, got %#v.", client.requestedAPIVersion) } } @@ -56,14 +80,45 @@ func TestNewVersionedClient(t *testing.T) { if client.HTTPClient != http.DefaultClient { t.Errorf("Expected http.Client %#v. Got %#v.", http.DefaultClient, client.HTTPClient) } - if reqVersion := client.requestedApiVersion.String(); reqVersion != "1.12" { - t.Errorf("Wrong requestApiVersion. Want %q. Got %q.", "1.12", reqVersion) + if reqVersion := client.requestedAPIVersion.String(); reqVersion != "1.12" { + t.Errorf("Wrong requestAPIVersion. Want %q. Got %q.", "1.12", reqVersion) } if client.SkipServerVersionCheck { t.Error("Expected SkipServerVersionCheck to be false, got true") } } +func TestNewTLSVersionedClient(t *testing.T) { + certPath := "testing/data/cert.pem" + keyPath := "testing/data/key.pem" + caPath := "testing/data/ca.pem" + endpoint := "https://localhost:4243" + client, err := NewVersionnedTLSClient(endpoint, certPath, keyPath, caPath, "1.14") + if err != nil { + t.Fatal(err) + } + if client.endpoint != endpoint { + t.Errorf("Expected endpoint %s. Got %s.", endpoint, client.endpoint) + } + if reqVersion := client.requestedAPIVersion.String(); reqVersion != "1.14" { + t.Errorf("Wrong requestAPIVersion. Want %q. Got %q.", "1.14", reqVersion) + } + if client.SkipServerVersionCheck { + t.Error("Expected SkipServerVersionCheck to be false, got true") + } +} + +func TestNewTLSVersionedClientInvalidCA(t *testing.T) { + certPath := "testing/data/cert.pem" + keyPath := "testing/data/key.pem" + caPath := "testing/data/key.pem" + endpoint := "https://localhost:4243" + _, err := NewVersionnedTLSClient(endpoint, certPath, keyPath, caPath, "1.14") + if err == nil { + t.Errorf("Expected invalid ca at %s", caPath) + } +} + func TestNewClientInvalidEndpoint(t *testing.T) { cases := []string{ "htp://localhost:3243", "http://localhost:a", "localhost:8080", @@ -81,6 +136,28 @@ func TestNewClientInvalidEndpoint(t *testing.T) { } } +func TestNewTLSClient2376(t *testing.T) { + var tests = []struct { + endpoint string + expected string + }{ + {"tcp://localhost:2376", "https"}, + {"tcp://localhost:2375", "http"}, + {"tcp://localhost:4000", "http"}, + } + + for _, tt := range tests { + client, err := newTLSClient(tt.endpoint) + if err != nil { + t.Error(err) + } + got := client.endpointURL.Scheme + if got != tt.expected { + t.Errorf("endpointURL.Scheme: Got %s. Want %s.", got, tt.expected) + } + } +} + func TestGetURL(t *testing.T) { var tests = []struct { endpoint string @@ -129,6 +206,7 @@ func TestQueryString(t *testing.T) { {ListContainersOptions{All: true}, "all=1"}, {ListContainersOptions{Before: "something"}, "before=something"}, {ListContainersOptions{Before: "something", Since: "other"}, "before=something&since=other"}, + {ListContainersOptions{Filters: map[string][]string{"status": {"paused", "running"}}}, "filters=%7B%22status%22%3A%5B%22paused%22%2C%22running%22%5D%7D"}, {dumb{X: 10, Y: 10.35000}, "x=10&y=10.35"}, {dumb{W: v, X: 10, Y: 10.35000}, f32QueryString}, {dumb{X: 10, Y: 10.35000, Z: 10}, "x=10&y=10.35&zee=10"}, @@ -147,7 +225,7 @@ func TestQueryString(t *testing.T) { } } -func TestNewApiVersionFailures(t *testing.T) { +func TestNewAPIVersionFailures(t *testing.T) { var tests = []struct { input string expectedError string @@ -156,17 +234,17 @@ func TestNewApiVersionFailures(t *testing.T) { {"1.0-beta", `Unable to parse version "1.0-beta": "0-beta" is not an integer`}, } for _, tt := range tests { - v, err := NewApiVersion(tt.input) + v, err := NewAPIVersion(tt.input) if v != nil { t.Errorf("Expected version, got %v.", v) } if err.Error() != tt.expectedError { - t.Errorf("NewApiVersion(%q): wrong error. Want %q. Got %q", tt.input, tt.expectedError, err.Error()) + t.Errorf("NewAPIVersion(%q): wrong error. Want %q. Got %q", tt.input, tt.expectedError, err.Error()) } } } -func TestApiVersions(t *testing.T) { +func TestAPIVersions(t *testing.T) { var tests = []struct { a string b string @@ -192,8 +270,8 @@ func TestApiVersions(t *testing.T) { } for _, tt := range tests { - a, _ := NewApiVersion(tt.a) - b, _ := NewApiVersion(tt.b) + a, _ := NewAPIVersion(tt.a) + b, _ := NewAPIVersion(tt.b) if tt.expectedALessThanB && !a.LessThan(b) { t.Errorf("Expected %#v < %#v", a, b) 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 c649b5a2e0b..c600c84d797 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/container.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/container.go @@ -18,15 +18,17 @@ import ( // ListContainersOptions specify parameters to the ListContainers function. // -// See http://goo.gl/XqtcyU for more details. +// See http://goo.gl/6Y4Gz7 for more details. type ListContainersOptions struct { - All bool - Size bool - Limit int - Since string - Before string + All bool + Size bool + Limit int + Since string + Before string + Filters map[string][]string } +// APIPort is a type that represents a port mapping returned by the Docker API type APIPort struct { PrivatePort int64 `json:"PrivatePort,omitempty" yaml:"PrivatePort,omitempty"` PublicPort int64 `json:"PublicPort,omitempty" yaml:"PublicPort,omitempty"` @@ -51,7 +53,7 @@ type APIContainers struct { // ListContainers returns a slice of containers matching the given criteria. // -// See http://goo.gl/XqtcyU for more details. +// 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) @@ -88,8 +90,10 @@ func (p Port) Proto() string { type State struct { Running bool `json:"Running,omitempty" yaml:"Running,omitempty"` Paused bool `json:"Paused,omitempty" yaml:"Paused,omitempty"` + OOMKilled bool `json:"OOMKilled,omitempty" yaml:"OOMKilled,omitempty"` Pid int `json:"Pid,omitempty" yaml:"Pid,omitempty"` ExitCode int `json:"ExitCode,omitempty" yaml:"ExitCode,omitempty"` + Error string `json:"Error,omitempty" yaml:"Error,omitempty"` StartedAt time.Time `json:"StartedAt,omitempty" yaml:"StartedAt,omitempty"` FinishedAt time.Time `json:"FinishedAt,omitempty" yaml:"FinishedAt,omitempty"` } @@ -105,13 +109,18 @@ func (s *State) String() string { return fmt.Sprintf("Exit %d", s.ExitCode) } +// PortBinding represents the host/container port mapping as returned in the +// `docker inspect` json type PortBinding struct { - HostIp string `json:"HostIP,omitempty" yaml:"HostIP,omitempty"` + HostIP string `json:"HostIP,omitempty" yaml:"HostIP,omitempty"` HostPort string `json:"HostPort,omitempty" yaml:"HostPort,omitempty"` } +// PortMapping represents a deprecated field in the `docker inspect` output, +// and its value as found in NetworkSettings should always be nil type PortMapping map[string]string +// NetworkSettings contains network-related information about a container type NetworkSettings struct { IPAddress string `json:"IPAddress,omitempty" yaml:"IPAddress,omitempty"` IPPrefixLen int `json:"IPPrefixLen,omitempty" yaml:"IPPrefixLen,omitempty"` @@ -121,6 +130,8 @@ type NetworkSettings struct { Ports map[Port][]PortBinding `json:"Ports,omitempty" yaml:"Ports,omitempty"` } +// PortMappingAPI translates the port mappings as contained in NetworkSettings +// into the format in which they would appear when returned by the API func (settings *NetworkSettings) PortMappingAPI() []APIPort { var mapping []APIPort for port, bindings := range settings.Ports { @@ -139,7 +150,7 @@ func (settings *NetworkSettings) PortMappingAPI() []APIPort { PrivatePort: int64(p), PublicPort: int64(h), Type: port.Proto(), - IP: binding.HostIp, + IP: binding.HostIP, }) } } @@ -154,14 +165,17 @@ func parsePort(rawPort string) (int, error) { return int(port), nil } +// 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 +// given host. Those are contained in HostConfig type Config struct { Hostname string `json:"Hostname,omitempty" yaml:"Hostname,omitempty"` Domainname string `json:"Domainname,omitempty" yaml:"Domainname,omitempty"` User string `json:"User,omitempty" yaml:"User,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"` + CPUShares int64 `json:"CpuShares,omitempty" yaml:"CpuShares,omitempty"` + CPUSet string `json:"Cpuset,omitempty" yaml:"Cpuset,omitempty"` AttachStdin bool `json:"AttachStdin,omitempty" yaml:"AttachStdin,omitempty"` AttachStdout bool `json:"AttachStdout,omitempty" yaml:"AttachStdout,omitempty"` AttachStderr bool `json:"AttachStderr,omitempty" yaml:"AttachStderr,omitempty"` @@ -172,7 +186,7 @@ type Config struct { StdinOnce bool `json:"StdinOnce,omitempty" yaml:"StdinOnce,omitempty"` Env []string `json:"Env,omitempty" yaml:"Env,omitempty"` Cmd []string `json:"Cmd,omitempty" yaml:"Cmd,omitempty"` - Dns []string `json:"Dns,omitempty" yaml:"Dns,omitempty"` // For Docker API v1.9 and below only + 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"` VolumesFrom string `json:"VolumesFrom,omitempty" yaml:"VolumesFrom,omitempty"` @@ -181,6 +195,8 @@ type Config struct { NetworkDisabled bool `json:"NetworkDisabled,omitempty" yaml:"NetworkDisabled,omitempty"` } +// Container is the type encompasing everything about a container - its config, +// hostconfig, etc. type Container struct { ID string `json:"Id" yaml:"Id"` @@ -249,10 +265,11 @@ func (c *Client) ContainerChanges(id string) ([]Change, error) { // CreateContainerOptions specify parameters to the CreateContainer function. // -// See http://goo.gl/mErxNp for more details. +// See http://goo.gl/2xxQQK for more details. type CreateContainerOptions struct { - Name string - Config *Config `qs:"-"` + Name string + Config *Config `qs:"-"` + HostConfig *HostConfig } // CreateContainer creates a new container, returning the container instance, @@ -261,7 +278,14 @@ type CreateContainerOptions struct { // See http://goo.gl/mErxNp for more details. func (c *Client) CreateContainer(opts CreateContainerOptions) (*Container, error) { path := "/containers/create?" + queryString(opts) - body, status, err := c.do("POST", path, opts.Config) + body, status, err := c.do("POST", path, struct { + *Config + HostConfig *HostConfig `json:"HostConfig,omitempty" yaml:"HostConfig,omitempty"` + }{ + opts.Config, + opts.HostConfig, + }) + if status == http.StatusNotFound { return nil, ErrNoSuchImage } @@ -279,6 +303,8 @@ func (c *Client) CreateContainer(opts CreateContainerOptions) (*Container, error return &container, nil } +// KeyValuePair is a type for generic key/value pairs as used in the Lxc +// configuration type KeyValuePair struct { Key string `json:"Key,omitempty" yaml:"Key,omitempty"` Value string `json:"Value,omitempty" yaml:"Value,omitempty"` @@ -315,6 +341,8 @@ func NeverRestart() RestartPolicy { return RestartPolicy{Name: "no"} } +// 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"` @@ -325,8 +353,9 @@ type HostConfig struct { 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"` + 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"` RestartPolicy RestartPolicy `json:"RestartPolicy,omitempty" yaml:"RestartPolicy,omitempty"` 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 d80270ba024..bfb1119205f 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 @@ -102,6 +102,14 @@ func TestListContainersParams(t *testing.T) { ListContainersOptions{All: true, Limit: 10, Since: "adf9983", Before: "abdeef"}, map[string][]string{"all": {"1"}, "limit": {"10"}, "since": {"adf9983"}, "before": {"abdeef"}}, }, + { + ListContainersOptions{Filters: map[string][]string{"status": {"paused", "running"}}}, + map[string][]string{"filters": {"{\"status\":[\"paused\",\"running\"]}"}}, + }, + { + ListContainersOptions{All: true, Filters: map[string][]string{"exited": {"0"}, "status": {"exited"}}}, + map[string][]string{"all": {"1"}, "filters": {"{\"exited\":[\"0\"],\"status\":[\"exited\"]}"}}, + }, } fakeRT := &FakeRoundTripper{message: "[]", status: http.StatusOK} client := newTestClient(fakeRT) @@ -440,6 +448,27 @@ func TestCreateContainerImageNotFound(t *testing.T) { } } +func TestCreateContainerWithHostConfig(t *testing.T) { + fakeRT := &FakeRoundTripper{message: "{}", status: http.StatusOK} + client := newTestClient(fakeRT) + config := Config{} + hostConfig := HostConfig{PublishAllPorts: true} + opts := CreateContainerOptions{Name: "TestCreateContainerWithHostConfig", Config: &config, HostConfig: &hostConfig} + _, err := client.CreateContainer(opts) + if err != nil { + t.Fatal(err) + } + req := fakeRT.requests[0] + var gotBody map[string]interface{} + err = json.NewDecoder(req.Body).Decode(&gotBody) + if err != nil { + t.Fatal(err) + } + if _, ok := gotBody["HostConfig"]; !ok { + t.Errorf("CreateContainer: wrong body. HostConfig was not serialized") + } +} + func TestStartContainer(t *testing.T) { fakeRT := &FakeRoundTripper{message: "", status: http.StatusOK} client := newTestClient(fakeRT) @@ -1251,7 +1280,7 @@ func TestLogsNoContainer(t *testing.T) { } func TestNoSuchContainerError(t *testing.T) { - var err error = &NoSuchContainer{ID: "i345"} + var err = &NoSuchContainer{ID: "i345"} expected := "No such container: i345" if got := err.Error(); got != expected { t.Errorf("NoSuchContainer: wrong message. Want %q. Got %q.", expected, got) 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 262d4ee2554..7c055c5ebd9 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/event.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/event.go @@ -5,6 +5,7 @@ package docker import ( + "crypto/tls" "encoding/json" "errors" "fmt" @@ -49,6 +50,11 @@ var ( // ErrListenerAlreadyExists is the error returned when the listerner already // exists. ErrListenerAlreadyExists = errors.New("listener already exists for docker events") + + // EOFEvent is sent when the event listener receives an EOF error. + EOFEvent = &APIEvents{ + Status: "EOF", + } ) // AddEventListener adds a new listener to container events in the Docker API. @@ -111,6 +117,16 @@ func (eventState *eventMonitoringState) removeListener(listener chan<- *APIEvent return nil } +func (eventState *eventMonitoringState) closeListeners() { + eventState.Lock() + defer eventState.Unlock() + for _, l := range eventState.listeners { + close(l) + eventState.Add(-1) + } + eventState.listeners = nil +} + func listenerExists(a chan<- *APIEvents, list *[]chan<- *APIEvents) bool { for _, b := range *list { if b == a { @@ -152,7 +168,7 @@ func (eventState *eventMonitoringState) monitorEvents(c *Client) { time.Sleep(10 * time.Millisecond) } if err = eventState.connectWithRetry(c); err != nil { - eventState.terminate(err) + eventState.terminate() } for eventState.isEnabled() { timeout := time.After(100 * time.Millisecond) @@ -161,11 +177,16 @@ func (eventState *eventMonitoringState) monitorEvents(c *Client) { if !ok { return } + if ev == EOFEvent { + eventState.closeListeners() + eventState.terminate() + return + } go eventState.sendEvent(ev) go eventState.updateLastSeen(ev) case err = <-eventState.errC: if err == ErrNoListeners { - eventState.terminate(nil) + eventState.terminate() return } else if err != nil { defer func() { go eventState.monitorEvents(c) }() @@ -225,7 +246,7 @@ func (eventState *eventMonitoringState) updateLastSeen(e *APIEvents) { } } -func (eventState *eventMonitoringState) terminate(err error) { +func (eventState *eventMonitoringState) terminate() { eventState.disableEventMonitoring() } @@ -240,7 +261,13 @@ func (c *Client) eventHijack(startTime int64, eventChan chan *APIEvents, errChan protocol = "tcp" address = c.endpointURL.Host } - dial, err := net.Dial(protocol, address) + var dial net.Conn + var err error + if c.TLSConfig == nil { + dial, err = net.Dial(protocol, address) + } else { + dial, err = tls.Dial(protocol, address, c.TLSConfig) + } if err != nil { return err } @@ -261,6 +288,10 @@ func (c *Client) eventHijack(startTime int64, eventChan chan *APIEvents, errChan var event APIEvents if err = decoder.Decode(&event); err != nil { if err == io.EOF || err == io.ErrUnexpectedEOF { + if c.eventMonitor.isEnabled() { + // Signal that we're exiting. + eventChan <- EOFEvent + } break } errChan <- err @@ -271,7 +302,7 @@ func (c *Client) eventHijack(startTime int64, eventChan chan *APIEvents, errChan if !c.eventMonitor.isEnabled() { return } - c.eventMonitor.C <- &event + eventChan <- &event } }(res, conn) return nil diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/event_test.go b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/event_test.go index cb54f4ae925..0de047487d4 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/event_test.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/event_test.go @@ -6,7 +6,10 @@ package docker import ( "bufio" + "crypto/tls" + "crypto/x509" "fmt" + "io/ioutil" "net/http" "net/http/httptest" "strings" @@ -15,6 +18,39 @@ import ( ) func TestEventListeners(t *testing.T) { + testEventListeners("TestEventListeners", t, httptest.NewServer, NewClient) +} + +func TestTLSEventListeners(t *testing.T) { + testEventListeners("TestTLSEventListeners", t, func(handler http.Handler) *httptest.Server { + server := httptest.NewUnstartedServer(handler) + + cert, err := tls.LoadX509KeyPair("testing/data/server.pem", "testing/data/serverkey.pem") + if err != nil { + t.Fatalf("Error loading server key pair: %s", err) + } + + caCert, err := ioutil.ReadFile("testing/data/ca.pem") + if err != nil { + t.Fatalf("Error loading ca certificate: %s", err) + } + caPool := x509.NewCertPool() + if !caPool.AppendCertsFromPEM(caCert) { + t.Fatalf("Could not add ca certificate") + } + + server.TLS = &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: caPool, + } + server.StartTLS() + return server + }, func(url string) (*Client, error) { + return NewTLSClient(url, "testing/data/cert.pem", "testing/data/key.pem", "testing/data/ca.pem") + }) +} + +func testEventListeners(testName string, t *testing.T, buildServer func(http.Handler) *httptest.Server, buildClient func(string) (*Client, error)) { response := `{"status":"create","id":"dfdf82bd3881","from":"base:latest","time":1374067924} {"status":"start","id":"dfdf82bd3881","from":"base:latest","time":1374067924} {"status":"stop","id":"dfdf82bd3881","from":"base:latest","time":1374067966} @@ -22,7 +58,7 @@ func TestEventListeners(t *testing.T) { ` var req http.Request - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + server := buildServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { rsc := bufio.NewScanner(strings.NewReader(response)) for rsc.Scan() { w.Write([]byte(rsc.Text())) @@ -33,7 +69,7 @@ func TestEventListeners(t *testing.T) { })) defer server.Close() - client, err := NewClient(server.URL) + client, err := buildClient(server.URL) if err != nil { t.Errorf("Failed to create client: %s", err) } @@ -53,7 +89,7 @@ func TestEventListeners(t *testing.T) { for { select { case msg := <-listener: - t.Logf("Recieved: %s", *msg) + t.Logf("Received: %s", *msg) count++ err = checkEvent(count, msg) if err != nil { @@ -63,7 +99,7 @@ func TestEventListeners(t *testing.T) { return } case <-timeout: - t.Fatal("TestAddEventListener timed out waiting on events") + t.Fatalf("%s timed out waiting on events", testName) } } } 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 62a0a80cd6e..9ce7b440d30 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/exec.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/exec.go @@ -50,8 +50,10 @@ 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"` + ID string `json:"Id,omitempty" yaml:"Id,omitempty"` } // CreateExec sets up an exec instance in a running container `id`, returning the exec @@ -76,9 +78,9 @@ func (c *Client) CreateExec(opts CreateExecOptions) (*Exec, error) { return &exec, nil } -// Starts a previously set up exec instance id. If opts.Detach is true, it returns -// after starting the exec command. Otherwise, it sets up an interactive session -// with the exec command. +// 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 func (c *Client) StartExec(id string, opts StartExecOptions) error { @@ -102,8 +104,9 @@ func (c *Client) StartExec(id string, opts StartExecOptions) error { return c.hijack("POST", path, opts.Success, opts.RawTerminal, opts.InputStream, opts.ErrorStream, opts.OutputStream, opts) } -// Resizes the tty session used by the exec command id. This API is valid only -// if Tty was specified as part of creating and starting the exec command. +// ResizeExecTTY resizes the tty session used by the exec command id. This API +// is valid only if Tty was specified as part of creating and starting the exec +// command. // // See http://goo.gl/YDSx1f for more details func (c *Client) ResizeExecTTY(id string, height, width int) error { 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 70fa64b4cee..31de1627c48 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 @@ -16,7 +16,7 @@ import ( func TestExecCreate(t *testing.T) { jsonContainer := `{"Id": "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2"}` - var expected struct{ Id string } + var expected struct{ ID string } err := json.Unmarshal([]byte(jsonContainer), &expected) if err != nil { t.Fatal(err) @@ -35,9 +35,9 @@ func TestExecCreate(t *testing.T) { if err != nil { t.Fatal(err) } - expectedId := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2" - if execObj.Id != expectedId { - t.Errorf("ExecCreate: wrong ID. Want %q. Got %q.", expectedId, execObj.Id) + expectedID := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2" + if execObj.ID != expectedID { + t.Errorf("ExecCreate: wrong ID. Want %q. Got %q.", expectedID, execObj.ID) } req := fakeRT.requests[0] if req.Method != "POST" { @@ -47,7 +47,7 @@ func TestExecCreate(t *testing.T) { if gotPath := req.URL.Path; gotPath != expectedURL.Path { t.Errorf("ExecCreate: Wrong path in request. Want %q. Got %q.", expectedURL.Path, gotPath) } - var gotBody struct{ Id string } + var gotBody struct{ ID string } err = json.NewDecoder(req.Body).Decode(&gotBody) if err != nil { t.Fatal(err) @@ -55,13 +55,13 @@ func TestExecCreate(t *testing.T) { } func TestExecStartDetached(t *testing.T) { - execId := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2" + execID := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2" fakeRT := &FakeRoundTripper{status: http.StatusOK} client := newTestClient(fakeRT) config := StartExecOptions{ Detach: true, } - err := client.StartExec(execId, config) + err := client.StartExec(execID, config) if err != nil { t.Fatal(err) } @@ -69,7 +69,7 @@ func TestExecStartDetached(t *testing.T) { if req.Method != "POST" { t.Errorf("ExecStart: wrong HTTP method. Want %q. Got %q.", "POST", req.Method) } - expectedURL, _ := url.Parse(client.getURL("/exec/" + execId + "/start")) + expectedURL, _ := url.Parse(client.getURL("/exec/" + execID + "/start")) if gotPath := req.URL.Path; gotPath != expectedURL.Path { t.Errorf("ExecCreate: Wrong path in request. Want %q. Got %q.", expectedURL.Path, gotPath) } @@ -97,7 +97,7 @@ func TestExecStartAndAttach(t *testing.T) { client.SkipServerVersionCheck = true var stdout, stderr bytes.Buffer success := make(chan struct{}) - execId := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2" + execID := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2" opts := StartExecOptions{ OutputStream: &stdout, ErrorStream: &stderr, @@ -105,15 +105,15 @@ func TestExecStartAndAttach(t *testing.T) { RawTerminal: true, Success: success, } - go client.StartExec(execId, opts) + go client.StartExec(execID, opts) <-success } func TestExecResize(t *testing.T) { - execId := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2" + execID := "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2" fakeRT := &FakeRoundTripper{status: http.StatusOK} client := newTestClient(fakeRT) - err := client.ResizeExecTTY(execId, 10, 20) + err := client.ResizeExecTTY(execID, 10, 20) if err != nil { t.Fatal(err) } @@ -121,7 +121,7 @@ func TestExecResize(t *testing.T) { if req.Method != "POST" { t.Errorf("ExecStart: wrong HTTP method. Want %q. Got %q.", "POST", req.Method) } - expectedURL, _ := url.Parse(client.getURL("/exec/" + execId + "/resize?h=10&w=20")) + expectedURL, _ := url.Parse(client.getURL("/exec/" + execID + "/resize?h=10&w=20")) if gotPath := req.URL.RequestURI(); gotPath != expectedURL.RequestURI() { t.Errorf("ExecCreate: Wrong path in request. Want %q. Got %q.", expectedURL.Path, gotPath) } diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/image.go b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/image.go index e1fbb234699..3d55155ff78 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/image.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/image.go @@ -25,9 +25,10 @@ type APIImages struct { Created int64 `json:"Created,omitempty" yaml:"Created,omitempty"` Size int64 `json:"Size,omitempty" yaml:"Size,omitempty"` VirtualSize int64 `json:"VirtualSize,omitempty" yaml:"VirtualSize,omitempty"` - ParentId string `json:"ParentId,omitempty" yaml:"ParentId,omitempty"` + ParentID string `json:"ParentId,omitempty" yaml:"ParentId,omitempty"` } +// Image is the type representing a docker image and its various properties type Image struct { ID string `json:"Id" yaml:"Id"` Parent string `json:"Parent,omitempty" yaml:"Parent,omitempty"` @@ -52,6 +53,8 @@ type ImageHistory struct { 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 { ID string `json:"id"` Parent string `json:"parent,omitempty"` @@ -66,6 +69,14 @@ type ImagePre012 struct { Size int64 `json:"size,omitempty"` } +// ListImagesOptions specify parameters to the ListImages function. +// +// See http://goo.gl/2rOLFF for more details. +type ListImagesOptions struct { + All bool + Filters map[string][]string +} + var ( // ErrNoSuchImage is the error returned when the image does not exist. ErrNoSuchImage = errors.New("no such image") @@ -77,18 +88,17 @@ var ( // ErrMissingOutputStream is the error returned when no output stream // is provided to some calls, like BuildImage. ErrMissingOutputStream = errors.New("missing output stream") + + // ErrMultipleContexts is the error returned when both a ContextDir and + // InputStream are provided in BuildImageOptions + ErrMultipleContexts = errors.New("image build may not be provided BOTH context dir and input stream") ) // ListImages returns the list of available images in the server. // -// See http://goo.gl/VmcR6v for more details. -func (c *Client) ListImages(all bool) ([]APIImages, error) { - path := "/images/json?all=" - if all { - path += "1" - } else { - path += "0" - } +// 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) if err != nil { return nil, err @@ -146,7 +156,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(apiVersion_1_12) { + if c.SkipServerVersionCheck || c.expectedAPIVersion.GreaterThanOrEqualTo(apiVersion1_12) { err = json.Unmarshal(body, &image) if err != nil { return nil, err @@ -194,9 +204,16 @@ type PushImageOptions struct { // 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"` + 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. @@ -212,12 +229,7 @@ func (c *Client) PushImage(opts PushImageOptions, auth AuthConfiguration) error name := opts.Name opts.Name = "" path := "/images/" + name + "/push?" + queryString(&opts) - var headers = make(map[string]string) - var buf bytes.Buffer - json.NewEncoder(&buf).Encode(auth) - - headers["X-Registry-Auth"] = base64.URLEncoding.EncodeToString(buf.Bytes()) - + headers := headersWithAuth(auth) return c.stream("POST", path, true, opts.RawJSONStream, headers, nil, opts.OutputStream, nil) } @@ -241,11 +253,7 @@ func (c *Client) PullImage(opts PullImageOptions, auth AuthConfiguration) error return ErrNoSuchImage } - var headers = make(map[string]string) - var buf bytes.Buffer - json.NewEncoder(&buf).Encode(auth) - headers["X-Registry-Auth"] = base64.URLEncoding.EncodeToString(buf.Bytes()) - + headers := headersWithAuth(auth) return c.createImage(queryString(&opts), headers, nil, opts.OutputStream, opts.RawJSONStream) } @@ -324,15 +332,18 @@ func (c *Client) ImportImage(opts ImportImageOptions) error { // For more details about the Docker building process, see // http://goo.gl/tlPXPu. type BuildImageOptions struct { - Name string `qs:"t"` - NoCache bool `qs:"nocache"` - SuppressOutput bool `qs:"q"` - RmTmpContainer bool `qs:"rm"` - ForceRmTmpContainer bool `qs:"forcerm"` - InputStream io.Reader `qs:"-"` - OutputStream io.Writer `qs:"-"` - RawJSONStream bool `qs:"-"` - Remote string `qs:"remote"` + Name string `qs:"t"` + NoCache bool `qs:"nocache"` + SuppressOutput bool `qs:"q"` + RmTmpContainer bool `qs:"rm"` + ForceRmTmpContainer bool `qs:"forcerm"` + InputStream io.Reader `qs:"-"` + OutputStream io.Writer `qs:"-"` + RawJSONStream bool `qs:"-"` + Remote string `qs:"remote"` + Auth AuthConfiguration `qs:"-"` // for older docker X-Registry-Auth header + AuthConfigs AuthConfigurations `qs:"-"` // for newer docker X-Registry-Config header + ContextDir string `qs:"-"` } // BuildImage builds an image from a tarball's url or a Dockerfile in the input @@ -343,15 +354,26 @@ func (c *Client) BuildImage(opts BuildImageOptions) error { if opts.OutputStream == nil { return ErrMissingOutputStream } - var headers map[string]string + var headers = headersWithAuth(opts.Auth, opts.AuthConfigs) + if opts.Remote != "" && opts.Name == "" { opts.Name = opts.Remote } - if opts.InputStream != nil { - headers = map[string]string{"Content-Type": "application/tar"} + if opts.InputStream != nil || opts.ContextDir != "" { + headers["Content-Type"] = "application/tar" } else if opts.Remote == "" { return ErrMissingRepo } + if opts.ContextDir != "" { + if opts.InputStream != nil { + return ErrMultipleContexts + } + var err error + if opts.InputStream, err = createTarStream(opts.ContextDir); err != nil { + return err + } + } + return c.stream("POST", fmt.Sprintf("/build?%s", queryString(&opts)), true, opts.RawJSONStream, headers, opts.InputStream, opts.OutputStream, nil) } @@ -389,6 +411,25 @@ func isURL(u string) bool { return p.Scheme == "http" || p.Scheme == "https" } +func headersWithAuth(auths ...interface{}) map[string]string { + var headers = make(map[string]string) + + for _, auth := range auths { + switch auth.(type) { + case AuthConfiguration: + var buf bytes.Buffer + json.NewEncoder(&buf).Encode(auth) + headers["X-Registry-Auth"] = base64.URLEncoding.EncodeToString(buf.Bytes()) + case AuthConfigurations: + var buf bytes.Buffer + json.NewEncoder(&buf).Encode(auth) + headers["X-Registry-Config"] = base64.URLEncoding.EncodeToString(buf.Bytes()) + } + } + + return headers +} + // APIImageSearch reflect the result of a search on the dockerHub // // See http://goo.gl/xI5lLZ for more details. 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 5a7b2de7d40..11776e8c4d1 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 @@ -88,7 +88,7 @@ func TestListImages(t *testing.T) { t.Fatal(err) } client := newTestClient(&FakeRoundTripper{message: body, status: http.StatusOK}) - images, err := client.ListImages(false) + images, err := client.ListImages(ListImagesOptions{}) if err != nil { t.Error(err) } @@ -100,25 +100,42 @@ func TestListImages(t *testing.T) { func TestListImagesParameters(t *testing.T) { fakeRT := &FakeRoundTripper{message: "null", status: http.StatusOK} client := newTestClient(fakeRT) - _, err := client.ListImages(false) + _, err := client.ListImages(ListImagesOptions{All: false}) if err != nil { t.Fatal(err) } req := fakeRT.requests[0] if req.Method != "GET" { - t.Errorf("ListImages(false: Wrong HTTP method. Want GET. Got %s.", req.Method) + t.Errorf("ListImages({All: false}: Wrong HTTP method. Want GET. Got %s.", req.Method) } - if all := req.URL.Query().Get("all"); all != "0" { - t.Errorf("ListImages(false): Wrong parameter. Want all=0. Got all=%s", all) + if all := req.URL.Query().Get("all"); all != "0" && all != "" { + t.Errorf("ListImages({All: false}): Wrong parameter. Want all=0 or not present at all. Got all=%s", all) } fakeRT.Reset() - _, err = client.ListImages(true) + _, err = client.ListImages(ListImagesOptions{All: true}) if err != nil { t.Fatal(err) } req = fakeRT.requests[0] if all := req.URL.Query().Get("all"); all != "1" { - t.Errorf("ListImages(true): Wrong parameter. Want all=1. Got all=%s", all) + t.Errorf("ListImages({All: true}): Wrong parameter. Want all=1. Got all=%s", all) + } + fakeRT.Reset() + _, err = client.ListImages(ListImagesOptions{Filters: map[string][]string{ + "dangling": {"true"}, + }}) + if err != nil { + t.Fatal(err) + } + req = fakeRT.requests[0] + body := req.URL.Query().Get("filters") + var filters map[string][]string + err = json.Unmarshal([]byte(body), &filters) + if err != nil { + t.Fatal(err) + } + if len(filters["dangling"]) != 1 || filters["dangling"][0] != "true" { + t.Errorf("ListImages(dangling=[true]): Wrong filter map. Want dangling=[true], got dangling=%v", filters["dangling"]) } } diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/tar.go b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/tar.go new file mode 100644 index 00000000000..7c4a2043d9e --- /dev/null +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/tar.go @@ -0,0 +1,99 @@ +// Copyright 2014 go-dockerclient authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package docker + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "path" + "path/filepath" + "strings" + + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/fileutils" +) + +func createTarStream(srcPath string) (io.ReadCloser, error) { + excludes, err := parseDockerignore(srcPath) + if err != nil { + return nil, err + } + + if err := validateContextDirectory(srcPath, excludes); err != nil { + return nil, err + } + tarOpts := &archive.TarOptions{ + Excludes: excludes, + Compression: archive.Uncompressed, + NoLchown: true, + } + return archive.TarWithOptions(srcPath, tarOpts) +} + +// validateContextDirectory checks if all the contents of the directory +// can be read and returns an error if some files can't be read. +// Symlinks which point to non-existing files don't trigger an error +func validateContextDirectory(srcPath string, excludes []string) error { + return filepath.Walk(filepath.Join(srcPath, "."), func(filePath string, f os.FileInfo, err error) error { + // skip this directory/file if it's not in the path, it won't get added to the context + if relFilePath, err := filepath.Rel(srcPath, filePath); err != nil { + return err + } else if skip, err := fileutils.Matches(relFilePath, excludes); err != nil { + return err + } else if skip { + if f.IsDir() { + return filepath.SkipDir + } + return nil + } + + if err != nil { + if os.IsPermission(err) { + return fmt.Errorf("can't stat '%s'", filePath) + } + if os.IsNotExist(err) { + return nil + } + return err + } + + // skip checking if symlinks point to non-existing files, such symlinks can be useful + // also skip named pipes, because they hanging on open + if f.Mode()&(os.ModeSymlink|os.ModeNamedPipe) != 0 { + return nil + } + + if !f.IsDir() { + currentFile, err := os.Open(filePath) + if err != nil && os.IsPermission(err) { + return fmt.Errorf("no permission to read from '%s'", filePath) + } + currentFile.Close() + } + return nil + }) +} + +func parseDockerignore(root string) ([]string, error) { + var excludes []string + ignore, err := ioutil.ReadFile(path.Join(root, ".dockerignore")) + if err != nil && !os.IsNotExist(err) { + return excludes, fmt.Errorf("error reading .dockerignore: '%s'", err) + } + for _, pattern := range strings.Split(string(ignore), "\n") { + matches, err := filepath.Match(pattern, "Dockerfile") + if err != nil { + return excludes, fmt.Errorf("bad .dockerignore pattern: '%s', error: %s", pattern, err) + } + if matches { + return excludes, fmt.Errorf("dockerfile was excluded by .dockerignore pattern '%s'", pattern) + } + excludes = append(excludes, pattern) + } + + return excludes, nil +} diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/bin/fmtpolice b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/bin/fmtpolice new file mode 100644 index 00000000000..d13bd0c1888 --- /dev/null +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/bin/fmtpolice @@ -0,0 +1,38 @@ +#!/bin/bash + +readonly GOPATH="${GOPATH%%:*}" + +main() { + check_fmt + check_lint +} + +check_fmt() { + eval "set -e" + for file in $(git ls-files '*.go') ; do + gofmt $file | diff -u $file - + done + eval "set +e" +} + +check_lint() { + _install_linter + + for file in $(git ls-files '*.go') ; do + if [[ ! "$(${GOPATH}/bin/golint $file)" =~ ^[[:blank:]]*$ ]] ; then + _lint_verbose && exit 1 + fi + done +} + +_lint_verbose() { + for file in $(git ls-files '*.go') ; do $GOPATH/bin/golint $file ; done +} + +_install_linter() { + if [[ ! -x "${GOPATH}/bin/golint" ]] ; then + go get -u github.com/golang/lint/golint + fi +} + +main "$@" diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/data/.dockerignore b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/data/.dockerignore new file mode 100644 index 00000000000..027e8c20e61 --- /dev/null +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/data/.dockerignore @@ -0,0 +1,3 @@ +container.tar +dockerfile.tar +foofile diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/data/barfile b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/data/barfile new file mode 100644 index 00000000000..e69de29bb2d diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/data/ca.pem b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/data/ca.pem new file mode 100644 index 00000000000..8e38bba13c6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/data/ca.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC1TCCAb+gAwIBAgIQJ9MsNxrUxumNbAytGi3GEDALBgkqhkiG9w0BAQswFjEU +MBIGA1UEChMLQm9vdDJEb2NrZXIwHhcNMTQxMDE2MjAyMTM4WhcNMTcwOTMwMjAy +MTM4WjAWMRQwEgYDVQQKEwtCb290MkRvY2tlcjCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBALpFCSARjG+5yXoqr7UMzuE0df7RRZfeRZI06lJ02ZqV4Iii +rgL7ML9yPxX50NbLnjiilSDTUhnyocYFItokzUzz8qpX/nlYhuN2Iqwh4d0aWS8z +f5y248F+H1z+HY2W8NPl/6DVlVwYaNW1/k+RPMlHS0INLR6j+3Ievew7RNE0NnM2 +znELW6NetekDt3GUcz0Z95vDUDfdPnIk1eIFMmYvLxZh23xOca4Q37a3S8F3d+dN ++OOpwjdgY9Qme0NQUaXpgp58jWuQfB8q7mZrdnLlLqRa8gx1HeDSotX7UmWtWPkb +vd9EdlKLYw5PVpxMV1rkwf2t4TdgD5NfkpXlXkkCAwEAAaMjMCEwDgYDVR0PAQH/ +BAQDAgCkMA8GA1UdEwEB/wQFMAMBAf8wCwYJKoZIhvcNAQELA4IBAQBxYjHVSKqE +MJw7CW0GddesULtXXVWGJuZdWJLQlPvPMfIfjIvlcZyS4cdVNiQ3sREFIZz8TpII +CT0/Pg3sgv/FcOQe1CN0xZYZcyiAZHK1z0fJQq2qVpdv7+tJcjI2vvU6NI24iQCo +W1wz25trJz9QbdB2MRLMjyz7TSWuafztIvcfEzaIdQ0Whqund/cSuPGQx5IwF83F +rvlkOyJSH2+VIEBTCIuykJeL0DLTt8cePBQR5L1ISXb4RUMK9ZtqRscBRv8sn7o2 +ixG3wtL0gYF4xLtsQWVxI3iFVrU3WzOH/3c5shVRkWBd+AQRSwCJI4mKH7penJCF +i3/zzlkvOnjV +-----END CERTIFICATE----- diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/data/cert.pem b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/data/cert.pem new file mode 100644 index 00000000000..5e7244b24ff --- /dev/null +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/data/cert.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC6DCCAdKgAwIBAgIRANO6ymxQAjp66KmEka1G6b0wCwYJKoZIhvcNAQELMBYx +FDASBgNVBAoTC0Jvb3QyRG9ja2VyMB4XDTE0MTAxNjIwMjE1MloXDTE3MDkzMDIw +MjE1MlowFjEUMBIGA1UEChMLQm9vdDJEb2NrZXIwggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDGA1mAhSOpZspD1dpZ7qVEQrIJw4Xo8252jHaORnEdDiFm +b6brEmr6jw8t4P3IGxbqBc/TqRV+SSXxwYEVvfpeQKH+SmqStoMNtD3Ura161az4 +V0BcxMtSlsUGpoz+//QCAq8qiaxMwgiyc5253mkQm88anj2cNt7xbewiu/KFWuf7 +BVpNK1+ltpJmlukfcj/G+I1bw7j1KxBjDrFqe5cyDuuZcDL2tmUXP/ZWDyXwSv+H +AOckqn44z6aXlBkVvOXDBZJqY76d/vWVDNCuZeXRnqlhP3t1kH4V0RQXo+JD2tgt +JgdU0unzyoFOSWNUBPm73tqmjUGGAmGHBmeegJr/AgMBAAGjNTAzMA4GA1UdDwEB +/wQEAwIAgDATBgNVHSUEDDAKBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMAsGCSqG +SIb3DQEBCwOCAQEABVTWl5SmBP+j5He5bQsgnIXjviSKqe40/10V4LJAOmilycRF +zLrzM+YMwfjg6PLIs8CldAMWHw9y9ktZY4MxkgCktaiaN/QmMTMwFWEcN4wy5IpM +U5l93eAg7xsnY430h3QBBADujX4wdF3fs8rSL8zAAQFL0ihurwU124K3yXKsrwpb +CiVUGfIN4sPwjy8Ws9oxHFDC9/P8lgjHZ1nBIf8KSHnMzlxDGj7isQfhtH+7mcCL +cM1qO2NirS2v7uaEPPY+MJstAz+W7EJCW9dfMSmHna2SDC37Xkin7uEY9z+qaKFL +8d/XxOB/L8Ucy8VZhdsv0dsBq5KfJntITM0ksQ== +-----END CERTIFICATE----- diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/data/foofile b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/data/foofile new file mode 100644 index 00000000000..e69de29bb2d diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/data/key.pem b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/data/key.pem new file mode 100644 index 00000000000..a9346bcf45a --- /dev/null +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/data/key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAxgNZgIUjqWbKQ9XaWe6lREKyCcOF6PNudox2jkZxHQ4hZm+m +6xJq+o8PLeD9yBsW6gXP06kVfkkl8cGBFb36XkCh/kpqkraDDbQ91K2tetWs+FdA +XMTLUpbFBqaM/v/0AgKvKomsTMIIsnOdud5pEJvPGp49nDbe8W3sIrvyhVrn+wVa +TStfpbaSZpbpH3I/xviNW8O49SsQYw6xanuXMg7rmXAy9rZlFz/2Vg8l8Er/hwDn +JKp+OM+ml5QZFbzlwwWSamO+nf71lQzQrmXl0Z6pYT97dZB+FdEUF6PiQ9rYLSYH +VNLp88qBTkljVAT5u97apo1BhgJhhwZnnoCa/wIDAQABAoIBAQCaGy9EC9pmU95l +DwGh7k5nIrUnTilg1FwLHWSDdCVCZKXv8ENrPelOWZqJrUo1u4eI2L8XTsewgkNq +tJu/DRzWz9yDaO0qg6rZNobMh+K076lvmZA44twOydJLS8H+D7ua+PXU2FLlZjmY +kMyXRJZmW6zCXZc7haTbJx6ZJccoquk/DkS4FcFurJP177u1YrWS9TTw9kensUtU +jQ63uf56UTN1i+0+Rxl7OW1TZlqwlri5I4njg5249+FxwwHzIq8+l7zD7K9pl8c/ +nG1HuulvU2bVlDlRdyslMPAH34vw9Sku1BD8furrJLr1na5lRSLKJODEaIPEsLwv +CdEUwP9JAoGBAO76ZW80RyNB2fA+wbTq70Sr8CwrXxYemXrez5LKDC7SsohKFCPE +IedpO/n+nmymiiJvMm874EExoG6BVrbkWkeb+2vinEfOQNlDMsDx7WLjPekP3t6i +rXHO3CjFooVFq2z3mZa/Nc5NZqu8fNWNCKJxZDJphdoj6sORNJIUvZVjAoGBANQd +++J+ITcu3/+A6JrGcgLunBFQYPqkiItk0J4QKYKuX5ik9rWcQDN8TTtfW2mDuiQ4 +NrCwuVPq1V1kB16JzH017SsYLo9g8I20YjnBZge9pKTeUaLVTb3C50LW8FBylop0 +Bnm597dNbtSjphjoTMg0XyC19o3Esf2YeWG0QNS1AoGAWWDfFRNJU99qIldmXULM +0DM6NVrXSk+ReYnhunXEzrJQwXZrR+EwCPurydk36Uz0NuK9yypquhdUeF/5TZfk +SAoHo5byekyipl9imRUigqyY2BTudvgCxKDoaHtaSFwBPFTyZZYICquaLbrmOXxw +8UhVgCFFRYvPXuts7QHC0h8CgYBWEvy9gfU0kV7wLX02IUTuj6jhFb7ktpN6DSTi +nyhZES1VoctDEu6ydcRZTW6ouH12aSE4Pd5WgTqntQmQgVZrkNB25k8ue2Xh+srJ +KQOgLIJ9LIHwE6KCWG7DnrjRzE3uTPq7to0g4tkQjH/AJ7PQof/gJDayfJjFkXPg +A+cy6QKBgEPbKpiqscm03gT2QanBut5pg4dqPOxp0SlErA3kSFNTRK3oYBQPC+LH +qA5nD5brdkeNBB58Rll8Zpzxiff50bcvLP/7/Sb3NjaXFTEY0gVbdRof3n6N0YP3 +Hu5XDNJ9RNkNzE5RIG1g86KE+aKlcrKMaigqAiuIy2PSnjkQeGk8 +-----END RSA PRIVATE KEY----- diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/data/server.pem b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/data/server.pem new file mode 100644 index 00000000000..89cc445e1ba --- /dev/null +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/data/server.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC/DCCAeagAwIBAgIQMUILcXtvmSOK63zEBo0VXzALBgkqhkiG9w0BAQswFjEU +MBIGA1UEChMLQm9vdDJEb2NrZXIwHhcNMTQxMDE2MjAyMTQ2WhcNMTcwOTMwMjAy +MTQ2WjAWMRQwEgYDVQQKEwtCb290MkRvY2tlcjCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBANxUOUhNnqFnrTlLsBYzfFRZWQo268l+4K4lOJCVbfDonP3g +Mz0vGi9fcyFqEWSA8Y+ShXna625HTnReCwFdsu0861qCIq7v95hFFCyOe0iIxpd0 +AKLnl90d+1vonE7andgFgoobbTiMly4UK4H6z8D148fFNIihoteOG3PIF89TFxP7 +CJ/3wXnx/IKpdlO8PAnub3tBPJHvGDj7KORLy4IBxRX5VBAdfGNybE66fcrehEva +rLA4m9pgiaR/Nnr9FdKhPyqYdjflLNvzydxNvMIV4M0hFlhXmYvpMjA5/XsTnsyV +t9JHJa5Upwqsbne08t7rsm7liZNxZlko8xPOTQcCAwEAAaNKMEgwDgYDVR0PAQH/ +BAQDAgCgMAwGA1UdEwEB/wQCMAAwKAYDVR0RBCEwH4ILYm9vdDJkb2NrZXKHBH8A +AAGHBAoAAg+HBMCoO2cwCwYJKoZIhvcNAQELA4IBAQAYoYcDkDWkl73FZ0WnPmAj +LiF7HU95Qg3KyEpFsAJeShSLPPbQntmwhdekEzY4tQ3eKQB/+zHFjzsCr/lmDUmH +Ea/ryQ17C+jyH+Ykg0IWW6L6veZhvRDg6Z9focVtPVBRxPTqC/Qhb54blWRASV+W +UreMuXQ5+1dQptAM7ixOeLVHjBi/bd9TL3jvwBVCr9QedteMjjK4TCF9Tbcou+MF +2w3OJJZMDhcD+YwoK9uJDqlKmcTm/vVMbSsp/pTMcnQ7jxCeR8/XyX+VwTZwaHAa +o92Q/eg3THAiWhvyT/SzyH9dHHBAyXynUwGCggKawHktfvW4QXRPuLxLrJ7iB5cy +-----END CERTIFICATE----- diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/data/serverkey.pem b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/data/serverkey.pem new file mode 100644 index 00000000000..c897e5da550 --- /dev/null +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/data/serverkey.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEoAIBAAKCAQEA3FQ5SE2eoWetOUuwFjN8VFlZCjbryX7griU4kJVt8Oic/eAz +PS8aL19zIWoRZIDxj5KFedrrbkdOdF4LAV2y7TzrWoIiru/3mEUULI57SIjGl3QA +oueX3R37W+icTtqd2AWCihttOIyXLhQrgfrPwPXjx8U0iKGi144bc8gXz1MXE/sI +n/fBefH8gql2U7w8Ce5ve0E8ke8YOPso5EvLggHFFflUEB18Y3JsTrp9yt6ES9qs +sDib2mCJpH82ev0V0qE/Kph2N+Us2/PJ3E28whXgzSEWWFeZi+kyMDn9exOezJW3 +0kclrlSnCqxud7Ty3uuybuWJk3FmWSjzE85NBwIDAQABAoIBAG0ak+cW8LeShHf7 +3+2Of0GxoOLrAWWdG5uAuPr31CJYve0FybnBimDtDjD8ujIfm/7xmoEWBEFutA3x +x9dcU88gvJbsHEqub9gKVQwfXjMz78tt2SbSMiR/xUnk7QorPcCMMfE71aEMFYzu +1gCed6Rg3vO81t/V0rKVH0j9S7UQz5v/oX15eVDV5LOqyCHwAi6K0eXXbqnbI0TH +SOQ/nexM2msVXWbO9t6ra6f5V7FXziDK5Xi+rPxRbX9mkrDzxDAevfuRqYBx5vtL +W2Q2hKjUAHFgXFniNSZBS7dCdAtz0el/3ct+cNmpuTMhhs7M6wC1CuYiZ/DxLiFh +Si73VckCgYEA+/ceh3+VjtQ0rgEw8sD9bqYEA8IaBiObjneIoFnKBYRG7yZd8JMm +HD4M/aQ1qhcRLPN7GR03YQULgQJURbKSjJHnhfTXHyeHC3NN4gMVHQXewu2MHCh6 +7FCQ9CfK0KcYLgegVVvL3PrF3hyWGnmTu+G0UkDQRYVnaNrB7snrW6UCgYEA39tq ++MCQdu0moJ5szSZf02undg9EeW6isk9qzi7TId3/MLci2eH7PEnipipPUK3+DERq +aba0y0TKgBR2EXvXLFJA/+kfdo2loIEHOfox85HVfxgUaFRti63ZI0uF8D0QT2Yy +oJal+RFghVoSnv4LjhRKEPbIkScTXGjdK+7wFjsCfz79iKRXQQx0ALd/lL0bgkAn +QNmvrNHcFQeI2p8700WNzC39aX67SsvEt3qxkrjzC1gxhpTAuReIK1gVPPwvqHN8 +BmV20FD5kMlMCix2mNCopwgUWvKvLAvoGFTxncKMA39+aJbuXAjiqJTekKgNvOE7 +i9kEWw0GTNPp3JHV6QECgYAPwb0M11kT1euDIMOdyRazpf86kyaJuZzgGjD1ZFxe +JOcigbGFTp/FhZnbglzk2+pm6KXo3QBq0mPCki4hWusxZnTGzpz1VlETNCHTFeZQ +M7KoaIR/N3oie9Et59H8r/+m5xWnMhNqratyl316DX24uXrhKM3DUdHODl+LCR2D +IwKBgE1MbHuwolUPEw3HeO4R7NMFVTFei7E/fpUsimPfArGg8UydwvloNT1myJos +N2JzfGGjN2KPVcBk9fOs71mJ6VcK3C3g5JIccplk6h9VNaw55+zdQvKPTzoBoTvy +A+Fwx2AlF61KeRF87DL2YTRJ6B9MHmWgf7+GVZOxomLgEAcZ +-----END RSA PRIVATE KEY----- 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 new file mode 120000 index 00000000000..3ddf86a3594 --- /dev/null +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/data/symlink @@ -0,0 +1 @@ +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 e2ca1d97d3f..da24fb246bf 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 @@ -34,6 +34,7 @@ import ( // For more details on the remote API, check http://goo.gl/G3plxW. type DockerServer struct { containers []*docker.Container + execs []*docker.Exec cMut sync.RWMutex images []docker.Image iMut sync.RWMutex @@ -90,18 +91,22 @@ func (s *DockerServer) buildMuxer() { s.mux.Path("/containers/{id:.*}/json").Methods("GET").HandlerFunc(s.handlerWrapper(s.inspectContainer)) s.mux.Path("/containers/{id:.*}/top").Methods("GET").HandlerFunc(s.handlerWrapper(s.topContainer)) s.mux.Path("/containers/{id:.*}/start").Methods("POST").HandlerFunc(s.handlerWrapper(s.startContainer)) + s.mux.Path("/containers/{id:.*}/kill").Methods("POST").HandlerFunc(s.handlerWrapper(s.stopContainer)) s.mux.Path("/containers/{id:.*}/stop").Methods("POST").HandlerFunc(s.handlerWrapper(s.stopContainer)) s.mux.Path("/containers/{id:.*}/pause").Methods("POST").HandlerFunc(s.handlerWrapper(s.pauseContainer)) s.mux.Path("/containers/{id:.*}/unpause").Methods("POST").HandlerFunc(s.handlerWrapper(s.unpauseContainer)) s.mux.Path("/containers/{id:.*}/wait").Methods("POST").HandlerFunc(s.handlerWrapper(s.waitContainer)) s.mux.Path("/containers/{id:.*}/attach").Methods("POST").HandlerFunc(s.handlerWrapper(s.attachContainer)) s.mux.Path("/containers/{id:.*}").Methods("DELETE").HandlerFunc(s.handlerWrapper(s.removeContainer)) + s.mux.Path("/containers/{id:.*}/exec").Methods("POST").HandlerFunc(s.handlerWrapper(s.createExecContainer)) + s.mux.Path("/exec/{id:.*}/start").Methods("POST").HandlerFunc(s.handlerWrapper(s.startExecContainer)) s.mux.Path("/images/create").Methods("POST").HandlerFunc(s.handlerWrapper(s.pullImage)) s.mux.Path("/build").Methods("POST").HandlerFunc(s.handlerWrapper(s.buildImage)) s.mux.Path("/images/json").Methods("GET").HandlerFunc(s.handlerWrapper(s.listImages)) s.mux.Path("/images/{id:.*}").Methods("DELETE").HandlerFunc(s.handlerWrapper(s.removeImage)) s.mux.Path("/images/{name:.*}/json").Methods("GET").HandlerFunc(s.handlerWrapper(s.inspectImage)) s.mux.Path("/images/{name:.*}/push").Methods("POST").HandlerFunc(s.handlerWrapper(s.pushImage)) + s.mux.Path("/images/{name:.*}/tag").Methods("POST").HandlerFunc(s.handlerWrapper(s.tagImage)) s.mux.Path("/events").Methods("GET").HandlerFunc(s.listEvents) s.mux.Path("/_ping").Methods("GET").HandlerFunc(s.handlerWrapper(s.pingDocker)) s.mux.Path("/images/load").Methods("POST").HandlerFunc(s.handlerWrapper(s.loadImage)) @@ -173,8 +178,8 @@ func (s *DockerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } -// Returns default http.Handler mux, it allows customHandlers to call the -// default behavior if wanted. +// DefaultHandler returns default http.Handler mux, it allows customHandlers to +// call the default behavior if wanted. func (s *DockerServer) DefaultHandler() http.Handler { return s.mux } @@ -210,6 +215,7 @@ func (s *DockerServer) listContainers(w http.ResponseWriter, r *http.Request) { Created: container.Created.Unix(), Status: container.State.String(), Ports: container.NetworkSettings.PortMappingAPI(), + Names: []string{fmt.Sprintf("/%s", container.Name)}, } } } @@ -269,8 +275,7 @@ func (s *DockerServer) createContainer(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusBadRequest) return } - image, err := s.findImage(config.Image) - if err != nil { + if _, err := s.findImage(config.Image); err != nil { http.Error(w, err.Error(), http.StatusNotFound) return } @@ -278,7 +283,7 @@ func (s *DockerServer) createContainer(w http.ResponseWriter, r *http.Request) { ports := map[docker.Port][]docker.PortBinding{} for port := range config.ExposedPorts { ports[port] = []docker.PortBinding{{ - HostIp: "0.0.0.0", + HostIP: "0.0.0.0", HostPort: strconv.Itoa(mathrand.Int() % 65536), }} } @@ -294,6 +299,7 @@ func (s *DockerServer) createContainer(w http.ResponseWriter, r *http.Request) { } container := docker.Container{ + Name: r.URL.Query().Get("name"), ID: s.generateID(), Created: time.Now(), Path: path, @@ -305,7 +311,7 @@ func (s *DockerServer) createContainer(w http.ResponseWriter, r *http.Request) { ExitCode: 0, StartedAt: time.Now(), }, - Image: image, + Image: config.Image, NetworkSettings: &docker.NetworkSettings{ IPAddress: fmt.Sprintf("172.16.42.%d", mathrand.Int()%250+2), IPPrefixLen: 24, @@ -557,8 +563,10 @@ func (s *DockerServer) buildImage(w http.ResponseWriter, r *http.Request) { } //we did not use that Dockerfile to build image cause we are a fake Docker daemon image := docker.Image{ - ID: s.generateID(), + ID: s.generateID(), + Created: time.Now(), } + query := r.URL.Query() repository := image.ID if t := query.Get("t"); t != "" { @@ -597,6 +605,22 @@ func (s *DockerServer) pushImage(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Pushed") } +func (s *DockerServer) tagImage(w http.ResponseWriter, r *http.Request) { + name := mux.Vars(r)["name"] + s.iMut.RLock() + if _, ok := s.imgIDs[name]; !ok { + s.iMut.RUnlock() + http.Error(w, "No such image", http.StatusNotFound) + return + } + s.iMut.RUnlock() + s.iMut.Lock() + defer s.iMut.Unlock() + newRepo := r.URL.Query().Get("repo") + s.imgIDs[newRepo] = s.imgIDs[name] + w.WriteHeader(http.StatusCreated) +} + func (s *DockerServer) removeImage(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] s.iMut.RLock() @@ -604,6 +628,12 @@ func (s *DockerServer) removeImage(w http.ResponseWriter, r *http.Request) { if img, ok := s.imgIDs[id]; ok { id, tag = img, id } + var tags []string + for tag, taggedID := range s.imgIDs { + if taggedID == id { + tags = append(tags, tag) + } + } s.iMut.RUnlock() _, index, err := s.findImageByID(id) if err != nil { @@ -613,8 +643,10 @@ func (s *DockerServer) removeImage(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) s.iMut.Lock() defer s.iMut.Unlock() - s.images[index] = s.images[len(s.images)-1] - s.images = s.images[:len(s.images)-1] + if len(tags) < 2 { + s.images[index] = s.images[len(s.images)-1] + s.images = s.images[:len(s.images)-1] + } if tag != "" { delete(s.imgIDs, tag) } @@ -690,3 +722,23 @@ func (s *DockerServer) getImage(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/tar") } + +func (s *DockerServer) createExecContainer(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + exec := docker.Exec{ID: "id-exec-created-by-test"} + s.execs = append(s.execs, &exec) + 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 + } + } + w.WriteHeader(http.StatusNotFound) +} 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 ad5ce7dad5e..d8763cbc1bf 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 @@ -120,6 +120,7 @@ func TestListContainers(t *testing.T) { Created: container.Created.Unix(), Status: container.State.String(), Ports: container.NetworkSettings.PortMappingAPI(), + Names: []string{"/" + container.Name}, } } var got []docker.APIContainers @@ -473,6 +474,23 @@ func TestStopContainer(t *testing.T) { } } +func TestKillContainer(t *testing.T) { + server := DockerServer{} + addContainers(&server, 1) + server.containers[0].State.Running = true + server.buildMuxer() + recorder := httptest.NewRecorder() + path := fmt.Sprintf("/containers/%s/kill", server.containers[0].ID) + request, _ := http.NewRequest("POST", path, nil) + server.ServeHTTP(recorder, request) + if recorder.Code != http.StatusNoContent { + t.Errorf("KillContainer: wrong status code. Want %d. Got %d.", http.StatusNoContent, recorder.Code) + } + if server.containers[0].State.Running { + t.Error("KillContainer: did not stop the container") + } +} + func TestStopContainerWithNotifyChannel(t *testing.T) { ch := make(chan *docker.Container, 1) server := DockerServer{} @@ -771,12 +789,38 @@ func TestPushImageNotFound(t *testing.T) { } } +func TestTagImage(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", 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"] { + t.Errorf("TagImage: did not tag the image") + } +} + +func TestTagImageNotFound(t *testing.T) { + server := DockerServer{} + server.buildMuxer() + recorder := httptest.NewRecorder() + request, _ := http.NewRequest("POST", "/images/tsuru/python/tag", nil) + server.ServeHTTP(recorder, request) + if recorder.Code != http.StatusNotFound { + t.Errorf("TagImage: wrong status. Want %d. Got %d.", http.StatusNotFound, recorder.Code) + } +} + func addContainers(server *DockerServer, n int) { server.cMut.Lock() defer server.cMut.Unlock() for i := 0; i < n; i++ { date := time.Now().Add(time.Duration((rand.Int() % (i + 1))) * time.Hour) container := docker.Container{ + Name: fmt.Sprintf("%x", rand.Int()%10000), ID: fmt.Sprintf("%x", rand.Int()%10000), Created: date, Path: "ls", @@ -896,6 +940,36 @@ func TestRemoveImageByName(t *testing.T) { } } +func TestRemoveImageWithMultipleTags(t *testing.T) { + server := DockerServer{} + addImages(&server, 1, true) + server.buildMuxer() + imgID := server.images[0].ID + imgName := "docker/python-" + imgID + server.imgIDs["docker/python-wat"] = imgID + recorder := httptest.NewRecorder() + path := fmt.Sprintf("/images/%s", imgName) + request, _ := http.NewRequest("DELETE", path, nil) + server.ServeHTTP(recorder, request) + _, ok := server.imgIDs[imgName] + if ok { + t.Error("RemoveImage: did not remove image tag name.") + } + id, ok := server.imgIDs["docker/python-wat"] + if !ok { + t.Error("RemoveImage: removed the wrong tag name.") + } + if id != imgID { + t.Error("RemoveImage: disassociated the wrong ID from the tag") + } + if len(server.images) < 1 { + t.Fatal("RemoveImage: removed the image, but should keep it") + } + if server.images[0].ID != imgID { + t.Error("RemoveImage: changed the ID of the image!") + } +} + func TestPrepareFailure(t *testing.T) { server := DockerServer{failures: make(map[string]string)} server.buildMuxer() diff --git a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/writer.go b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/writer.go index 42752b0358a..4ef857a6d49 100644 --- a/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/writer.go +++ b/Godeps/_workspace/src/github.com/fsouza/go-dockerclient/testing/writer.go @@ -13,9 +13,9 @@ import ( type stdType [8]byte var ( - stdin stdType = stdType{0: 0} - stdout stdType = stdType{0: 1} - stderr stdType = stdType{0: 2} + stdin = stdType{0: 0} + stdout = stdType{0: 1} + stderr = stdType{0: 2} ) type stdWriter struct {