github.com/Prakhar-Agarwal-byte/moby@v0.0.0-20231027092010-a14e3e8ab87e/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/Prakhar-Agarwal-byte/moby/api/types/container" 23 "github.com/Prakhar-Agarwal-byte/moby/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/Prakhar-Agarwal-byte/moby/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/Prakhar-Agarwal-byte/moby/api" 55 "github.com/Prakhar-Agarwal-byte/moby/api/types" 56 "github.com/Prakhar-Agarwal-byte/moby/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 // Client is the API client that performs all operations 94 // against a docker server. 95 type Client struct { 96 // scheme sets the scheme for the client 97 scheme string 98 // host holds the server address to connect to 99 host string 100 // proto holds the client protocol i.e. unix. 101 proto string 102 // addr holds the client address. 103 addr string 104 // basePath holds the path to prepend to the requests. 105 basePath string 106 // client used to send and receive http requests. 107 client *http.Client 108 // version of the server to talk to. 109 version string 110 // userAgent is the User-Agent header to use for HTTP requests. It takes 111 // precedence over User-Agent headers set in customHTTPHeaders, and other 112 // header variables. When set to an empty string, the User-Agent header 113 // is removed, and no header is sent. 114 userAgent *string 115 // custom HTTP headers configured by users. 116 customHTTPHeaders map[string]string 117 // manualOverride is set to true when the version was set by users. 118 manualOverride bool 119 120 // negotiateVersion indicates if the client should automatically negotiate 121 // the API version to use when making requests. API version negotiation is 122 // performed on the first request, after which negotiated is set to "true" 123 // so that subsequent requests do not re-negotiate. 124 negotiateVersion bool 125 126 // negotiated indicates that API version negotiation took place 127 negotiated bool 128 129 tp trace.TracerProvider 130 131 // When the client transport is an *http.Transport (default) we need to do some extra things (like closing idle connections). 132 // Store the original transport as the http.Client transport will be wrapped with tracing libs. 133 baseTransport *http.Transport 134 } 135 136 // ErrRedirect is the error returned by checkRedirect when the request is non-GET. 137 var ErrRedirect = errors.New("unexpected redirect in response") 138 139 // CheckRedirect specifies the policy for dealing with redirect responses. It 140 // can be set on [http.Client.CheckRedirect] to prevent HTTP redirects for 141 // non-GET requests. It returns an [ErrRedirect] for non-GET request, otherwise 142 // returns a [http.ErrUseLastResponse], which is special-cased by http.Client 143 // to use the last response. 144 // 145 // Go 1.8 changed behavior for HTTP redirects (specifically 301, 307, and 308) 146 // in the client. The client (and by extension API client) can be made to send 147 // a request like "POST /containers//start" where what would normally be in the 148 // name section of the URL is empty. This triggers an HTTP 301 from the daemon. 149 // 150 // In go 1.8 this 301 is converted to a GET request, and ends up getting 151 // a 404 from the daemon. This behavior change manifests in the client in that 152 // before, the 301 was not followed and the client did not generate an error, 153 // but now results in a message like "Error response from daemon: page not found". 154 func CheckRedirect(_ *http.Request, via []*http.Request) error { 155 if via[0].Method == http.MethodGet { 156 return http.ErrUseLastResponse 157 } 158 return ErrRedirect 159 } 160 161 // NewClientWithOpts initializes a new API client with a default HTTPClient, and 162 // default API host and version. It also initializes the custom HTTP headers to 163 // add to each request. 164 // 165 // It takes an optional list of [Opt] functional arguments, which are applied in 166 // the order they're provided, which allows modifying the defaults when creating 167 // the client. For example, the following initializes a client that configures 168 // itself with values from environment variables ([FromEnv]), and has automatic 169 // API version negotiation enabled ([WithAPIVersionNegotiation]). 170 // 171 // cli, err := client.NewClientWithOpts( 172 // client.FromEnv, 173 // client.WithAPIVersionNegotiation(), 174 // ) 175 func NewClientWithOpts(ops ...Opt) (*Client, error) { 176 hostURL, err := ParseHostURL(DefaultDockerHost) 177 if err != nil { 178 return nil, err 179 } 180 181 client, err := defaultHTTPClient(hostURL) 182 if err != nil { 183 return nil, err 184 } 185 c := &Client{ 186 host: DefaultDockerHost, 187 version: api.DefaultVersion, 188 client: client, 189 proto: hostURL.Scheme, 190 addr: hostURL.Host, 191 } 192 193 for _, op := range ops { 194 if err := op(c); err != nil { 195 return nil, err 196 } 197 } 198 199 if tr, ok := c.client.Transport.(*http.Transport); ok { 200 // Store the base transport before we wrap it in tracing libs below 201 // This is used, as an example, to close idle connections when the client is closed 202 c.baseTransport = tr 203 } 204 205 if c.scheme == "" { 206 // TODO(stevvooe): This isn't really the right way to write clients in Go. 207 // `NewClient` should probably only take an `*http.Client` and work from there. 208 // Unfortunately, the model of having a host-ish/url-thingy as the connection 209 // string has us confusing protocol and transport layers. We continue doing 210 // this to avoid breaking existing clients but this should be addressed. 211 if c.tlsConfig() != nil { 212 c.scheme = "https" 213 } else { 214 c.scheme = "http" 215 } 216 } 217 218 c.client.Transport = otelhttp.NewTransport( 219 c.client.Transport, 220 otelhttp.WithTracerProvider(c.tp), 221 otelhttp.WithSpanNameFormatter(func(_ string, req *http.Request) string { 222 return req.Method + " " + req.URL.Path 223 }), 224 ) 225 226 return c, nil 227 } 228 229 func (cli *Client) tlsConfig() *tls.Config { 230 if cli.baseTransport == nil { 231 return nil 232 } 233 return cli.baseTransport.TLSClientConfig 234 } 235 236 func defaultHTTPClient(hostURL *url.URL) (*http.Client, error) { 237 transport := &http.Transport{} 238 err := sockets.ConfigureTransport(transport, hostURL.Scheme, hostURL.Host) 239 if err != nil { 240 return nil, err 241 } 242 return &http.Client{ 243 Transport: transport, 244 CheckRedirect: CheckRedirect, 245 }, nil 246 } 247 248 // Close the transport used by the client 249 func (cli *Client) Close() error { 250 if cli.baseTransport != nil { 251 cli.baseTransport.CloseIdleConnections() 252 return nil 253 } 254 return nil 255 } 256 257 // checkVersion manually triggers API version negotiation (if configured). 258 // This allows for version-dependent code to use the same version as will 259 // be negotiated when making the actual requests, and for which cases 260 // we cannot do the negotiation lazily. 261 func (cli *Client) checkVersion(ctx context.Context) { 262 if cli.negotiateVersion && !cli.negotiated { 263 cli.NegotiateAPIVersion(ctx) 264 } 265 } 266 267 // getAPIPath returns the versioned request path to call the API. 268 // It appends the query parameters to the path if they are not empty. 269 func (cli *Client) getAPIPath(ctx context.Context, p string, query url.Values) string { 270 var apiPath string 271 cli.checkVersion(ctx) 272 if cli.version != "" { 273 v := strings.TrimPrefix(cli.version, "v") 274 apiPath = path.Join(cli.basePath, "/v"+v, p) 275 } else { 276 apiPath = path.Join(cli.basePath, p) 277 } 278 return (&url.URL{Path: apiPath, RawQuery: query.Encode()}).String() 279 } 280 281 // ClientVersion returns the API version used by this client. 282 func (cli *Client) ClientVersion() string { 283 return cli.version 284 } 285 286 // NegotiateAPIVersion queries the API and updates the version to match the API 287 // version. NegotiateAPIVersion downgrades the client's API version to match the 288 // APIVersion if the ping version is lower than the default version. If the API 289 // version reported by the server is higher than the maximum version supported 290 // by the client, it uses the client's maximum version. 291 // 292 // If a manual override is in place, either through the "DOCKER_API_VERSION" 293 // ([EnvOverrideAPIVersion]) environment variable, or if the client is initialized 294 // with a fixed version ([WithVersion]), no negotiation is performed. 295 // 296 // If the API server's ping response does not contain an API version, or if the 297 // client did not get a successful ping response, it assumes it is connected with 298 // an old daemon that does not support API version negotiation, in which case it 299 // downgrades to the latest version of the API before version negotiation was 300 // added (1.24). 301 func (cli *Client) NegotiateAPIVersion(ctx context.Context) { 302 if !cli.manualOverride { 303 ping, _ := cli.Ping(ctx) 304 cli.negotiateAPIVersionPing(ping) 305 } 306 } 307 308 // NegotiateAPIVersionPing downgrades the client's API version to match the 309 // APIVersion in the ping response. If the API version in pingResponse is higher 310 // than the maximum version supported by the client, it uses the client's maximum 311 // version. 312 // 313 // If a manual override is in place, either through the "DOCKER_API_VERSION" 314 // ([EnvOverrideAPIVersion]) environment variable, or if the client is initialized 315 // with a fixed version ([WithVersion]), no negotiation is performed. 316 // 317 // If the API server's ping response does not contain an API version, we assume 318 // we are connected with an old daemon without API version negotiation support, 319 // and downgrade to the latest version of the API before version negotiation was 320 // added (1.24). 321 func (cli *Client) NegotiateAPIVersionPing(pingResponse types.Ping) { 322 if !cli.manualOverride { 323 cli.negotiateAPIVersionPing(pingResponse) 324 } 325 } 326 327 // negotiateAPIVersionPing queries the API and updates the version to match the 328 // API version from the ping response. 329 func (cli *Client) negotiateAPIVersionPing(pingResponse types.Ping) { 330 // default to the latest version before versioning headers existed 331 if pingResponse.APIVersion == "" { 332 pingResponse.APIVersion = "1.24" 333 } 334 335 // if the client is not initialized with a version, start with the latest supported version 336 if cli.version == "" { 337 cli.version = api.DefaultVersion 338 } 339 340 // if server version is lower than the client version, downgrade 341 if versions.LessThan(pingResponse.APIVersion, cli.version) { 342 cli.version = pingResponse.APIVersion 343 } 344 345 // Store the results, so that automatic API version negotiation (if enabled) 346 // won't be performed on the next request. 347 if cli.negotiateVersion { 348 cli.negotiated = true 349 } 350 } 351 352 // DaemonHost returns the host address used by the client 353 func (cli *Client) DaemonHost() string { 354 return cli.host 355 } 356 357 // HTTPClient returns a copy of the HTTP client bound to the server 358 func (cli *Client) HTTPClient() *http.Client { 359 c := *cli.client 360 return &c 361 } 362 363 // ParseHostURL parses a url string, validates the string is a host url, and 364 // returns the parsed URL 365 func ParseHostURL(host string) (*url.URL, error) { 366 proto, addr, ok := strings.Cut(host, "://") 367 if !ok || addr == "" { 368 return nil, errors.Errorf("unable to parse docker host `%s`", host) 369 } 370 371 var basePath string 372 if proto == "tcp" { 373 parsed, err := url.Parse("tcp://" + addr) 374 if err != nil { 375 return nil, err 376 } 377 addr = parsed.Host 378 basePath = parsed.Path 379 } 380 return &url.URL{ 381 Scheme: proto, 382 Host: addr, 383 Path: basePath, 384 }, nil 385 } 386 387 func (cli *Client) dialerFromTransport() func(context.Context, string, string) (net.Conn, error) { 388 if cli.baseTransport == nil || cli.baseTransport.DialContext == nil { 389 return nil 390 } 391 392 if cli.baseTransport.TLSClientConfig != nil { 393 // When using a tls config we don't use the configured dialer but instead a fallback dialer... 394 // Note: It seems like this should use the normal dialer and wrap the returned net.Conn in a tls.Conn 395 // 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. 396 return nil 397 } 398 return cli.baseTransport.DialContext 399 } 400 401 // Dialer returns a dialer for a raw stream connection, with an HTTP/1.1 header, 402 // that can be used for proxying the daemon connection. It is used by 403 // ["docker dial-stdio"]. 404 // 405 // ["docker dial-stdio"]: https://github.com/docker/cli/pull/1014 406 func (cli *Client) Dialer() func(context.Context) (net.Conn, error) { 407 return func(ctx context.Context) (net.Conn, error) { 408 if dialFn := cli.dialerFromTransport(); dialFn != nil { 409 return dialFn(ctx, cli.proto, cli.addr) 410 } 411 switch cli.proto { 412 case "unix": 413 return net.Dial(cli.proto, cli.addr) 414 case "npipe": 415 return sockets.DialPipe(cli.addr, 32*time.Second) 416 default: 417 if tlsConfig := cli.tlsConfig(); tlsConfig != nil { 418 return tls.Dial(cli.proto, cli.addr, tlsConfig) 419 } 420 return net.Dial(cli.proto, cli.addr) 421 } 422 } 423 }