docker_api_exec_test.go 9.5 KB

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