docker api version negotiation

Signed-off-by: runzexia <runzexia@yunify.com>
This commit is contained in:
runzexia
2019-09-16 14:44:53 +08:00
parent 7c049a24da
commit 2759b20be6
23 changed files with 149 additions and 158 deletions

View File

@@ -81,6 +81,15 @@ type Client struct {
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 bool
}
// CheckRedirect specifies the policy for dealing with redirect responses:
@@ -107,7 +116,7 @@ func CheckRedirect(req *http.Request, via []*http.Request) error {
// 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 ...func(*Client) error) (*Client, error) {
func NewClientWithOpts(ops ...Opt) (*Client, error) {
client, err := defaultHTTPClient(DefaultDockerHost)
if err != nil {
return nil, err
@@ -169,8 +178,11 @@ func (cli *Client) Close() error {
// 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(p string, query url.Values) string {
func (cli *Client) getAPIPath(ctx context.Context, p string, query url.Values) string {
var apiPath string
if cli.negotiateVersion && !cli.negotiated {
cli.NegotiateAPIVersion(ctx)
}
if cli.version != "" {
v := strings.TrimPrefix(cli.version, "v")
apiPath = path.Join(cli.basePath, "/v"+v, p)
@@ -186,19 +198,31 @@ func (cli *Client) ClientVersion() string {
}
// NegotiateAPIVersion queries the API and updates the version to match the
// API version. Any errors are silently ignored.
// 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.
func (cli *Client) NegotiateAPIVersion(ctx context.Context) {
ping, _ := cli.Ping(ctx)
cli.NegotiateAPIVersionPing(ping)
if !cli.manualOverride {
ping, _ := cli.Ping(ctx)
cli.negotiateAPIVersionPing(ping)
}
}
// NegotiateAPIVersionPing updates the client version to match the Ping.APIVersion
// if the ping version is less than the default version.
// 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) {
if cli.manualOverride {
return
if !cli.manualOverride {
cli.negotiateAPIVersionPing(p)
}
}
// 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"
@@ -213,6 +237,12 @@ func (cli *Client) NegotiateAPIVersionPing(p types.Ping) {
if versions.LessThan(p.APIVersion, cli.version) {
cli.version = p.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 = true
}
}
// DaemonHost returns the host address used by the client

View File

@@ -23,7 +23,7 @@ func (cli *Client) postHijacked(ctx context.Context, path string, query url.Valu
return types.HijackedResponse{}, err
}
apiPath := cli.getAPIPath(path, query)
apiPath := cli.getAPIPath(ctx, path, query)
req, err := http.NewRequest("POST", apiPath, bodyEncoded)
if err != nil {
return types.HijackedResponse{}, err
@@ -38,6 +38,17 @@ func (cli *Client) postHijacked(ctx context.Context, path string, query url.Valu
return types.HijackedResponse{Conn: conn, Reader: bufio.NewReader(conn)}, err
}
// DialHijack returns a hijacked connection with negotiated protocol proto.
func (cli *Client) DialHijack(ctx context.Context, url, proto string, meta map[string][]string) (net.Conn, error) {
req, err := http.NewRequest("POST", url, nil)
if err != nil {
return nil, err
}
req = cli.addHeaders(req, meta)
return cli.setupHijackConn(ctx, req, proto)
}
// fallbackDial is used when WithDialer() was not called.
// See cli.Dialer().
func fallbackDial(proto, addr string, tlsConfig *tls.Config) (net.Conn, error) {

View File

@@ -38,7 +38,7 @@ type CommonAPIClient interface {
ServerVersion(ctx context.Context) (types.Version, error)
NegotiateAPIVersion(ctx context.Context)
NegotiateAPIVersionPing(types.Ping)
DialSession(ctx context.Context, proto string, meta map[string][]string) (net.Conn, error)
DialHijack(ctx context.Context, url, proto string, meta map[string][]string) (net.Conn, error)
Dialer() func(context.Context) (net.Conn, error)
Close() error
}

View File

@@ -6,12 +6,16 @@ import (
"net/http"
"os"
"path/filepath"
"time"
"github.com/docker/go-connections/sockets"
"github.com/docker/go-connections/tlsconfig"
"github.com/pkg/errors"
)
// Opt is a configuration option to initialize a client
type Opt func(*Client) error
// FromEnv configures the client with values from environment variables.
//
// Supported environment variables:
@@ -55,13 +59,13 @@ func FromEnv(c *Client) error {
// WithDialer applies the dialer.DialContext to the client transport. This can be
// used to set the Timeout and KeepAlive settings of the client.
// Deprecated: use WithDialContext
func WithDialer(dialer *net.Dialer) func(*Client) error {
func WithDialer(dialer *net.Dialer) Opt {
return WithDialContext(dialer.DialContext)
}
// WithDialContext applies the dialer to the client transport. This can be
// used to set the Timeout and KeepAlive settings of the client.
func WithDialContext(dialContext func(ctx context.Context, network, addr string) (net.Conn, error)) func(*Client) error {
func WithDialContext(dialContext func(ctx context.Context, network, addr string) (net.Conn, error)) Opt {
return func(c *Client) error {
if transport, ok := c.client.Transport.(*http.Transport); ok {
transport.DialContext = dialContext
@@ -72,7 +76,7 @@ func WithDialContext(dialContext func(ctx context.Context, network, addr string)
}
// WithHost overrides the client host with the specified one.
func WithHost(host string) func(*Client) error {
func WithHost(host string) Opt {
return func(c *Client) error {
hostURL, err := ParseHostURL(host)
if err != nil {
@@ -90,7 +94,7 @@ func WithHost(host string) func(*Client) error {
}
// WithHTTPClient overrides the client http client with the specified one
func WithHTTPClient(client *http.Client) func(*Client) error {
func WithHTTPClient(client *http.Client) Opt {
return func(c *Client) error {
if client != nil {
c.client = client
@@ -99,8 +103,16 @@ func WithHTTPClient(client *http.Client) func(*Client) error {
}
}
// WithTimeout configures the time limit for requests made by the HTTP client
func WithTimeout(timeout time.Duration) Opt {
return func(c *Client) error {
c.client.Timeout = timeout
return nil
}
}
// WithHTTPHeaders overrides the client default http headers
func WithHTTPHeaders(headers map[string]string) func(*Client) error {
func WithHTTPHeaders(headers map[string]string) Opt {
return func(c *Client) error {
c.customHTTPHeaders = headers
return nil
@@ -108,7 +120,7 @@ func WithHTTPHeaders(headers map[string]string) func(*Client) error {
}
// WithScheme overrides the client scheme with the specified one
func WithScheme(scheme string) func(*Client) error {
func WithScheme(scheme string) Opt {
return func(c *Client) error {
c.scheme = scheme
return nil
@@ -116,7 +128,7 @@ func WithScheme(scheme string) func(*Client) error {
}
// WithTLSClientConfig applies a tls config to the client transport.
func WithTLSClientConfig(cacertPath, certPath, keyPath string) func(*Client) error {
func WithTLSClientConfig(cacertPath, certPath, keyPath string) Opt {
return func(c *Client) error {
opts := tlsconfig.Options{
CAFile: cacertPath,
@@ -136,11 +148,25 @@ func WithTLSClientConfig(cacertPath, certPath, keyPath string) func(*Client) err
}
}
// WithVersion overrides the client version with the specified one
func WithVersion(version string) func(*Client) error {
// WithVersion overrides the client version with the specified one. If an empty
// version is specified, the value will be ignored to allow version negotiation.
func WithVersion(version string) Opt {
return func(c *Client) error {
c.version = version
c.manualOverride = true
if version != "" {
c.version = version
c.manualOverride = true
}
return nil
}
}
// 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 will not re-negotiate.
func WithAPIVersionNegotiation() Opt {
return func(c *Client) error {
c.negotiateVersion = true
return nil
}
}

View File

@@ -31,6 +31,8 @@ func (cli *Client) Ping(ctx context.Context) (types.Ping, error) {
// Server handled the request, so parse the response
return parsePingResponse(cli, serverResp)
}
} else if IsErrConnectionFailed(err) {
return ping, err
}
req, err = cli.buildRequest("GET", path.Join(cli.basePath, "/_ping"), nil, nil)

View File

@@ -115,7 +115,7 @@ func (cli *Client) buildRequest(method, path string, body io.Reader, headers hea
}
func (cli *Client) sendRequest(ctx context.Context, method, path string, query url.Values, body io.Reader, headers headers) (serverResponse, error) {
req, err := cli.buildRequest(method, cli.getAPIPath(path, query), body, headers)
req, err := cli.buildRequest(method, cli.getAPIPath(ctx, path, query), body, headers)
if err != nil {
return serverResponse{}, err
}

View File

@@ -1,18 +0,0 @@
package client // import "github.com/docker/docker/client"
import (
"context"
"net"
"net/http"
)
// DialSession returns a connection that can be used communication with daemon
func (cli *Client) DialSession(ctx context.Context, proto string, meta map[string][]string) (net.Conn, error) {
req, err := http.NewRequest("POST", "/session", nil)
if err != nil {
return nil, err
}
req = cli.addHeaders(req, meta)
return cli.setupHijackConn(ctx, req, proto)
}