github.com/rish1988/moby@v25.0.2+incompatible/client/client_test.go (about) 1 package client // import "github.com/docker/docker/client" 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "io" 8 "net/http" 9 "net/url" 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 22 func 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 env.PatchAll(t, nil) 90 for _, tc := range testcases { 91 tc := tc 92 t.Run(tc.doc, func(t *testing.T) { 93 env.PatchAll(t, tc.envs) 94 client, err := NewClientWithOpts(FromEnv) 95 if tc.expectedError != "" { 96 assert.Check(t, is.Error(err, tc.expectedError)) 97 } else { 98 assert.Check(t, err) 99 assert.Check(t, is.Equal(client.ClientVersion(), tc.expectedVersion)) 100 } 101 102 if tc.envs["DOCKER_TLS_VERIFY"] != "" { 103 // pedantic checking that this is handled correctly 104 tlsConfig := client.tlsConfig() 105 assert.Assert(t, tlsConfig != nil) 106 assert.Check(t, is.Equal(tlsConfig.InsecureSkipVerify, false)) 107 } 108 }) 109 } 110 } 111 112 func TestGetAPIPath(t *testing.T) { 113 tests := []struct { 114 version string 115 path string 116 query url.Values 117 expected string 118 }{ 119 { 120 path: "/containers/json", 121 expected: "/v" + api.DefaultVersion + "/containers/json", 122 }, 123 { 124 path: "/containers/json", 125 query: url.Values{}, 126 expected: "/v" + api.DefaultVersion + "/containers/json", 127 }, 128 { 129 path: "/containers/json", 130 query: url.Values{"s": []string{"c"}}, 131 expected: "/v" + api.DefaultVersion + "/containers/json?s=c", 132 }, 133 { 134 version: "1.22", 135 path: "/containers/json", 136 expected: "/v1.22/containers/json", 137 }, 138 { 139 version: "1.22", 140 path: "/containers/json", 141 query: url.Values{}, 142 expected: "/v1.22/containers/json", 143 }, 144 { 145 version: "1.22", 146 path: "/containers/json", 147 query: url.Values{"s": []string{"c"}}, 148 expected: "/v1.22/containers/json?s=c", 149 }, 150 { 151 version: "v1.22", 152 path: "/containers/json", 153 expected: "/v1.22/containers/json", 154 }, 155 { 156 version: "v1.22", 157 path: "/containers/json", 158 query: url.Values{}, 159 expected: "/v1.22/containers/json", 160 }, 161 { 162 version: "v1.22", 163 path: "/containers/json", 164 query: url.Values{"s": []string{"c"}}, 165 expected: "/v1.22/containers/json?s=c", 166 }, 167 { 168 version: "v1.22", 169 path: "/networks/kiwl$%^", 170 expected: "/v1.22/networks/kiwl$%25%5E", 171 }, 172 } 173 174 ctx := context.TODO() 175 for _, tc := range tests { 176 client, err := NewClientWithOpts( 177 WithVersion(tc.version), 178 WithHost("tcp://localhost:2375"), 179 ) 180 assert.NilError(t, err) 181 actual := client.getAPIPath(ctx, tc.path, tc.query) 182 assert.Check(t, is.Equal(actual, tc.expected)) 183 } 184 } 185 186 func TestParseHostURL(t *testing.T) { 187 testcases := []struct { 188 host string 189 expected *url.URL 190 expectedErr string 191 }{ 192 { 193 host: "", 194 expectedErr: "unable to parse docker host", 195 }, 196 { 197 host: "foobar", 198 expectedErr: "unable to parse docker host", 199 }, 200 { 201 host: "foo://bar", 202 expected: &url.URL{Scheme: "foo", Host: "bar"}, 203 }, 204 { 205 host: "tcp://localhost:2476", 206 expected: &url.URL{Scheme: "tcp", Host: "localhost:2476"}, 207 }, 208 { 209 host: "tcp://localhost:2476/path", 210 expected: &url.URL{Scheme: "tcp", Host: "localhost:2476", Path: "/path"}, 211 }, 212 { 213 host: "unix:///var/run/docker.sock", 214 expected: &url.URL{Scheme: "unix", Host: "/var/run/docker.sock"}, 215 }, 216 { 217 host: "npipe:////./pipe/docker_engine", 218 expected: &url.URL{Scheme: "npipe", Host: "//./pipe/docker_engine"}, 219 }, 220 } 221 222 for _, testcase := range testcases { 223 actual, err := ParseHostURL(testcase.host) 224 if testcase.expectedErr != "" { 225 assert.Check(t, is.ErrorContains(err, testcase.expectedErr)) 226 } 227 assert.Check(t, is.DeepEqual(actual, testcase.expected)) 228 } 229 } 230 231 func TestNewClientWithOpsFromEnvSetsDefaultVersion(t *testing.T) { 232 env.PatchAll(t, map[string]string{ 233 "DOCKER_HOST": "", 234 "DOCKER_API_VERSION": "", 235 "DOCKER_TLS_VERIFY": "", 236 "DOCKER_CERT_PATH": "", 237 }) 238 239 client, err := NewClientWithOpts(FromEnv) 240 if err != nil { 241 t.Fatal(err) 242 } 243 assert.Check(t, is.Equal(client.ClientVersion(), api.DefaultVersion)) 244 245 const expected = "1.22" 246 t.Setenv("DOCKER_API_VERSION", expected) 247 client, err = NewClientWithOpts(FromEnv) 248 if err != nil { 249 t.Fatal(err) 250 } 251 assert.Check(t, is.Equal(client.ClientVersion(), expected)) 252 } 253 254 // TestNegotiateAPIVersionEmpty asserts that client.Client version negotiation 255 // downgrades to the correct API version if the API's ping response does not 256 // return an API version. 257 func TestNegotiateAPIVersionEmpty(t *testing.T) { 258 t.Setenv("DOCKER_API_VERSION", "") 259 260 client, err := NewClientWithOpts(FromEnv) 261 assert.NilError(t, err) 262 263 // set our version to something new 264 client.version = "1.25" 265 266 // if no version from server, expect the earliest 267 // version before APIVersion was implemented 268 const expected = "1.24" 269 270 // test downgrade 271 client.NegotiateAPIVersionPing(types.Ping{}) 272 assert.Equal(t, client.ClientVersion(), expected) 273 } 274 275 // TestNegotiateAPIVersion asserts that client.Client can 276 // negotiate a compatible APIVersion with the server 277 func TestNegotiateAPIVersion(t *testing.T) { 278 tests := []struct { 279 doc string 280 clientVersion string 281 pingVersion string 282 expectedVersion string 283 }{ 284 { 285 // client should downgrade to the version reported by the daemon. 286 doc: "downgrade from default", 287 pingVersion: "1.21", 288 expectedVersion: "1.21", 289 }, 290 { 291 // client should not downgrade to the version reported by the 292 // daemon if a custom version was set. 293 doc: "no downgrade from custom version", 294 clientVersion: "1.25", 295 pingVersion: "1.21", 296 expectedVersion: "1.25", 297 }, 298 { 299 // client should downgrade to the last version before version 300 // negotiation was added (1.24) if the daemon does not report 301 // a version. 302 doc: "downgrade legacy", 303 pingVersion: "", 304 expectedVersion: "1.24", 305 }, 306 { 307 // client should downgrade to the version reported by the daemon. 308 // version negotiation was added in API 1.25, so this is theoretical, 309 // but it should negotiate to versions before that if the daemon 310 // gives that as a response. 311 doc: "downgrade old", 312 pingVersion: "1.19", 313 expectedVersion: "1.19", 314 }, 315 { 316 // client should not upgrade to a newer version if a version was set, 317 // even if both the daemon and the client support it. 318 doc: "no upgrade", 319 clientVersion: "1.20", 320 pingVersion: "1.21", 321 expectedVersion: "1.20", 322 }, 323 } 324 325 for _, tc := range tests { 326 tc := tc 327 t.Run(tc.doc, func(t *testing.T) { 328 opts := make([]Opt, 0) 329 if tc.clientVersion != "" { 330 // Note that this check is redundant, as WithVersion() considers 331 // an empty version equivalent to "not setting a version", but 332 // doing this just to be explicit we are using the default. 333 opts = append(opts, WithVersion(tc.clientVersion)) 334 } 335 client, err := NewClientWithOpts(opts...) 336 assert.NilError(t, err) 337 client.NegotiateAPIVersionPing(types.Ping{APIVersion: tc.pingVersion}) 338 assert.Equal(t, tc.expectedVersion, client.ClientVersion()) 339 }) 340 } 341 } 342 343 // TestNegotiateAPIVersionOverride asserts that we honor the DOCKER_API_VERSION 344 // environment variable when negotiating versions. 345 func TestNegotiateAPVersionOverride(t *testing.T) { 346 const expected = "9.99" 347 t.Setenv("DOCKER_API_VERSION", expected) 348 349 client, err := NewClientWithOpts(FromEnv) 350 assert.NilError(t, err) 351 352 // test that we honored the env var 353 client.NegotiateAPIVersionPing(types.Ping{APIVersion: "1.24"}) 354 assert.Equal(t, client.ClientVersion(), expected) 355 } 356 357 func TestNegotiateAPIVersionAutomatic(t *testing.T) { 358 var pingVersion string 359 httpClient := newMockClient(func(req *http.Request) (*http.Response, error) { 360 resp := &http.Response{StatusCode: http.StatusOK, Header: http.Header{}} 361 resp.Header.Set("API-Version", pingVersion) 362 resp.Body = io.NopCloser(strings.NewReader("OK")) 363 return resp, nil 364 }) 365 366 ctx := context.Background() 367 client, err := NewClientWithOpts( 368 WithHTTPClient(httpClient), 369 WithAPIVersionNegotiation(), 370 ) 371 assert.NilError(t, err) 372 373 // Client defaults to use api.DefaultVersion before version-negotiation. 374 expected := api.DefaultVersion 375 assert.Equal(t, client.ClientVersion(), expected) 376 377 // First request should trigger negotiation 378 pingVersion = "1.35" 379 expected = "1.35" 380 _, _ = client.Info(ctx) 381 assert.Equal(t, client.ClientVersion(), expected) 382 383 // Once successfully negotiated, subsequent requests should not re-negotiate 384 pingVersion = "1.25" 385 expected = "1.35" 386 _, _ = client.Info(ctx) 387 assert.Equal(t, client.ClientVersion(), expected) 388 } 389 390 // TestNegotiateAPIVersionWithEmptyVersion asserts that initializing a client 391 // with an empty version string does still allow API-version negotiation 392 func TestNegotiateAPIVersionWithEmptyVersion(t *testing.T) { 393 client, err := NewClientWithOpts(WithVersion("")) 394 assert.NilError(t, err) 395 396 const expected = "1.35" 397 client.NegotiateAPIVersionPing(types.Ping{APIVersion: expected}) 398 assert.Equal(t, client.ClientVersion(), expected) 399 } 400 401 // TestNegotiateAPIVersionWithFixedVersion asserts that initializing a client 402 // with a fixed version disables API-version negotiation 403 func TestNegotiateAPIVersionWithFixedVersion(t *testing.T) { 404 const customVersion = "1.35" 405 client, err := NewClientWithOpts(WithVersion(customVersion)) 406 assert.NilError(t, err) 407 408 client.NegotiateAPIVersionPing(types.Ping{APIVersion: "1.31"}) 409 assert.Equal(t, client.ClientVersion(), customVersion) 410 } 411 412 type roundTripFunc func(*http.Request) (*http.Response, error) 413 414 func (rtf roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { 415 return rtf(req) 416 } 417 418 type bytesBufferClose struct { 419 *bytes.Buffer 420 } 421 422 func (bbc bytesBufferClose) Close() error { 423 return nil 424 } 425 426 func TestClientRedirect(t *testing.T) { 427 client := &http.Client{ 428 CheckRedirect: CheckRedirect, 429 Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { 430 if req.URL.String() == "/bla" { 431 return &http.Response{StatusCode: http.StatusNotFound}, nil 432 } 433 return &http.Response{ 434 StatusCode: http.StatusMovedPermanently, 435 Header: http.Header{"Location": {"/bla"}}, 436 Body: bytesBufferClose{bytes.NewBuffer(nil)}, 437 }, nil 438 }), 439 } 440 441 tests := []struct { 442 httpMethod string 443 expectedErr *url.Error 444 statusCode int 445 }{ 446 { 447 httpMethod: http.MethodGet, 448 statusCode: http.StatusMovedPermanently, 449 }, 450 { 451 httpMethod: http.MethodPost, 452 expectedErr: &url.Error{Op: "Post", URL: "/bla", Err: ErrRedirect}, 453 statusCode: http.StatusMovedPermanently, 454 }, 455 { 456 httpMethod: http.MethodPut, 457 expectedErr: &url.Error{Op: "Put", URL: "/bla", Err: ErrRedirect}, 458 statusCode: http.StatusMovedPermanently, 459 }, 460 { 461 httpMethod: http.MethodDelete, 462 expectedErr: &url.Error{Op: "Delete", URL: "/bla", Err: ErrRedirect}, 463 statusCode: http.StatusMovedPermanently, 464 }, 465 } 466 467 for _, tc := range tests { 468 tc := tc 469 t.Run(tc.httpMethod, func(t *testing.T) { 470 req, err := http.NewRequest(tc.httpMethod, "/redirectme", nil) 471 assert.Check(t, err) 472 resp, err := client.Do(req) 473 assert.Check(t, is.Equal(resp.StatusCode, tc.statusCode)) 474 if tc.expectedErr == nil { 475 assert.Check(t, err) 476 } else { 477 assert.Check(t, is.ErrorType(err, &url.Error{})) 478 var urlError *url.Error 479 assert.Assert(t, errors.As(err, &urlError), "%T is not *url.Error", err) 480 assert.Check(t, is.Equal(*urlError, *tc.expectedErr)) 481 } 482 }) 483 } 484 }