github.com/outbrain/consul@v1.4.5/agent/checks/docker.go (about) 1 package checks 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "net/http" 10 "net/url" 11 "strings" 12 13 "github.com/armon/circbuf" 14 "github.com/docker/go-connections/sockets" 15 ) 16 17 // DockerClient is a simplified client for the Docker Engine API 18 // to execute the health checks and avoid significant dependencies. 19 // It also consumes all data returned from the Docker API through 20 // a ring buffer with a fixed limit to avoid excessive resource 21 // consumption. 22 type DockerClient struct { 23 host string 24 scheme string 25 proto string 26 addr string 27 basepath string 28 maxbuf int64 29 client *http.Client 30 } 31 32 func NewDockerClient(host string, maxbuf int64) (*DockerClient, error) { 33 if host == "" { 34 host = DefaultDockerHost 35 } 36 37 proto, addr, basepath, err := ParseHost(host) 38 if err != nil { 39 return nil, err 40 } 41 42 transport := new(http.Transport) 43 sockets.ConfigureTransport(transport, proto, addr) 44 client := &http.Client{Transport: transport} 45 46 return &DockerClient{ 47 host: host, 48 scheme: "http", 49 proto: proto, 50 addr: addr, 51 basepath: basepath, 52 maxbuf: maxbuf, 53 client: client, 54 }, nil 55 } 56 57 func (c *DockerClient) Close() error { 58 if t, ok := c.client.Transport.(*http.Transport); ok { 59 t.CloseIdleConnections() 60 } 61 return nil 62 } 63 64 func (c *DockerClient) Host() string { 65 return c.host 66 } 67 68 // ParseHost verifies that the given host strings is valid. 69 // copied from github.com/docker/docker/client.go 70 func ParseHost(host string) (string, string, string, error) { 71 protoAddrParts := strings.SplitN(host, "://", 2) 72 if len(protoAddrParts) == 1 { 73 return "", "", "", fmt.Errorf("unable to parse docker host `%s`", host) 74 } 75 76 var basePath string 77 proto, addr := protoAddrParts[0], protoAddrParts[1] 78 if proto == "tcp" { 79 parsed, err := url.Parse("tcp://" + addr) 80 if err != nil { 81 return "", "", "", err 82 } 83 addr = parsed.Host 84 basePath = parsed.Path 85 } 86 return proto, addr, basePath, nil 87 } 88 89 func (c *DockerClient) call(method, uri string, v interface{}) (*circbuf.Buffer, int, error) { 90 req, err := http.NewRequest(method, uri, nil) 91 if err != nil { 92 return nil, 0, err 93 } 94 95 if c.proto == "unix" || c.proto == "npipe" { 96 // For local communications, it doesn't matter what the host is. We just 97 // need a valid and meaningful host name. (See #189) 98 req.Host = "docker" 99 } 100 101 req.URL.Host = c.addr 102 req.URL.Scheme = c.scheme 103 104 if v != nil { 105 var b bytes.Buffer 106 if err := json.NewEncoder(&b).Encode(v); err != nil { 107 return nil, 0, err 108 } 109 req.Body = ioutil.NopCloser(&b) 110 req.Header.Set("Content-Type", "application/json") 111 } 112 113 resp, err := c.client.Do(req) 114 if err != nil { 115 return nil, 0, err 116 } 117 defer resp.Body.Close() 118 119 b, err := circbuf.NewBuffer(c.maxbuf) 120 if err != nil { 121 return nil, 0, err 122 } 123 _, err = io.Copy(b, resp.Body) 124 return b, resp.StatusCode, err 125 } 126 127 func (c *DockerClient) CreateExec(containerID string, cmd []string) (string, error) { 128 data := struct { 129 AttachStdin bool 130 AttachStdout bool 131 AttachStderr bool 132 Tty bool 133 Cmd []string 134 }{ 135 AttachStderr: true, 136 AttachStdout: true, 137 Cmd: cmd, 138 } 139 140 uri := fmt.Sprintf("/containers/%s/exec", url.QueryEscape(containerID)) 141 b, code, err := c.call("POST", uri, data) 142 switch { 143 case err != nil: 144 return "", fmt.Errorf("create exec failed for container %s: %v", containerID, err) 145 case code == 201: 146 var resp struct{ Id string } 147 if err = json.NewDecoder(bytes.NewReader(b.Bytes())).Decode(&resp); err != nil { 148 return "", fmt.Errorf("create exec response for container %s cannot be parsed: %s", containerID, err) 149 } 150 return resp.Id, nil 151 case code == 404: 152 return "", fmt.Errorf("create exec failed for unknown container %s", containerID) 153 case code == 409: 154 return "", fmt.Errorf("create exec failed since container %s is paused or stopped", containerID) 155 default: 156 return "", fmt.Errorf("create exec failed for container %s with status %d: %s", containerID, code, b) 157 } 158 } 159 160 func (c *DockerClient) StartExec(containerID, execID string) (*circbuf.Buffer, error) { 161 data := struct{ Detach, Tty bool }{Detach: false, Tty: true} 162 uri := fmt.Sprintf("/exec/%s/start", execID) 163 b, code, err := c.call("POST", uri, data) 164 switch { 165 // todo(fs): https://github.com/hashicorp/consul/pull/3621 166 // todo(fs): for some reason the docker agent closes the connection during the 167 // todo(fs): io.Copy call in c.call which causes a "connection reset by peer" error 168 // todo(fs): even though both body and status code have been received. My current is 169 // todo(fs): that the docker agent closes this prematurely but I don't understand why. 170 // todo(fs): the code below ignores this error. 171 case err != nil && !strings.Contains(err.Error(), "connection reset by peer"): 172 return nil, fmt.Errorf("start exec failed for container %s: %v", containerID, err) 173 case code == 200: 174 return b, nil 175 case code == 404: 176 return nil, fmt.Errorf("start exec failed for container %s: invalid exec id %s", containerID, execID) 177 case code == 409: 178 return nil, fmt.Errorf("start exec failed since container %s is paused or stopped", containerID) 179 default: 180 return nil, fmt.Errorf("start exec failed for container %s with status %d: body: %s err: %v", containerID, code, b, err) 181 } 182 } 183 184 func (c *DockerClient) InspectExec(containerID, execID string) (int, error) { 185 uri := fmt.Sprintf("/exec/%s/json", execID) 186 b, code, err := c.call("GET", uri, nil) 187 switch { 188 case err != nil: 189 return 0, fmt.Errorf("inspect exec failed for container %s: %s", containerID, err) 190 case code == 200: 191 var resp struct{ ExitCode int } 192 if err := json.NewDecoder(bytes.NewReader(b.Bytes())).Decode(&resp); err != nil { 193 return 0, fmt.Errorf("inspect exec response for container %s cannot be parsed: %v", containerID, err) 194 } 195 return resp.ExitCode, nil 196 case code == 404: 197 return 0, fmt.Errorf("inspect exec failed for container %s: invalid exec id %s", containerID, execID) 198 default: 199 return 0, fmt.Errorf("inspect exec failed for container %s with status %d: %s", containerID, code, b) 200 } 201 }