Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

testutil: copy slim version of the docker client into testutil #5731

Merged
merged 1 commit into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,6 @@ require (
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/moby/sys/mount v0.3.4 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626 // indirect
github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect
Expand Down
4 changes: 0 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0/go.mod h1:oDrbWx4ewMylP7xHivfgixbfGBT6APAwsSoHRKotnIc=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.5.0 h1:mlmW46Q0B79I+Aj4azKC6xDMFN9a9SyZWESlGWYXbFs=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.5.0/go.mod h1:PXe2h+LKcWTX9afWdZoHyODqR4fBa5boUM/8uJfZ0Jo=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ=
Expand Down Expand Up @@ -294,8 +292,6 @@ github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
Expand Down
Original file line number Diff line number Diff line change
@@ -1,65 +1,16 @@
/*
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 [NewClientWithOpts]
and calling methods on it. The client can be configured from environment
variables by passing the [FromEnv] option, or configured manually by passing any
of the other available [Opts].

For example, to list running containers (the equivalent of "docker ps"):

package main

import (
"context"
"fmt"

"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
)

func main() {
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
panic(err)
}

containers, err := cli.ContainerList(context.Background(), container.ListOptions{})
if err != nil {
panic(err)
}

for _, ctr := range containers {
fmt.Printf("%s %s\n", ctr.ID, ctr.Image)
}
}
*/
package client // import "github.com/docker/docker/client"
package client

import (
"context"
"crypto/tls"
"net"
"net/http"
"net/url"
"path"
"strings"
"sync"
"sync/atomic"
"time"

"github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/versions"
"github.com/docker/go-connections/sockets"
"github.com/pkg/errors"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel/trace"
)

// DummyHost is a hostname used for local communication.
Expand Down Expand Up @@ -92,12 +43,8 @@ import (
// [Go stdlib]: https://github.com/golang/go/blob/6244b1946bc2101b01955468f1be502dbadd6807/src/net/http/transport.go#L558-L569
const DummyHost = "api.moby.localhost"

// fallbackAPIVersion is the version to fallback to if API-version negotiation
// fails. This version is the highest version of the API before API-version
// negotiation was introduced. If negotiation fails (or no API version was
// included in the API response), we assume the API server uses the most
// recent version before negotiation was introduced.
const fallbackAPIVersion = "1.24"
// DefaultVersion is the pinned version of the docker API we utilize.
const DefaultVersion = "1.47"

// Client is the API client that performs all operations
// against a docker server.
Expand All @@ -116,29 +63,6 @@ type Client struct {
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
// manualOverride is set to true when the version was set by users.
manualOverride bool

// negotiateVersion indicates if the client should automatically negotiate
// the API version to use when making requests. API version negotiation is
// performed on the first request, after which negotiated is set to "true"
// so that subsequent requests do not re-negotiate.
negotiateVersion bool

// negotiated indicates that API version negotiation took place
negotiated atomic.Bool

// negotiateLock is used to single-flight the version negotiation process
negotiateLock sync.Mutex

tp trace.TracerProvider

// 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.
Expand Down Expand Up @@ -196,7 +120,7 @@ func NewClientWithOpts(ops ...Opt) (*Client, error) {
}
c := &Client{
host: DefaultDockerHost,
version: api.DefaultVersion,
version: DefaultVersion,
client: client,
proto: hostURL.Scheme,
addr: hostURL.Host,
Expand Down Expand Up @@ -226,15 +150,6 @@ func NewClientWithOpts(ops ...Opt) (*Client, error) {
c.scheme = "http"
}
}

c.client.Transport = otelhttp.NewTransport(
c.client.Transport,
otelhttp.WithTracerProvider(c.tp),
otelhttp.WithSpanNameFormatter(func(_ string, req *http.Request) string {
return req.Method + " " + req.URL.Path
}),
)

return c, nil
}

Expand All @@ -246,17 +161,17 @@ func (cli *Client) tlsConfig() *tls.Config {
}

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 {
transport := &http.Transport{
MaxIdleConns: 6,
IdleConnTimeout: 30 * time.Second,
}
if err := sockets.ConfigureTransport(transport, hostURL.Scheme, hostURL.Host); err != nil {
return nil, err
}
return &http.Client{
Expand All @@ -274,138 +189,6 @@ func (cli *Client) Close() error {
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.manualOverride && cli.negotiateVersion && !cli.negotiated.Load() {
// Ensure exclusive write access to version and negotiated fields
cli.negotiateLock.Lock()
defer cli.negotiateLock.Unlock()

// May have been set during last execution of critical zone
if cli.negotiated.Load() {
return nil
}

ping, err := cli.Ping(ctx)
if err != nil {
return err
}
cli.negotiateAPIVersionPing(ping)
}
return nil
}

// 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 != "" {
v := strings.TrimPrefix(cli.version, "v")
apiPath = path.Join(cli.basePath, "/v"+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 queries the API and updates the version to match the API
// version. NegotiateAPIVersion downgrades the client's API version to match the
// APIVersion if the ping version is lower than the default version. If the API
// version reported by the server is higher than the maximum version supported
// by the client, it uses the client's maximum version.
//
// If a manual override is in place, either through the "DOCKER_API_VERSION"
// ([EnvOverrideAPIVersion]) environment variable, or if the client is initialized
// with a fixed version ([WithVersion]), no negotiation is performed.
//
// If the API server's ping response does not contain an API version, or if the
// client did not get a successful ping response, it assumes it is connected with
// an old daemon that does not support API version negotiation, in which case it
// downgrades to the latest version of the API before version negotiation was
// added (1.24).
func (cli *Client) NegotiateAPIVersion(ctx context.Context) {
if !cli.manualOverride {
// Avoid concurrent modification of version-related fields
cli.negotiateLock.Lock()
defer cli.negotiateLock.Unlock()

ping, err := cli.Ping(ctx)
if err != nil {
// FIXME(thaJeztah): Ping returns an error when failing to connect to the API; we should not swallow the error here, and instead returning it.
return
}
cli.negotiateAPIVersionPing(ping)
}
}

// NegotiateAPIVersionPing downgrades the client's API version to match the
// APIVersion in the ping response. If the API version in pingResponse is higher
// than the maximum version supported by the client, it uses the client's maximum
// version.
//
// If a manual override is in place, either through the "DOCKER_API_VERSION"
// ([EnvOverrideAPIVersion]) environment variable, or if the client is initialized
// with a fixed version ([WithVersion]), no negotiation is performed.
//
// If the API server's ping response does not contain an API version, we assume
// we are connected with an old daemon without API version negotiation support,
// and downgrade to the latest version of the API before version negotiation was
// added (1.24).
func (cli *Client) NegotiateAPIVersionPing(pingResponse types.Ping) {
if !cli.manualOverride {
// Avoid concurrent modification of version-related fields
cli.negotiateLock.Lock()
defer cli.negotiateLock.Unlock()

cli.negotiateAPIVersionPing(pingResponse)
}
}

// negotiateAPIVersionPing queries the API and updates the version to match the
// API version from the ping response.
func (cli *Client) negotiateAPIVersionPing(pingResponse types.Ping) {
// default to the latest version before versioning headers existed
if pingResponse.APIVersion == "" {
pingResponse.APIVersion = fallbackAPIVersion
}

// if the client is not initialized with a version, start with the latest supported version
if cli.version == "" {
cli.version = api.DefaultVersion
}

// if server version is lower than the client version, downgrade
if versions.LessThan(pingResponse.APIVersion, cli.version) {
cli.version = pingResponse.APIVersion
}

// Store the results, so that automatic API version negotiation (if enabled)
// won't be performed on the next request.
if cli.negotiateVersion {
cli.negotiated.Store(true)
}
}

// DaemonHost returns the host address used by the client
func (cli *Client) DaemonHost() string {
return cli.host
}

// HTTPClient returns a copy of the HTTP client bound to the server
func (cli *Client) HTTPClient() *http.Client {
c := *cli.client
return &c
}

// ParseHostURL parses a url string, validates the string is a host url, and
// returns the parsed URL
func ParseHostURL(host string) (*url.URL, error) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//go:build !windows

package client // import "github.com/docker/docker/client"
package client

// DefaultDockerHost defines OS-specific default host if the DOCKER_HOST
// (EnvOverrideHost) environment variable is unset or empty.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package client // import "github.com/docker/docker/client"
package client

// DefaultDockerHost defines OS-specific default host if the DOCKER_HOST
// (EnvOverrideHost) environment variable is unset or empty.
Expand Down
13 changes: 13 additions & 0 deletions util/testutil/dockerd/client/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package client

import (
"github.com/pkg/errors"
)

// ErrorConnectionFailed returns an error with host in the error message when connection to docker daemon failed.
func ErrorConnectionFailed(host string) error {
if host == "" {
return errors.New("Cannot connect to the Docker daemon. Is the docker daemon running on this host?")
}
return errors.Errorf("Cannot connect to the Docker daemon at %s. Is the docker daemon running?", host)
}
Loading
Loading