moby/client/client_test.go
Sebastiaan van Stijn b26aa97914
Add client.WithAPIVersionNegotiation() option
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.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-04-10 19:23:47 +02:00

374 lines
10 KiB
Go

package client // import "github.com/docker/docker/client"
import (
"bytes"
"context"
"io/ioutil"
"net/http"
"net/url"
"os"
"runtime"
"strings"
"testing"
"github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
"gotest.tools/env"
"gotest.tools/skip"
)
func TestNewClientWithOpsFromEnv(t *testing.T) {
skip.If(t, runtime.GOOS == "windows")
testcases := []struct {
doc string
envs map[string]string
expectedError string
expectedVersion string
}{
{
doc: "default api version",
envs: map[string]string{},
expectedVersion: api.DefaultVersion,
},
{
doc: "invalid cert path",
envs: map[string]string{
"DOCKER_CERT_PATH": "invalid/path",
},
expectedError: "Could not load X509 key pair: open invalid/path/cert.pem: no such file or directory",
},
{
doc: "default api version with cert path",
envs: map[string]string{
"DOCKER_CERT_PATH": "testdata/",
},
expectedVersion: api.DefaultVersion,
},
{
doc: "default api version with cert path and tls verify",
envs: map[string]string{
"DOCKER_CERT_PATH": "testdata/",
"DOCKER_TLS_VERIFY": "1",
},
expectedVersion: api.DefaultVersion,
},
{
doc: "default api version with cert path and host",
envs: map[string]string{
"DOCKER_CERT_PATH": "testdata/",
"DOCKER_HOST": "https://notaunixsocket",
},
expectedVersion: api.DefaultVersion,
},
{
doc: "invalid docker host",
envs: map[string]string{
"DOCKER_HOST": "host",
},
expectedError: "unable to parse docker host `host`",
},
{
doc: "invalid docker host, with good format",
envs: map[string]string{
"DOCKER_HOST": "invalid://url",
},
expectedVersion: api.DefaultVersion,
},
{
doc: "override api version",
envs: map[string]string{
"DOCKER_API_VERSION": "1.22",
},
expectedVersion: "1.22",
},
}
defer env.PatchAll(t, nil)()
for _, c := range testcases {
env.PatchAll(t, c.envs)
apiclient, err := NewClientWithOpts(FromEnv)
if c.expectedError != "" {
assert.Check(t, is.Error(err, c.expectedError), c.doc)
} else {
assert.Check(t, err, c.doc)
version := apiclient.ClientVersion()
assert.Check(t, is.Equal(c.expectedVersion, version), c.doc)
}
if c.envs["DOCKER_TLS_VERIFY"] != "" {
// pedantic checking that this is handled correctly
tr := apiclient.client.Transport.(*http.Transport)
assert.Assert(t, tr.TLSClientConfig != nil, c.doc)
assert.Check(t, is.Equal(tr.TLSClientConfig.InsecureSkipVerify, false), c.doc)
}
}
}
func TestGetAPIPath(t *testing.T) {
testcases := []struct {
version string
path string
query url.Values
expected string
}{
{"", "/containers/json", nil, "/containers/json"},
{"", "/containers/json", url.Values{}, "/containers/json"},
{"", "/containers/json", url.Values{"s": []string{"c"}}, "/containers/json?s=c"},
{"1.22", "/containers/json", nil, "/v1.22/containers/json"},
{"1.22", "/containers/json", url.Values{}, "/v1.22/containers/json"},
{"1.22", "/containers/json", url.Values{"s": []string{"c"}}, "/v1.22/containers/json?s=c"},
{"v1.22", "/containers/json", nil, "/v1.22/containers/json"},
{"v1.22", "/containers/json", url.Values{}, "/v1.22/containers/json"},
{"v1.22", "/containers/json", url.Values{"s": []string{"c"}}, "/v1.22/containers/json?s=c"},
{"v1.22", "/networks/kiwl$%^", nil, "/v1.22/networks/kiwl$%25%5E"},
}
ctx := context.TODO()
for _, testcase := range testcases {
c := Client{version: testcase.version, basePath: "/"}
actual := c.getAPIPath(ctx, testcase.path, testcase.query)
assert.Check(t, is.Equal(actual, testcase.expected))
}
}
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 != "" {
assert.Check(t, is.ErrorContains(err, testcase.expectedErr))
}
assert.Check(t, is.DeepEqual(testcase.expected, actual))
}
}
func TestNewClientWithOpsFromEnvSetsDefaultVersion(t *testing.T) {
defer env.PatchAll(t, map[string]string{
"DOCKER_HOST": "",
"DOCKER_API_VERSION": "",
"DOCKER_TLS_VERIFY": "",
"DOCKER_CERT_PATH": "",
})()
client, err := NewClientWithOpts(FromEnv)
if err != nil {
t.Fatal(err)
}
assert.Check(t, is.Equal(client.version, api.DefaultVersion))
expected := "1.22"
os.Setenv("DOCKER_API_VERSION", expected)
client, err = NewClientWithOpts(FromEnv)
if err != nil {
t.Fatal(err)
}
assert.Check(t, is.Equal(expected, client.version))
}
// TestNegotiateAPIVersionEmpty asserts that client.Client can
// negotiate a compatible APIVersion when omitted
func TestNegotiateAPIVersionEmpty(t *testing.T) {
defer env.PatchAll(t, map[string]string{"DOCKER_API_VERSION": ""})()
client, err := NewClientWithOpts(FromEnv)
assert.NilError(t, err)
ping := types.Ping{
APIVersion: "",
OSType: "linux",
Experimental: false,
}
// set our version to something new
client.version = "1.25"
// if no version from server, expect the earliest
// version before APIVersion was implemented
expected := "1.24"
// test downgrade
client.NegotiateAPIVersionPing(ping)
assert.Check(t, is.Equal(expected, client.version))
}
// TestNegotiateAPIVersion asserts that client.Client can
// negotiate a compatible APIVersion with the server
func TestNegotiateAPIVersion(t *testing.T) {
client, err := NewClientWithOpts(FromEnv)
assert.NilError(t, err)
expected := "1.21"
ping := types.Ping{
APIVersion: expected,
OSType: "linux",
Experimental: false,
}
// set our version to something new
client.version = "1.22"
// test downgrade
client.NegotiateAPIVersionPing(ping)
assert.Check(t, is.Equal(expected, client.version))
// set the client version to something older, and verify that we keep the
// original setting.
expected = "1.20"
client.version = expected
client.NegotiateAPIVersionPing(ping)
assert.Check(t, is.Equal(expected, client.version))
}
// TestNegotiateAPIVersionOverride asserts that we honor
// the environment variable DOCKER_API_VERSION when negotiating versions
func TestNegotiateAPVersionOverride(t *testing.T) {
expected := "9.99"
defer env.PatchAll(t, map[string]string{"DOCKER_API_VERSION": expected})()
client, err := NewClientWithOpts(FromEnv)
assert.NilError(t, err)
ping := types.Ping{
APIVersion: "1.24",
OSType: "linux",
Experimental: false,
}
// test that we honored the env var
client.NegotiateAPIVersionPing(ping)
assert.Check(t, is.Equal(expected, client.version))
}
func TestNegotiateAPIVersionAutomatic(t *testing.T) {
var pingVersion string
httpClient := newMockClient(func(req *http.Request) (*http.Response, error) {
resp := &http.Response{StatusCode: http.StatusOK, Header: http.Header{}}
resp.Header.Set("API-Version", pingVersion)
resp.Body = ioutil.NopCloser(strings.NewReader("OK"))
return resp, nil
})
client, err := NewClientWithOpts(
WithHTTPClient(httpClient),
WithAPIVersionNegotiation(),
)
assert.NilError(t, err)
ctx := context.Background()
assert.Equal(t, client.ClientVersion(), api.DefaultVersion)
// First request should trigger negotiation
pingVersion = "1.35"
_, _ = client.Info(ctx)
assert.Equal(t, client.ClientVersion(), "1.35")
// Once successfully negotiated, subsequent requests should not re-negotiate
pingVersion = "1.25"
_, _ = client.Info(ctx)
assert.Equal(t, client.ClientVersion(), "1.35")
}
// TestNegotiateAPIVersionWithEmptyVersion asserts that initializing a client
// with an empty version string does still allow API-version negotiation
func TestNegotiateAPIVersionWithEmptyVersion(t *testing.T) {
client, err := NewClientWithOpts(WithVersion(""))
assert.NilError(t, err)
client.NegotiateAPIVersionPing(types.Ping{APIVersion: "1.35"})
assert.Equal(t, client.version, "1.35")
}
// TestNegotiateAPIVersionWithFixedVersion asserts that initializing a client
// with an fixed version disables API-version negotiation
func TestNegotiateAPIVersionWithFixedVersion(t *testing.T) {
client, err := NewClientWithOpts(WithVersion("1.35"))
assert.NilError(t, err)
client.NegotiateAPIVersionPing(types.Ping{APIVersion: "1.31"})
assert.Equal(t, client.version, "1.35")
}
type roundTripFunc func(*http.Request) (*http.Response, error)
func (rtf roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return rtf(req)
}
type bytesBufferClose struct {
*bytes.Buffer
}
func (bbc bytesBufferClose) Close() error {
return nil
}
func TestClientRedirect(t *testing.T) {
client := &http.Client{
CheckRedirect: CheckRedirect,
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
if req.URL.String() == "/bla" {
return &http.Response{StatusCode: 404}, nil
}
return &http.Response{
StatusCode: 301,
Header: map[string][]string{"Location": {"/bla"}},
Body: bytesBufferClose{bytes.NewBuffer(nil)},
}, nil
}),
}
cases := []struct {
httpMethod string
expectedErr *url.Error
statusCode int
}{
{http.MethodGet, nil, 301},
{http.MethodPost, &url.Error{Op: "Post", URL: "/bla", Err: ErrRedirect}, 301},
{http.MethodPut, &url.Error{Op: "Put", URL: "/bla", Err: ErrRedirect}, 301},
{http.MethodDelete, &url.Error{Op: "Delete", URL: "/bla", Err: ErrRedirect}, 301},
}
for _, tc := range cases {
req, err := http.NewRequest(tc.httpMethod, "/redirectme", nil)
assert.Check(t, err)
resp, err := client.Do(req)
assert.Check(t, is.Equal(tc.statusCode, resp.StatusCode))
if tc.expectedErr == nil {
assert.Check(t, is.Nil(err))
} else {
urlError, ok := err.(*url.Error)
assert.Assert(t, ok, "%T is not *url.Error", err)
assert.Check(t, is.Equal(*tc.expectedErr, *urlError))
}
}
}