client.go 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. /*
  2. Package client is a Go client for the Docker Engine API.
  3. For more information about the Engine API, see the documentation:
  4. https://docs.docker.com/engine/reference/api/
  5. Usage
  6. You use the library by creating a client object and calling methods on it. The
  7. client can be created either from environment variables with NewEnvClient, or
  8. configured manually with NewClient.
  9. For example, to list running containers (the equivalent of "docker ps"):
  10. package main
  11. import (
  12. "context"
  13. "fmt"
  14. "github.com/docker/docker/api/types"
  15. "github.com/docker/docker/client"
  16. )
  17. func main() {
  18. cli, err := client.NewEnvClient()
  19. if err != nil {
  20. panic(err)
  21. }
  22. containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{})
  23. if err != nil {
  24. panic(err)
  25. }
  26. for _, container := range containers {
  27. fmt.Printf("%s %s\n", container.ID[:10], container.Image)
  28. }
  29. }
  30. */
  31. package client
  32. import (
  33. "errors"
  34. "fmt"
  35. "net/http"
  36. "net/url"
  37. "os"
  38. "path"
  39. "path/filepath"
  40. "strings"
  41. "github.com/docker/docker/api"
  42. "github.com/docker/docker/api/types"
  43. "github.com/docker/docker/api/types/versions"
  44. "github.com/docker/go-connections/sockets"
  45. "github.com/docker/go-connections/tlsconfig"
  46. "golang.org/x/net/context"
  47. )
  48. // ErrRedirect is the error returned by checkRedirect when the request is non-GET.
  49. var ErrRedirect = errors.New("unexpected redirect in response")
  50. // Client is the API client that performs all operations
  51. // against a docker server.
  52. type Client struct {
  53. // scheme sets the scheme for the client
  54. scheme string
  55. // host holds the server address to connect to
  56. host string
  57. // proto holds the client protocol i.e. unix.
  58. proto string
  59. // addr holds the client address.
  60. addr string
  61. // basePath holds the path to prepend to the requests.
  62. basePath string
  63. // client used to send and receive http requests.
  64. client *http.Client
  65. // version of the server to talk to.
  66. version string
  67. // custom http headers configured by users.
  68. customHTTPHeaders map[string]string
  69. // manualOverride is set to true when the version was set by users.
  70. manualOverride bool
  71. }
  72. // CheckRedirect specifies the policy for dealing with redirect responses:
  73. // If the request is non-GET return `ErrRedirect`. Otherwise use the last response.
  74. //
  75. // Go 1.8 changes behavior for HTTP redirects (specifically 301, 307, and 308) in the client .
  76. // The Docker client (and by extension docker API client) can be made to to send a request
  77. // like POST /containers//start where what would normally be in the name section of the URL is empty.
  78. // This triggers an HTTP 301 from the daemon.
  79. // In go 1.8 this 301 will be converted to a GET request, and ends up getting a 404 from the daemon.
  80. // This behavior change manifests in the client in that before the 301 was not followed and
  81. // the client did not generate an error, but now results in a message like Error response from daemon: page not found.
  82. func CheckRedirect(req *http.Request, via []*http.Request) error {
  83. if via[0].Method == http.MethodGet {
  84. return http.ErrUseLastResponse
  85. }
  86. return ErrRedirect
  87. }
  88. // NewEnvClient initializes a new API client based on environment variables.
  89. // Use DOCKER_HOST to set the url to the docker server.
  90. // Use DOCKER_API_VERSION to set the version of the API to reach, leave empty for latest.
  91. // Use DOCKER_CERT_PATH to load the TLS certificates from.
  92. // Use DOCKER_TLS_VERIFY to enable or disable TLS verification, off by default.
  93. func NewEnvClient() (*Client, error) {
  94. var client *http.Client
  95. if dockerCertPath := os.Getenv("DOCKER_CERT_PATH"); dockerCertPath != "" {
  96. options := tlsconfig.Options{
  97. CAFile: filepath.Join(dockerCertPath, "ca.pem"),
  98. CertFile: filepath.Join(dockerCertPath, "cert.pem"),
  99. KeyFile: filepath.Join(dockerCertPath, "key.pem"),
  100. InsecureSkipVerify: os.Getenv("DOCKER_TLS_VERIFY") == "",
  101. }
  102. tlsc, err := tlsconfig.Client(options)
  103. if err != nil {
  104. return nil, err
  105. }
  106. client = &http.Client{
  107. Transport: &http.Transport{
  108. TLSClientConfig: tlsc,
  109. },
  110. CheckRedirect: CheckRedirect,
  111. }
  112. }
  113. host := os.Getenv("DOCKER_HOST")
  114. if host == "" {
  115. host = DefaultDockerHost
  116. }
  117. version := os.Getenv("DOCKER_API_VERSION")
  118. if version == "" {
  119. version = api.DefaultVersion
  120. }
  121. cli, err := NewClient(host, version, client, nil)
  122. if err != nil {
  123. return cli, err
  124. }
  125. if os.Getenv("DOCKER_API_VERSION") != "" {
  126. cli.manualOverride = true
  127. }
  128. return cli, nil
  129. }
  130. // NewClient initializes a new API client for the given host and API version.
  131. // It uses the given http client as transport.
  132. // It also initializes the custom http headers to add to each request.
  133. //
  134. // It won't send any version information if the version number is empty. It is
  135. // highly recommended that you set a version or your client may break if the
  136. // server is upgraded.
  137. func NewClient(host string, version string, client *http.Client, httpHeaders map[string]string) (*Client, error) {
  138. hostURL, err := ParseHostURL(host)
  139. if err != nil {
  140. return nil, err
  141. }
  142. if client != nil {
  143. if _, ok := client.Transport.(http.RoundTripper); !ok {
  144. return nil, fmt.Errorf("unable to verify TLS configuration, invalid transport %v", client.Transport)
  145. }
  146. } else {
  147. transport := new(http.Transport)
  148. sockets.ConfigureTransport(transport, hostURL.Scheme, hostURL.Host)
  149. client = &http.Client{
  150. Transport: transport,
  151. CheckRedirect: CheckRedirect,
  152. }
  153. }
  154. scheme := "http"
  155. tlsConfig := resolveTLSConfig(client.Transport)
  156. if tlsConfig != nil {
  157. // TODO(stevvooe): This isn't really the right way to write clients in Go.
  158. // `NewClient` should probably only take an `*http.Client` and work from there.
  159. // Unfortunately, the model of having a host-ish/url-thingy as the connection
  160. // string has us confusing protocol and transport layers. We continue doing
  161. // this to avoid breaking existing clients but this should be addressed.
  162. scheme = "https"
  163. }
  164. // TODO: store URL instead of proto/addr/basePath
  165. return &Client{
  166. scheme: scheme,
  167. host: host,
  168. proto: hostURL.Scheme,
  169. addr: hostURL.Host,
  170. basePath: hostURL.Path,
  171. client: client,
  172. version: version,
  173. customHTTPHeaders: httpHeaders,
  174. }, nil
  175. }
  176. // Close the transport used by the client
  177. func (cli *Client) Close() error {
  178. if t, ok := cli.client.Transport.(*http.Transport); ok {
  179. t.CloseIdleConnections()
  180. }
  181. return nil
  182. }
  183. // getAPIPath returns the versioned request path to call the api.
  184. // It appends the query parameters to the path if they are not empty.
  185. func (cli *Client) getAPIPath(p string, query url.Values) string {
  186. var apiPath string
  187. if cli.version != "" {
  188. v := strings.TrimPrefix(cli.version, "v")
  189. apiPath = path.Join(cli.basePath, "/v"+v, p)
  190. } else {
  191. apiPath = path.Join(cli.basePath, p)
  192. }
  193. return (&url.URL{Path: apiPath, RawQuery: query.Encode()}).String()
  194. }
  195. // ClientVersion returns the API version used by this client.
  196. func (cli *Client) ClientVersion() string {
  197. return cli.version
  198. }
  199. // NegotiateAPIVersion queries the API and updates the version to match the
  200. // API version. Any errors are silently ignored.
  201. func (cli *Client) NegotiateAPIVersion(ctx context.Context) {
  202. ping, _ := cli.Ping(ctx)
  203. cli.NegotiateAPIVersionPing(ping)
  204. }
  205. // NegotiateAPIVersionPing updates the client version to match the Ping.APIVersion
  206. // if the ping version is less than the default version.
  207. func (cli *Client) NegotiateAPIVersionPing(p types.Ping) {
  208. if cli.manualOverride {
  209. return
  210. }
  211. // try the latest version before versioning headers existed
  212. if p.APIVersion == "" {
  213. p.APIVersion = "1.24"
  214. }
  215. // if the client is not initialized with a version, start with the latest supported version
  216. if cli.version == "" {
  217. cli.version = api.DefaultVersion
  218. }
  219. // if server version is lower than the maximum version supported by the Client, downgrade
  220. if versions.LessThan(p.APIVersion, api.DefaultVersion) {
  221. cli.version = p.APIVersion
  222. }
  223. }
  224. // DaemonHost returns the host address used by the client
  225. func (cli *Client) DaemonHost() string {
  226. return cli.host
  227. }
  228. // ParseHost parses a url string, validates the strings is a host url, and returns
  229. // the parsed host as: protocol, address, and base path
  230. // Deprecated: use ParseHostURL
  231. func ParseHost(host string) (string, string, string, error) {
  232. hostURL, err := ParseHostURL(host)
  233. if err != nil {
  234. return "", "", "", err
  235. }
  236. return hostURL.Scheme, hostURL.Host, hostURL.Path, nil
  237. }
  238. // ParseHostURL parses a url string, validates the string is a host url, and
  239. // returns the parsed URL
  240. func ParseHostURL(host string) (*url.URL, error) {
  241. protoAddrParts := strings.SplitN(host, "://", 2)
  242. if len(protoAddrParts) == 1 {
  243. return nil, fmt.Errorf("unable to parse docker host `%s`", host)
  244. }
  245. var basePath string
  246. proto, addr := protoAddrParts[0], protoAddrParts[1]
  247. if proto == "tcp" {
  248. parsed, err := url.Parse("tcp://" + addr)
  249. if err != nil {
  250. return nil, err
  251. }
  252. addr = parsed.Host
  253. basePath = parsed.Path
  254. }
  255. return &url.URL{
  256. Scheme: proto,
  257. Host: addr,
  258. Path: basePath,
  259. }, nil
  260. }
  261. // CustomHTTPHeaders returns the custom http headers stored by the client.
  262. func (cli *Client) CustomHTTPHeaders() map[string]string {
  263. m := make(map[string]string)
  264. for k, v := range cli.customHTTPHeaders {
  265. m[k] = v
  266. }
  267. return m
  268. }
  269. // SetCustomHTTPHeaders that will be set on every HTTP request made by the client.
  270. func (cli *Client) SetCustomHTTPHeaders(headers map[string]string) {
  271. cli.customHTTPHeaders = headers
  272. }