1package client // import "github.com/docker/docker/client"
2
3import (
4	"bytes"
5	"context"
6	"io/ioutil"
7	"net/http"
8	"net/url"
9	"os"
10	"runtime"
11	"strings"
12	"testing"
13
14	"github.com/docker/docker/api"
15	"github.com/docker/docker/api/types"
16	"gotest.tools/v3/assert"
17	is "gotest.tools/v3/assert/cmp"
18	"gotest.tools/v3/env"
19	"gotest.tools/v3/skip"
20)
21
22func TestNewClientWithOpsFromEnv(t *testing.T) {
23	skip.If(t, runtime.GOOS == "windows")
24
25	testcases := []struct {
26		doc             string
27		envs            map[string]string
28		expectedError   string
29		expectedVersion string
30	}{
31		{
32			doc:             "default api version",
33			envs:            map[string]string{},
34			expectedVersion: api.DefaultVersion,
35		},
36		{
37			doc: "invalid cert path",
38			envs: map[string]string{
39				"DOCKER_CERT_PATH": "invalid/path",
40			},
41			expectedError: "Could not load X509 key pair: open invalid/path/cert.pem: no such file or directory",
42		},
43		{
44			doc: "default api version with cert path",
45			envs: map[string]string{
46				"DOCKER_CERT_PATH": "testdata/",
47			},
48			expectedVersion: api.DefaultVersion,
49		},
50		{
51			doc: "default api version with cert path and tls verify",
52			envs: map[string]string{
53				"DOCKER_CERT_PATH":  "testdata/",
54				"DOCKER_TLS_VERIFY": "1",
55			},
56			expectedVersion: api.DefaultVersion,
57		},
58		{
59			doc: "default api version with cert path and host",
60			envs: map[string]string{
61				"DOCKER_CERT_PATH": "testdata/",
62				"DOCKER_HOST":      "https://notaunixsocket",
63			},
64			expectedVersion: api.DefaultVersion,
65		},
66		{
67			doc: "invalid docker host",
68			envs: map[string]string{
69				"DOCKER_HOST": "host",
70			},
71			expectedError: "unable to parse docker host `host`",
72		},
73		{
74			doc: "invalid docker host, with good format",
75			envs: map[string]string{
76				"DOCKER_HOST": "invalid://url",
77			},
78			expectedVersion: api.DefaultVersion,
79		},
80		{
81			doc: "override api version",
82			envs: map[string]string{
83				"DOCKER_API_VERSION": "1.22",
84			},
85			expectedVersion: "1.22",
86		},
87	}
88
89	defer env.PatchAll(t, nil)()
90	for _, c := range testcases {
91		env.PatchAll(t, c.envs)
92		apiclient, err := NewClientWithOpts(FromEnv)
93		if c.expectedError != "" {
94			assert.Check(t, is.Error(err, c.expectedError), c.doc)
95		} else {
96			assert.Check(t, err, c.doc)
97			version := apiclient.ClientVersion()
98			assert.Check(t, is.Equal(c.expectedVersion, version), c.doc)
99		}
100
101		if c.envs["DOCKER_TLS_VERIFY"] != "" {
102			// pedantic checking that this is handled correctly
103			tr := apiclient.client.Transport.(*http.Transport)
104			assert.Assert(t, tr.TLSClientConfig != nil, c.doc)
105			assert.Check(t, is.Equal(tr.TLSClientConfig.InsecureSkipVerify, false), c.doc)
106		}
107	}
108}
109
110func TestGetAPIPath(t *testing.T) {
111	testcases := []struct {
112		version  string
113		path     string
114		query    url.Values
115		expected string
116	}{
117		{"", "/containers/json", nil, "/containers/json"},
118		{"", "/containers/json", url.Values{}, "/containers/json"},
119		{"", "/containers/json", url.Values{"s": []string{"c"}}, "/containers/json?s=c"},
120		{"1.22", "/containers/json", nil, "/v1.22/containers/json"},
121		{"1.22", "/containers/json", url.Values{}, "/v1.22/containers/json"},
122		{"1.22", "/containers/json", url.Values{"s": []string{"c"}}, "/v1.22/containers/json?s=c"},
123		{"v1.22", "/containers/json", nil, "/v1.22/containers/json"},
124		{"v1.22", "/containers/json", url.Values{}, "/v1.22/containers/json"},
125		{"v1.22", "/containers/json", url.Values{"s": []string{"c"}}, "/v1.22/containers/json?s=c"},
126		{"v1.22", "/networks/kiwl$%^", nil, "/v1.22/networks/kiwl$%25%5E"},
127	}
128
129	ctx := context.TODO()
130	for _, testcase := range testcases {
131		c := Client{version: testcase.version, basePath: "/"}
132		actual := c.getAPIPath(ctx, testcase.path, testcase.query)
133		assert.Check(t, is.Equal(actual, testcase.expected))
134	}
135}
136
137func TestParseHostURL(t *testing.T) {
138	testcases := []struct {
139		host        string
140		expected    *url.URL
141		expectedErr string
142	}{
143		{
144			host:        "",
145			expectedErr: "unable to parse docker host",
146		},
147		{
148			host:        "foobar",
149			expectedErr: "unable to parse docker host",
150		},
151		{
152			host:     "foo://bar",
153			expected: &url.URL{Scheme: "foo", Host: "bar"},
154		},
155		{
156			host:     "tcp://localhost:2476",
157			expected: &url.URL{Scheme: "tcp", Host: "localhost:2476"},
158		},
159		{
160			host:     "tcp://localhost:2476/path",
161			expected: &url.URL{Scheme: "tcp", Host: "localhost:2476", Path: "/path"},
162		},
163	}
164
165	for _, testcase := range testcases {
166		actual, err := ParseHostURL(testcase.host)
167		if testcase.expectedErr != "" {
168			assert.Check(t, is.ErrorContains(err, testcase.expectedErr))
169		}
170		assert.Check(t, is.DeepEqual(testcase.expected, actual))
171	}
172}
173
174func TestNewClientWithOpsFromEnvSetsDefaultVersion(t *testing.T) {
175	defer env.PatchAll(t, map[string]string{
176		"DOCKER_HOST":        "",
177		"DOCKER_API_VERSION": "",
178		"DOCKER_TLS_VERIFY":  "",
179		"DOCKER_CERT_PATH":   "",
180	})()
181
182	client, err := NewClientWithOpts(FromEnv)
183	if err != nil {
184		t.Fatal(err)
185	}
186	assert.Check(t, is.Equal(client.version, api.DefaultVersion))
187
188	expected := "1.22"
189	os.Setenv("DOCKER_API_VERSION", expected)
190	client, err = NewClientWithOpts(FromEnv)
191	if err != nil {
192		t.Fatal(err)
193	}
194	assert.Check(t, is.Equal(expected, client.version))
195}
196
197// TestNegotiateAPIVersionEmpty asserts that client.Client can
198// negotiate a compatible APIVersion when omitted
199func TestNegotiateAPIVersionEmpty(t *testing.T) {
200	defer env.PatchAll(t, map[string]string{"DOCKER_API_VERSION": ""})()
201
202	client, err := NewClientWithOpts(FromEnv)
203	assert.NilError(t, err)
204
205	ping := types.Ping{
206		APIVersion:   "",
207		OSType:       "linux",
208		Experimental: false,
209	}
210
211	// set our version to something new
212	client.version = "1.25"
213
214	// if no version from server, expect the earliest
215	// version before APIVersion was implemented
216	expected := "1.24"
217
218	// test downgrade
219	client.NegotiateAPIVersionPing(ping)
220	assert.Check(t, is.Equal(expected, client.version))
221}
222
223// TestNegotiateAPIVersion asserts that client.Client can
224// negotiate a compatible APIVersion with the server
225func TestNegotiateAPIVersion(t *testing.T) {
226	client, err := NewClientWithOpts(FromEnv)
227	assert.NilError(t, err)
228
229	expected := "1.21"
230	ping := types.Ping{
231		APIVersion:   expected,
232		OSType:       "linux",
233		Experimental: false,
234	}
235
236	// set our version to something new
237	client.version = "1.22"
238
239	// test downgrade
240	client.NegotiateAPIVersionPing(ping)
241	assert.Check(t, is.Equal(expected, client.version))
242
243	// set the client version to something older, and verify that we keep the
244	// original setting.
245	expected = "1.20"
246	client.version = expected
247	client.NegotiateAPIVersionPing(ping)
248	assert.Check(t, is.Equal(expected, client.version))
249
250}
251
252// TestNegotiateAPIVersionOverride asserts that we honor
253// the environment variable DOCKER_API_VERSION when negotiating versions
254func TestNegotiateAPVersionOverride(t *testing.T) {
255	expected := "9.99"
256	defer env.PatchAll(t, map[string]string{"DOCKER_API_VERSION": expected})()
257
258	client, err := NewClientWithOpts(FromEnv)
259	assert.NilError(t, err)
260
261	ping := types.Ping{
262		APIVersion:   "1.24",
263		OSType:       "linux",
264		Experimental: false,
265	}
266
267	// test that we honored the env var
268	client.NegotiateAPIVersionPing(ping)
269	assert.Check(t, is.Equal(expected, client.version))
270}
271
272func TestNegotiateAPIVersionAutomatic(t *testing.T) {
273	var pingVersion string
274	httpClient := newMockClient(func(req *http.Request) (*http.Response, error) {
275		resp := &http.Response{StatusCode: http.StatusOK, Header: http.Header{}}
276		resp.Header.Set("API-Version", pingVersion)
277		resp.Body = ioutil.NopCloser(strings.NewReader("OK"))
278		return resp, nil
279	})
280
281	client, err := NewClientWithOpts(
282		WithHTTPClient(httpClient),
283		WithAPIVersionNegotiation(),
284	)
285	assert.NilError(t, err)
286
287	ctx := context.Background()
288	assert.Equal(t, client.ClientVersion(), api.DefaultVersion)
289
290	// First request should trigger negotiation
291	pingVersion = "1.35"
292	_, _ = client.Info(ctx)
293	assert.Equal(t, client.ClientVersion(), "1.35")
294
295	// Once successfully negotiated, subsequent requests should not re-negotiate
296	pingVersion = "1.25"
297	_, _ = client.Info(ctx)
298	assert.Equal(t, client.ClientVersion(), "1.35")
299}
300
301// TestNegotiateAPIVersionWithEmptyVersion asserts that initializing a client
302// with an empty version string does still allow API-version negotiation
303func TestNegotiateAPIVersionWithEmptyVersion(t *testing.T) {
304	client, err := NewClientWithOpts(WithVersion(""))
305	assert.NilError(t, err)
306
307	client.NegotiateAPIVersionPing(types.Ping{APIVersion: "1.35"})
308	assert.Equal(t, client.version, "1.35")
309}
310
311// TestNegotiateAPIVersionWithFixedVersion asserts that initializing a client
312// with an fixed version disables API-version negotiation
313func TestNegotiateAPIVersionWithFixedVersion(t *testing.T) {
314	client, err := NewClientWithOpts(WithVersion("1.35"))
315	assert.NilError(t, err)
316
317	client.NegotiateAPIVersionPing(types.Ping{APIVersion: "1.31"})
318	assert.Equal(t, client.version, "1.35")
319}
320
321type roundTripFunc func(*http.Request) (*http.Response, error)
322
323func (rtf roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
324	return rtf(req)
325}
326
327type bytesBufferClose struct {
328	*bytes.Buffer
329}
330
331func (bbc bytesBufferClose) Close() error {
332	return nil
333}
334
335func TestClientRedirect(t *testing.T) {
336	client := &http.Client{
337		CheckRedirect: CheckRedirect,
338		Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
339			if req.URL.String() == "/bla" {
340				return &http.Response{StatusCode: 404}, nil
341			}
342			return &http.Response{
343				StatusCode: 301,
344				Header:     map[string][]string{"Location": {"/bla"}},
345				Body:       bytesBufferClose{bytes.NewBuffer(nil)},
346			}, nil
347		}),
348	}
349
350	cases := []struct {
351		httpMethod  string
352		expectedErr *url.Error
353		statusCode  int
354	}{
355		{http.MethodGet, nil, 301},
356		{http.MethodPost, &url.Error{Op: "Post", URL: "/bla", Err: ErrRedirect}, 301},
357		{http.MethodPut, &url.Error{Op: "Put", URL: "/bla", Err: ErrRedirect}, 301},
358		{http.MethodDelete, &url.Error{Op: "Delete", URL: "/bla", Err: ErrRedirect}, 301},
359	}
360
361	for _, tc := range cases {
362		req, err := http.NewRequest(tc.httpMethod, "/redirectme", nil)
363		assert.Check(t, err)
364		resp, err := client.Do(req)
365		assert.Check(t, is.Equal(tc.statusCode, resp.StatusCode))
366		if tc.expectedErr == nil {
367			assert.Check(t, is.Nil(err))
368		} else {
369			urlError, ok := err.(*url.Error)
370			assert.Assert(t, ok, "%T is not *url.Error", err)
371			assert.Check(t, is.Equal(*tc.expectedErr, *urlError))
372		}
373	}
374}
375