// 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 provides a client for the Docker remote API. // // See http://goo.gl/mxyql for more details on the remote API. package docker import ( "bytes" "encoding/json" "errors" "fmt" "io" "io/ioutil" "net" "net/http" "net/http/httputil" "net/url" "reflect" "strconv" "strings" "sync" "github.com/fsouza/go-dockerclient/utils" ) const userAgent = "go-dockerclient" var ( // ErrInvalidEndpoint is returned when the endpoint is not a valid HTTP URL. ErrInvalidEndpoint = errors.New("invalid endpoint") // 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") ) // 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. // // The given string must be in the form .., where , // and are integer numbers. 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)) var err error for i, val := range arr { ret[i], err = strconv.Atoi(val) if err != nil { return nil, fmt.Errorf("Unable to parse version %q: %q is not an integer", input, val) } } return ret, nil } func (version ApiVersion) String() string { var str string for i, val := range version { str += strconv.Itoa(val) if i < len(version)-1 { str += "." } } return str } func (version ApiVersion) LessThan(other ApiVersion) bool { return version.compare(other) < 0 } func (version ApiVersion) LessThanOrEqualTo(other ApiVersion) bool { return version.compare(other) <= 0 } func (version ApiVersion) GreaterThan(other ApiVersion) bool { return version.compare(other) > 0 } func (version ApiVersion) GreaterThanOrEqualTo(other ApiVersion) bool { return version.compare(other) >= 0 } func (version ApiVersion) compare(other ApiVersion) int { for i, v := range version { if i <= len(other)-1 { otherVersion := other[i] if v < otherVersion { return -1 } else if v > otherVersion { return 1 } } } if len(version) > len(other) { return 1 } if len(version) < len(other) { return -1 } return 0 } // Client is the basic type of this package. It provides methods for // interaction with the API. type Client struct { SkipServerVersionCheck bool endpoint string endpointURL *url.URL eventMonitor *eventMonitoringState client *http.Client requestedApiVersion ApiVersion serverApiVersion ApiVersion expectedApiVersion ApiVersion } // NewClient returns a Client instance ready for communication with the given // server endpoint. It will use the latest remote API version available in the // server. func NewClient(endpoint string) (*Client, error) { client, err := NewVersionedClient(endpoint, "") 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) { u, err := parseEndpoint(endpoint) if err != nil { return nil, err } var requestedApiVersion ApiVersion if strings.Contains(apiVersionString, ".") { requestedApiVersion, err = NewApiVersion(apiVersionString) if err != nil { return nil, err } } return &Client{ endpoint: endpoint, endpointURL: u, client: http.DefaultClient, 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 } func parseApiVersionString(input string) (version uint16, err error) { version = 0 if !strings.Contains(input, ".") { return 0, fmt.Errorf("Unable to parse version '%s'", input) } arr := strings.Split(input, ".") major, err := strconv.Atoi(arr[0]) if err != nil { return version, err } minor, err := strconv.Atoi(arr[1]) if err != nil { return version, err } version = uint16(major)<<8 | uint16(minor) return version, nil } // Ping pings the docker server // // See http://goo.gl/stJENm for more details. func (c *Client) Ping() error { path := "/_ping" body, status, err := c.do("GET", path, nil) if err != nil { return err } if status != http.StatusOK { return newError(status, body) } return nil } func (c *Client) getServerApiVersionString() (version string, err error) { body, status, err := c.do("GET", "/version", nil) if err != nil { return "", err } if status != http.StatusOK { return "", fmt.Errorf("Received unexpected status %d while trying to retrieve the server version", status) } var versionResponse map[string]string err = json.Unmarshal(body, &versionResponse) if err != nil { return "", err } version = versionResponse["ApiVersion"] return version, nil } func (c *Client) do(method, path string, data interface{}) ([]byte, int, error) { var params io.Reader if data != nil { buf, err := json.Marshal(data) if err != nil { return nil, -1, err } params = bytes.NewBuffer(buf) } if path != "/version" && !c.SkipServerVersionCheck && c.expectedApiVersion == nil { err := c.checkApiVersion() if err != nil { return nil, -1, err } } req, err := http.NewRequest(method, c.getURL(path), params) if err != nil { return nil, -1, err } req.Header.Set("User-Agent", userAgent) if data != nil { req.Header.Set("Content-Type", "application/json") } else if method == "POST" { req.Header.Set("Content-Type", "plain/text") } var resp *http.Response protocol := c.endpointURL.Scheme address := c.endpointURL.Path if protocol == "unix" { dial, err := net.Dial(protocol, address) if err != nil { return nil, -1, err } defer dial.Close() clientconn := httputil.NewClientConn(dial, nil) resp, err = clientconn.Do(req) if err != nil { return nil, -1, err } defer clientconn.Close() } else { resp, err = c.client.Do(req) } if err != nil { if strings.Contains(err.Error(), "connection refused") { return nil, -1, ErrConnectionRefused } return nil, -1, err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, -1, err } if resp.StatusCode < 200 || resp.StatusCode >= 400 { return nil, resp.StatusCode, newError(resp.StatusCode, body) } return body, resp.StatusCode, nil } func (c *Client) stream(method, path string, setRawTerminal bool, headers map[string]string, in io.Reader, stdout, stderr io.Writer) error { if (method == "POST" || method == "PUT") && in == nil { in = bytes.NewReader(nil) } if path != "/version" && !c.SkipServerVersionCheck && c.expectedApiVersion == nil { err := c.checkApiVersion() if err != nil { return err } } req, err := http.NewRequest(method, c.getURL(path), in) if err != nil { return err } req.Header.Set("User-Agent", userAgent) if method == "POST" { req.Header.Set("Content-Type", "plain/text") } for key, val := range headers { req.Header.Set(key, val) } var resp *http.Response protocol := c.endpointURL.Scheme address := c.endpointURL.Path if stdout == nil { stdout = ioutil.Discard } if stderr == nil { stderr = ioutil.Discard } if protocol == "unix" { dial, err := net.Dial(protocol, address) if err != nil { return err } clientconn := httputil.NewClientConn(dial, nil) resp, err = clientconn.Do(req) defer clientconn.Close() } else { resp, err = c.client.Do(req) } if err != nil { if strings.Contains(err.Error(), "connection refused") { return ErrConnectionRefused } return err } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 400 { body, err := ioutil.ReadAll(resp.Body) if err != nil { return err } return newError(resp.StatusCode, body) } if resp.Header.Get("Content-Type") == "application/json" { dec := json.NewDecoder(resp.Body) for { var m jsonMessage if err := dec.Decode(&m); err == io.EOF { break } else if err != nil { return err } if m.Stream != "" { fmt.Fprint(stdout, m.Stream) } else if m.Progress != "" { fmt.Fprintf(stdout, "%s %s\r", m.Status, m.Progress) } else if m.Error != "" { return errors.New(m.Error) } if m.Status != "" { fmt.Fprintln(stdout, m.Status) } } } else { if setRawTerminal { _, err = io.Copy(stdout, resp.Body) } else { _, err = utils.StdCopy(stdout, stderr, resp.Body) } return err } return nil } func (c *Client) hijack(method, path string, success chan struct{}, setRawTerminal bool, in io.Reader, stderr, stdout io.Writer) error { if path != "/version" && !c.SkipServerVersionCheck && c.expectedApiVersion == nil { err := c.checkApiVersion() if err != nil { return err } } if stdout == nil { stdout = ioutil.Discard } if stderr == nil { stderr = ioutil.Discard } req, err := http.NewRequest(method, c.getURL(path), nil) if err != nil { return err } req.Header.Set("Content-Type", "plain/text") protocol := c.endpointURL.Scheme address := c.endpointURL.Path if protocol != "unix" { protocol = "tcp" address = c.endpointURL.Host } dial, err := net.Dial(protocol, address) if err != nil { return err } defer dial.Close() clientconn := httputil.NewClientConn(dial, nil) clientconn.Do(req) if success != nil { success <- struct{}{} <-success } rwc, br := clientconn.Hijack() var wg sync.WaitGroup wg.Add(2) errs := make(chan error, 2) go func() { var err error if setRawTerminal { _, err = io.Copy(stdout, br) } else { _, err = utils.StdCopy(stdout, stderr, br) } errs <- err wg.Done() }() go func() { var err error if in != nil { _, err = io.Copy(rwc, in) } rwc.(interface { CloseWrite() error }).CloseWrite() errs <- err wg.Done() }() wg.Wait() close(errs) if err := <-errs; err != nil { return err } return nil } func (c *Client) getURL(path string) string { urlStr := strings.TrimRight(c.endpointURL.String(), "/") if c.endpointURL.Scheme == "unix" { urlStr = "" } if c.requestedApiVersion != nil { return fmt.Sprintf("%s/v%s%s", urlStr, c.requestedApiVersion, path) } else { return fmt.Sprintf("%s%s", urlStr, path) } } type jsonMessage struct { Status string `json:"status,omitempty"` Progress string `json:"progress,omitempty"` Error string `json:"error,omitempty"` Stream string `json:"stream,omitempty"` } func queryString(opts interface{}) string { if opts == nil { return "" } value := reflect.ValueOf(opts) if value.Kind() == reflect.Ptr { value = value.Elem() } if value.Kind() != reflect.Struct { return "" } items := url.Values(map[string][]string{}) for i := 0; i < value.NumField(); i++ { field := value.Type().Field(i) if field.PkgPath != "" { continue } key := field.Tag.Get("qs") if key == "" { key = strings.ToLower(field.Name) } else if key == "-" { continue } v := value.Field(i) switch v.Kind() { case reflect.Bool: if v.Bool() { items.Add(key, "1") } case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: if v.Int() > 0 { items.Add(key, strconv.FormatInt(v.Int(), 10)) } case reflect.Float32, reflect.Float64: if v.Float() > 0 { items.Add(key, strconv.FormatFloat(v.Float(), 'f', -1, 64)) } case reflect.String: if v.String() != "" { items.Add(key, v.String()) } case reflect.Ptr: if !v.IsNil() { if b, err := json.Marshal(v.Interface()); err == nil { items.Add(key, string(b)) } } } } return items.Encode() } // Error represents failures in the API. It represents a failure from the API. type Error struct { Status int Message string } func newError(status int, body []byte) *Error { return &Error{Status: status, Message: string(body)} } func (e *Error) Error() string { return fmt.Sprintf("API error (%d): %s", e.Status, e.Message) } func parseEndpoint(endpoint string) (*url.URL, error) { u, err := url.Parse(endpoint) if err != nil { return nil, ErrInvalidEndpoint } if u.Scheme == "tcp" { u.Scheme = "http" } if u.Scheme != "http" && u.Scheme != "https" && u.Scheme != "unix" { return nil, ErrInvalidEndpoint } if u.Scheme != "unix" { _, 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 > 0 && number < 65536 { return u, nil } } else { return u, nil // we don't need port when using a unix socket } return nil, ErrInvalidEndpoint }