diff --git a/client/client.go b/client/client.go index f7a8c07d3a..3c1606c340 100644 --- a/client/client.go +++ b/client/client.go @@ -1,10 +1,6 @@ /* Package client is a Go client for the Docker Engine API. -The "docker" command uses this package to communicate with the daemon. It can also -be used by your own Go applications to do anything the command-line interface does -- running containers, pulling images, managing swarms, etc. - For more information about the Engine API, see the documentation: https://docs.docker.com/engine/reference/api/ @@ -160,7 +156,7 @@ func NewEnvClient() (*Client, error) { // highly recommended that you set a version or your client may break if the // server is upgraded. func NewClient(host string, version string, client *http.Client, httpHeaders map[string]string) (*Client, error) { - proto, addr, basePath, err := ParseHost(host) + hostURL, err := ParseHostURL(host) if err != nil { return nil, err } @@ -171,7 +167,7 @@ func NewClient(host string, version string, client *http.Client, httpHeaders map } } else { transport := new(http.Transport) - sockets.ConfigureTransport(transport, proto, addr) + sockets.ConfigureTransport(transport, hostURL.Scheme, hostURL.Host) client = &http.Client{ Transport: transport, CheckRedirect: CheckRedirect, @@ -189,28 +185,24 @@ func NewClient(host string, version string, client *http.Client, httpHeaders map scheme = "https" } + // TODO: store URL instead of proto/addr/basePath return &Client{ scheme: scheme, host: host, - proto: proto, - addr: addr, - basePath: basePath, + proto: hostURL.Scheme, + addr: hostURL.Host, + basePath: hostURL.Path, client: client, version: version, customHTTPHeaders: httpHeaders, }, nil } -// Close ensures that transport.Client is closed -// especially needed while using NewClient with *http.Client = nil -// for example -// client.NewClient("unix:///var/run/docker.sock", nil, "v1.18", map[string]string{"User-Agent": "engine-api-cli-1.0"}) +// Close the transport used by the client func (cli *Client) Close() error { - if t, ok := cli.client.Transport.(*http.Transport); ok { t.CloseIdleConnections() } - return nil } @@ -234,23 +226,20 @@ func (cli *Client) getAPIPath(p string, query url.Values) string { return u.String() } -// ClientVersion returns the version string associated with this -// instance of the Client. Note that this value can be changed -// via the DOCKER_API_VERSION env var. -// This operation doesn't acquire a mutex. +// ClientVersion returns the API version used by this client. func (cli *Client) ClientVersion() string { return cli.version } -// NegotiateAPIVersion updates the version string associated with this -// instance of the Client to match the latest version the server supports +// NegotiateAPIVersion queries the API and updates the version to match the +// API version. Any errors are silently ignored. func (cli *Client) NegotiateAPIVersion(ctx context.Context) { ping, _ := cli.Ping(ctx) cli.NegotiateAPIVersionPing(ping) } -// NegotiateAPIVersionPing updates the version string associated with this -// instance of the Client to match the latest version the server supports +// NegotiateAPIVersionPing updates the client version to match the Ping.APIVersion +// if the ping version is less than the default version. func (cli *Client) NegotiateAPIVersionPing(p types.Ping) { if cli.manualOverride { return @@ -272,17 +261,28 @@ func (cli *Client) NegotiateAPIVersionPing(p types.Ping) { } } -// DaemonHost returns the host associated with this instance of the Client. -// This operation doesn't acquire a mutex. +// DaemonHost returns the host address used by the client func (cli *Client) DaemonHost() string { return cli.host } -// ParseHost verifies that the given host strings is valid. +// ParseHost parses a url string, validates the strings is a host url, and returns +// the parsed host as: protocol, address, and base path +// Deprecated: use ParseHostURL func ParseHost(host string) (string, string, string, error) { + hostURL, err := ParseHostURL(host) + if err != nil { + return "", "", "", err + } + return hostURL.Scheme, hostURL.Host, hostURL.Path, nil +} + +// ParseHostURL parses a url string, validates the string is a host url, and +// returns the parsed URL +func ParseHostURL(host string) (*url.URL, error) { protoAddrParts := strings.SplitN(host, "://", 2) if len(protoAddrParts) == 1 { - return "", "", "", fmt.Errorf("unable to parse docker host `%s`", host) + return nil, fmt.Errorf("unable to parse docker host `%s`", host) } var basePath string @@ -290,16 +290,19 @@ func ParseHost(host string) (string, string, string, error) { if proto == "tcp" { parsed, err := url.Parse("tcp://" + addr) if err != nil { - return "", "", "", err + return nil, err } addr = parsed.Host basePath = parsed.Path } - return proto, addr, basePath, nil + return &url.URL{ + Scheme: proto, + Host: addr, + Path: basePath, + }, nil } -// CustomHTTPHeaders returns the custom http headers associated with this -// instance of the Client. This operation doesn't acquire a mutex. +// CustomHTTPHeaders returns the custom http headers stored by the client. func (cli *Client) CustomHTTPHeaders() map[string]string { m := make(map[string]string) for k, v := range cli.customHTTPHeaders { @@ -308,8 +311,7 @@ func (cli *Client) CustomHTTPHeaders() map[string]string { return m } -// SetCustomHTTPHeaders updates the custom http headers associated with this -// instance of the Client. This operation doesn't acquire a mutex. +// SetCustomHTTPHeaders that will be set on every HTTP request made by the client. func (cli *Client) SetCustomHTTPHeaders(headers map[string]string) { cli.customHTTPHeaders = headers } diff --git a/client/client_test.go b/client/client_test.go index bc911c0c4a..c63021748b 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -11,6 +11,7 @@ import ( "github.com/docker/docker/api" "github.com/docker/docker/api/types" + "github.com/docker/docker/internal/testutil" "github.com/stretchr/testify/assert" ) @@ -152,7 +153,6 @@ func TestParseHost(t *testing.T) { for _, cs := range cases { p, a, b, e := ParseHost(cs.host) - // if we expected an error to be returned... if cs.err { assert.Error(t, e) } @@ -162,6 +162,43 @@ func TestParseHost(t *testing.T) { } } +func TestParseHostURL(t *testing.T) { + testcases := []struct { + host string + expected *url.URL + expectedErr string + }{ + { + host: "", + expectedErr: "unable to parse docker host", + }, + { + host: "foobar", + expectedErr: "unable to parse docker host", + }, + { + host: "foo://bar", + expected: &url.URL{Scheme: "foo", Host: "bar"}, + }, + { + host: "tcp://localhost:2476", + expected: &url.URL{Scheme: "tcp", Host: "localhost:2476"}, + }, + { + host: "tcp://localhost:2476/path", + expected: &url.URL{Scheme: "tcp", Host: "localhost:2476", Path: "/path"}, + }, + } + + for _, testcase := range testcases { + actual, err := ParseHostURL(testcase.host) + if testcase.expectedErr != "" { + testutil.ErrorContains(t, err, testcase.expectedErr) + } + assert.Equal(t, testcase.expected, actual) + } +} + func TestNewEnvClientSetsDefaultVersion(t *testing.T) { env := envToMap() defer mapToEnv(env) diff --git a/client/errors.go b/client/errors.go index fc7df9f1eb..3f9d36dc14 100644 --- a/client/errors.go +++ b/client/errors.go @@ -36,8 +36,8 @@ type notFound interface { NotFound() bool // Is the error a NotFound error } -// IsErrNotFound returns true if the error is caused with an -// object (image, container, network, volume, …) is not found in the docker host. +// IsErrNotFound returns true if the error is a NotFound error, which is returned +// by the API when some object is not found. func IsErrNotFound(err error) bool { te, ok := err.(notFound) return ok && te.NotFound() @@ -60,6 +60,8 @@ func (e imageNotFoundError) Error() string { // IsErrImageNotFound returns true if the error is caused // when an image is not found in the docker host. +// +// Deprecated: Use IsErrNotFound func IsErrImageNotFound(err error) bool { return IsErrNotFound(err) } @@ -81,6 +83,8 @@ func (e containerNotFoundError) Error() string { // IsErrContainerNotFound returns true if the error is caused // when a container is not found in the docker host. +// +// Deprecated: Use IsErrNotFound func IsErrContainerNotFound(err error) bool { return IsErrNotFound(err) } @@ -102,6 +106,8 @@ func (e networkNotFoundError) Error() string { // IsErrNetworkNotFound returns true if the error is caused // when a network is not found in the docker host. +// +// Deprecated: Use IsErrNotFound func IsErrNetworkNotFound(err error) bool { return IsErrNotFound(err) } @@ -123,6 +129,8 @@ func (e volumeNotFoundError) Error() string { // IsErrVolumeNotFound returns true if the error is caused // when a volume is not found in the docker host. +// +// Deprecated: Use IsErrNotFound func IsErrVolumeNotFound(err error) bool { return IsErrNotFound(err) } @@ -161,6 +169,8 @@ func (e nodeNotFoundError) NotFound() bool { // IsErrNodeNotFound returns true if the error is caused // when a node is not found. +// +// Deprecated: Use IsErrNotFound func IsErrNodeNotFound(err error) bool { _, ok := err.(nodeNotFoundError) return ok @@ -183,6 +193,8 @@ func (e serviceNotFoundError) NotFound() bool { // IsErrServiceNotFound returns true if the error is caused // when a service is not found. +// +// Deprecated: Use IsErrNotFound func IsErrServiceNotFound(err error) bool { _, ok := err.(serviceNotFoundError) return ok @@ -205,6 +217,8 @@ func (e taskNotFoundError) NotFound() bool { // IsErrTaskNotFound returns true if the error is caused // when a task is not found. +// +// Deprecated: Use IsErrNotFound func IsErrTaskNotFound(err error) bool { _, ok := err.(taskNotFoundError) return ok @@ -251,6 +265,8 @@ func (e secretNotFoundError) NotFound() bool { // IsErrSecretNotFound returns true if the error is caused // when a secret is not found. +// +// Deprecated: Use IsErrNotFound func IsErrSecretNotFound(err error) bool { _, ok := err.(secretNotFoundError) return ok @@ -273,6 +289,8 @@ func (e configNotFoundError) NotFound() bool { // IsErrConfigNotFound returns true if the error is caused // when a config is not found. +// +// Deprecated: Use IsErrNotFound func IsErrConfigNotFound(err error) bool { _, ok := err.(configNotFoundError) return ok @@ -295,6 +313,8 @@ func (e pluginNotFoundError) Error() string { // IsErrPluginNotFound returns true if the error is caused // when a plugin is not found in the docker host. +// +// Deprecated: Use IsErrNotFound func IsErrPluginNotFound(err error) bool { return IsErrNotFound(err) } diff --git a/client/parse_logs.go b/client/parse_logs.go deleted file mode 100644 index e427f80a77..0000000000 --- a/client/parse_logs.go +++ /dev/null @@ -1,41 +0,0 @@ -package client - -// parse_logs.go contains utility helpers for getting information out of docker -// log lines. really, it only contains ParseDetails right now. maybe in the -// future there will be some desire to parse log messages back into a struct? -// that would go here if we did - -import ( - "net/url" - "strings" - - "github.com/pkg/errors" -) - -// ParseLogDetails takes a details string of key value pairs in the form -// "k=v,l=w", where the keys and values are url query escaped, and each pair -// is separated by a comma, returns a map. returns an error if the details -// string is not in a valid format -// the exact form of details encoding is implemented in -// api/server/httputils/write_log_stream.go -func ParseLogDetails(details string) (map[string]string, error) { - pairs := strings.Split(details, ",") - detailsMap := make(map[string]string, len(pairs)) - for _, pair := range pairs { - p := strings.SplitN(pair, "=", 2) - // if there is no equals sign, we will only get 1 part back - if len(p) != 2 { - return nil, errors.New("invalid details format") - } - k, err := url.QueryUnescape(p[0]) - if err != nil { - return nil, err - } - v, err := url.QueryUnescape(p[1]) - if err != nil { - return nil, err - } - detailsMap[k] = v - } - return detailsMap, nil -} diff --git a/client/parse_logs_test.go b/client/parse_logs_test.go deleted file mode 100644 index ac7f61679d..0000000000 --- a/client/parse_logs_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package client - -import ( - "reflect" - "testing" - - "github.com/pkg/errors" -) - -func TestParseLogDetails(t *testing.T) { - testCases := []struct { - line string - expected map[string]string - err error - }{ - {"key=value", map[string]string{"key": "value"}, nil}, - {"key1=value1,key2=value2", map[string]string{"key1": "value1", "key2": "value2"}, nil}, - {"key+with+spaces=value%3Dequals,asdf%2C=", map[string]string{"key with spaces": "value=equals", "asdf,": ""}, nil}, - {"key=,=nothing", map[string]string{"key": "", "": "nothing"}, nil}, - {"=", map[string]string{"": ""}, nil}, - {"errors", nil, errors.New("invalid details format")}, - } - for _, tc := range testCases { - tc := tc // capture range variable - t.Run(tc.line, func(t *testing.T) { - t.Parallel() - res, err := ParseLogDetails(tc.line) - if err != nil && (err.Error() != tc.err.Error()) { - t.Fatalf("unexpected error parsing logs:\nExpected:\n\t%v\nActual:\n\t%v", tc.err, err) - } - if !reflect.DeepEqual(tc.expected, res) { - t.Errorf("result does not match expected:\nExpected:\n\t%#v\nActual:\n\t%#v", tc.expected, res) - } - }) - } -}