Browse Source

Merge pull request #43343 from thaJeztah/client_improve_docs

client: add const for environment variables, improve GoDoc, and minor touch-ups
Sebastiaan van Stijn 3 years ago
parent
commit
a6919e12b1
5 changed files with 189 additions and 58 deletions
  1. 70 39
      client/client.go
  2. 2 1
      client/client_unix.go
  3. 2 1
      client/client_windows.go
  4. 90 0
      client/envvars.go
  5. 25 17
      client/options.go

+ 70 - 39
client/client.go

@@ -43,7 +43,6 @@ package client // import "github.com/docker/docker/client"
 
 import (
 	"context"
-	"fmt"
 	"net"
 	"net/http"
 	"net/url"
@@ -93,15 +92,18 @@ type Client struct {
 }
 
 // CheckRedirect specifies the policy for dealing with redirect responses:
-// If the request is non-GET return `ErrRedirect`. Otherwise use the last response.
+// If the request is non-GET return ErrRedirect, otherwise use the last response.
 //
-// Go 1.8 changes behavior for HTTP redirects (specifically 301, 307, and 308) in the client .
-// The Docker client (and by extension docker 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 will be 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.
+// Go 1.8 changes behavior for HTTP redirects (specifically 301, 307, and 308)
+// in the client. The Docker client (and by extension docker 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 will be 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(req *http.Request, via []*http.Request) error {
 	if via[0].Method == http.MethodGet {
 		return http.ErrUseLastResponse
@@ -109,13 +111,22 @@ func CheckRedirect(req *http.Request, via []*http.Request) error {
 	return ErrRedirect
 }
 
-// NewClientWithOpts initializes a new API client with default values. It takes functors
-// to modify values when creating it, like `NewClientWithOpts(WithVersion(…))`
-// It also initializes the custom http headers to add to each request.
+// NewClientWithOpts 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 (client.FromEnv), and has
+// automatic API version negotiation enabled (client.WithAPIVersionNegotiation()).
+//
+//
+//	cli, err := client.NewClientWithOpts(
+//		client.FromEnv,
+//		client.WithAPIVersionNegotiation(),
+//	)
 //
-// It won't send any version information if the version number is empty. It is
-// highly recommended that you set a version or your client may break if the
-// server is upgraded.
 func NewClientWithOpts(ops ...Opt) (*Client, error) {
 	client, err := defaultHTTPClient(DefaultDockerHost)
 	if err != nil {
@@ -153,12 +164,12 @@ func NewClientWithOpts(ops ...Opt) (*Client, error) {
 }
 
 func defaultHTTPClient(host string) (*http.Client, error) {
-	url, err := ParseHostURL(host)
+	hostURL, err := ParseHostURL(host)
 	if err != nil {
 		return nil, err
 	}
-	transport := new(http.Transport)
-	sockets.ConfigureTransport(transport, url.Scheme, url.Host)
+	transport := &http.Transport{}
+	_ = sockets.ConfigureTransport(transport, hostURL.Scheme, hostURL.Host)
 	return &http.Client{
 		Transport:     transport,
 		CheckRedirect: CheckRedirect,
@@ -194,11 +205,21 @@ func (cli *Client) ClientVersion() string {
 	return cli.version
 }
 
-// NegotiateAPIVersion queries the API and updates the version to match the
-// API version. Any errors are silently ignored. If a manual override is in place,
-// either through the `DOCKER_API_VERSION` environment variable, or if the client
-// was initialized with a fixed version (`opts.WithVersion(xx)`), no negotiation
-// will be performed.
+// 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(xx)), 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 {
 		ping, _ := cli.Ping(ctx)
@@ -206,23 +227,31 @@ func (cli *Client) NegotiateAPIVersion(ctx context.Context) {
 	}
 }
 
-// NegotiateAPIVersionPing updates the client version to match the Ping.APIVersion
-// if the ping version is less than the default version.  If a manual override is
-// in place, either through the `DOCKER_API_VERSION` environment variable, or if
-// the client was initialized with a fixed version (`opts.WithVersion(xx)`), no
-// negotiation is performed.
-func (cli *Client) NegotiateAPIVersionPing(p types.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(xx)), 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 {
-		cli.negotiateAPIVersionPing(p)
+		cli.negotiateAPIVersionPing(pingResponse)
 	}
 }
 
 // negotiateAPIVersionPing queries the API and updates the version to match the
-// API version. Any errors are silently ignored.
-func (cli *Client) negotiateAPIVersionPing(p types.Ping) {
-	// try the latest version before versioning headers existed
-	if p.APIVersion == "" {
-		p.APIVersion = "1.24"
+// 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 = "1.24"
 	}
 
 	// if the client is not initialized with a version, start with the latest supported version
@@ -231,8 +260,8 @@ func (cli *Client) negotiateAPIVersionPing(p types.Ping) {
 	}
 
 	// if server version is lower than the client version, downgrade
-	if versions.LessThan(p.APIVersion, cli.version) {
-		cli.version = p.APIVersion
+	if versions.LessThan(pingResponse.APIVersion, cli.version) {
+		cli.version = pingResponse.APIVersion
 	}
 
 	// Store the results, so that automatic API version negotiation (if enabled)
@@ -258,7 +287,7 @@ func (cli *Client) HTTPClient() *http.Client {
 func ParseHostURL(host string) (*url.URL, error) {
 	protoAddrParts := strings.SplitN(host, "://", 2)
 	if len(protoAddrParts) == 1 {
-		return nil, fmt.Errorf("unable to parse docker host `%s`", host)
+		return nil, errors.Errorf("unable to parse docker host `%s`", host)
 	}
 
 	var basePath string
@@ -278,7 +307,9 @@ func ParseHostURL(host string) (*url.URL, error) {
 	}, nil
 }
 
-// Dialer returns a dialer for a raw stream connection, with HTTP/1.1 header, that can be used for proxying the daemon connection.
+// Dialer returns a dialer for a raw stream connection, with an HTTP/1.1 header,
+// that can be used for proxying the daemon connection.
+//
 // Used by `docker dial-stdio` (docker/cli#889).
 func (cli *Client) Dialer() func(context.Context) (net.Conn, error) {
 	return func(ctx context.Context) (net.Conn, error) {

+ 2 - 1
client/client_unix.go

@@ -3,7 +3,8 @@
 
 package client // import "github.com/docker/docker/client"
 
-// DefaultDockerHost defines os specific default if DOCKER_HOST is unset
+// DefaultDockerHost defines OS-specific default host if the DOCKER_HOST
+// (EnvOverrideHost) environment variable is unset or empty.
 const DefaultDockerHost = "unix:///var/run/docker.sock"
 
 const defaultProto = "unix"

+ 2 - 1
client/client_windows.go

@@ -1,6 +1,7 @@
 package client // import "github.com/docker/docker/client"
 
-// DefaultDockerHost defines os specific default if DOCKER_HOST is unset
+// DefaultDockerHost defines OS-specific default host if the DOCKER_HOST
+// (EnvOverrideHost) environment variable is unset or empty.
 const DefaultDockerHost = "npipe:////./pipe/docker_engine"
 
 const defaultProto = "npipe"

+ 90 - 0
client/envvars.go

@@ -0,0 +1,90 @@
+package client // import "github.com/docker/docker/client"
+
+const (
+	// EnvOverrideHost is the name of the environment variable that can be used
+	// to override the default host to connect to (DefaultDockerHost).
+	//
+	// This env-var is read by FromEnv and WithHostFromEnv and when set to a
+	// non-empty value, takes precedence over the default host (which is platform
+	// specific), or any host already set.
+	EnvOverrideHost = "DOCKER_HOST"
+
+	// EnvOverrideAPIVersion is the name of the environment variable that can
+	// be used to override the API version to use. Value should be
+	// formatted as MAJOR.MINOR, for example, "1.19".
+	//
+	// This env-var is read by FromEnv and WithVersionFromEnv and when set to a
+	// non-empty value, takes precedence over API version negotiation.
+	//
+	// This environment variable should be used for debugging purposes only, as
+	// it can set the client to use an incompatible (or invalid) API version.
+	EnvOverrideAPIVersion = "DOCKER_API_VERSION"
+
+	// EnvOverrideCertPath is the name of the environment variable that can be
+	// used to specify the directory from which to load the TLS certificates
+	// (ca.pem, cert.pem, key.pem) from. These certificates are used to configure
+	// the Client for a TCP connection protected by TLS client authentication.
+	//
+	// TLS certificate verification is enabled by default if the Client is configured
+	// to use a TLS connection. Refer to EnvTLSVerify below to learn how to
+	// disable verification for testing purposes.
+	//
+	// WARNING: Access to the remote API is equivalent to root access to the
+	// host where the daemon runs. Do not expose the API without protection,
+	// and only if needed. Make sure you are familiar with the "daemon attack
+	// surface" (https://docs.docker.com/go/attack-surface/).
+	//
+	// For local access to the API, it is recommended to connect with the daemon
+	// using the default local socket connection (on Linux), or the named pipe
+	// (on Windows).
+	//
+	// If you need to access the API of a remote daemon, consider using an SSH
+	// (ssh://) connection, which is easier to set up, and requires no additional
+	// configuration if the host is accessible using ssh.
+	//
+	// If you cannot use the alternatives above, and you must expose the API over
+	// a TCP connection, refer to https://docs.docker.com/engine/security/protect-access/
+	// to learn how to configure the daemon and client to use a TCP connection
+	// with TLS client authentication. Make sure you know the differences between
+	// a regular TLS connection and a TLS connection protected by TLS client
+	// authentication, and verify that the API cannot be accessed by other clients.
+	EnvOverrideCertPath = "DOCKER_CERT_PATH"
+
+	// EnvTLSVerify is the name of the environment variable that can be used to
+	// enable or disable TLS certificate verification. When set to a non-empty
+	// value, TLS certificate verification is enabled, and the client is configured
+	// to use a TLS connection, using certificates from the default directories
+	// (within `~/.docker`); refer to EnvOverrideCertPath above for additional
+	// details.
+	//
+	// WARNING: Access to the remote API is equivalent to root access to the
+	// host where the daemon runs. Do not expose the API without protection,
+	// and only if needed. Make sure you are familiar with the "daemon attack
+	// surface" (https://docs.docker.com/go/attack-surface/).
+	//
+	// Before setting up your client and daemon to use a TCP connection with TLS
+	// client authentication, consider using one of the alternatives mentioned
+	// in EnvOverrideCertPath above.
+	//
+	// Disabling TLS certificate verification (for testing purposes)
+	//
+	// TLS certificate verification is enabled by default if the Client is configured
+	// to use a TLS connection, and it is highly recommended to keep verification
+	// enabled to prevent machine-in-the-middle attacks. Refer to the documentation
+	// at https://docs.docker.com/engine/security/protect-access/ and pages linked
+	// from that page to learn how to configure the daemon and client to use a
+	// TCP connection with TLS client authentication enabled.
+	//
+	// Set the "DOCKER_TLS_VERIFY" environment to an empty string ("") to
+	// disable TLS certificate verification. Disabling verification is insecure,
+	// so should only be done for testing purposes. From the Go documentation
+	// (https://pkg.go.dev/crypto/tls#Config):
+	//
+	// InsecureSkipVerify controls whether a client verifies the server's
+	// certificate chain and host name. If InsecureSkipVerify is true, crypto/tls
+	// accepts any certificate presented by the server and any host name in that
+	// certificate. In this mode, TLS is susceptible to machine-in-the-middle
+	// attacks unless custom verification is used. This should be used only for
+	// testing or in combination with VerifyConnection or VerifyPeerCertificate.
+	EnvTLSVerify = "DOCKER_TLS_VERIFY"
+)

+ 25 - 17
client/options.go

@@ -18,11 +18,18 @@ type Opt func(*Client) error
 
 // FromEnv configures the client with values from environment variables.
 //
-// Supported environment variables:
-// DOCKER_HOST to set the url to the docker server.
-// DOCKER_API_VERSION to set the version of the API to reach, leave empty for latest.
-// DOCKER_CERT_PATH to load the TLS certificates from.
-// DOCKER_TLS_VERIFY to enable or disable TLS verification, off by default.
+// 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 *Client) error {
 	ops := []Opt{
 		WithTLSClientConfigFromEnv(),
@@ -75,11 +82,11 @@ func WithHost(host string) Opt {
 }
 
 // WithHostFromEnv overrides the client host with the host specified in the
-// DOCKER_HOST environment variable. If DOCKER_HOST is not set, the host is
-// not modified.
+// 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 *Client) error {
-		if host := os.Getenv("DOCKER_HOST"); host != "" {
+		if host := os.Getenv(EnvOverrideHost); host != "" {
 			return WithHost(host)(c)
 		}
 		return nil
@@ -145,12 +152,16 @@ func WithTLSClientConfig(cacertPath, certPath, keyPath string) Opt {
 // settings in the DOCKER_CERT_PATH and DOCKER_TLS_VERIFY environment variables.
 // If DOCKER_CERT_PATH is not set or empty, TLS configuration is not modified.
 //
-// Supported environment variables:
-// DOCKER_CERT_PATH  directory to load the TLS certificates (ca.pem, cert.pem, key.pem) from.
-// DOCKER_TLS_VERIFY to enable or disable TLS verification, off by default.
+// 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 *Client) error {
-		dockerCertPath := os.Getenv("DOCKER_CERT_PATH")
+		dockerCertPath := os.Getenv(EnvOverrideCertPath)
 		if dockerCertPath == "" {
 			return nil
 		}
@@ -158,7 +169,7 @@ func WithTLSClientConfigFromEnv() Opt {
 			CAFile:             filepath.Join(dockerCertPath, "ca.pem"),
 			CertFile:           filepath.Join(dockerCertPath, "cert.pem"),
 			KeyFile:            filepath.Join(dockerCertPath, "key.pem"),
-			InsecureSkipVerify: os.Getenv("DOCKER_TLS_VERIFY") == "",
+			InsecureSkipVerify: os.Getenv(EnvTLSVerify) == "",
 		}
 		tlsc, err := tlsconfig.Client(options)
 		if err != nil {
@@ -190,10 +201,7 @@ func WithVersion(version string) Opt {
 // the version is not modified.
 func WithVersionFromEnv() Opt {
 	return func(c *Client) error {
-		if version := os.Getenv("DOCKER_API_VERSION"); version != "" {
-			return WithVersion(version)(c)
-		}
-		return nil
+		return WithVersion(os.Getenv(EnvOverrideAPIVersion))(c)
 	}
 }