client_test.go 13 KB

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