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