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  }