├── .gitignore ├── LICENSE ├── README.md ├── auth.go ├── auth_test.go ├── checks ├── README.md ├── dockerchecks.go ├── dockerchecks_test.go └── example │ └── main.go ├── docker.go ├── docker_darwin.go ├── docker_linux.go ├── docker_test.go └── types.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2014 Sam Alba 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golang Docker client 2 | 3 | [![GoDoc](http://godoc.org/github.com/yhat/go-docker?status.png)](http://godoc.org/github.com/yhat/go-docker) 4 | 5 | This is a fork of the samalba/dockerclient library. It adds missing API calls 6 | such as wait, commit, and attach as well as a splitter for Docker stream events 7 | (like containers stdout and stderr). The fork also removes event callbacks and 8 | tests against a Docker installation rather than mocks. 9 | 10 | Example: 11 | 12 | ```go 13 | package main 14 | 15 | import ( 16 | "fmt" 17 | "os" 18 | "time" 19 | 20 | "github.com/yhat/go-docker" 21 | ) 22 | 23 | func SayHi() error { 24 | timeout := 3 * time.Second 25 | 26 | cli, err := docker.NewDefaultClient(timeout) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | // create a container 32 | config := &docker.ContainerConfig{ 33 | Image: "ubuntu:14.04", 34 | Cmd: []string{"echo", "hello from docker land"}, 35 | } 36 | cid, err := cli.CreateContainer(config, "myimage") 37 | if err != nil { 38 | return err 39 | } 40 | 41 | // always remember to clean up after yourself 42 | defer cli.RemoveContainer(cid, true, false) 43 | 44 | // attach to the container 45 | streamOpts := &docker.AttachOptions{Stream: true, Stdout: true, Stderr: true} 46 | stream, err := cli.Attach(cid, streamOpts) 47 | if err != nil { 48 | return err 49 | } 50 | defer stream.Close() 51 | 52 | // concurrently write stream to stdout and stderr 53 | go docker.SplitStream(stream, os.Stdout, os.Stderr) 54 | 55 | // start the container 56 | err = cli.StartContainer(cid, &docker.HostConfig{}) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | // wait for the container to exit 62 | statusCode, err := cli.Wait(cid) 63 | if err != nil { 64 | return err 65 | } 66 | if statusCode != 0 { 67 | return fmt.Errorf("process returned bad status code: %d", statusCode) 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func main() { 74 | if err := SayHi(); err != nil { 75 | panic(err) 76 | } 77 | } 78 | ``` 79 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/json" 7 | ) 8 | 9 | // AuthConfig hold parameters for authenticating with the docker registry 10 | type AuthConfig struct { 11 | Username string `json:"username,omitempty"` 12 | Password string `json:"password,omitempty"` 13 | Email string `json:"email,omitempty"` 14 | } 15 | 16 | // encode the auth configuration struct into base64 for the X-Registry-Auth header 17 | func (c *AuthConfig) encode() string { 18 | var buf bytes.Buffer 19 | json.NewEncoder(&buf).Encode(c) 20 | return base64.URLEncoding.EncodeToString(buf.Bytes()) 21 | } 22 | -------------------------------------------------------------------------------- /auth_test.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAuthEncode(t *testing.T) { 8 | a := AuthConfig{Username: "foo", Password: "password", Email: "bar@baz.com"} 9 | expected := "eyJ1c2VybmFtZSI6ImZvbyIsInBhc3N3b3JkIjoicGFzc3dvcmQiLCJlbWFpbCI6ImJhckBiYXouY29tIn0K" 10 | got := a.encode() 11 | 12 | if expected != got { 13 | t.Errorf("testAuthEncode failed. Expected [%s] got [%s]", expected, got) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /checks/README.md: -------------------------------------------------------------------------------- 1 | Checks to make sure docker is properly installed. 2 | 3 | These tests require the `ubuntu:14.04` image. There's an example file in 4 | `example/main.go` which gives an example of how to run the tests. 5 | 6 | $ go run example/main.go 7 | CheckDockerSocketAccess ... OK (37.39603ms) 8 | CheckVersion ... OK (704.645µs) 9 | CheckAUFSDriver ... OK (38.370624ms) 10 | CheckSimpleCommand ... OK (486.655433ms) 11 | CheckInternetAccess ... OK (1.427594181s) 12 | CheckExposingPort ... OK (1.0361052s) 13 | CheckFileMounting ... OK (486.73578ms) 14 | -------------------------------------------------------------------------------- /checks/dockerchecks.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "net/http" 11 | "net/url" 12 | "os" 13 | "path/filepath" 14 | "strconv" 15 | "strings" 16 | "time" 17 | 18 | "github.com/yhat/go-docker" 19 | ) 20 | 21 | var dockerSock = "unix:///var/run/docker.sock" 22 | 23 | type Check func() error 24 | 25 | type namedCheck struct { 26 | Name string 27 | Check Check 28 | } 29 | 30 | type Checker struct { 31 | checks []namedCheck 32 | } 33 | 34 | func (c *Checker) Register(check Check, name string) { 35 | c.checks = append(c.checks, namedCheck{ 36 | Name: name, 37 | Check: check, 38 | }) 39 | } 40 | 41 | func (c *Checker) Run() (exitCode int) { 42 | maxNameLen := 0 43 | for _, check := range c.checks { 44 | if n := len(check.Name); maxNameLen < n { 45 | maxNameLen = n 46 | } 47 | } 48 | s := strconv.Itoa(maxNameLen) 49 | pad := func(name string) string { 50 | return fmt.Sprintf("%-"+s+"s", name) 51 | } 52 | var err error 53 | for _, check := range c.checks { 54 | name := pad(check.Name) 55 | fmt.Print(name + " ... ") 56 | start := time.Now() 57 | err = check.Check() 58 | delta := time.Now().Sub(start) 59 | if err != nil { 60 | exitCode = 2 61 | fmt.Println("ERROR") 62 | fmt.Printf("\t%v\n", err) 63 | } else { 64 | fmt.Printf("OK (%s)\n", delta) 65 | } 66 | } 67 | return 0 68 | } 69 | 70 | func newClient() (*docker.Client, error) { 71 | return docker.NewClient(dockerSock, nil, 3*time.Second) 72 | } 73 | 74 | // CheckDockerSocket confirms that docker can be reached 75 | func CheckDockerSocket() error { 76 | cli, err := newClient() 77 | if err != nil { 78 | return fmt.Errorf("creating client: %v", err) 79 | } 80 | _, err = cli.Info() 81 | if err != nil { 82 | return fmt.Errorf("simple ping: %v", err) 83 | } 84 | return nil 85 | } 86 | 87 | // CheckVersion confirms docker is at least the version passed. 88 | func CheckVersion(minVersion []int) Check { 89 | return func() error { 90 | cli, err := newClient() 91 | if err != nil { 92 | return fmt.Errorf("creating client: %v", err) 93 | } 94 | v, err := cli.Version() 95 | if err != nil { 96 | return fmt.Errorf("getting version: %v", err) 97 | } 98 | return versionAtLeast(v.Version, minVersion) 99 | } 100 | } 101 | 102 | func versionAtLeast(vStr string, minVersion []int) error { 103 | vParts := strings.Split(vStr, ".") 104 | n := len(vParts) 105 | if n == 0 { 106 | return fmt.Errorf("parsing version '%s'", vStr) 107 | } 108 | 109 | var err error 110 | version := make([]int, len(vParts)) 111 | for i, p := range vParts { 112 | version[i], err = strconv.Atoi(p) 113 | if err != nil { 114 | return fmt.Errorf("parsing version '%s' %v", vStr, err) 115 | } 116 | } 117 | 118 | if n > len(minVersion) { 119 | version = version[:len(minVersion)] 120 | } 121 | for i, vp := range version { 122 | minVP := minVersion[i] 123 | if minVP > vp { 124 | return fmt.Errorf("'%s' is not at least '%d'", vStr, minVersion) 125 | } else if minVP < vp { 126 | return nil 127 | } 128 | } 129 | if n < len(minVersion) { 130 | return fmt.Errorf("'%s' is not at least '%d'", vStr, minVersion) 131 | } 132 | return nil 133 | } 134 | 135 | // CheckDriver confirms that docker is using the provided driver. 136 | func CheckDriver(driver string) Check { 137 | return func() error { 138 | cli, err := newClient() 139 | if err != nil { 140 | return fmt.Errorf("creating client: %v", err) 141 | } 142 | info, err := cli.Info() 143 | if err != nil { 144 | return fmt.Errorf("getting info: %v", err) 145 | } 146 | if info.Driver != driver { 147 | return fmt.Errorf("driver is '%s' not 'driver'", info.Driver, driver) 148 | } 149 | return nil 150 | } 151 | } 152 | 153 | // CheckSimpleCommand executes a simple echo command to ensure docker can 154 | // create and run a container. 155 | func CheckSimpleCommand() (err error) { 156 | cli, err := newClient() 157 | if err != nil { 158 | return fmt.Errorf("creating client: %v", err) 159 | } 160 | echoStr := "hello" 161 | config := &docker.ContainerConfig{ 162 | Image: "ubuntu:14.04", 163 | Cmd: []string{"echo", "-n", echoStr}, 164 | } 165 | cid, err := cli.CreateContainer(config, "hellotest") 166 | if err != nil { 167 | return fmt.Errorf("creating container: %v", err) 168 | } 169 | defer func() { 170 | rcErr := cli.RemoveContainer(cid, true, false) 171 | if err == nil { 172 | err = rcErr 173 | } else if rcErr != nil { 174 | fmt.Fprintf(os.Stderr, "removing container %v\n", rcErr) 175 | } 176 | }() 177 | 178 | // attach to the container 179 | streamOpts := &docker.AttachOptions{Stream: true, Stdout: true, Stderr: true} 180 | stream, err := cli.Attach(cid, streamOpts) 181 | if err != nil { 182 | return err 183 | } 184 | defer stream.Close() 185 | 186 | // start the container 187 | err = cli.StartContainer(cid, &docker.HostConfig{}) 188 | if err != nil { 189 | return err 190 | } 191 | 192 | var stderr bytes.Buffer 193 | var stdout bytes.Buffer 194 | if err = docker.SplitStream(stream, &stdout, &stderr); err != nil { 195 | return fmt.Errorf("reading stdout: %v", err) 196 | } 197 | 198 | // wait for the container to exit 199 | exitCode, err := cli.Wait(cid) 200 | if err != nil { 201 | return err 202 | } 203 | 204 | if exitCode != 0 { 205 | return fmt.Errorf("bad exit code %d %s", exitCode, stderr) 206 | } 207 | if out := stdout.String(); out != echoStr { 208 | return fmt.Errorf("expected '%s' from echo, got '%s'", echoStr, out) 209 | } 210 | if sErr := stderr.String(); sErr != "" { 211 | return fmt.Errorf("expected no response from stderr, got '%s'", sErr) 212 | } 213 | 214 | return nil 215 | } 216 | 217 | // CheckExposedPort confirms that docker can expose a port to the host machine 218 | // when creating a container. 219 | func CheckExposedPort() (err error) { 220 | cli, err := newClient() 221 | if err != nil { 222 | return fmt.Errorf("creating client: %v", err) 223 | } 224 | port := "8000/tcp" 225 | 226 | hostConfig := docker.HostConfig{ 227 | PortBindings: map[string][]docker.PortBinding{ 228 | port: []docker.PortBinding{ 229 | // Docker will auto assign the port if left blank 230 | {HostIp: "0.0.0.0", HostPort: ""}, 231 | }, 232 | }, 233 | } 234 | 235 | config := &docker.ContainerConfig{ 236 | Image: "ubuntu:14.04", 237 | Cmd: []string{"python3", "-m", "http.server"}, 238 | ExposedPorts: map[string]struct{}{ 239 | port: struct{}{}, 240 | }, 241 | HostConfig: hostConfig, 242 | } 243 | cid, err := cli.CreateContainer(config, "exposedporttest") 244 | if err != nil { 245 | return fmt.Errorf("creating container: %v", err) 246 | } 247 | defer func() { 248 | rcErr := cli.RemoveContainer(cid, true, false) 249 | if err == nil { 250 | err = rcErr 251 | } else if rcErr != nil { 252 | fmt.Fprintf(os.Stderr, "removing container %v\n", rcErr) 253 | } 254 | }() 255 | // start the container and get the port Docker assigned to it 256 | if err = cli.StartContainer(cid, &hostConfig); err != nil { 257 | return fmt.Errorf("start container: %v", err) 258 | } 259 | info, err := cli.InspectContainer(cid) 260 | if err != nil { 261 | return fmt.Errorf("inspect container: %v", err) 262 | } 263 | exposed, ok := info.NetworkSettings.Ports[port] 264 | if !ok { 265 | return fmt.Errorf("no ports exposed") 266 | } 267 | if len(exposed) != 1 { 268 | return fmt.Errorf("expected one exposed port, got %s", exposed) 269 | } 270 | hostIp, hostPort := exposed[0].HostIp, exposed[0].HostPort 271 | 272 | u := (&url.URL{ 273 | Scheme: "http", 274 | Host: hostIp + ":" + hostPort, 275 | Path: "/", 276 | }).String() 277 | 278 | // give the simple http server a second to start up 279 | time.Sleep(500 * time.Millisecond) 280 | resp, err := http.Get(u) 281 | if resp != nil { 282 | defer resp.Body.Close() 283 | } 284 | if err != nil { 285 | return fmt.Errorf("connecting to docker port %v", err) 286 | } 287 | return nil 288 | } 289 | 290 | // CheckInternetAccess confirms that processes within docker containers can 291 | // access the outside internet by making a request to a PyPi JSON page and 292 | // parsing the response. 293 | func CheckInternetAccess() (err error) { 294 | cli, err := newClient() 295 | if err != nil { 296 | return fmt.Errorf("creating client: %v", err) 297 | } 298 | 299 | u := "https://pypi.python.org/pypi/yhat/json" 300 | // attempt to access a remote address 301 | cmd := `import urllib.request as r; print(r.urlopen("` + u + `").read().decode("utf-8"))` 302 | config := &docker.ContainerConfig{ 303 | Image: "ubuntu:14.04", 304 | Cmd: []string{"python3", "-c", cmd}, 305 | } 306 | 307 | cid, err := cli.CreateContainer(config, "internetaccesstest") 308 | if err != nil { 309 | return fmt.Errorf("creating container: %v", err) 310 | } 311 | defer func() { 312 | rcErr := cli.RemoveContainer(cid, true, false) 313 | if err == nil { 314 | err = rcErr 315 | } else if rcErr != nil { 316 | fmt.Fprintf(os.Stderr, "removing container %v\n", rcErr) 317 | } 318 | }() 319 | 320 | // attach to the container 321 | streamOpts := &docker.AttachOptions{Stream: true, Stdout: true, Stderr: true} 322 | stream, err := cli.Attach(cid, streamOpts) 323 | if err != nil { 324 | return err 325 | } 326 | defer stream.Close() 327 | 328 | // start the container 329 | err = cli.StartContainer(cid, &docker.HostConfig{}) 330 | if err != nil { 331 | return err 332 | } 333 | 334 | var stderr bytes.Buffer 335 | var stdout bytes.Buffer 336 | if err = docker.SplitStream(stream, &stdout, &stderr); err != nil { 337 | return fmt.Errorf("reading stdout: %v", err) 338 | } 339 | 340 | // wait for the container to exit 341 | exitCode, err := cli.Wait(cid) 342 | if err != nil { 343 | return err 344 | } 345 | 346 | if exitCode != 0 { 347 | return fmt.Errorf("bad exit code %d %s", exitCode, stderr) 348 | } 349 | 350 | var data struct { 351 | Info struct { 352 | HomePage string `json:"home_page"` 353 | } `json:"info"` 354 | } 355 | if err = json.Unmarshal(stdout.Bytes(), &data); err != nil { 356 | return fmt.Errorf("decoding JSON response from pypi: %v", err) 357 | } 358 | if data.Info.HomePage != "https://github.com/yhat/yhat-client" { 359 | return fmt.Errorf("bad JSON returned from %s", u) 360 | } 361 | if sErr := stderr.String(); sErr != "" { 362 | return fmt.Errorf("expected no response from stderr, got '%s'", sErr) 363 | } 364 | 365 | return nil 366 | } 367 | 368 | // CheckFileMounting confirms that docker can mount a file on the host machine 369 | // into a container. 370 | func CheckFileMounting() (err error) { 371 | cli, err := newClient() 372 | if err != nil { 373 | return fmt.Errorf("creating client: %v", err) 374 | } 375 | 376 | tempDir, err := ioutil.TempDir("", "") 377 | if err != nil { 378 | return fmt.Errorf("creating temp dir: %v", err) 379 | } 380 | defer os.RemoveAll(tempDir) 381 | 382 | fileName := "foo" 383 | 384 | file, err := os.Create(filepath.Join(tempDir, fileName)) 385 | if err != nil { 386 | return fmt.Errorf("creating file: %v", err) 387 | } 388 | 389 | // write 1 KiB of random data to the file 390 | randData := make([]byte, 1<<10) 391 | if _, err = io.ReadFull(rand.Reader, randData); err != nil { 392 | return fmt.Errorf("reading random data: %v", err) 393 | } 394 | if _, err = file.Write(randData); err != nil { 395 | return fmt.Errorf("writing random data: %v", err) 396 | } 397 | 398 | targetDir := "/tmp/bar" 399 | targetFile := "/tmp/bar/" + fileName 400 | 401 | volumes := map[string]struct{}{targetDir: struct{}{}} 402 | bindings := []string{tempDir + ":" + targetDir} 403 | 404 | hostConfig := docker.HostConfig{Binds: bindings} 405 | 406 | config := &docker.ContainerConfig{ 407 | Image: "ubuntu:14.04", 408 | Cmd: []string{"cat", targetFile}, 409 | Volumes: volumes, 410 | HostConfig: hostConfig, 411 | } 412 | cid, err := cli.CreateContainer(config, "filemounttest") 413 | if err != nil { 414 | return fmt.Errorf("creating container: %v", err) 415 | } 416 | defer func() { 417 | rcErr := cli.RemoveContainer(cid, true, false) 418 | if err == nil { 419 | err = rcErr 420 | } else if rcErr != nil { 421 | fmt.Fprintf(os.Stderr, "removing container %v\n", rcErr) 422 | } 423 | }() 424 | 425 | // attach to the container 426 | streamOpts := &docker.AttachOptions{Stream: true, Stdout: true, Stderr: true} 427 | stream, err := cli.Attach(cid, streamOpts) 428 | if err != nil { 429 | return err 430 | } 431 | defer stream.Close() 432 | 433 | // start the container 434 | err = cli.StartContainer(cid, &docker.HostConfig{}) 435 | if err != nil { 436 | return err 437 | } 438 | 439 | var stderr bytes.Buffer 440 | var stdout bytes.Buffer 441 | if err = docker.SplitStream(stream, &stdout, &stderr); err != nil { 442 | return fmt.Errorf("reading stdout: %v", err) 443 | } 444 | 445 | // wait for the container to exit 446 | exitCode, err := cli.Wait(cid) 447 | if err != nil { 448 | return err 449 | } 450 | 451 | if exitCode != 0 { 452 | return fmt.Errorf("bad exit code %d %s", exitCode, stderr) 453 | } 454 | if bytes.Compare(stdout.Bytes(), randData) != 0 { 455 | return fmt.Errorf("mounted file did not match") 456 | } 457 | if sErr := stderr.String(); sErr != "" { 458 | return fmt.Errorf("expected no response from stderr, got '%s'", sErr) 459 | } 460 | 461 | return nil 462 | } 463 | -------------------------------------------------------------------------------- /checks/dockerchecks_test.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import "testing" 4 | 5 | func TestVersionComp(t *testing.T) { 6 | versionTests := []struct { 7 | VStr string 8 | V []int 9 | OK bool 10 | }{ 11 | {"1.5.4", []int{1, 5, 4}, true}, 12 | {"1.5.3", []int{1, 5, 4}, false}, 13 | {"1.5.3.5", []int{1, 5, 4}, false}, 14 | {"1.5.4", []int{1, 5}, true}, 15 | {"1.6", []int{1, 5}, true}, 16 | } 17 | 18 | for _, vt := range versionTests { 19 | err := versionAtLeast(vt.VStr, vt.V) 20 | if vt.OK && (err != nil) { 21 | t.Errorf("%s %s %v", vt.VStr, vt.V, err) 22 | } else if !vt.OK && (err == nil) { 23 | t.Errorf("expected %s and %s to cause error", vt.VStr, vt.V) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /checks/example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/yhat/go-docker/checks" 7 | ) 8 | 9 | func main() { 10 | c := checks.Checker{} 11 | c.Register(checks.CheckDockerSocket, "CheckDockerSocketAccess") 12 | c.Register(checks.CheckVersion([]int{1, 5, 0}), "CheckVersion") 13 | c.Register(checks.CheckDriver("aufs"), "CheckAUFSDriver") 14 | c.Register(checks.CheckSimpleCommand, "CheckSimpleCommand") 15 | c.Register(checks.CheckInternetAccess, "CheckInternetAccess") 16 | c.Register(checks.CheckExposedPort, "CheckExposingPort") 17 | c.Register(checks.CheckFileMounting, "CheckFileMounting") 18 | os.Exit(c.Run()) 19 | } 20 | -------------------------------------------------------------------------------- /docker.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "archive/tar" 5 | "bytes" 6 | "crypto/tls" 7 | "encoding/base64" 8 | "encoding/binary" 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "io" 13 | "io/ioutil" 14 | "net" 15 | "net/http" 16 | "net/url" 17 | "strconv" 18 | "strings" 19 | "time" 20 | ) 21 | 22 | const APIVersion = "v1.17" 23 | 24 | type Client struct { 25 | URL *url.URL 26 | HTTPClient *http.Client 27 | TLSConfig *tls.Config 28 | } 29 | 30 | type Error struct { 31 | StatusCode int 32 | Status string 33 | msg string 34 | } 35 | 36 | func (e *Error) Error() string { 37 | return fmt.Sprintf("%s: %s", e.Status, e.msg) 38 | } 39 | 40 | func NewClient(daemonURL string, tlsConfig *tls.Config, timeout time.Duration) (*Client, error) { 41 | u, err := url.Parse(daemonURL) 42 | if err != nil { 43 | return nil, err 44 | } 45 | if u.Scheme == "" || u.Scheme == "tcp" { 46 | if tlsConfig == nil { 47 | u.Scheme = "http" 48 | } else { 49 | u.Scheme = "https" 50 | } 51 | } 52 | httpClient := newHTTPClient(u, tlsConfig, timeout) 53 | return &Client{u, httpClient, tlsConfig}, nil 54 | } 55 | 56 | func newHTTPClient(u *url.URL, tlsConfig *tls.Config, timeout time.Duration) *http.Client { 57 | httpTransport := &http.Transport{ 58 | TLSClientConfig: tlsConfig, 59 | DisableKeepAlives: true, 60 | } 61 | 62 | switch u.Scheme { 63 | default: 64 | httpTransport.Dial = func(proto, addr string) (net.Conn, error) { 65 | return net.DialTimeout(proto, addr, timeout) 66 | } 67 | case "unix": 68 | socketPath := u.Path 69 | unixDial := func(proto, addr string) (net.Conn, error) { 70 | return net.DialTimeout("unix", socketPath, timeout) 71 | } 72 | httpTransport.Dial = unixDial 73 | // Override the main URL object so the HTTP lib won't complain 74 | u.Scheme = "http" 75 | u.Host = "unix.sock" 76 | u.Path = "" 77 | } 78 | return &http.Client{Transport: httpTransport} 79 | } 80 | 81 | func (client *Client) DoRequest(method string, path string, body io.Reader) (*http.Response, error) { 82 | req, err := client.NewRequest(method, path, body) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | req.Header.Add("Content-Type", "application/json") 88 | resp, err := client.HTTPClient.Do(req) 89 | if err != nil { 90 | if !strings.Contains(err.Error(), "connection refused") && client.TLSConfig == nil { 91 | return nil, fmt.Errorf("%v. Are you trying to connect to a TLS-enabled daemon without TLS?", err) 92 | } 93 | return nil, err 94 | } 95 | if resp.StatusCode >= 400 { 96 | data, err := ioutil.ReadAll(resp.Body) 97 | resp.Body.Close() 98 | if err != nil { 99 | return nil, fmt.Errorf("failed to read body from response: %v", err) 100 | } 101 | return nil, &Error{StatusCode: resp.StatusCode, Status: resp.Status, msg: string(data)} 102 | } 103 | return resp, nil 104 | } 105 | 106 | func (client *Client) NewRequest(method string, path string, body io.Reader) (*http.Request, error) { 107 | return http.NewRequest(method, client.URL.String()+path, body) 108 | } 109 | 110 | // jsonUnmarshal reads the full body from the response and attempts to unmarshal 111 | // the result into the value v. 112 | // For convenience it also closes the response body. 113 | func jsonUnmarshal(resp *http.Response, v interface{}) error { 114 | data, err := ioutil.ReadAll(resp.Body) 115 | resp.Body.Close() 116 | if err != nil { 117 | return fmt.Errorf("failed to read body from response: %v", err) 118 | } 119 | return json.Unmarshal(data, v) 120 | } 121 | 122 | func (client *Client) Info() (*Info, error) { 123 | uri := fmt.Sprintf("/%s/info", APIVersion) 124 | data, err := client.DoRequest("GET", uri, nil) 125 | if err != nil { 126 | return nil, err 127 | } 128 | ret := &Info{} 129 | err = jsonUnmarshal(data, &ret) 130 | if err != nil { 131 | return nil, err 132 | } 133 | return ret, nil 134 | } 135 | 136 | func (client *Client) Push(name, tag string, auth *AuthConfig) error { 137 | data, err := json.Marshal(auth) 138 | if err != nil { 139 | return err 140 | } 141 | authHeader := base64.URLEncoding.EncodeToString(data) 142 | path := "/images/" + name + "/push" 143 | if tag != "" { 144 | path = path + "?" + (url.Values{"tag": []string{tag}}).Encode() 145 | } 146 | 147 | req, err := client.NewRequest("POST", "/images/"+name+"/push", nil) 148 | if err != nil { 149 | return err 150 | } 151 | req.Header.Add("X-Registry-Auth", authHeader) 152 | resp, err := client.HTTPClient.Do(req) 153 | if err != nil { 154 | return err 155 | } 156 | defer resp.Body.Close() 157 | if resp.StatusCode >= 400 { 158 | data, err := ioutil.ReadAll(resp.Body) 159 | if err != nil { 160 | return fmt.Errorf("failed to read body from response: %v", err) 161 | } 162 | return &Error{StatusCode: resp.StatusCode, Status: resp.Status, msg: string(data)} 163 | } 164 | 165 | decoder := json.NewDecoder(resp.Body) 166 | for { 167 | var s struct { 168 | Error string `json:"error"` 169 | } 170 | if err := decoder.Decode(&s); err != nil { 171 | if err == io.EOF { 172 | // Don't know if this is the correct logic, but the documentation 173 | // on the remote api doesn't actually describe how to ensure a 174 | // push is successful as of v1.20 175 | return nil 176 | } 177 | return err 178 | } 179 | if s.Error != "" { 180 | return errors.New(s.Error) 181 | } 182 | } 183 | } 184 | 185 | func (client *Client) ListContainers(all bool, size bool, filters string) ([]Container, error) { 186 | argAll := 0 187 | if all == true { 188 | argAll = 1 189 | } 190 | showSize := 0 191 | if size == true { 192 | showSize = 1 193 | } 194 | uri := fmt.Sprintf("/%s/containers/json?all=%d&size=%d", APIVersion, argAll, showSize) 195 | 196 | if filters != "" { 197 | uri += "&filters=" + filters 198 | } 199 | 200 | data, err := client.DoRequest("GET", uri, nil) 201 | if err != nil { 202 | return nil, err 203 | } 204 | ret := []Container{} 205 | err = jsonUnmarshal(data, &ret) 206 | if err != nil { 207 | return nil, err 208 | } 209 | return ret, nil 210 | } 211 | 212 | func (client *Client) InspectContainer(id string) (*ContainerInfo, error) { 213 | uri := fmt.Sprintf("/%s/containers/%s/json", APIVersion, id) 214 | data, err := client.DoRequest("GET", uri, nil) 215 | if err != nil { 216 | return nil, err 217 | } 218 | info := &ContainerInfo{} 219 | err = jsonUnmarshal(data, info) 220 | if err != nil { 221 | return nil, err 222 | } 223 | return info, nil 224 | } 225 | 226 | func (client *Client) CreateContainer(config *ContainerConfig, name string) (string, error) { 227 | data, err := json.Marshal(config) 228 | if err != nil { 229 | return "", err 230 | } 231 | uri := fmt.Sprintf("/%s/containers/create", APIVersion) 232 | if name != "" { 233 | v := url.Values{} 234 | v.Set("name", name) 235 | uri = fmt.Sprintf("%s?%s", uri, v.Encode()) 236 | } 237 | resp, err := client.DoRequest("POST", uri, bytes.NewReader(data)) 238 | if err != nil { 239 | return "", err 240 | } 241 | result := &RespContainersCreate{} 242 | err = jsonUnmarshal(resp, result) 243 | if err != nil { 244 | return "", err 245 | } 246 | return result.Id, nil 247 | } 248 | 249 | func (client *Client) ContainerLogs(id string, options *LogOptions) (io.ReadCloser, error) { 250 | v := url.Values{} 251 | v.Add("follow", strconv.FormatBool(options.Follow)) 252 | v.Add("stdout", strconv.FormatBool(options.Stdout)) 253 | v.Add("stderr", strconv.FormatBool(options.Stderr)) 254 | v.Add("timestamps", strconv.FormatBool(options.Timestamps)) 255 | if options.Tail > 0 { 256 | v.Add("tail", strconv.FormatInt(options.Tail, 10)) 257 | } 258 | 259 | uri := fmt.Sprintf("/%s/containers/%s/logs?%s", APIVersion, id, v.Encode()) 260 | req, err := http.NewRequest("GET", client.URL.String()+uri, nil) 261 | if err != nil { 262 | return nil, err 263 | } 264 | req.Header.Add("Content-Type", "application/json") 265 | resp, err := client.HTTPClient.Do(req) 266 | if err != nil { 267 | return nil, err 268 | } 269 | return resp.Body, nil 270 | } 271 | 272 | func (client *Client) StartContainer(id string, config *HostConfig) error { 273 | data, err := json.Marshal(config) 274 | if err != nil { 275 | return err 276 | } 277 | uri := fmt.Sprintf("/%s/containers/%s/start", APIVersion, id) 278 | resp, err := client.DoRequest("POST", uri, bytes.NewReader(data)) 279 | if err != nil { 280 | return err 281 | } 282 | resp.Body.Close() 283 | return nil 284 | } 285 | 286 | func (client *Client) StopContainer(id string, timeout int) error { 287 | uri := fmt.Sprintf("/%s/containers/%s/stop?t=%d", APIVersion, id, timeout) 288 | resp, err := client.DoRequest("POST", uri, nil) 289 | if err != nil { 290 | return err 291 | } 292 | resp.Body.Close() 293 | return nil 294 | } 295 | 296 | func (client *Client) RestartContainer(id string, timeout int) error { 297 | uri := fmt.Sprintf("/%s/containers/%s/restart?t=%d", APIVersion, id, timeout) 298 | resp, err := client.DoRequest("POST", uri, nil) 299 | if err != nil { 300 | return err 301 | } 302 | resp.Body.Close() 303 | return nil 304 | } 305 | 306 | func (client *Client) KillContainer(id, signal string) error { 307 | uri := fmt.Sprintf("/%s/containers/%s/kill?signal=%s", APIVersion, id, signal) 308 | resp, err := client.DoRequest("POST", uri, nil) 309 | if err != nil { 310 | return err 311 | } 312 | resp.Body.Close() 313 | return nil 314 | } 315 | 316 | func (client *Client) Version() (*Version, error) { 317 | uri := fmt.Sprintf("/%s/version", APIVersion) 318 | data, err := client.DoRequest("GET", uri, nil) 319 | if err != nil { 320 | return nil, err 321 | } 322 | version := &Version{} 323 | err = jsonUnmarshal(data, version) 324 | if err != nil { 325 | return nil, err 326 | } 327 | return version, nil 328 | } 329 | 330 | func (client *Client) PullImage(name string, auth *AuthConfig) error { 331 | v := url.Values{} 332 | v.Set("fromImage", name) 333 | uri := fmt.Sprintf("/%s/images/create?%s", APIVersion, v.Encode()) 334 | req, err := http.NewRequest("POST", client.URL.String()+uri, nil) 335 | if auth != nil { 336 | req.Header.Add("X-Registry-Auth", auth.encode()) 337 | } 338 | resp, err := client.HTTPClient.Do(req) 339 | if err != nil { 340 | return err 341 | } 342 | defer resp.Body.Close() 343 | var finalObj map[string]interface{} 344 | for decoder := json.NewDecoder(resp.Body); err == nil; err = decoder.Decode(&finalObj) { 345 | } 346 | if err != io.EOF { 347 | return err 348 | } 349 | if err, ok := finalObj["error"]; ok { 350 | return fmt.Errorf("%v", err) 351 | } 352 | return nil 353 | } 354 | 355 | func (client *Client) LoadImage(reader io.Reader) error { 356 | uri := fmt.Sprintf("/%s/images/load", APIVersion) 357 | req, err := http.NewRequest("POST", client.URL.String()+uri, reader) 358 | if err != nil { 359 | return err 360 | } 361 | req.Header.Add("Content-Type", "application/json") 362 | resp, err := client.HTTPClient.Do(req) 363 | if err != nil { 364 | return err 365 | } 366 | defer resp.Body.Close() 367 | if resp.StatusCode >= 400 { 368 | data, err := ioutil.ReadAll(resp.Body) 369 | if err != nil { 370 | return err 371 | } 372 | return &Error{StatusCode: resp.StatusCode, Status: resp.Status, msg: string(data)} 373 | } 374 | return nil 375 | } 376 | 377 | func (client *Client) RemoveContainer(id string, force, volumes bool) error { 378 | argForce := 0 379 | argVolumes := 0 380 | if force == true { 381 | argForce = 1 382 | } 383 | if volumes == true { 384 | argVolumes = 1 385 | } 386 | args := fmt.Sprintf("force=%d&v=%d", argForce, argVolumes) 387 | uri := fmt.Sprintf("/%s/containers/%s?%s", APIVersion, id, args) 388 | resp, err := client.DoRequest("DELETE", uri, nil) 389 | if err == nil { 390 | resp.Body.Close() 391 | } 392 | return err 393 | } 394 | 395 | func (client *Client) ListImages(all bool) ([]*Image, error) { 396 | vals := url.Values{} 397 | if all { 398 | vals.Set("all", "1") 399 | } 400 | uri := fmt.Sprintf("/%s/images/json", APIVersion) 401 | if len(vals) > 0 { 402 | uri = uri + "?" + vals.Encode() 403 | } 404 | data, err := client.DoRequest("GET", uri, nil) 405 | if err != nil { 406 | return nil, err 407 | } 408 | var images []*Image 409 | if err := jsonUnmarshal(data, &images); err != nil { 410 | return nil, err 411 | } 412 | return images, nil 413 | } 414 | 415 | func (client *Client) RemoveImage(name string) ([]*ImageDelete, error) { 416 | uri := fmt.Sprintf("/%s/images/%s", APIVersion, name) 417 | data, err := client.DoRequest("DELETE", uri, nil) 418 | if err != nil { 419 | return nil, err 420 | } 421 | var imageDelete []*ImageDelete 422 | if err := jsonUnmarshal(data, &imageDelete); err != nil { 423 | return nil, err 424 | } 425 | return imageDelete, nil 426 | } 427 | 428 | func (client *Client) PauseContainer(id string) error { 429 | uri := fmt.Sprintf("/%s/containers/%s/pause", APIVersion, id) 430 | resp, err := client.DoRequest("POST", uri, nil) 431 | if err == nil { 432 | resp.Body.Close() 433 | } 434 | return err 435 | } 436 | func (client *Client) UnpauseContainer(id string) error { 437 | uri := fmt.Sprintf("/%s/containers/%s/unpause", APIVersion, id) 438 | resp, err := client.DoRequest("POST", uri, nil) 439 | if err == nil { 440 | resp.Body.Close() 441 | } 442 | return err 443 | } 444 | 445 | func (client *Client) Exec(config *ExecConfig) (string, error) { 446 | data, err := json.Marshal(config) 447 | if err != nil { 448 | return "", err 449 | } 450 | uri := fmt.Sprintf("/containers/%s/exec", config.Container) 451 | resp, err := client.DoRequest("POST", uri, bytes.NewReader(data)) 452 | if err != nil { 453 | return "", err 454 | } 455 | var createExecResp struct { 456 | Id string 457 | } 458 | if err = jsonUnmarshal(resp, &createExecResp); err != nil { 459 | return "", err 460 | } 461 | uri = fmt.Sprintf("/exec/%s/start", createExecResp.Id) 462 | resp, err = client.DoRequest("POST", uri, bytes.NewReader(data)) 463 | if err != nil { 464 | return "", err 465 | } 466 | resp.Body.Close() 467 | return createExecResp.Id, nil 468 | } 469 | 470 | func (client *Client) InspectImage(name string) (ImageInfo, error) { 471 | uri := fmt.Sprintf("/images/%s/json", name) 472 | resp, err := client.DoRequest("GET", uri, nil) 473 | if err != nil { 474 | return ImageInfo{}, err 475 | } 476 | img := ImageInfo{} 477 | if err := jsonUnmarshal(resp, &img); err != nil { 478 | return ImageInfo{}, fmt.Errorf("docker: InspectImage: %v", err) 479 | } 480 | return img, nil 481 | } 482 | 483 | func (client *Client) History(id string) ([]ImageLayer, error) { 484 | uri := fmt.Sprintf("%s/images/%s/history", APIVersion, id) 485 | data, err := client.DoRequest("GET", uri, nil) 486 | if err != nil { 487 | return nil, err 488 | } 489 | layers := []ImageLayer{} 490 | err = jsonUnmarshal(data, &layers) 491 | if err != nil { 492 | return nil, err 493 | } 494 | return layers, nil 495 | } 496 | 497 | func (client *Client) Commit(options *CommitOptions, config *ContainerConfig) (string, error) { 498 | 499 | values := url.Values{} 500 | add := func(name, val string) { 501 | if val != "" { 502 | values.Add(name, val) 503 | } 504 | } 505 | add("container", options.Container) 506 | add("repo", options.Repo) 507 | add("tag", options.Tag) 508 | add("comment", options.Comment) 509 | add("author", options.Author) 510 | 511 | data, err := json.Marshal(&config) 512 | if err != nil { 513 | return "", err 514 | } 515 | uri := "/commit?" + values.Encode() 516 | resp, err := client.DoRequest("POST", uri, bytes.NewReader(data)) 517 | if err != nil { 518 | return "", err 519 | } 520 | var commitResp struct { 521 | Id string 522 | } 523 | if err = jsonUnmarshal(resp, &commitResp); err != nil { 524 | return "", fmt.Errorf("docker: commit: %v", err) 525 | } 526 | if commitResp.Id == "" { 527 | return "", fmt.Errorf("docker: commit: response did not have Id field") 528 | } 529 | return commitResp.Id, nil 530 | } 531 | 532 | func (client *Client) Tag(imgId string, ops *TagOptions) error { 533 | values := url.Values{} 534 | if ops.Repo != "" { 535 | values.Add("repo", ops.Repo) 536 | } 537 | if ops.Tag != "" { 538 | values.Add("tag", ops.Tag) 539 | } 540 | values.Add("force", strconv.FormatBool(ops.Force)) 541 | // urlencode image id 542 | imgId = (&url.URL{Path: imgId}).String() 543 | 544 | uri := fmt.Sprintf("/images/%s/tag?%s", imgId, values.Encode()) 545 | resp, err := client.DoRequest("POST", uri, nil) 546 | if err == nil { 547 | resp.Body.Close() 548 | } 549 | return err 550 | } 551 | 552 | // Changes provides a list of changes made to a container. 553 | func (client *Client) Changes(cid string) ([]ContainerChange, error) { 554 | uri := fmt.Sprintf("/containers/%s/changes", cid) 555 | resp, err := client.DoRequest("GET", uri, nil) 556 | var changes []ContainerChange 557 | if err != nil { 558 | return changes, err 559 | } 560 | if err = jsonUnmarshal(resp, &changes); err != nil { 561 | return changes, fmt.Errorf("docker: changes: %v", err) 562 | } 563 | return changes, nil 564 | } 565 | 566 | // TarReader wraps tar.Reader with a close method. 567 | type TarReader struct { 568 | *tar.Reader 569 | c io.Closer 570 | } 571 | 572 | func (tr TarReader) Close() error { 573 | return tr.c.Close() 574 | } 575 | 576 | // Copy copies files or folders from a container. 577 | // It is the caller's responsiblity to call Close on returned TarReader. 578 | func (client *Client) Copy(cid, resource string) (TarReader, error) { 579 | data, err := json.Marshal(map[string]string{"Resource": resource}) 580 | if err != nil { 581 | return TarReader{}, err 582 | } 583 | 584 | uri := fmt.Sprintf("/containers/%s/copy", cid) 585 | 586 | resp, err := client.DoRequest("POST", uri, bytes.NewReader(data)) 587 | if err != nil { 588 | return TarReader{}, err 589 | } 590 | return TarReader{ 591 | tar.NewReader(resp.Body), 592 | resp.Body, 593 | }, nil 594 | } 595 | 596 | // Wait blocks until a container has exited. Wait returns the StatusCode of the 597 | // exited process. 598 | func (client *Client) Wait(cid string) (int, error) { 599 | uri := fmt.Sprintf("/containers/%s/wait", cid) 600 | resp, err := client.DoRequest("POST", uri, nil) 601 | if err != nil { 602 | return 0, err 603 | } 604 | waitResp := struct { 605 | StatusCode int 606 | }{-1} 607 | if err := jsonUnmarshal(resp, &waitResp); err != nil { 608 | return 0, fmt.Errorf("docker: wait: %v", err) 609 | } 610 | return waitResp.StatusCode, nil 611 | } 612 | 613 | // Attach returns the stdout and stderr stream of a stopped or running 614 | // container. It is the callers responsibility to close the returned stream. 615 | // Use SplitStream to parse stdout and stderr. 616 | func (client *Client) Attach(cid string, options *AttachOptions) (io.ReadCloser, error) { 617 | values := url.Values{} 618 | add := func(name string, val bool) { 619 | values.Add(name, strconv.FormatBool(val)) 620 | } 621 | add("logs", options.Logs) 622 | add("stream", options.Stream) 623 | add("stdin", options.Stdin) 624 | add("stdout", options.Stdout) 625 | add("stderr", options.Stderr) 626 | 627 | p := fmt.Sprintf("/containers/%s/attach?%s", cid, values.Encode()) 628 | req, err := http.NewRequest("POST", client.URL.String()+p, nil) 629 | if err != nil { 630 | return nil, fmt.Errorf("could not construct request to docker") 631 | } 632 | resp, err := client.HTTPClient.Do(req) 633 | if err != nil { 634 | if !strings.Contains(err.Error(), "connection refused") && client.TLSConfig == nil { 635 | return nil, fmt.Errorf("%v. Are you trying to connect to a TLS-enabled daemon without TLS?", err) 636 | } 637 | return nil, err 638 | } 639 | if resp.StatusCode == http.StatusOK { 640 | return resp.Body, nil 641 | } 642 | 643 | resp.Body.Close() 644 | switch resp.StatusCode { 645 | case http.StatusSwitchingProtocols: 646 | return nil, fmt.Errorf("docker: attach: did not send websocket request but got 101 back") 647 | case http.StatusBadRequest: 648 | // Probably shouldn't get here 649 | return nil, fmt.Errorf("docker: attach: invalid request parameters") 650 | case http.StatusInternalServerError: 651 | return nil, fmt.Errorf("docker: attach: internal server error") 652 | default: 653 | return nil, fmt.Errorf("docker: attach: unexpected status code %s", resp.Status) 654 | } 655 | } 656 | 657 | const ( 658 | StdinStream byte = 0 659 | StdoutStream = 1 660 | StderrStream = 2 661 | ) 662 | 663 | // SplitStream splits docker stream data into stdout and stderr. 664 | // For specifications see http://goo.gl/Dnbcye 665 | func SplitStream(stream io.Reader, stdout, stderr io.Writer) error { 666 | header := make([]byte, 8) 667 | for { 668 | if _, err := io.ReadFull(stream, header); err != nil { 669 | if err == io.EOF { 670 | return nil 671 | } else { 672 | return fmt.Errorf("could not read header: %v", err) 673 | } 674 | } 675 | 676 | var dest io.Writer 677 | switch header[0] { 678 | case StdinStream, StdoutStream: 679 | dest = stdout 680 | case StderrStream: 681 | dest = stderr 682 | default: 683 | return fmt.Errorf("bad STREAM_TYPE given: %x", header[0]) 684 | } 685 | 686 | frameSize := int64(binary.BigEndian.Uint32(header[4:])) 687 | if _, err := io.CopyN(dest, stream, frameSize); err != nil { 688 | return fmt.Errorf("copying stream payload failed: %v", err) 689 | } 690 | } 691 | } 692 | 693 | func (client *Client) MonitorStats(id string) (*Stats, error) { 694 | 695 | uri := fmt.Sprintf("%s/containers/%s/stats?stream=0", client.URL.String(), id) 696 | resp, err := client.HTTPClient.Get(uri) 697 | if err != nil { 698 | return nil, err 699 | } 700 | defer resp.Body.Close() 701 | if resp.StatusCode >= 400 { 702 | data, err := ioutil.ReadAll(resp.Body) 703 | if err != nil { 704 | return nil, err 705 | } 706 | return nil, &Error{StatusCode: resp.StatusCode, 707 | Status: resp.Status, msg: string(data)} 708 | } 709 | var stats *Stats 710 | dec := json.NewDecoder(resp.Body) 711 | if err := dec.Decode(&stats); err != nil { 712 | return nil, fmt.Errorf("could not decode stats: %v", err) 713 | } 714 | return stats, nil 715 | } 716 | -------------------------------------------------------------------------------- /docker_darwin.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "time" 11 | ) 12 | 13 | // NewDefaultClient provides an arch specific default docker client. 14 | // On linux it connects to the default docker socket, on OS X it looks for 15 | // boot2docker environment variables. 16 | func NewDefaultClient(timeout time.Duration) (*Client, error) { 17 | host := os.Getenv("DOCKER_HOST") 18 | if host == "" { 19 | return nil, fmt.Errorf("DOCKER_HOST environment variable not set") 20 | } 21 | certPath := os.Getenv("DOCKER_CERT_PATH") 22 | if certPath == "" { 23 | return NewClient(host, nil, timeout) 24 | } 25 | 26 | tlsConfig := tls.Config{} 27 | tlsConfig.InsecureSkipVerify = true 28 | 29 | tlsVerify := os.Getenv("DOCKER_TLS_VERIFY") 30 | if tlsVerify == "1" { 31 | certPool := x509.NewCertPool() 32 | ca := filepath.Join(certPath, "ca.pem") 33 | file, err := ioutil.ReadFile(ca) 34 | if err != nil { 35 | return nil, fmt.Errorf("Couldn't read ca cert %s: %s", ca, err) 36 | } 37 | certPool.AppendCertsFromPEM(file) 38 | tlsConfig.RootCAs = certPool 39 | tlsConfig.InsecureSkipVerify = false 40 | certFile := filepath.Join(certPath, "cert.pem") 41 | keyFile := filepath.Join(certPath, "key.pem") 42 | 43 | cert, err := tls.LoadX509KeyPair(certFile, keyFile) 44 | if err != nil { 45 | return nil, fmt.Errorf("Couldn't load X509 key pair: %v", err) 46 | } 47 | tlsConfig.Certificates = []tls.Certificate{cert} 48 | // Avoid fallback to SSL protocols < TLS1.0 49 | tlsConfig.MinVersion = tls.VersionTLS10 50 | } 51 | 52 | return NewClient(host, &tlsConfig, timeout) 53 | } 54 | -------------------------------------------------------------------------------- /docker_linux.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import "time" 4 | 5 | // NewDefaultClient provides an arch specific default docker client. 6 | // On linux it connects to the default docker socket, on OS X it looks for 7 | // boot2docker environment variables. 8 | func NewDefaultClient(timeout time.Duration) (*Client, error) { 9 | dockerHost := "unix:///var/run/docker.sock" 10 | return NewClient(dockerHost, nil, timeout) 11 | } 12 | -------------------------------------------------------------------------------- /docker_test.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "net/http" 10 | "os" 11 | "reflect" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | var TestClientTimeout = time.Second 17 | 18 | func newClient(t *testing.T) *Client { 19 | t.Parallel() 20 | cli, err := NewDefaultClient(TestClientTimeout) 21 | if err != nil { 22 | t.Fatalf("could not create client: %v", err) 23 | } 24 | return cli 25 | } 26 | 27 | func randName(t *testing.T) string { 28 | randBytes := make([]byte, 16) 29 | if _, err := rand.Read(randBytes); err != nil { 30 | t.Fatalf("could not read random bytes: %v", err) 31 | } 32 | return fmt.Sprintf("%x", randBytes) 33 | } 34 | 35 | func TestInfo(t *testing.T) { 36 | cli := newClient(t) 37 | _, err := cli.Info() 38 | if err != nil { 39 | t.Fatalf("could not get info: %v", err) 40 | } 41 | } 42 | 43 | func TestListImages(t *testing.T) { 44 | cli := newClient(t) 45 | imgs, err := cli.ListImages(false) 46 | if err != nil { 47 | t.Fatalf("could not list docker images: %v", err) 48 | } 49 | allImgs, err := cli.ListImages(true) 50 | if err != nil { 51 | t.Fatalf("could not list all docker images: %v", err) 52 | } 53 | // we expect using all=true to return more images than when it's not turned on 54 | if len(imgs) >= len(allImgs) { 55 | t.Errorf("allimages=true returned less than false, %d vs %d", len(allImgs), len(imgs)) 56 | } 57 | } 58 | 59 | func TestListContainers(t *testing.T) { 60 | cli := newClient(t) 61 | config := ContainerConfig{ 62 | Cmd: []string{"echo", "hi"}, 63 | } 64 | name := randName(t) 65 | cid, err := cli.CreateContainer(&config, name) 66 | if err != nil { 67 | t.Fatalf("could not create container: %v", err) 68 | } 69 | defer func(cid string) { 70 | if err := cli.RemoveContainer(cid, true, false); err != nil { 71 | t.Errorf("could not remove container: %v", err) 72 | } 73 | }(cid) 74 | 75 | containers, err := cli.ListContainers(true, false, "") 76 | if err != nil { 77 | t.Errorf("could not list containers: %v", err) 78 | return 79 | } 80 | found := func() bool { 81 | for _, container := range containers { 82 | if container.Id == cid { 83 | return true 84 | } 85 | } 86 | return false 87 | }() 88 | if !found { 89 | t.Errorf("container '%s' not listed", name) 90 | } 91 | } 92 | 93 | func TestInspectContainer(t *testing.T) { 94 | cli := newClient(t) 95 | _, err := cli.InspectContainer("idonotexist") 96 | if err == nil { 97 | t.Fatalf("expected ErrNotFound, got: %v", err) 98 | } 99 | dockerErr, ok := err.(*Error) 100 | if !ok { 101 | t.Fatalf("expected error to be of type *docker.Error, got %s", reflect.TypeOf(err)) 102 | } 103 | if dockerErr.StatusCode != http.StatusNotFound { 104 | t.Fatalf("was able to inspect a non existent container") 105 | } 106 | } 107 | 108 | func TestCreateContainer(t *testing.T) { 109 | cli := newClient(t) 110 | name := randName(t) 111 | config := &ContainerConfig{Cmd: []string{"echo", "hi"}} 112 | cid, err := cli.CreateContainer(config, name) 113 | if err != nil { 114 | t.Fatalf("could not create container: %v", err) 115 | } 116 | if err := cli.RemoveContainer(cid, true, false); err != nil { 117 | t.Errorf("could not remove container: %v", err) 118 | } 119 | } 120 | 121 | func TestSplitStream(t *testing.T) { 122 | cli := newClient(t) 123 | name := randName(t) 124 | n := 10 << 10 // 10GiB 125 | randBytes := make([]byte, n) 126 | tempFile, err := ioutil.TempFile("", "go-docker") 127 | if err != nil { 128 | t.Fatalf("could not create temp file: %v", err) 129 | } 130 | p := tempFile.Name() 131 | defer os.Remove(p) 132 | if _, err := tempFile.Write(randBytes); err != nil { 133 | t.Errorf("error writing to temp file: %v", err) 134 | return 135 | } 136 | 137 | dest := "/tmp/foo.txt" 138 | hostConfig := HostConfig{Binds: []string{p + ":" + dest}} 139 | config := &ContainerConfig{ 140 | Image: "ubuntu:trusty", 141 | Cmd: []string{"cat", dest}, 142 | Volumes: map[string]struct{}{dest: {}}, 143 | HostConfig: hostConfig, 144 | } 145 | 146 | cid, err := cli.CreateContainer(config, name) 147 | if err != nil { 148 | t.Errorf("could not create container: %v", err) 149 | return 150 | } 151 | defer func() { 152 | if err := cli.RemoveContainer(cid, true, false); err != nil { 153 | t.Errorf("could not remove container: %v", err) 154 | } 155 | }() 156 | o := &AttachOptions{ 157 | Stream: true, 158 | Stdout: true, 159 | Stderr: true, 160 | } 161 | stream, err := cli.Attach(cid, o) 162 | if err != nil { 163 | t.Errorf("could not attach to container: %v", err) 164 | return 165 | } 166 | stdout := bytes.NewBuffer([]byte{}) 167 | stderr := bytes.NewBuffer([]byte{}) 168 | streamErr := make(chan error, 1) 169 | go func() { 170 | streamErr <- SplitStream(stream, stdout, stderr) 171 | }() 172 | 173 | if err := cli.StartContainer(cid, &hostConfig); err != nil { 174 | t.Errorf("could not start container: %v", err) 175 | return 176 | } 177 | rc, err := cli.Wait(cid) 178 | if err != nil { 179 | t.Errorf("error waiting for container: %v", err) 180 | return 181 | } 182 | if rc != 0 { 183 | t.Errorf("non zero return code: %d", rc) 184 | return 185 | } 186 | if err := <-streamErr; err != nil { 187 | t.Errorf("could not split stream: %v", err) 188 | return 189 | } 190 | 191 | stdoutBytes := stdout.Bytes() 192 | stderrBytes := stderr.Bytes() 193 | if n := len(stderrBytes); n != 0 { 194 | t.Errorf("did not expect any bytes to stderr, got: %d", n) 195 | } 196 | 197 | if bytes.Compare(stdoutBytes, randBytes) != 0 { 198 | t.Errorf("stdout bytes were not identical to rand bytes") 199 | } 200 | } 201 | 202 | func TestCommit(t *testing.T) { 203 | cli := newClient(t) 204 | name1 := randName(t) 205 | name2 := randName(t) 206 | 207 | config := &ContainerConfig{ 208 | Image: "ubuntu:trusty", 209 | Cmd: []string{"touch", "/tmp/foo"}, 210 | } 211 | 212 | cid, err := cli.CreateContainer(config, name1) 213 | if err != nil { 214 | t.Errorf("could not create container: %v", err) 215 | return 216 | } 217 | defer func(cid string) { 218 | if err := cli.RemoveContainer(cid, true, false); err != nil { 219 | t.Errorf("could not remove container: %v", err) 220 | } 221 | }(cid) 222 | startWait := func(cid string) error { 223 | if err := cli.StartContainer(cid, &HostConfig{}); err != nil { 224 | return fmt.Errorf("could not start container: %v", err) 225 | } 226 | rc, err := cli.Wait(cid) 227 | if err != nil { 228 | return fmt.Errorf("error waiting for container: %v", err) 229 | } 230 | if rc != 0 { 231 | return fmt.Errorf("non zero return code: %d", rc) 232 | } 233 | return nil 234 | } 235 | if err := startWait(cid); err != nil { 236 | t.Error(err) 237 | return 238 | } 239 | ops := &CommitOptions{ 240 | Container: cid, 241 | Repo: "go-docker", 242 | Tag: "TestCommit", 243 | } 244 | id, err := cli.Commit(ops, config) 245 | if err != nil { 246 | t.Errorf("could not commit container: %v", err) 247 | return 248 | } 249 | 250 | defer func(id string) { 251 | if _, err := cli.RemoveImage(id); err != nil { 252 | t.Errorf("could not remove container: %v", err) 253 | } 254 | }(id) 255 | 256 | config2 := &ContainerConfig{ 257 | Image: id, 258 | Cmd: []string{"cat", "/tmp/foo"}, 259 | } 260 | cid, err = cli.CreateContainer(config2, name2) 261 | if err != nil { 262 | t.Errorf("could not create container: %v", err) 263 | return 264 | } 265 | defer func(cid string) { 266 | if err := cli.RemoveContainer(cid, true, false); err != nil { 267 | t.Errorf("could not remove container: %v", err) 268 | } 269 | }(cid) 270 | 271 | if err := startWait(cid); err != nil { 272 | t.Error(err) 273 | } 274 | } 275 | 276 | func TestChanges(t *testing.T) { 277 | cli := newClient(t) 278 | name1 := randName(t) 279 | 280 | config := &ContainerConfig{ 281 | Image: "ubuntu:trusty", 282 | Cmd: []string{ 283 | "/bin/bash", "-c", 284 | "touch /tmp/foo && rm /etc/debian_version && touch /etc/passwd", 285 | }, 286 | } 287 | 288 | cid, err := cli.CreateContainer(config, name1) 289 | if err != nil { 290 | t.Errorf("could not create container: %v", err) 291 | return 292 | } 293 | defer func(cid string) { 294 | if err := cli.RemoveContainer(cid, true, false); err != nil { 295 | t.Errorf("could not remove container: %v", err) 296 | } 297 | }(cid) 298 | startWait := func(cid string) error { 299 | if err := cli.StartContainer(cid, &HostConfig{}); err != nil { 300 | return fmt.Errorf("could not start container: %v", err) 301 | } 302 | rc, err := cli.Wait(cid) 303 | if err != nil { 304 | return fmt.Errorf("error waiting for container: %v", err) 305 | } 306 | if rc != 0 { 307 | return fmt.Errorf("non zero return code: %d", rc) 308 | } 309 | return nil 310 | } 311 | if err := startWait(cid); err != nil { 312 | t.Error(err) 313 | return 314 | } 315 | changes, err := cli.Changes(cid) 316 | if err != nil { 317 | t.Errorf("could not get changes for container: %v", err) 318 | return 319 | } 320 | 321 | // Make sure we we all the appropriate changes. 322 | if len(changes) != 5 { 323 | t.Errorf("5 changes expected, %d returned: %v", len(changes), changes) 324 | } 325 | 326 | // Deleted /etc/debian_version 327 | if changes[0].Kind != ChangeModify || changes[0].Path != "/etc" { 328 | t.Errorf("/etc not modified") 329 | } 330 | if changes[1].Kind != ChangeDelete || changes[1].Path != "/etc/debian_version" { 331 | t.Errorf("/etc/debian_version not deleted") 332 | } 333 | 334 | // Modified /etc/passwd 335 | if changes[2].Kind != ChangeModify || changes[2].Path != "/etc/passwd" { 336 | t.Errorf("/etc/passwd not modified") 337 | } 338 | 339 | // Created /tmp/foo 340 | if changes[3].Kind != ChangeModify || changes[3].Path != "/tmp" { 341 | t.Errorf("/tmp not modified") 342 | } 343 | if changes[4].Kind != ChangeAdd || changes[4].Path != "/tmp/foo" { 344 | t.Errorf("/tmp/foo not created") 345 | } 346 | } 347 | 348 | func TestCopy(t *testing.T) { 349 | cli := newClient(t) 350 | name1 := randName(t) 351 | 352 | contents := "foobarbaz" 353 | filename := "/tmp/xyzzy" 354 | config := &ContainerConfig{ 355 | Image: "ubuntu:trusty", 356 | Cmd: []string{ 357 | "/bin/bash", "-c", 358 | fmt.Sprintf("echo %s > %s", contents, filename), 359 | }, 360 | } 361 | 362 | cid, err := cli.CreateContainer(config, name1) 363 | if err != nil { 364 | t.Errorf("could not create container: %v", err) 365 | return 366 | } 367 | defer func(cid string) { 368 | if err := cli.RemoveContainer(cid, true, false); err != nil { 369 | t.Errorf("could not remove container: %v", err) 370 | } 371 | }(cid) 372 | startWait := func(cid string) error { 373 | if err := cli.StartContainer(cid, &HostConfig{}); err != nil { 374 | return fmt.Errorf("could not start container: %v", err) 375 | } 376 | rc, err := cli.Wait(cid) 377 | if err != nil { 378 | return fmt.Errorf("error waiting for container: %v", err) 379 | } 380 | if rc != 0 { 381 | return fmt.Errorf("non zero return code: %d", rc) 382 | } 383 | return nil 384 | } 385 | if err := startWait(cid); err != nil { 386 | t.Error(err) 387 | return 388 | } 389 | tarReader, err := cli.Copy(cid, filename) 390 | if err != nil { 391 | t.Errorf("could not get changes for container: %v", err) 392 | return 393 | } 394 | 395 | // Make sure we have the right contents. 396 | hdr, err := tarReader.Next() 397 | if err == io.EOF { 398 | t.Errorf("%s not found in tar archive", filename) 399 | } 400 | if err != nil { 401 | t.Error(err) 402 | } 403 | 404 | if hdr.Name != "xyzzy" { 405 | t.Errorf("expected file name xyzzy, got %s", hdr.Name) 406 | } 407 | 408 | b := make([]byte, len(contents)) 409 | n, err := tarReader.Read(b) 410 | if err != nil { 411 | t.Error(err) 412 | } 413 | if n != len(contents) { 414 | t.Errorf("content length = %d, expected %d", n, len(contents)) 415 | } 416 | 417 | bStr := string(b) 418 | if bStr != contents { 419 | t.Errorf("expected contents %v, got %v", contents, bStr) 420 | } 421 | } 422 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import "time" 4 | 5 | type ChangeType int 6 | 7 | const ( 8 | ChangeModify = iota 9 | ChangeAdd 10 | ChangeDelete 11 | ) 12 | 13 | type ContainerConfig struct { 14 | Hostname string 15 | Domainname string 16 | User string 17 | Memory int64 18 | MemorySwap int64 19 | CpuShares int64 20 | Cpuset string 21 | AttachStdin bool 22 | AttachStdout bool 23 | AttachStderr bool 24 | PortSpecs []string 25 | ExposedPorts map[string]struct{} 26 | Tty bool 27 | OpenStdin bool 28 | StdinOnce bool 29 | Env []string 30 | Cmd []string 31 | Image string 32 | Volumes map[string]struct{} 33 | WorkingDir string 34 | Entrypoint []string 35 | NetworkDisabled bool 36 | OnBuild []string 37 | 38 | // This is used only by the create command 39 | HostConfig HostConfig 40 | } 41 | 42 | type HostConfig struct { 43 | Binds []string 44 | ContainerIDFile string 45 | LxcConf []map[string]string 46 | Privileged bool 47 | PortBindings map[string][]PortBinding 48 | Links []string 49 | PublishAllPorts bool 50 | Dns []string 51 | DnsSearch []string 52 | VolumesFrom []string 53 | NetworkMode string 54 | RestartPolicy RestartPolicy 55 | Memory int64 56 | MemorySwap int64 57 | CpuShares int64 58 | CpuPeriod int64 59 | CpusetCpus string 60 | CpusetMems string 61 | } 62 | 63 | type ExecConfig struct { 64 | AttachStdin bool 65 | AttachStdout bool 66 | AttachStderr bool 67 | Tty bool 68 | Cmd []string 69 | Container string 70 | Detach bool 71 | } 72 | 73 | type LogOptions struct { 74 | Follow bool 75 | Stdout bool 76 | Stderr bool 77 | Timestamps bool 78 | Tail int64 79 | } 80 | 81 | type RestartPolicy struct { 82 | Name string 83 | MaximumRetryCount int64 84 | } 85 | 86 | type PortBinding struct { 87 | HostIp string 88 | HostPort string 89 | } 90 | 91 | type ContainerInfo struct { 92 | Id string 93 | Created string 94 | Path string 95 | Name string 96 | Args []string 97 | ExecIDs []string 98 | Config *ContainerConfig 99 | State struct { 100 | Running bool 101 | Paused bool 102 | Restarting bool 103 | Pid int 104 | ExitCode int 105 | StartedAt time.Time 106 | FinishedAt time.Time 107 | Ghost bool 108 | } 109 | Image string 110 | NetworkSettings struct { 111 | IpAddress string 112 | IpPrefixLen int 113 | Gateway string 114 | Bridge string 115 | Ports map[string][]PortBinding 116 | } 117 | SysInitPath string 118 | ResolvConfPath string 119 | Volumes map[string]string 120 | HostConfig *HostConfig 121 | } 122 | 123 | type Port struct { 124 | IP string 125 | PrivatePort int 126 | PublicPort int 127 | Type string 128 | } 129 | 130 | type Container struct { 131 | Id string 132 | Names []string 133 | Image string 134 | Command string 135 | Created int64 136 | Status string 137 | Ports []Port 138 | SizeRw int64 139 | SizeRootFs int64 140 | } 141 | 142 | type Event struct { 143 | Id string 144 | Status string 145 | From string 146 | Time int64 147 | } 148 | 149 | type Version struct { 150 | Version string 151 | GitCommit string 152 | GoVersion string 153 | } 154 | 155 | type RespContainersCreate struct { 156 | Id string 157 | Warnings []string 158 | } 159 | 160 | type Image struct { 161 | Created int64 162 | Id string 163 | ParentId string 164 | RepoTags []string 165 | Size int64 166 | VirtualSize int64 167 | } 168 | 169 | //returned by History 170 | type ImageLayer struct { 171 | Id string 172 | Created int64 173 | CreatedBy string 174 | Comment string 175 | Size int64 176 | Tags []string 177 | } 178 | 179 | // returned by InspectImage 180 | type ImageInfo struct { 181 | Created string 182 | Container string 183 | Id string 184 | Parent string 185 | Size int 186 | } 187 | 188 | type Info struct { 189 | ID string 190 | Containers int64 191 | DockerRootDir string 192 | Driver string 193 | DriverStatus [][]string 194 | ExecutionDriver string 195 | Images int64 196 | KernelVersion string 197 | OperatingSystem string 198 | NCPU int64 199 | MemTotal int64 200 | Name string 201 | Labels []string 202 | } 203 | 204 | type ImageDelete struct { 205 | Deleted string 206 | Untagged string 207 | } 208 | 209 | type AttachOptions struct { 210 | Logs bool 211 | Stream bool 212 | Stdin bool 213 | Stdout bool 214 | Stderr bool 215 | } 216 | 217 | type CommitOptions struct { 218 | Container string 219 | Repo string 220 | Tag string 221 | Comment string 222 | Author string 223 | } 224 | 225 | type ContainerChange struct { 226 | Kind int 227 | Path string 228 | } 229 | 230 | type TagOptions struct { 231 | Repo string 232 | Force bool 233 | Tag string 234 | } 235 | 236 | type Stats struct { 237 | Read time.Time `json:"read"` 238 | NetworkStats NetworkStats `json:"network,omitempty"` 239 | CpuStats CpuStats `json:"cpu_stats,omitempty"` 240 | MemoryStats MemoryStats `json:"memory_stats,omitempty"` 241 | BlkioStats BlkioStats `json:"blkio_stats,omitempty"` 242 | } 243 | 244 | type NetworkStats struct { 245 | RxBytes uint64 `json:"rx_bytes"` 246 | RxPackets uint64 `json:"rx_packets"` 247 | RxErrors uint64 `json:"rx_errors"` 248 | RxDropped uint64 `json:"rx_dropped"` 249 | TxBytes uint64 `json:"tx_bytes"` 250 | TxPackets uint64 `json:"tx_packets"` 251 | TxErrors uint64 `json:"tx_errors"` 252 | TxDropped uint64 `json:"tx_dropped"` 253 | } 254 | 255 | type CpuStats struct { 256 | CpuUsage CpuUsage `json:"cpu_usage"` 257 | SystemUsage uint64 `json:"system_cpu_usage"` 258 | ThrottlingData ThrottlingData `json:"throttling_data,omitempty"` 259 | } 260 | 261 | type MemoryStats struct { 262 | Usage uint64 `json:"usage"` 263 | MaxUsage uint64 `json:"max_usage"` 264 | Stats MemDetails `json:"stats"` 265 | Failcnt uint64 `json:"failcnt"` 266 | Limit uint64 `json:"limit"` 267 | } 268 | 269 | type MemDetails struct { 270 | TotalPgmajFault uint64 `json:"total_pgmajfault"` 271 | Cache uint64 `json:"cache"` 272 | MappedFile uint64 `json:"mapped_file"` 273 | TotalInactiveFile uint64 `json:"total_inactive_file"` 274 | PgpgOut uint64 `json:"pgpgout"` 275 | Rss uint64 `json:"rss"` 276 | TotalMappedFile uint64 `json:"total_mapped_file"` 277 | Writeback uint64 `json:"writeback"` 278 | Unevictable uint64 `json:"unevictable"` 279 | PgpgIn uint64 `json:"pgpgin"` 280 | TotalUnevictable uint64 `json:"total_unevictable"` 281 | PgmajFault uint64 `json:"pgmajfault"` 282 | TotalRss uint64 `json:"total_rss"` 283 | TotalRssHuge uint64 `json:"total_rss_huge"` 284 | TotalWriteback uint64 `json:"total_writeback"` 285 | TotalInactiveAnon uint64 `json:"total_inactive_anon"` 286 | RssHuge uint64 `json:"rss_huge"` 287 | HierarchicalMemoryLimit uint64 `json:"hierarchical_memory_limit"` 288 | TotalPgFault uint64 `json:"total_pgfault"` 289 | TotalActiveFile uint64 `json:"total_active_file"` 290 | ActiveAnon uint64 `json:"active_anon"` 291 | TotalActiveAnon uint64 `json:"total_active_anon"` 292 | TotalPgpgOut uint64 `json:"total_pgpgout"` 293 | TotalCache uint64 `json:"total_cache"` 294 | InactiveAnon uint64 `json:"inactive_anon"` 295 | ActiveFile uint64 `json:"active_file"` 296 | PgFault uint64 `json:"pgfault"` 297 | InactiveFile uint64 `json:"inactive_file"` 298 | TotalPgpgIn uint64 `json:"total_pgpgin"` 299 | } 300 | 301 | type BlkioStats struct { 302 | // number of bytes tranferred to and from the block device 303 | IoServiceBytesRecursive []BlkioStatEntry `json:"io_service_bytes_recursive"` 304 | IoServicedRecursive []BlkioStatEntry `json:"io_serviced_recursive"` 305 | IoQueuedRecursive []BlkioStatEntry `json:"io_queue_recursive"` 306 | IoServiceTimeRecursive []BlkioStatEntry `json:"io_service_time_recursive"` 307 | IoWaitTimeRecursive []BlkioStatEntry `json:"io_wait_time_recursive"` 308 | IoMergedRecursive []BlkioStatEntry `json:"io_merged_recursive"` 309 | IoTimeRecursive []BlkioStatEntry `json:"io_time_recursive"` 310 | SectorsRecursive []BlkioStatEntry `json:"sectors_recursive"` 311 | } 312 | 313 | type BlkioStatEntry struct { 314 | Major uint64 `json:"major"` 315 | Minor uint64 `json:"minor"` 316 | Op string `json:"op"` 317 | Value uint64 `json:"value"` 318 | } 319 | 320 | type CpuUsage struct { 321 | // Total CPU time consumed. 322 | // Units: nanoseconds. 323 | TotalUsage uint64 `json:"total_usage"` 324 | // Total CPU time consumed per core. 325 | // Units: nanoseconds. 326 | PercpuUsage []uint64 `json:"percpu_usage"` 327 | // Time spent by tasks of the cgroup in kernel mode. 328 | // Units: nanoseconds. 329 | UsageInKernelmode uint64 `json:"usage_in_kernelmode"` 330 | // Time spent by tasks of the cgroup in user mode. 331 | // Units: nanoseconds. 332 | UsageInUsermode uint64 `json:"usage_in_usermode"` 333 | } 334 | 335 | type ThrottlingData struct { 336 | // Number of periods with throttling active 337 | Periods uint64 `json:"periods"` 338 | // Number of periods when the container hit its throttling limit. 339 | ThrottledPeriods uint64 `json:"throttled_periods"` 340 | // Aggregate time the container was throttled for in nanoseconds. 341 | ThrottledTime uint64 `json:"throttled_time"` 342 | } 343 | --------------------------------------------------------------------------------