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