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