client_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. package client // import "github.com/docker/docker/client"
  2. import (
  3. "bytes"
  4. "context"
  5. "io"
  6. "net/http"
  7. "net/url"
  8. "os"
  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. defer env.PatchAll(t, nil)()
  85. for _, tc := range testcases {
  86. env.PatchAll(t, tc.envs)
  87. client, err := NewClientWithOpts(FromEnv)
  88. if tc.expectedError != "" {
  89. assert.Check(t, is.Error(err, tc.expectedError), tc.doc)
  90. } else {
  91. assert.Check(t, err, tc.doc)
  92. assert.Check(t, is.Equal(client.ClientVersion(), tc.expectedVersion), tc.doc)
  93. }
  94. if tc.envs["DOCKER_TLS_VERIFY"] != "" {
  95. // pedantic checking that this is handled correctly
  96. tr := client.client.Transport.(*http.Transport)
  97. assert.Assert(t, tr.TLSClientConfig != nil, tc.doc)
  98. assert.Check(t, is.Equal(tr.TLSClientConfig.InsecureSkipVerify, false), tc.doc)
  99. }
  100. }
  101. }
  102. func TestGetAPIPath(t *testing.T) {
  103. testcases := []struct {
  104. version string
  105. path string
  106. query url.Values
  107. expected string
  108. }{
  109. {"", "/containers/json", nil, "/v" + api.DefaultVersion + "/containers/json"},
  110. {"", "/containers/json", url.Values{}, "/v" + api.DefaultVersion + "/containers/json"},
  111. {"", "/containers/json", url.Values{"s": []string{"c"}}, "/v" + api.DefaultVersion + "/containers/json?s=c"},
  112. {"1.22", "/containers/json", nil, "/v1.22/containers/json"},
  113. {"1.22", "/containers/json", url.Values{}, "/v1.22/containers/json"},
  114. {"1.22", "/containers/json", url.Values{"s": []string{"c"}}, "/v1.22/containers/json?s=c"},
  115. {"v1.22", "/containers/json", nil, "/v1.22/containers/json"},
  116. {"v1.22", "/containers/json", url.Values{}, "/v1.22/containers/json"},
  117. {"v1.22", "/containers/json", url.Values{"s": []string{"c"}}, "/v1.22/containers/json?s=c"},
  118. {"v1.22", "/networks/kiwl$%^", nil, "/v1.22/networks/kiwl$%25%5E"},
  119. }
  120. ctx := context.TODO()
  121. for _, tc := range testcases {
  122. client, err := NewClientWithOpts(
  123. WithVersion(tc.version),
  124. WithHost("tcp://localhost:2375"),
  125. )
  126. assert.NilError(t, err)
  127. actual := client.getAPIPath(ctx, tc.path, tc.query)
  128. assert.Check(t, is.Equal(actual, tc.expected))
  129. }
  130. }
  131. func TestParseHostURL(t *testing.T) {
  132. testcases := []struct {
  133. host string
  134. expected *url.URL
  135. expectedErr string
  136. }{
  137. {
  138. host: "",
  139. expectedErr: "unable to parse docker host",
  140. },
  141. {
  142. host: "foobar",
  143. expectedErr: "unable to parse docker host",
  144. },
  145. {
  146. host: "foo://bar",
  147. expected: &url.URL{Scheme: "foo", Host: "bar"},
  148. },
  149. {
  150. host: "tcp://localhost:2476",
  151. expected: &url.URL{Scheme: "tcp", Host: "localhost:2476"},
  152. },
  153. {
  154. host: "tcp://localhost:2476/path",
  155. expected: &url.URL{Scheme: "tcp", Host: "localhost:2476", Path: "/path"},
  156. },
  157. }
  158. for _, testcase := range testcases {
  159. actual, err := ParseHostURL(testcase.host)
  160. if testcase.expectedErr != "" {
  161. assert.Check(t, is.ErrorContains(err, testcase.expectedErr))
  162. }
  163. assert.Check(t, is.DeepEqual(actual, testcase.expected))
  164. }
  165. }
  166. func TestNewClientWithOpsFromEnvSetsDefaultVersion(t *testing.T) {
  167. defer env.PatchAll(t, map[string]string{
  168. "DOCKER_HOST": "",
  169. "DOCKER_API_VERSION": "",
  170. "DOCKER_TLS_VERIFY": "",
  171. "DOCKER_CERT_PATH": "",
  172. })()
  173. client, err := NewClientWithOpts(FromEnv)
  174. if err != nil {
  175. t.Fatal(err)
  176. }
  177. assert.Check(t, is.Equal(client.ClientVersion(), api.DefaultVersion))
  178. const expected = "1.22"
  179. _ = os.Setenv("DOCKER_API_VERSION", expected)
  180. client, err = NewClientWithOpts(FromEnv)
  181. if err != nil {
  182. t.Fatal(err)
  183. }
  184. assert.Check(t, is.Equal(client.ClientVersion(), expected))
  185. }
  186. // TestNegotiateAPIVersionEmpty asserts that client.Client version negotiation
  187. // downgrades to the correct API version if the API's ping response does not
  188. // return an API version.
  189. func TestNegotiateAPIVersionEmpty(t *testing.T) {
  190. defer env.PatchAll(t, map[string]string{"DOCKER_API_VERSION": ""})()
  191. client, err := NewClientWithOpts(FromEnv)
  192. assert.NilError(t, err)
  193. // set our version to something new
  194. client.version = "1.25"
  195. // if no version from server, expect the earliest
  196. // version before APIVersion was implemented
  197. const expected = "1.24"
  198. // test downgrade
  199. client.NegotiateAPIVersionPing(types.Ping{})
  200. assert.Equal(t, client.ClientVersion(), expected)
  201. }
  202. // TestNegotiateAPIVersion asserts that client.Client can
  203. // negotiate a compatible APIVersion with the server
  204. func TestNegotiateAPIVersion(t *testing.T) {
  205. tests := []struct {
  206. doc string
  207. clientVersion string
  208. pingVersion string
  209. expectedVersion string
  210. }{
  211. {
  212. // client should downgrade to the version reported by the daemon.
  213. doc: "downgrade from default",
  214. pingVersion: "1.21",
  215. expectedVersion: "1.21",
  216. },
  217. {
  218. // client should not downgrade to the version reported by the
  219. // daemon if a custom version was set.
  220. doc: "no downgrade from custom version",
  221. clientVersion: "1.25",
  222. pingVersion: "1.21",
  223. expectedVersion: "1.25",
  224. },
  225. {
  226. // client should downgrade to the last version before version
  227. // negotiation was added (1.24) if the daemon does not report
  228. // a version.
  229. doc: "downgrade legacy",
  230. pingVersion: "",
  231. expectedVersion: "1.24",
  232. },
  233. {
  234. // client should downgrade to the version reported by the daemon.
  235. // version negotiation was added in API 1.25, so this is theoretical,
  236. // but it should negotiate to versions before that if the daemon
  237. // gives that as a response.
  238. doc: "downgrade old",
  239. pingVersion: "1.19",
  240. expectedVersion: "1.19",
  241. },
  242. {
  243. // client should not upgrade to a newer version if a version was set,
  244. // even if both the daemon and the client support it.
  245. doc: "no upgrade",
  246. clientVersion: "1.20",
  247. pingVersion: "1.21",
  248. expectedVersion: "1.20",
  249. },
  250. }
  251. for _, tc := range tests {
  252. tc := tc
  253. t.Run(tc.doc, func(t *testing.T) {
  254. opts := make([]Opt, 0)
  255. if tc.clientVersion != "" {
  256. // Note that this check is redundant, as WithVersion() considers
  257. // an empty version equivalent to "not setting a version", but
  258. // doing this just to be explicit we are using the default.
  259. opts = append(opts, WithVersion(tc.clientVersion))
  260. }
  261. client, err := NewClientWithOpts(opts...)
  262. assert.NilError(t, err)
  263. client.NegotiateAPIVersionPing(types.Ping{APIVersion: tc.pingVersion})
  264. assert.Equal(t, tc.expectedVersion, client.ClientVersion())
  265. })
  266. }
  267. }
  268. // TestNegotiateAPIVersionOverride asserts that we honor the DOCKER_API_VERSION
  269. // environment variable when negotiating versions.
  270. func TestNegotiateAPVersionOverride(t *testing.T) {
  271. const expected = "9.99"
  272. defer env.PatchAll(t, map[string]string{"DOCKER_API_VERSION": expected})()
  273. client, err := NewClientWithOpts(FromEnv)
  274. assert.NilError(t, err)
  275. // test that we honored the env var
  276. client.NegotiateAPIVersionPing(types.Ping{APIVersion: "1.24"})
  277. assert.Equal(t, client.ClientVersion(), expected)
  278. }
  279. func TestNegotiateAPIVersionAutomatic(t *testing.T) {
  280. var pingVersion string
  281. httpClient := newMockClient(func(req *http.Request) (*http.Response, error) {
  282. resp := &http.Response{StatusCode: http.StatusOK, Header: http.Header{}}
  283. resp.Header.Set("API-Version", pingVersion)
  284. resp.Body = io.NopCloser(strings.NewReader("OK"))
  285. return resp, nil
  286. })
  287. ctx := context.Background()
  288. client, err := NewClientWithOpts(
  289. WithHTTPClient(httpClient),
  290. WithAPIVersionNegotiation(),
  291. )
  292. assert.NilError(t, err)
  293. // Client defaults to use api.DefaultVersion before version-negotiation.
  294. expected := api.DefaultVersion
  295. assert.Equal(t, client.ClientVersion(), expected)
  296. // First request should trigger negotiation
  297. pingVersion = "1.35"
  298. expected = "1.35"
  299. _, _ = client.Info(ctx)
  300. assert.Equal(t, client.ClientVersion(), expected)
  301. // Once successfully negotiated, subsequent requests should not re-negotiate
  302. pingVersion = "1.25"
  303. expected = "1.35"
  304. _, _ = client.Info(ctx)
  305. assert.Equal(t, client.ClientVersion(), expected)
  306. }
  307. // TestNegotiateAPIVersionWithEmptyVersion asserts that initializing a client
  308. // with an empty version string does still allow API-version negotiation
  309. func TestNegotiateAPIVersionWithEmptyVersion(t *testing.T) {
  310. client, err := NewClientWithOpts(WithVersion(""))
  311. assert.NilError(t, err)
  312. const expected = "1.35"
  313. client.NegotiateAPIVersionPing(types.Ping{APIVersion: expected})
  314. assert.Equal(t, client.ClientVersion(), expected)
  315. }
  316. // TestNegotiateAPIVersionWithFixedVersion asserts that initializing a client
  317. // with a fixed version disables API-version negotiation
  318. func TestNegotiateAPIVersionWithFixedVersion(t *testing.T) {
  319. const customVersion = "1.35"
  320. client, err := NewClientWithOpts(WithVersion(customVersion))
  321. assert.NilError(t, err)
  322. client.NegotiateAPIVersionPing(types.Ping{APIVersion: "1.31"})
  323. assert.Equal(t, client.ClientVersion(), customVersion)
  324. }
  325. type roundTripFunc func(*http.Request) (*http.Response, error)
  326. func (rtf roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
  327. return rtf(req)
  328. }
  329. type bytesBufferClose struct {
  330. *bytes.Buffer
  331. }
  332. func (bbc bytesBufferClose) Close() error {
  333. return nil
  334. }
  335. func 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. cases := []struct {
  350. httpMethod string
  351. expectedErr *url.Error
  352. statusCode int
  353. }{
  354. {http.MethodGet, nil, 301},
  355. {http.MethodPost, &url.Error{Op: "Post", URL: "/bla", Err: ErrRedirect}, 301},
  356. {http.MethodPut, &url.Error{Op: "Put", URL: "/bla", Err: ErrRedirect}, 301},
  357. {http.MethodDelete, &url.Error{Op: "Delete", URL: "/bla", Err: ErrRedirect}, 301},
  358. }
  359. for _, tc := range cases {
  360. tc := tc
  361. t.Run(tc.httpMethod, func(t *testing.T) {
  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(resp.StatusCode, tc.statusCode))
  366. if tc.expectedErr == nil {
  367. assert.NilError(t, 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(*urlError, *tc.expectedErr))
  372. }
  373. })
  374. }
  375. }