mirror of
https://github.com/moby/moby.git
synced 2026-01-11 18:51:37 +00:00
The http.Client passed into client.WithHTTPClient() is modified by the constructor in-place: the value of its Transport field is mutated and wrapped in an OpenTelemetry decorator. This can lead to very surprising behaviour when a second client is constructed reusing the same http.Client value. If the http.Client is configured for TLS, the second client will fail to detect that and will incorrectly dial the Engine API socket as cleartext HTTP. Copy the provided http.Client so our modifications don't leak out to unexpected places. Signed-off-by: Cory Snider <csnider@mirantis.com>
351 lines
12 KiB
Go
351 lines
12 KiB
Go
package client
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/docker/go-connections/sockets"
|
|
"github.com/docker/go-connections/tlsconfig"
|
|
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
|
"go.opentelemetry.io/otel/trace"
|
|
)
|
|
|
|
type clientConfig struct {
|
|
// scheme sets the scheme for the client
|
|
scheme string
|
|
// host holds the server address to connect to
|
|
host string
|
|
// proto holds the client protocol i.e. unix.
|
|
proto string
|
|
// addr holds the client address.
|
|
addr string
|
|
// basePath holds the path to prepend to the requests.
|
|
basePath string
|
|
// client used to send and receive http requests.
|
|
client *http.Client
|
|
// version of the server to talk to.
|
|
version string
|
|
// userAgent is the User-Agent header to use for HTTP requests. It takes
|
|
// precedence over User-Agent headers set in customHTTPHeaders, and other
|
|
// header variables. When set to an empty string, the User-Agent header
|
|
// is removed, and no header is sent.
|
|
userAgent *string
|
|
// custom HTTP headers configured by users.
|
|
customHTTPHeaders map[string]string
|
|
|
|
// manualAPIVersion contains the API version set by users. This field
|
|
// will only be non-empty if a valid-formed version was set through
|
|
// [WithAPIVersion].
|
|
//
|
|
// If both manualAPIVersion and envAPIVersion are set, manualAPIVersion
|
|
// takes precedence. Either field disables API-version negotiation.
|
|
manualAPIVersion string
|
|
|
|
// envAPIVersion contains the API version set by users. This field
|
|
// will only be non-empty if a valid-formed version was set through
|
|
// [WithAPIVersionFromEnv].
|
|
//
|
|
// If both manualAPIVersion and envAPIVersion are set, manualAPIVersion
|
|
// takes precedence. Either field disables API-version negotiation.
|
|
envAPIVersion string
|
|
|
|
// traceOpts is a list of options to configure the tracing span.
|
|
traceOpts []otelhttp.Option
|
|
}
|
|
|
|
// Opt is a configuration option to initialize a [Client].
|
|
type Opt func(*clientConfig) error
|
|
|
|
// FromEnv configures the client with values from environment variables. It
|
|
// is the equivalent of using the [WithTLSClientConfigFromEnv], [WithHostFromEnv],
|
|
// and [WithAPIVersionFromEnv] options.
|
|
//
|
|
// FromEnv uses the following environment variables:
|
|
//
|
|
// - DOCKER_HOST ([EnvOverrideHost]) to set the URL to the docker server.
|
|
// - DOCKER_API_VERSION ([EnvOverrideAPIVersion]) to set the version of the
|
|
// API to use, leave empty for latest.
|
|
// - DOCKER_CERT_PATH ([EnvOverrideCertPath]) to specify the directory from
|
|
// which to load the TLS certificates ("ca.pem", "cert.pem", "key.pem').
|
|
// - DOCKER_TLS_VERIFY ([EnvTLSVerify]) to enable or disable TLS verification
|
|
// (off by default).
|
|
func FromEnv(c *clientConfig) error {
|
|
ops := []Opt{
|
|
WithTLSClientConfigFromEnv(),
|
|
WithHostFromEnv(),
|
|
WithAPIVersionFromEnv(),
|
|
}
|
|
for _, op := range ops {
|
|
if err := op(c); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WithDialContext applies the dialer to the client transport. This can be
|
|
// used to set the Timeout and KeepAlive settings of the client. It returns
|
|
// an error if the client does not have a [http.Transport] configured.
|
|
func WithDialContext(dialContext func(ctx context.Context, network, addr string) (net.Conn, error)) Opt {
|
|
return func(c *clientConfig) error {
|
|
if transport, ok := c.client.Transport.(*http.Transport); ok {
|
|
transport.DialContext = dialContext
|
|
return nil
|
|
}
|
|
return fmt.Errorf("cannot apply dialer to transport: %T", c.client.Transport)
|
|
}
|
|
}
|
|
|
|
// WithHost overrides the client host with the specified one.
|
|
func WithHost(host string) Opt {
|
|
return func(c *clientConfig) error {
|
|
hostURL, err := ParseHostURL(host)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.host = host
|
|
c.proto = hostURL.Scheme
|
|
c.addr = hostURL.Host
|
|
c.basePath = hostURL.Path
|
|
if transport, ok := c.client.Transport.(*http.Transport); ok {
|
|
return sockets.ConfigureTransport(transport, c.proto, c.addr)
|
|
}
|
|
// For test transports, we skip transport configuration but still
|
|
// set the host fields so that the client can use them for headers
|
|
if _, ok := c.client.Transport.(testRoundTripper); ok {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("cannot apply host to transport: %T", c.client.Transport)
|
|
}
|
|
}
|
|
|
|
// testRoundTripper allows us to inject a mock-transport for testing. We define it
|
|
// here so we can detect the tlsconfig and return nil for only this type.
|
|
type testRoundTripper func(*http.Request) (*http.Response, error)
|
|
|
|
func (tf testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
return tf(req)
|
|
}
|
|
|
|
// WithHostFromEnv overrides the client host with the host specified in the
|
|
// DOCKER_HOST ([EnvOverrideHost]) environment variable. If DOCKER_HOST is not set,
|
|
// or set to an empty value, the host is not modified.
|
|
func WithHostFromEnv() Opt {
|
|
return func(c *clientConfig) error {
|
|
if host := os.Getenv(EnvOverrideHost); host != "" {
|
|
return WithHost(host)(c)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithHTTPClient overrides the client's HTTP client with the specified one.
|
|
func WithHTTPClient(client *http.Client) Opt {
|
|
return func(c *clientConfig) error {
|
|
if client != nil {
|
|
// Make a clone of client so modifications do not affect
|
|
// the caller's client. Clone here instead of in New()
|
|
// as other options (WithHost) also mutate c.client.
|
|
// Cloned clients share the same CookieJar as the
|
|
// original.
|
|
hc := *client
|
|
if ht, ok := hc.Transport.(*http.Transport); ok {
|
|
hc.Transport = ht.Clone()
|
|
}
|
|
c.client = &hc
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithTimeout configures the time limit for requests made by the HTTP client.
|
|
func WithTimeout(timeout time.Duration) Opt {
|
|
return func(c *clientConfig) error {
|
|
c.client.Timeout = timeout
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithUserAgent configures the User-Agent header to use for HTTP requests.
|
|
// It overrides any User-Agent set in headers. When set to an empty string,
|
|
// the User-Agent header is removed, and no header is sent.
|
|
func WithUserAgent(ua string) Opt {
|
|
return func(c *clientConfig) error {
|
|
c.userAgent = &ua
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithHTTPHeaders appends custom HTTP headers to the client's default headers.
|
|
// It does not allow for built-in headers (such as "User-Agent", if set) to
|
|
// be overridden. Also see [WithUserAgent].
|
|
func WithHTTPHeaders(headers map[string]string) Opt {
|
|
return func(c *clientConfig) error {
|
|
c.customHTTPHeaders = headers
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithScheme overrides the client scheme with the specified one.
|
|
func WithScheme(scheme string) Opt {
|
|
return func(c *clientConfig) error {
|
|
c.scheme = scheme
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithTLSClientConfig applies a TLS config to the client transport.
|
|
func WithTLSClientConfig(cacertPath, certPath, keyPath string) Opt {
|
|
return func(c *clientConfig) error {
|
|
transport, ok := c.client.Transport.(*http.Transport)
|
|
if !ok {
|
|
return fmt.Errorf("cannot apply tls config to transport: %T", c.client.Transport)
|
|
}
|
|
config, err := tlsconfig.Client(tlsconfig.Options{
|
|
CAFile: cacertPath,
|
|
CertFile: certPath,
|
|
KeyFile: keyPath,
|
|
ExclusiveRootPools: true,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create tls config: %w", err)
|
|
}
|
|
transport.TLSClientConfig = config
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithTLSClientConfigFromEnv configures the client's TLS settings with the
|
|
// settings in the DOCKER_CERT_PATH ([EnvOverrideCertPath]) and DOCKER_TLS_VERIFY
|
|
// ([EnvTLSVerify]) environment variables. If DOCKER_CERT_PATH is not set or empty,
|
|
// TLS configuration is not modified.
|
|
//
|
|
// WithTLSClientConfigFromEnv uses the following environment variables:
|
|
//
|
|
// - DOCKER_CERT_PATH ([EnvOverrideCertPath]) to specify the directory from
|
|
// which to load the TLS certificates ("ca.pem", "cert.pem", "key.pem").
|
|
// - DOCKER_TLS_VERIFY ([EnvTLSVerify]) to enable or disable TLS verification
|
|
// (off by default).
|
|
func WithTLSClientConfigFromEnv() Opt {
|
|
return func(c *clientConfig) error {
|
|
dockerCertPath := os.Getenv(EnvOverrideCertPath)
|
|
if dockerCertPath == "" {
|
|
return nil
|
|
}
|
|
tlsc, err := tlsconfig.Client(tlsconfig.Options{
|
|
CAFile: filepath.Join(dockerCertPath, "ca.pem"),
|
|
CertFile: filepath.Join(dockerCertPath, "cert.pem"),
|
|
KeyFile: filepath.Join(dockerCertPath, "key.pem"),
|
|
InsecureSkipVerify: os.Getenv(EnvTLSVerify) == "",
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.client = &http.Client{
|
|
Transport: &http.Transport{TLSClientConfig: tlsc},
|
|
CheckRedirect: CheckRedirect,
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithAPIVersion overrides the client's API version with the specified one,
|
|
// and disables API version negotiation. If an empty version is provided,
|
|
// this option is ignored to allow version negotiation. The given version
|
|
// should be formatted "<major>.<minor>" (for example, "1.52"). It returns
|
|
// an error if the given value not in the correct format.
|
|
//
|
|
// WithAPIVersion does not validate if the client supports the given version,
|
|
// and callers should verify if the version lower than the maximum supported
|
|
// version as defined by [MaxAPIVersion].
|
|
//
|
|
// [WithAPIVersionFromEnv] takes precedence if [WithAPIVersion] and
|
|
// [WithAPIVersionFromEnv] are both set.
|
|
func WithAPIVersion(version string) Opt {
|
|
return func(c *clientConfig) error {
|
|
version = strings.TrimSpace(version)
|
|
if val := strings.TrimPrefix(version, "v"); val != "" {
|
|
ver, err := parseAPIVersion(val)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid API version (%s): %w", version, err)
|
|
}
|
|
c.manualAPIVersion = ver
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithVersion overrides the client version with the specified one.
|
|
//
|
|
// Deprecated: use [WithAPIVersion] instead.
|
|
func WithVersion(version string) Opt {
|
|
return WithAPIVersion(version)
|
|
}
|
|
|
|
// WithAPIVersionFromEnv overrides the client version with the version specified in
|
|
// the DOCKER_API_VERSION ([EnvOverrideAPIVersion]) environment variable.
|
|
// If DOCKER_API_VERSION is not set, or set to an empty value, the version
|
|
// is not modified.
|
|
//
|
|
// WithAPIVersion does not validate if the client supports the given version,
|
|
// and callers should verify if the version lower than the maximum supported
|
|
// version as defined by [MaxAPIVersion].
|
|
//
|
|
// [WithAPIVersionFromEnv] takes precedence if [WithAPIVersion] and
|
|
// [WithAPIVersionFromEnv] are both set.
|
|
func WithAPIVersionFromEnv() Opt {
|
|
return func(c *clientConfig) error {
|
|
version := strings.TrimSpace(os.Getenv(EnvOverrideAPIVersion))
|
|
if val := strings.TrimPrefix(version, "v"); val != "" {
|
|
ver, err := parseAPIVersion(val)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid API version (%s): %w", version, err)
|
|
}
|
|
c.envAPIVersion = ver
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithVersionFromEnv overrides the client version with the version specified in
|
|
// the DOCKER_API_VERSION ([EnvOverrideAPIVersion]) environment variable.
|
|
//
|
|
// Deprecated: use [WithAPIVersionFromEnv] instead.
|
|
func WithVersionFromEnv() Opt {
|
|
return WithAPIVersionFromEnv()
|
|
}
|
|
|
|
// WithAPIVersionNegotiation enables automatic API version negotiation for the client.
|
|
// With this option enabled, the client automatically negotiates the API version
|
|
// to use when making requests. API version negotiation is performed on the first
|
|
// request; subsequent requests do not re-negotiate.
|
|
//
|
|
// Deprecated: API-version negotiation is now enabled by default. Use [WithAPIVersion]
|
|
// or [WithAPIVersionFromEnv] to disable API version negotiation.
|
|
func WithAPIVersionNegotiation() Opt {
|
|
return func(c *clientConfig) error {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithTraceProvider sets the trace provider for the client.
|
|
// If this is not set then the global trace provider is used.
|
|
func WithTraceProvider(provider trace.TracerProvider) Opt {
|
|
return WithTraceOptions(otelhttp.WithTracerProvider(provider))
|
|
}
|
|
|
|
// WithTraceOptions sets tracing span options for the client.
|
|
func WithTraceOptions(opts ...otelhttp.Option) Opt {
|
|
return func(c *clientConfig) error {
|
|
c.traceOpts = append(c.traceOpts, opts...)
|
|
return nil
|
|
}
|
|
}
|