123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180 |
- package container // import "github.com/docker/docker/integration/container"
- import (
- "bytes"
- "context"
- "fmt"
- "io"
- "strconv"
- "strings"
- "testing"
- "time"
- "github.com/docker/docker/api/types"
- containertypes "github.com/docker/docker/api/types/container"
- "github.com/docker/docker/client"
- "github.com/docker/docker/errdefs"
- "github.com/docker/docker/integration/internal/container"
- "github.com/docker/docker/pkg/stdcopy"
- "gotest.tools/v3/assert"
- is "gotest.tools/v3/assert/cmp"
- "gotest.tools/v3/icmd"
- "gotest.tools/v3/poll"
- "gotest.tools/v3/skip"
- )
- // TestStopContainerWithTimeout checks that ContainerStop with
- // a timeout works as documented, i.e. in case of negative timeout
- // waiting is not limited (issue #35311).
- func TestStopContainerWithTimeout(t *testing.T) {
- defer setupTest(t)()
- client := testEnv.APIClient()
- ctx := context.Background()
- testCmd := container.WithCmd("sh", "-c", "sleep 2 && exit 42")
- testData := []struct {
- doc string
- timeout int
- expectedExitCode int
- }{
- // In case container is forcefully killed, 137 is returned,
- // otherwise the exit code from the above script
- {
- "zero timeout: expect forceful container kill",
- 0, 137,
- },
- {
- "too small timeout: expect forceful container kill",
- 1, 137,
- },
- {
- "big enough timeout: expect graceful container stop",
- 3, 42,
- },
- {
- "unlimited timeout: expect graceful container stop",
- -1, 42,
- },
- }
- for _, d := range testData {
- d := d
- t.Run(strconv.Itoa(d.timeout), func(t *testing.T) {
- t.Parallel()
- id := container.Run(ctx, t, client, testCmd)
- err := client.ContainerStop(ctx, id, containertypes.StopOptions{Timeout: &d.timeout})
- assert.NilError(t, err)
- poll.WaitOn(t, container.IsStopped(ctx, client, id),
- poll.WithDelay(100*time.Millisecond))
- inspect, err := client.ContainerInspect(ctx, id)
- assert.NilError(t, err)
- assert.Equal(t, inspect.State.ExitCode, d.expectedExitCode)
- })
- }
- }
- func TestDeleteDevicemapper(t *testing.T) {
- skip.If(t, testEnv.DaemonInfo.Driver != "devicemapper")
- skip.If(t, testEnv.IsRemoteDaemon)
- defer setupTest(t)()
- client := testEnv.APIClient()
- ctx := context.Background()
- id := container.Run(ctx, t, client, container.WithName("foo-"+t.Name()), container.WithCmd("echo"))
- poll.WaitOn(t, container.IsStopped(ctx, client, id), poll.WithDelay(100*time.Millisecond))
- inspect, err := client.ContainerInspect(ctx, id)
- assert.NilError(t, err)
- deviceID := inspect.GraphDriver.Data["DeviceId"]
- // Find pool name from device name
- deviceName := inspect.GraphDriver.Data["DeviceName"]
- devicePrefix := deviceName[:strings.LastIndex(deviceName, "-")]
- devicePool := fmt.Sprintf("/dev/mapper/%s-pool", devicePrefix)
- result := icmd.RunCommand("dmsetup", "message", devicePool, "0", fmt.Sprintf("delete %s", deviceID))
- result.Assert(t, icmd.Success)
- err = client.ContainerRemove(ctx, id, types.ContainerRemoveOptions{})
- assert.NilError(t, err)
- }
- // TestStopContainerWithTimeoutCancel checks that ContainerStop is not cancelled
- // if the request is cancelled.
- // See issue https://github.com/moby/moby/issues/45731
- func TestStopContainerWithTimeoutCancel(t *testing.T) {
- t.Parallel()
- defer setupTest(t)()
- apiClient := testEnv.APIClient()
- t.Cleanup(func() { _ = apiClient.Close() })
- ctx := context.Background()
- id := container.Run(ctx, t, apiClient,
- container.WithCmd("sh", "-c", "trap 'echo received TERM' TERM; while true; do usleep 10; done"),
- )
- poll.WaitOn(t, container.IsInState(ctx, apiClient, id, "running"))
- ctxCancel, cancel := context.WithCancel(context.Background())
- t.Cleanup(cancel)
- const stopTimeout = 3
- stoppedCh := make(chan error)
- go func() {
- sto := stopTimeout
- stoppedCh <- apiClient.ContainerStop(ctxCancel, id, containertypes.StopOptions{Timeout: &sto})
- }()
- poll.WaitOn(t, logsContains(ctx, apiClient, id, "received TERM"))
- // Cancel the context once we verified the container was signaled, and check
- // that the container is not killed immediately
- cancel()
- select {
- case stoppedErr := <-stoppedCh:
- assert.Check(t, is.ErrorType(stoppedErr, errdefs.IsCancelled))
- case <-time.After(5 * time.Second):
- t.Fatal("timeout waiting for stop request to be cancelled")
- }
- inspect, err := apiClient.ContainerInspect(ctx, id)
- assert.Check(t, err)
- assert.Check(t, inspect.State.Running)
- // container should be stopped after stopTimeout is reached. The daemon.containerStop
- // code is rather convoluted, and waits another 2 seconds for the container to
- // terminate after signaling it;
- // https://github.com/moby/moby/blob/97455cc31ffa08078db6591f018256ed59c35bbc/daemon/stop.go#L101-L112
- //
- // Adding 3 seconds to the specified stopTimeout to take this into account,
- // and add another second margin to try to avoid flakiness.
- poll.WaitOn(t, container.IsStopped(ctx, apiClient, id), poll.WithTimeout((3+stopTimeout)*time.Second))
- }
- // logsContains verifies the container contains the given text in the log's stdout.
- func logsContains(ctx context.Context, client client.APIClient, containerID string, logString string) func(log poll.LogT) poll.Result {
- return func(log poll.LogT) poll.Result {
- logs, err := client.ContainerLogs(ctx, containerID, types.ContainerLogsOptions{
- ShowStdout: true,
- })
- if err != nil {
- return poll.Error(err)
- }
- defer logs.Close()
- var stdout bytes.Buffer
- _, err = stdcopy.StdCopy(&stdout, io.Discard, logs)
- if err != nil {
- return poll.Error(err)
- }
- if strings.Contains(stdout.String(), logString) {
- return poll.Success()
- }
- return poll.Continue("waiting for logstring '%s' in container", logString)
- }
- }
|