client.go 14 KB

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