| /* | |
| Package client is a Go client for the Docker Engine API. | |
| For more information about the Engine API, see the documentation: | |
| https://docs.docker.com/reference/api/engine/ | |
| # Usage | |
| You use the library by constructing a client object using [New] | |
| and calling methods on it. The client can be configured from environment | |
| variables by passing the [FromEnv] option. Other options can be configured | |
| manually by passing any of the available [Opt] options. | |
| For example, to list running containers (the equivalent of "docker ps"): | |
| package main | |
| import ( | |
| "context" | |
| "fmt" | |
| "log" | |
| "github.com/moby/moby/client" | |
| ) | |
| func main() { | |
| // Create a new client that handles common environment variables | |
| // for configuration (DOCKER_HOST, DOCKER_API_VERSION), and does | |
| // API-version negotiation to allow downgrading the API version | |
| // when connecting with an older daemon version. | |
| apiClient, err := client.New(client.FromEnv) | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| // List all containers (both stopped and running). | |
| result, err := apiClient.ContainerList(context.Background(), client.ContainerListOptions{ | |
| All: true, | |
| }) | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| // Print each container's ID, status and the image it was created from. | |
| fmt.Printf("%s %-22s %s\n", "ID", "STATUS", "IMAGE") | |
| for _, ctr := range result.Items { | |
| fmt.Printf("%s %-22s %s\n", ctr.ID, ctr.Status, ctr.Image) | |
| } | |
| } | |
| */ | |
| package client | |
| import ( | |
| "context" | |
| "crypto/tls" | |
| "errors" | |
| "fmt" | |
| "net" | |
| "net/http" | |
| "net/url" | |
| "path" | |
| "runtime" | |
| "slices" | |
| "strings" | |
| "sync" | |
| "sync/atomic" | |
| "time" | |
| cerrdefs "github.com/containerd/errdefs" | |
| "github.com/docker/go-connections/sockets" | |
| "github.com/moby/moby/client/internal/mod" | |
| "github.com/moby/moby/client/pkg/versions" | |
| "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" | |
| ) | |
| // DummyHost is a hostname used for local communication. | |
| // | |
| // It acts as a valid formatted hostname for local connections (such as "unix://" | |
| // or "npipe://") which do not require a hostname. It should never be resolved, | |
| // but uses the special-purpose ".localhost" TLD (as defined in [RFC 2606, Section 2] | |
| // and [RFC 6761, Section 6.3]). | |
| // | |
| // [RFC 7230, Section 5.4] defines that an empty header must be used for such | |
| // cases: | |
| // | |
| // If the authority component is missing or undefined for the target URI, | |
| // then a client MUST send a Host header field with an empty field-value. | |
| // | |
| // However, [Go stdlib] enforces the semantics of HTTP(S) over TCP, does not | |
| // allow an empty header to be used, and requires req.URL.Scheme to be either | |
| // "http" or "https". | |
| // | |
| // For further details, refer to: | |
| // | |
| // - https://github.com/docker/engine-api/issues/189 | |
| // - https://github.com/golang/go/issues/13624 | |
| // - https://github.com/golang/go/issues/61076 | |
| // - https://github.com/moby/moby/issues/45935 | |
| // | |
| // [RFC 2606, Section 2]: https://www.rfc-editor.org/rfc/rfc2606.html#section-2 | |
| // [RFC 6761, Section 6.3]: https://www.rfc-editor.org/rfc/rfc6761#section-6.3 | |
| // [RFC 7230, Section 5.4]: https://datatracker.ietf.org/doc/html/rfc7230#section-5.4 | |
| // [Go stdlib]: https://github.com/golang/go/blob/6244b1946bc2101b01955468f1be502dbadd6807/src/net/http/transport.go#L558-L569 | |
| const DummyHost = "api.moby.localhost" | |
| // MaxAPIVersion is the highest REST API version supported by the client. | |
| // If API-version negotiation is enabled, the client may downgrade its API version. | |
| // Similarly, the [WithAPIVersion] and [WithAPIVersionFromEnv] options allow | |
| // overriding the version and disable API-version negotiation. | |
| // | |
| // This version may be lower than the version of the api library module used. | |
| const MaxAPIVersion = "1.54" | |
| // MinAPIVersion is the minimum API version supported by the client. API versions | |
| // below this version are not considered when performing API-version negotiation. | |
| const MinAPIVersion = "1.40" | |
| // defaultUserAgent returns the default User-Agent to use if none is set. | |
| // It defaults to "moby-client/<module version> os/arch" | |
| var defaultUserAgent = sync.OnceValue(userAgent) | |
| // Ensure that Client always implements APIClient. | |
| var _ APIClient = &Client{} | |
| // Client is the API client that performs all operations | |
| // against a docker server. | |
| type Client struct { | |
| clientConfig | |
| // negotiated indicates that API version negotiation took place | |
| negotiated atomic.Bool | |
| // negotiateLock is used to single-flight the version negotiation process | |
| negotiateLock sync.Mutex | |
| // When the client transport is an *http.Transport (default) we need to do some extra things (like closing idle connections). | |
| // Store the original transport as the http.Client transport will be wrapped with tracing libs. | |
| baseTransport *http.Transport | |
| } | |
| // ErrRedirect is the error returned by checkRedirect when the request is non-GET. | |
| var ErrRedirect = errors.New("unexpected redirect in response") | |
| // CheckRedirect specifies the policy for dealing with redirect responses. It | |
| // can be set on [http.Client.CheckRedirect] to prevent HTTP redirects for | |
| // non-GET requests. It returns an [ErrRedirect] for non-GET request, otherwise | |
| // returns a [http.ErrUseLastResponse], which is special-cased by http.Client | |
| // to use the last response. | |
| // | |
| // Go 1.8 changed behavior for HTTP redirects (specifically 301, 307, and 308) | |
| // in the client. The client (and by extension API client) can be made to send | |
| // a request like "POST /containers//start" where what would normally be in the | |
| // name section of the URL is empty. This triggers an HTTP 301 from the daemon. | |
| // | |
| // In go 1.8 this 301 is converted to a GET request, and ends up getting | |
| // a 404 from the daemon. This behavior change manifests in the client in that | |
| // before, the 301 was not followed and the client did not generate an error, | |
| // but now results in a message like "Error response from daemon: page not found". | |
| func CheckRedirect(_ *http.Request, via []*http.Request) error { | |
| if via[0].Method == http.MethodGet { | |
| return http.ErrUseLastResponse | |
| } | |
| return ErrRedirect | |
| } | |
| // NewClientWithOpts initializes a new API client. | |
| // | |
| // Deprecated: use [New]. This function will be removed in the next release. | |
| // | |
| //go:fix inline | |
| func NewClientWithOpts(ops ...Opt) (*Client, error) { | |
| return New(ops...) | |
| } | |
| // New initializes a new API client with a default HTTPClient, and | |
| // default API host and version. It also initializes the custom HTTP headers to | |
| // add to each request. | |
| // | |
| // It takes an optional list of [Opt] functional arguments, which are applied in | |
| // the order they're provided, which allows modifying the defaults when creating | |
| // the client. For example, the following initializes a client that configures | |
| // itself with values from environment variables ([FromEnv]). | |
| // | |
| // By default, 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. Use [WithAPIVersion] or | |
| // [WithAPIVersionFromEnv] to configure the client with a fixed API version | |
| // and disable API version negotiation. | |
| // | |
| // cli, err := client.New(client.FromEnv) | |
| func New(ops ...Opt) (*Client, error) { | |
| hostURL, err := ParseHostURL(DefaultDockerHost) | |
| if err != nil { | |
| return nil, err | |
| } | |
| client, err := defaultHTTPClient(hostURL) | |
| if err != nil { | |
| return nil, err | |
| } | |
| c := &Client{ | |
| clientConfig: clientConfig{ | |
| host: DefaultDockerHost, | |
| version: MaxAPIVersion, | |
| client: client, | |
| proto: hostURL.Scheme, | |
| addr: hostURL.Host, | |
| traceOpts: []otelhttp.Option{ | |
| otelhttp.WithSpanNameFormatter(func(_ string, req *http.Request) string { | |
| return req.Method + " " + req.URL.Path | |
| }), | |
| }, | |
| }, | |
| } | |
| cfg := &c.clientConfig | |
| for _, op := range ops { | |
| if op == nil { | |
| continue | |
| } | |
| if err := op(cfg); err != nil { | |
| return nil, err | |
| } | |
| } | |
| if cfg.envAPIVersion != "" { | |
| c.setAPIVersion(cfg.envAPIVersion) | |
| } else if cfg.manualAPIVersion != "" { | |
| c.setAPIVersion(cfg.manualAPIVersion) | |
| } | |
| if tr, ok := c.client.Transport.(*http.Transport); ok { | |
| // Store the base transport before we wrap it in tracing libs below | |
| // This is used, as an example, to close idle connections when the client is closed | |
| c.baseTransport = tr | |
| } | |
| if c.scheme == "" { | |
| // TODO(stevvooe): This isn't really the right way to write clients in Go. | |
| // `NewClient` should probably only take an `*http.Client` and work from there. | |
| // Unfortunately, the model of having a host-ish/url-thingy as the connection | |
| // string has us confusing protocol and transport layers. We continue doing | |
| // this to avoid breaking existing clients but this should be addressed. | |
| if c.tlsConfig() != nil { | |
| c.scheme = "https" | |
| } else { | |
| c.scheme = "http" | |
| } | |
| } | |
| c.client.Transport = otelhttp.NewTransport(c.client.Transport, c.traceOpts...) | |
| if len(cfg.responseHooks) > 0 { | |
| c.client.Transport = &responseHookTransport{ | |
| base: c.client.Transport, | |
| hooks: slices.Clone(cfg.responseHooks), | |
| } | |
| } | |
| return c, nil | |
| } | |
| func (cli *Client) tlsConfig() *tls.Config { | |
| if cli.baseTransport == nil { | |
| return nil | |
| } | |
| return cli.baseTransport.TLSClientConfig | |
| } | |
| func defaultHTTPClient(hostURL *url.URL) (*http.Client, error) { | |
| transport := &http.Transport{} | |
| // Necessary to prevent long-lived processes using the | |
| // client from leaking connections due to idle connections | |
| // not being released. | |
| // TODO: see if we can also address this from the server side, | |
| // or in go-connections. | |
| // see: https://github.com/moby/moby/issues/45539 | |
| transport.MaxIdleConns = 6 | |
| transport.IdleConnTimeout = 30 * time.Second | |
| err := sockets.ConfigureTransport(transport, hostURL.Scheme, hostURL.Host) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return &http.Client{ | |
| Transport: transport, | |
| CheckRedirect: CheckRedirect, | |
| }, nil | |
| } | |
| // Close the transport used by the client | |
| func (cli *Client) Close() error { | |
| if cli.baseTransport != nil { | |
| cli.baseTransport.CloseIdleConnections() | |
| return nil | |
| } | |
| return nil | |
| } | |
| // checkVersion manually triggers API version negotiation (if configured). | |
| // This allows for version-dependent code to use the same version as will | |
| // be negotiated when making the actual requests, and for which cases | |
| // we cannot do the negotiation lazily. | |
| func (cli *Client) checkVersion(ctx context.Context) error { | |
| if cli.negotiated.Load() { | |
| return nil | |
| } | |
| _, err := cli.Ping(ctx, PingOptions{ | |
| NegotiateAPIVersion: true, | |
| }) | |
| return err | |
| } | |
| // getAPIPath returns the versioned request path to call the API. | |
| // It appends the query parameters to the path if they are not empty. | |
| func (cli *Client) getAPIPath(ctx context.Context, p string, query url.Values) string { | |
| var apiPath string | |
| _ = cli.checkVersion(ctx) | |
| if cli.version != "" { | |
| apiPath = path.Join(cli.basePath, "/v"+strings.TrimPrefix(cli.version, "v"), p) | |
| } else { | |
| apiPath = path.Join(cli.basePath, p) | |
| } | |
| return (&url.URL{Path: apiPath, RawQuery: query.Encode()}).String() | |
| } | |
| // ClientVersion returns the API version used by this client. | |
| func (cli *Client) ClientVersion() string { | |
| return cli.version | |
| } | |
| // negotiateAPIVersion updates the version to match the API version from | |
| // the ping response. | |
| // | |
| // It returns an error if version is invalid, or lower than the minimum | |
| // supported API version in which case the client's API version is not | |
| // updated, and negotiation is not marked as completed. | |
| func (cli *Client) negotiateAPIVersion(pingVersion string) error { | |
| var err error | |
| pingVersion, err = parseAPIVersion(pingVersion) | |
| if err != nil { | |
| return err | |
| } | |
| if versions.LessThan(pingVersion, MinAPIVersion) { | |
| return cerrdefs.ErrInvalidArgument.WithMessage(fmt.Sprintf("API version %s is not supported by this client: the minimum supported API version is %s", pingVersion, MinAPIVersion)) | |
| } | |
| // if the client is not initialized with a version, start with the latest supported version | |
| negotiatedVersion := cli.version | |
| if negotiatedVersion == "" { | |
| negotiatedVersion = MaxAPIVersion | |
| } | |
| // if server version is lower than the client version, downgrade | |
| if versions.LessThan(pingVersion, negotiatedVersion) { | |
| negotiatedVersion = pingVersion | |
| } | |
| // Store the results, so that automatic API version negotiation (if enabled) | |
| // won't be performed on the next request. | |
| cli.setAPIVersion(negotiatedVersion) | |
| return nil | |
| } | |
| // setAPIVersion sets the client's API version and marks API version negotiation | |
| // as completed, so that automatic API version negotiation (if enabled) won't | |
| // be performed on the next request. | |
| func (cli *Client) setAPIVersion(version string) { | |
| cli.version = version | |
| cli.negotiated.Store(true) | |
| } | |
| // DaemonHost returns the host address used by the client | |
| func (cli *Client) DaemonHost() string { | |
| return cli.host | |
| } | |
| // ParseHostURL parses a url string, validates the string is a host url, and | |
| // returns the parsed URL | |
| func ParseHostURL(host string) (*url.URL, error) { | |
| proto, addr, ok := strings.Cut(host, "://") | |
| if !ok || addr == "" { | |
| return nil, fmt.Errorf("unable to parse docker host `%s`", host) | |
| } | |
| var basePath string | |
| if proto == "tcp" { | |
| parsed, err := url.Parse("tcp://" + addr) | |
| if err != nil { | |
| return nil, err | |
| } | |
| addr = parsed.Host | |
| basePath = parsed.Path | |
| } | |
| return &url.URL{ | |
| Scheme: proto, | |
| Host: addr, | |
| Path: basePath, | |
| }, nil | |
| } | |
| func (cli *Client) dialerFromTransport() func(context.Context, string, string) (net.Conn, error) { | |
| if cli.baseTransport == nil || cli.baseTransport.DialContext == nil { | |
| return nil | |
| } | |
| if cli.baseTransport.TLSClientConfig != nil { | |
| // When using a tls config we don't use the configured dialer but instead a fallback dialer... | |
| // Note: It seems like this should use the normal dialer and wrap the returned net.Conn in a tls.Conn | |
| // 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. | |
| return nil | |
| } | |
| return cli.baseTransport.DialContext | |
| } | |
| // Dialer returns a dialer for a raw stream connection, with an HTTP/1.1 header, | |
| // that can be used for proxying the daemon connection. It is used by | |
| // ["docker dial-stdio"]. | |
| // | |
| // ["docker dial-stdio"]: https://github.com/docker/cli/pull/1014 | |
| func (cli *Client) Dialer() func(context.Context) (net.Conn, error) { | |
| return cli.dialer() | |
| } | |
| func (cli *Client) dialer() func(context.Context) (net.Conn, error) { | |
| return func(ctx context.Context) (net.Conn, error) { | |
| if dialFn := cli.dialerFromTransport(); dialFn != nil { | |
| return dialFn(ctx, cli.proto, cli.addr) | |
| } | |
| switch cli.proto { | |
| case "unix": | |
| return net.Dial(cli.proto, cli.addr) | |
| case "npipe": | |
| ctx, cancel := context.WithTimeout(ctx, 32*time.Second) | |
| defer cancel() | |
| return dialPipeContext(ctx, cli.addr) | |
| default: | |
| if tlsConfig := cli.tlsConfig(); tlsConfig != nil { | |
| return tls.Dial(cli.proto, cli.addr, tlsConfig) | |
| } | |
| return net.Dial(cli.proto, cli.addr) | |
| } | |
| } | |
| } | |
| func userAgent() string { | |
| const defaultVersion = "v0.0.0+unknown" | |
| const moduleName = "github.com/moby/moby/client" | |
| version := defaultVersion | |
| if v := mod.Version(moduleName); v != "" { | |
| version = v | |
| } | |
| return "moby-client/" + version + " " + runtime.GOOS + "/" + runtime.GOARCH | |
| } |