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  }