github.com/rish1988/moby@v25.0.2+incompatible/integration/container/stop_linux_test.go (about)

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