client.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  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/api/
  5. # Usage
  6. You use the library by constructing a client object using [NewClientWithOpts]
  7. and calling methods on it. The client can be configured from environment
  8. variables by passing the [FromEnv] option, or configured manually by passing any
  9. of the other available [Opts].
  10. For example, to list running containers (the equivalent of "docker ps"):
  11. package main
  12. import (
  13. "context"
  14. "fmt"
  15. "github.com/docker/docker/api/types"
  16. "github.com/docker/docker/client"
  17. )
  18. func main() {
  19. cli, err := client.NewClientWithOpts(client.FromEnv)
  20. if err != nil {
  21. panic(err)
  22. }
  23. containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{})
  24. if err != nil {
  25. panic(err)
  26. }
  27. for _, container := range containers {
  28. fmt.Printf("%s %s\n", container.ID[:10], container.Image)
  29. }
  30. }
  31. */
  32. package client // import "github.com/docker/docker/client"
  33. import (
  34. "context"
  35. "net"
  36. "net/http"
  37. "net/url"
  38. "path"
  39. "strings"
  40. "github.com/docker/docker/api"
  41. "github.com/docker/docker/api/types"
  42. "github.com/docker/docker/api/types/versions"
  43. "github.com/docker/go-connections/sockets"
  44. "github.com/pkg/errors"
  45. )
  46. // DummyHost is a hostname used for local communication.
  47. //
  48. // It acts as a valid formatted hostname for local connections (such as "unix://"
  49. // or "npipe://") which do not require a hostname. It should never be resolved,
  50. // but uses the special-purpose ".localhost" TLD (as defined in [RFC 2606, Section 2]
  51. // and [RFC 6761, Section 6.3]).
  52. //
  53. // [RFC 7230, Section 5.4] defines that an empty header must be used for such
  54. // cases:
  55. //
  56. // If the authority component is missing or undefined for the target URI,
  57. // then a client MUST send a Host header field with an empty field-value.
  58. //
  59. // However, [Go stdlib] enforces the semantics of HTTP(S) over TCP, does not
  60. // allow an empty header to be used, and requires req.URL.Scheme to be either
  61. // "http" or "https".
  62. //
  63. // For further details, refer to:
  64. //
  65. // - https://github.com/docker/engine-api/issues/189
  66. // - https://github.com/golang/go/issues/13624
  67. // - https://github.com/golang/go/issues/61076
  68. // - https://github.com/moby/moby/issues/45935
  69. //
  70. // [RFC 2606, Section 2]: https://www.rfc-editor.org/rfc/rfc2606.html#section-2
  71. // [RFC 6761, Section 6.3]: https://www.rfc-editor.org/rfc/rfc6761#section-6.3
  72. // [RFC 7230, Section 5.4]: https://datatracker.ietf.org/doc/html/rfc7230#section-5.4
  73. // [Go stdlib]: https://github.com/golang/go/blob/6244b1946bc2101b01955468f1be502dbadd6807/src/net/http/transport.go#L558-L569
  74. const DummyHost = "api.moby.localhost"
  75. // Client is the API client that performs all operations
  76. // against a docker server.
  77. type Client struct {
  78. // scheme sets the scheme for the client
  79. scheme string
  80. // host holds the server address to connect to
  81. host string
  82. // proto holds the client protocol i.e. unix.
  83. proto string
  84. // addr holds the client address.
  85. addr string
  86. // basePath holds the path to prepend to the requests.
  87. basePath string
  88. // client used to send and receive http requests.
  89. client *http.Client
  90. // version of the server to talk to.
  91. version string
  92. // userAgent is the User-Agent header to use for HTTP requests. It takes
  93. // precedence over User-Agent headers set in customHTTPHeaders, and other
  94. // header variables. When set to an empty string, the User-Agent header
  95. // is removed, and no header is sent.
  96. userAgent *string
  97. // custom HTTP headers configured by users.
  98. customHTTPHeaders map[string]string
  99. // manualOverride is set to true when the version was set by users.
  100. manualOverride bool
  101. // negotiateVersion indicates if the client should automatically negotiate
  102. // the API version to use when making requests. API version negotiation is
  103. // performed on the first request, after which negotiated is set to "true"
  104. // so that subsequent requests do not re-negotiate.
  105. negotiateVersion bool
  106. // negotiated indicates that API version negotiation took place
  107. negotiated bool
  108. }
  109. // ErrRedirect is the error returned by checkRedirect when the request is non-GET.
  110. var ErrRedirect = errors.New("unexpected redirect in response")
  111. // CheckRedirect specifies the policy for dealing with redirect responses. It
  112. // can be set on [http.Client.CheckRedirect] to prevent HTTP redirects for
  113. // non-GET requests. It returns an [ErrRedirect] for non-GET request, otherwise
  114. // returns a [http.ErrUseLastResponse], which is special-cased by http.Client
  115. // to use the last response.
  116. //
  117. // Go 1.8 changed behavior for HTTP redirects (specifically 301, 307, and 308)
  118. // in the client. The client (and by extension API client) can be made to send
  119. // a request like "POST /containers//start" where what would normally be in the
  120. // name section of the URL is empty. This triggers an HTTP 301 from the daemon.
  121. //
  122. // In go 1.8 this 301 is converted to a GET request, and ends up getting
  123. // a 404 from the daemon. This behavior change manifests in the client in that
  124. // before, the 301 was not followed and the client did not generate an error,
  125. // but now results in a message like "Error response from daemon: page not found".
  126. func CheckRedirect(_ *http.Request, via []*http.Request) error {
  127. if via[0].Method == http.MethodGet {
  128. return http.ErrUseLastResponse
  129. }
  130. return ErrRedirect
  131. }
  132. // NewClientWithOpts initializes a new API client with a default HTTPClient, and
  133. // default API host and version. It also initializes the custom HTTP headers to
  134. // add to each request.
  135. //
  136. // It takes an optional list of [Opt] functional arguments, which are applied in
  137. // the order they're provided, which allows modifying the defaults when creating
  138. // the client. For example, the following initializes a client that configures
  139. // itself with values from environment variables ([FromEnv]), and has automatic
  140. // API version negotiation enabled ([WithAPIVersionNegotiation]).
  141. //
  142. // cli, err := client.NewClientWithOpts(
  143. // client.FromEnv,
  144. // client.WithAPIVersionNegotiation(),
  145. // )
  146. func NewClientWithOpts(ops ...Opt) (*Client, error) {
  147. hostURL, err := ParseHostURL(DefaultDockerHost)
  148. if err != nil {
  149. return nil, err
  150. }
  151. client, err := defaultHTTPClient(hostURL)
  152. if err != nil {
  153. return nil, err
  154. }
  155. c := &Client{
  156. host: DefaultDockerHost,
  157. version: api.DefaultVersion,
  158. client: client,
  159. proto: hostURL.Scheme,
  160. addr: hostURL.Host,
  161. }
  162. for _, op := range ops {
  163. if err := op(c); err != nil {
  164. return nil, err
  165. }
  166. }
  167. if c.scheme == "" {
  168. c.scheme = "http"
  169. tlsConfig := resolveTLSConfig(c.client.Transport)
  170. if tlsConfig != nil {
  171. // TODO(stevvooe): This isn't really the right way to write clients in Go.
  172. // `NewClient` should probably only take an `*http.Client` and work from there.
  173. // Unfortunately, the model of having a host-ish/url-thingy as the connection
  174. // string has us confusing protocol and transport layers. We continue doing
  175. // this to avoid breaking existing clients but this should be addressed.
  176. c.scheme = "https"
  177. }
  178. }
  179. return c, nil
  180. }
  181. func defaultHTTPClient(hostURL *url.URL) (*http.Client, error) {
  182. transport := &http.Transport{}
  183. err := sockets.ConfigureTransport(transport, hostURL.Scheme, hostURL.Host)
  184. if err != nil {
  185. return nil, err
  186. }
  187. return &http.Client{
  188. Transport: transport,
  189. CheckRedirect: CheckRedirect,
  190. }, nil
  191. }
  192. // Close the transport used by the client
  193. func (cli *Client) Close() error {
  194. if t, ok := cli.client.Transport.(*http.Transport); ok {
  195. t.CloseIdleConnections()
  196. }
  197. return nil
  198. }
  199. // getAPIPath returns the versioned request path to call the API.
  200. // It appends the query parameters to the path if they are not empty.
  201. func (cli *Client) getAPIPath(ctx context.Context, p string, query url.Values) string {
  202. var apiPath string
  203. if cli.negotiateVersion && !cli.negotiated {
  204. cli.NegotiateAPIVersion(ctx)
  205. }
  206. if cli.version != "" {
  207. v := strings.TrimPrefix(cli.version, "v")
  208. apiPath = path.Join(cli.basePath, "/v"+v, p)
  209. } else {
  210. apiPath = path.Join(cli.basePath, p)
  211. }
  212. return (&url.URL{Path: apiPath, RawQuery: query.Encode()}).String()
  213. }
  214. // ClientVersion returns the API version used by this client.
  215. func (cli *Client) ClientVersion() string {
  216. return cli.version
  217. }
  218. // NegotiateAPIVersion queries the API and updates the version to match the API
  219. // version. NegotiateAPIVersion downgrades the client's API version to match the
  220. // APIVersion if the ping version is lower than the default version. If the API
  221. // version reported by the server is higher than the maximum version supported
  222. // by the client, it uses the client's maximum version.
  223. //
  224. // If a manual override is in place, either through the "DOCKER_API_VERSION"
  225. // ([EnvOverrideAPIVersion]) environment variable, or if the client is initialized
  226. // with a fixed version ([WithVersion]), no negotiation is performed.
  227. //
  228. // If the API server's ping response does not contain an API version, or if the
  229. // client did not get a successful ping response, it assumes it is connected with
  230. // an old daemon that does not support API version negotiation, in which case it
  231. // downgrades to the latest version of the API before version negotiation was
  232. // added (1.24).
  233. func (cli *Client) NegotiateAPIVersion(ctx context.Context) {
  234. if !cli.manualOverride {
  235. ping, _ := cli.Ping(ctx)
  236. cli.negotiateAPIVersionPing(ping)
  237. }
  238. }
  239. // NegotiateAPIVersionPing downgrades the client's API version to match the
  240. // APIVersion in the ping response. If the API version in pingResponse is higher
  241. // than the maximum version supported by the client, it uses the client's maximum
  242. // version.
  243. //
  244. // If a manual override is in place, either through the "DOCKER_API_VERSION"
  245. // ([EnvOverrideAPIVersion]) environment variable, or if the client is initialized
  246. // with a fixed version ([WithVersion]), no negotiation is performed.
  247. //
  248. // If the API server's ping response does not contain an API version, we assume
  249. // we are connected with an old daemon without API version negotiation support,
  250. // and downgrade to the latest version of the API before version negotiation was
  251. // added (1.24).
  252. func (cli *Client) NegotiateAPIVersionPing(pingResponse types.Ping) {
  253. if !cli.manualOverride {
  254. cli.negotiateAPIVersionPing(pingResponse)
  255. }
  256. }
  257. // negotiateAPIVersionPing queries the API and updates the version to match the
  258. // API version from the ping response.
  259. func (cli *Client) negotiateAPIVersionPing(pingResponse types.Ping) {
  260. // default to the latest version before versioning headers existed
  261. if pingResponse.APIVersion == "" {
  262. pingResponse.APIVersion = "1.24"
  263. }
  264. // if the client is not initialized with a version, start with the latest supported version
  265. if cli.version == "" {
  266. cli.version = api.DefaultVersion
  267. }
  268. // if server version is lower than the client version, downgrade
  269. if versions.LessThan(pingResponse.APIVersion, cli.version) {
  270. cli.version = pingResponse.APIVersion
  271. }
  272. // Store the results, so that automatic API version negotiation (if enabled)
  273. // won't be performed on the next request.
  274. if cli.negotiateVersion {
  275. cli.negotiated = true
  276. }
  277. }
  278. // DaemonHost returns the host address used by the client
  279. func (cli *Client) DaemonHost() string {
  280. return cli.host
  281. }
  282. // HTTPClient returns a copy of the HTTP client bound to the server
  283. func (cli *Client) HTTPClient() *http.Client {
  284. c := *cli.client
  285. return &c
  286. }
  287. // ParseHostURL parses a url string, validates the string is a host url, and
  288. // returns the parsed URL
  289. func ParseHostURL(host string) (*url.URL, error) {
  290. proto, addr, ok := strings.Cut(host, "://")
  291. if !ok || addr == "" {
  292. return nil, errors.Errorf("unable to parse docker host `%s`", host)
  293. }
  294. var basePath string
  295. if proto == "tcp" {
  296. parsed, err := url.Parse("tcp://" + addr)
  297. if err != nil {
  298. return nil, err
  299. }
  300. addr = parsed.Host
  301. basePath = parsed.Path
  302. }
  303. return &url.URL{
  304. Scheme: proto,
  305. Host: addr,
  306. Path: basePath,
  307. }, nil
  308. }
  309. // Dialer returns a dialer for a raw stream connection, with an HTTP/1.1 header,
  310. // that can be used for proxying the daemon connection. It is used by
  311. // ["docker dial-stdio"].
  312. //
  313. // ["docker dial-stdio"]: https://github.com/docker/cli/pull/1014
  314. func (cli *Client) Dialer() func(context.Context) (net.Conn, error) {
  315. return func(ctx context.Context) (net.Conn, error) {
  316. if transport, ok := cli.client.Transport.(*http.Transport); ok {
  317. if transport.DialContext != nil && transport.TLSClientConfig == nil {
  318. return transport.DialContext(ctx, cli.proto, cli.addr)
  319. }
  320. }
  321. return fallbackDial(cli.proto, cli.addr, resolveTLSConfig(cli.client.Transport))
  322. }
  323. }