docker_api_exec_test.go 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. package main
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "fmt"
  7. "io"
  8. "net/http"
  9. "os"
  10. "strings"
  11. "testing"
  12. "time"
  13. "github.com/docker/docker/api/types"
  14. "github.com/docker/docker/api/types/versions"
  15. "github.com/docker/docker/client"
  16. "github.com/docker/docker/integration-cli/checker"
  17. "github.com/docker/docker/testutil"
  18. "github.com/docker/docker/testutil/request"
  19. "gotest.tools/v3/assert"
  20. is "gotest.tools/v3/assert/cmp"
  21. "gotest.tools/v3/poll"
  22. )
  23. // Regression test for #9414
  24. func (s *DockerAPISuite) TestExecAPICreateNoCmd(c *testing.T) {
  25. name := "exec_test"
  26. dockerCmd(c, "run", "-d", "-t", "--name", name, "busybox", "/bin/sh")
  27. res, body, err := request.Post(testutil.GetContext(c), fmt.Sprintf("/containers/%s/exec", name), request.JSONBody(map[string]interface{}{"Cmd": nil}))
  28. assert.NilError(c, err)
  29. if versions.LessThan(testEnv.DaemonAPIVersion(), "1.32") {
  30. assert.Equal(c, res.StatusCode, http.StatusInternalServerError)
  31. } else {
  32. assert.Equal(c, res.StatusCode, http.StatusBadRequest)
  33. }
  34. b, err := request.ReadBody(body)
  35. assert.NilError(c, err)
  36. assert.Assert(c, strings.Contains(getErrorMessage(c, b), "No exec command specified"), "Expected message when creating exec command with no Cmd specified")
  37. }
  38. func (s *DockerAPISuite) TestExecAPICreateNoValidContentType(c *testing.T) {
  39. name := "exec_test"
  40. dockerCmd(c, "run", "-d", "-t", "--name", name, "busybox", "/bin/sh")
  41. jsonData := bytes.NewBuffer(nil)
  42. if err := json.NewEncoder(jsonData).Encode(map[string]interface{}{"Cmd": nil}); err != nil {
  43. c.Fatalf("Can not encode data to json %s", err)
  44. }
  45. res, body, err := request.Post(testutil.GetContext(c), fmt.Sprintf("/containers/%s/exec", name), request.RawContent(io.NopCloser(jsonData)), request.ContentType("test/plain"))
  46. assert.NilError(c, err)
  47. if versions.LessThan(testEnv.DaemonAPIVersion(), "1.32") {
  48. assert.Equal(c, res.StatusCode, http.StatusInternalServerError)
  49. } else {
  50. assert.Equal(c, res.StatusCode, http.StatusBadRequest)
  51. }
  52. b, err := request.ReadBody(body)
  53. assert.NilError(c, err)
  54. assert.Assert(c, is.Contains(getErrorMessage(c, b), "unsupported Content-Type header (test/plain): must be 'application/json'"))
  55. }
  56. func (s *DockerAPISuite) TestExecAPICreateContainerPaused(c *testing.T) {
  57. // Not relevant on Windows as Windows containers cannot be paused
  58. testRequires(c, DaemonIsLinux)
  59. name := "exec_create_test"
  60. dockerCmd(c, "run", "-d", "-t", "--name", name, "busybox", "/bin/sh")
  61. dockerCmd(c, "pause", name)
  62. apiClient, err := client.NewClientWithOpts(client.FromEnv)
  63. assert.NilError(c, err)
  64. defer apiClient.Close()
  65. config := types.ExecConfig{
  66. Cmd: []string{"true"},
  67. }
  68. _, err = apiClient.ContainerExecCreate(testutil.GetContext(c), name, config)
  69. assert.ErrorContains(c, err, "Container "+name+" is paused, unpause the container before exec", "Expected message when creating exec command with Container %s is paused", name)
  70. }
  71. func (s *DockerAPISuite) TestExecAPIStart(c *testing.T) {
  72. testRequires(c, DaemonIsLinux) // Uses pause/unpause but bits may be salvageable to Windows to Windows CI
  73. dockerCmd(c, "run", "-d", "--name", "test", "busybox", "top")
  74. id := createExec(c, "test")
  75. startExec(c, id, http.StatusOK)
  76. var execJSON struct{ PID int }
  77. inspectExec(testutil.GetContext(c), c, id, &execJSON)
  78. assert.Assert(c, execJSON.PID > 1)
  79. id = createExec(c, "test")
  80. dockerCmd(c, "stop", "test")
  81. startExec(c, id, http.StatusNotFound)
  82. dockerCmd(c, "start", "test")
  83. startExec(c, id, http.StatusNotFound)
  84. // make sure exec is created before pausing
  85. id = createExec(c, "test")
  86. dockerCmd(c, "pause", "test")
  87. startExec(c, id, http.StatusConflict)
  88. dockerCmd(c, "unpause", "test")
  89. startExec(c, id, http.StatusOK)
  90. }
  91. func (s *DockerAPISuite) TestExecAPIStartEnsureHeaders(c *testing.T) {
  92. testRequires(c, DaemonIsLinux)
  93. dockerCmd(c, "run", "-d", "--name", "test", "busybox", "top")
  94. id := createExec(c, "test")
  95. resp, _, err := request.Post(testutil.GetContext(c), fmt.Sprintf("/exec/%s/start", id), request.RawString(`{"Detach": true}`), request.JSON)
  96. assert.NilError(c, err)
  97. assert.Assert(c, resp.Header.Get("Server") != "")
  98. }
  99. func (s *DockerAPISuite) TestExecAPIStartBackwardsCompatible(c *testing.T) {
  100. testRequires(c, DaemonIsLinux) // Windows only supports 1.25 or later
  101. runSleepingContainer(c, "-d", "--name", "test")
  102. id := createExec(c, "test")
  103. resp, body, err := request.Post(testutil.GetContext(c), fmt.Sprintf("/v1.20/exec/%s/start", id), request.RawString(`{"Detach": true}`), request.ContentType("text/plain"))
  104. assert.NilError(c, err)
  105. b, err := request.ReadBody(body)
  106. comment := fmt.Sprintf("response body: %s", b)
  107. assert.NilError(c, err, comment)
  108. assert.Equal(c, resp.StatusCode, http.StatusOK, comment)
  109. }
  110. // #19362
  111. func (s *DockerAPISuite) TestExecAPIStartMultipleTimesError(c *testing.T) {
  112. runSleepingContainer(c, "-d", "--name", "test")
  113. execID := createExec(c, "test")
  114. startExec(c, execID, http.StatusOK)
  115. waitForExec(testutil.GetContext(c), c, execID)
  116. startExec(c, execID, http.StatusConflict)
  117. }
  118. // #20638
  119. func (s *DockerAPISuite) TestExecAPIStartWithDetach(c *testing.T) {
  120. name := "foo"
  121. runSleepingContainer(c, "-d", "-t", "--name", name)
  122. ctx := testutil.GetContext(c)
  123. config := types.ExecConfig{
  124. Cmd: []string{"true"},
  125. AttachStderr: true,
  126. }
  127. apiClient, err := client.NewClientWithOpts(client.FromEnv)
  128. assert.NilError(c, err)
  129. defer apiClient.Close()
  130. createResp, err := apiClient.ContainerExecCreate(ctx, name, config)
  131. assert.NilError(c, err)
  132. _, body, err := request.Post(ctx, fmt.Sprintf("/exec/%s/start", createResp.ID), request.RawString(`{"Detach": true}`), request.JSON)
  133. assert.NilError(c, err)
  134. b, err := request.ReadBody(body)
  135. comment := fmt.Sprintf("response body: %s", b)
  136. assert.NilError(c, err, comment)
  137. resp, _, err := request.Get(ctx, "/_ping")
  138. assert.NilError(c, err)
  139. if resp.StatusCode != http.StatusOK {
  140. c.Fatal("daemon is down, it should alive")
  141. }
  142. }
  143. // #30311
  144. func (s *DockerAPISuite) TestExecAPIStartValidCommand(c *testing.T) {
  145. name := "exec_test"
  146. dockerCmd(c, "run", "-d", "-t", "--name", name, "busybox", "/bin/sh")
  147. id := createExecCmd(c, name, "true")
  148. startExec(c, id, http.StatusOK)
  149. ctx := testutil.GetContext(c)
  150. waitForExec(ctx, c, id)
  151. var inspectJSON struct{ ExecIDs []string }
  152. inspectContainer(ctx, c, name, &inspectJSON)
  153. assert.Assert(c, inspectJSON.ExecIDs == nil)
  154. }
  155. // #30311
  156. func (s *DockerAPISuite) TestExecAPIStartInvalidCommand(c *testing.T) {
  157. name := "exec_test"
  158. dockerCmd(c, "run", "-d", "-t", "--name", name, "busybox", "/bin/sh")
  159. id := createExecCmd(c, name, "invalid")
  160. if versions.LessThan(testEnv.DaemonAPIVersion(), "1.32") {
  161. startExec(c, id, http.StatusNotFound)
  162. } else {
  163. startExec(c, id, http.StatusBadRequest)
  164. }
  165. ctx := testutil.GetContext(c)
  166. waitForExec(ctx, c, id)
  167. var inspectJSON struct{ ExecIDs []string }
  168. inspectContainer(ctx, c, name, &inspectJSON)
  169. assert.Assert(c, inspectJSON.ExecIDs == nil)
  170. }
  171. func (s *DockerAPISuite) TestExecStateCleanup(c *testing.T) {
  172. testRequires(c, DaemonIsLinux, testEnv.IsLocalDaemon)
  173. // This test checks accidental regressions. Not part of stable API.
  174. name := "exec_cleanup"
  175. cid, _ := dockerCmd(c, "run", "-d", "-t", "--name", name, "busybox", "/bin/sh")
  176. cid = strings.TrimSpace(cid)
  177. stateDir := "/var/run/docker/containerd/" + cid
  178. checkReadDir := func(c *testing.T) (interface{}, string) {
  179. fi, err := os.ReadDir(stateDir)
  180. assert.NilError(c, err)
  181. return len(fi), ""
  182. }
  183. fi, err := os.ReadDir(stateDir)
  184. assert.NilError(c, err)
  185. assert.Assert(c, len(fi) > 1)
  186. id := createExecCmd(c, name, "ls")
  187. startExec(c, id, http.StatusOK)
  188. ctx := testutil.GetContext(c)
  189. waitForExec(ctx, c, id)
  190. poll.WaitOn(c, pollCheck(c, checkReadDir, checker.Equals(len(fi))), poll.WithTimeout(5*time.Second))
  191. id = createExecCmd(c, name, "invalid")
  192. startExec(c, id, http.StatusBadRequest)
  193. waitForExec(ctx, c, id)
  194. poll.WaitOn(c, pollCheck(c, checkReadDir, checker.Equals(len(fi))), poll.WithTimeout(5*time.Second))
  195. dockerCmd(c, "stop", name)
  196. _, err = os.Stat(stateDir)
  197. assert.ErrorContains(c, err, "")
  198. assert.Assert(c, os.IsNotExist(err))
  199. }
  200. func createExec(c *testing.T, name string) string {
  201. return createExecCmd(c, name, "true")
  202. }
  203. func createExecCmd(c *testing.T, name string, cmd string) string {
  204. _, reader, err := request.Post(testutil.GetContext(c), fmt.Sprintf("/containers/%s/exec", name), request.JSONBody(map[string]interface{}{"Cmd": []string{cmd}}))
  205. assert.NilError(c, err)
  206. b, err := io.ReadAll(reader)
  207. assert.NilError(c, err)
  208. defer reader.Close()
  209. createResp := struct {
  210. ID string `json:"Id"`
  211. }{}
  212. assert.NilError(c, json.Unmarshal(b, &createResp), string(b))
  213. return createResp.ID
  214. }
  215. func startExec(c *testing.T, id string, code int) {
  216. resp, body, err := request.Post(testutil.GetContext(c), fmt.Sprintf("/exec/%s/start", id), request.RawString(`{"Detach": true}`), request.JSON)
  217. assert.NilError(c, err)
  218. b, err := request.ReadBody(body)
  219. assert.NilError(c, err, "response body: %s", b)
  220. assert.Equal(c, resp.StatusCode, code, "response body: %s", b)
  221. }
  222. func inspectExec(ctx context.Context, c *testing.T, id string, out interface{}) {
  223. resp, body, err := request.Get(ctx, fmt.Sprintf("/exec/%s/json", id))
  224. assert.NilError(c, err)
  225. defer body.Close()
  226. assert.Equal(c, resp.StatusCode, http.StatusOK)
  227. err = json.NewDecoder(body).Decode(out)
  228. assert.NilError(c, err)
  229. }
  230. func waitForExec(ctx context.Context, c *testing.T, id string) {
  231. timeout := time.After(60 * time.Second)
  232. var execJSON struct{ Running bool }
  233. for {
  234. select {
  235. case <-timeout:
  236. c.Fatal("timeout waiting for exec to start")
  237. default:
  238. }
  239. inspectExec(ctx, c, id, &execJSON)
  240. if !execJSON.Running {
  241. break
  242. }
  243. }
  244. }
  245. func inspectContainer(ctx context.Context, c *testing.T, id string, out interface{}) {
  246. resp, body, err := request.Get(ctx, "/containers/"+id+"/json")
  247. assert.NilError(c, err)
  248. defer body.Close()
  249. assert.Equal(c, resp.StatusCode, http.StatusOK)
  250. err = json.NewDecoder(body).Decode(out)
  251. assert.NilError(c, err)
  252. }