github.com/rish1988/moby@v25.0.2+incompatible/integration-cli/docker_api_exec_test.go (about)

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