stop_linux_test.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. package container // import "github.com/docker/docker/integration/container"
  2. import (
  3. "bytes"
  4. "context"
  5. "io"
  6. "strconv"
  7. "strings"
  8. "testing"
  9. "time"
  10. containertypes "github.com/docker/docker/api/types/container"
  11. "github.com/docker/docker/client"
  12. "github.com/docker/docker/errdefs"
  13. "github.com/docker/docker/integration/internal/container"
  14. "github.com/docker/docker/pkg/stdcopy"
  15. "github.com/docker/docker/testutil"
  16. "gotest.tools/v3/assert"
  17. is "gotest.tools/v3/assert/cmp"
  18. "gotest.tools/v3/poll"
  19. )
  20. // TestStopContainerWithTimeout checks that ContainerStop with
  21. // a timeout works as documented, i.e. in case of negative timeout
  22. // waiting is not limited (issue #35311).
  23. func TestStopContainerWithTimeout(t *testing.T) {
  24. ctx := setupTest(t)
  25. apiClient := testEnv.APIClient()
  26. testCmd := container.WithCmd("sh", "-c", "sleep 2 && exit 42")
  27. testData := []struct {
  28. doc string
  29. timeout int
  30. expectedExitCode int
  31. }{
  32. // In case container is forcefully killed, 137 is returned,
  33. // otherwise the exit code from the above script
  34. {
  35. "zero timeout: expect forceful container kill",
  36. 0, 137,
  37. },
  38. {
  39. "too small timeout: expect forceful container kill",
  40. 1, 137,
  41. },
  42. {
  43. "big enough timeout: expect graceful container stop",
  44. 3, 42,
  45. },
  46. {
  47. "unlimited timeout: expect graceful container stop",
  48. -1, 42,
  49. },
  50. }
  51. for _, d := range testData {
  52. d := d
  53. t.Run(strconv.Itoa(d.timeout), func(t *testing.T) {
  54. t.Parallel()
  55. ctx := testutil.StartSpan(ctx, t)
  56. id := container.Run(ctx, t, apiClient, testCmd)
  57. err := apiClient.ContainerStop(ctx, id, containertypes.StopOptions{Timeout: &d.timeout})
  58. assert.NilError(t, err)
  59. poll.WaitOn(t, container.IsStopped(ctx, apiClient, id),
  60. poll.WithDelay(100*time.Millisecond))
  61. inspect, err := apiClient.ContainerInspect(ctx, id)
  62. assert.NilError(t, err)
  63. assert.Equal(t, inspect.State.ExitCode, d.expectedExitCode)
  64. })
  65. }
  66. }
  67. // TestStopContainerWithTimeoutCancel checks that ContainerStop is not cancelled
  68. // if the request is cancelled.
  69. // See issue https://github.com/moby/moby/issues/45731
  70. func TestStopContainerWithTimeoutCancel(t *testing.T) {
  71. t.Parallel()
  72. ctx := setupTest(t)
  73. apiClient := testEnv.APIClient()
  74. t.Cleanup(func() { _ = apiClient.Close() })
  75. id := container.Run(ctx, t, apiClient,
  76. container.WithCmd("sh", "-c", "trap 'echo received TERM' TERM; while true; do usleep 10; done"),
  77. )
  78. ctxCancel, cancel := context.WithCancel(ctx)
  79. t.Cleanup(cancel)
  80. const stopTimeout = 3
  81. stoppedCh := make(chan error)
  82. go func() {
  83. sto := stopTimeout
  84. stoppedCh <- apiClient.ContainerStop(ctxCancel, id, containertypes.StopOptions{Timeout: &sto})
  85. }()
  86. poll.WaitOn(t, logsContains(ctx, apiClient, id, "received TERM"))
  87. // Cancel the context once we verified the container was signaled, and check
  88. // that the container is not killed immediately
  89. cancel()
  90. select {
  91. case stoppedErr := <-stoppedCh:
  92. assert.Check(t, is.ErrorType(stoppedErr, errdefs.IsCancelled))
  93. case <-time.After(5 * time.Second):
  94. t.Fatal("timeout waiting for stop request to be cancelled")
  95. }
  96. inspect, err := apiClient.ContainerInspect(ctx, id)
  97. assert.Check(t, err)
  98. assert.Check(t, inspect.State.Running)
  99. // container should be stopped after stopTimeout is reached. The daemon.containerStop
  100. // code is rather convoluted, and waits another 2 seconds for the container to
  101. // terminate after signaling it;
  102. // https://github.com/moby/moby/blob/97455cc31ffa08078db6591f018256ed59c35bbc/daemon/stop.go#L101-L112
  103. //
  104. // Adding 3 seconds to the specified stopTimeout to take this into account,
  105. // and add another second margin to try to avoid flakiness.
  106. poll.WaitOn(t, container.IsStopped(ctx, apiClient, id), poll.WithTimeout((3+stopTimeout)*time.Second))
  107. }
  108. // logsContains verifies the container contains the given text in the log's stdout.
  109. func logsContains(ctx context.Context, client client.APIClient, containerID string, logString string) func(log poll.LogT) poll.Result {
  110. return func(log poll.LogT) poll.Result {
  111. logs, err := client.ContainerLogs(ctx, containerID, containertypes.LogsOptions{
  112. ShowStdout: true,
  113. })
  114. if err != nil {
  115. return poll.Error(err)
  116. }
  117. defer logs.Close()
  118. var stdout bytes.Buffer
  119. _, err = stdcopy.StdCopy(&stdout, io.Discard, logs)
  120. if err != nil {
  121. return poll.Error(err)
  122. }
  123. if strings.Contains(stdout.String(), logString) {
  124. return poll.Success()
  125. }
  126. return poll.Continue("waiting for logstring '%s' in container", logString)
  127. }
  128. }