github.com/tonistiigi/docker@v0.10.1-0.20240229224939-974013b0dc6a/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  // #19362
   113  func (s *DockerAPISuite) TestExecAPIStartMultipleTimesError(c *testing.T) {
   114  	runSleepingContainer(c, "-d", "--name", "test")
   115  	execID := createExec(c, "test")
   116  	startExec(c, execID, http.StatusOK)
   117  	waitForExec(testutil.GetContext(c), c, execID)
   118  
   119  	startExec(c, execID, http.StatusConflict)
   120  }
   121  
   122  // #20638
   123  func (s *DockerAPISuite) TestExecAPIStartWithDetach(c *testing.T) {
   124  	name := "foo"
   125  	runSleepingContainer(c, "-d", "-t", "--name", name)
   126  
   127  	ctx := testutil.GetContext(c)
   128  
   129  	config := types.ExecConfig{
   130  		Cmd:          []string{"true"},
   131  		AttachStderr: true,
   132  	}
   133  
   134  	apiClient, err := client.NewClientWithOpts(client.FromEnv)
   135  	assert.NilError(c, err)
   136  	defer apiClient.Close()
   137  
   138  	createResp, err := apiClient.ContainerExecCreate(ctx, name, config)
   139  	assert.NilError(c, err)
   140  
   141  	_, body, err := request.Post(ctx, fmt.Sprintf("/exec/%s/start", createResp.ID), request.RawString(`{"Detach": true}`), request.JSON)
   142  	assert.NilError(c, err)
   143  
   144  	b, err := request.ReadBody(body)
   145  	comment := fmt.Sprintf("response body: %s", b)
   146  	assert.NilError(c, err, comment)
   147  
   148  	resp, _, err := request.Get(ctx, "/_ping")
   149  	assert.NilError(c, err)
   150  	if resp.StatusCode != http.StatusOK {
   151  		c.Fatal("daemon is down, it should alive")
   152  	}
   153  }
   154  
   155  // #30311
   156  func (s *DockerAPISuite) TestExecAPIStartValidCommand(c *testing.T) {
   157  	name := "exec_test"
   158  	cli.DockerCmd(c, "run", "-d", "-t", "--name", name, "busybox", "/bin/sh")
   159  
   160  	id := createExecCmd(c, name, "true")
   161  	startExec(c, id, http.StatusOK)
   162  
   163  	ctx := testutil.GetContext(c)
   164  	waitForExec(ctx, c, id)
   165  
   166  	var inspectJSON struct{ ExecIDs []string }
   167  	inspectContainer(ctx, c, name, &inspectJSON)
   168  
   169  	assert.Assert(c, inspectJSON.ExecIDs == nil)
   170  }
   171  
   172  // #30311
   173  func (s *DockerAPISuite) TestExecAPIStartInvalidCommand(c *testing.T) {
   174  	name := "exec_test"
   175  	cli.DockerCmd(c, "run", "-d", "-t", "--name", name, "busybox", "/bin/sh")
   176  
   177  	id := createExecCmd(c, name, "invalid")
   178  	startExec(c, id, http.StatusBadRequest)
   179  	ctx := testutil.GetContext(c)
   180  	waitForExec(ctx, c, id)
   181  
   182  	var inspectJSON struct{ ExecIDs []string }
   183  	inspectContainer(ctx, c, name, &inspectJSON)
   184  
   185  	assert.Assert(c, inspectJSON.ExecIDs == nil)
   186  }
   187  
   188  func (s *DockerAPISuite) TestExecStateCleanup(c *testing.T) {
   189  	testRequires(c, DaemonIsLinux, testEnv.IsLocalDaemon)
   190  
   191  	// This test checks accidental regressions. Not part of stable API.
   192  
   193  	name := "exec_cleanup"
   194  	cid := cli.DockerCmd(c, "run", "-d", "-t", "--name", name, "busybox", "/bin/sh").Stdout()
   195  	cid = strings.TrimSpace(cid)
   196  
   197  	stateDir := "/var/run/docker/containerd/" + cid
   198  
   199  	checkReadDir := func(c *testing.T) (interface{}, string) {
   200  		fi, err := os.ReadDir(stateDir)
   201  		assert.NilError(c, err)
   202  		return len(fi), ""
   203  	}
   204  
   205  	fi, err := os.ReadDir(stateDir)
   206  	assert.NilError(c, err)
   207  	assert.Assert(c, len(fi) > 1)
   208  
   209  	id := createExecCmd(c, name, "ls")
   210  	startExec(c, id, http.StatusOK)
   211  
   212  	ctx := testutil.GetContext(c)
   213  	waitForExec(ctx, c, id)
   214  
   215  	poll.WaitOn(c, pollCheck(c, checkReadDir, checker.Equals(len(fi))), poll.WithTimeout(5*time.Second))
   216  
   217  	id = createExecCmd(c, name, "invalid")
   218  	startExec(c, id, http.StatusBadRequest)
   219  	waitForExec(ctx, c, id)
   220  
   221  	poll.WaitOn(c, pollCheck(c, checkReadDir, checker.Equals(len(fi))), poll.WithTimeout(5*time.Second))
   222  
   223  	cli.DockerCmd(c, "stop", name)
   224  	_, err = os.Stat(stateDir)
   225  	assert.ErrorContains(c, err, "")
   226  	assert.Assert(c, os.IsNotExist(err))
   227  }
   228  
   229  func createExec(c *testing.T, name string) string {
   230  	return createExecCmd(c, name, "true")
   231  }
   232  
   233  func createExecCmd(c *testing.T, name string, cmd string) string {
   234  	_, reader, err := request.Post(testutil.GetContext(c), fmt.Sprintf("/containers/%s/exec", name), request.JSONBody(map[string]interface{}{"Cmd": []string{cmd}}))
   235  	assert.NilError(c, err)
   236  	b, err := io.ReadAll(reader)
   237  	assert.NilError(c, err)
   238  	defer reader.Close()
   239  	createResp := struct {
   240  		ID string `json:"Id"`
   241  	}{}
   242  	assert.NilError(c, json.Unmarshal(b, &createResp), string(b))
   243  	return createResp.ID
   244  }
   245  
   246  func startExec(c *testing.T, id string, code int) {
   247  	resp, body, err := request.Post(testutil.GetContext(c), fmt.Sprintf("/exec/%s/start", id), request.RawString(`{"Detach": true}`), request.JSON)
   248  	assert.NilError(c, err)
   249  
   250  	b, err := request.ReadBody(body)
   251  	assert.NilError(c, err, "response body: %s", b)
   252  	assert.Equal(c, resp.StatusCode, code, "response body: %s", b)
   253  }
   254  
   255  func inspectExec(ctx context.Context, c *testing.T, id string, out interface{}) {
   256  	resp, body, err := request.Get(ctx, fmt.Sprintf("/exec/%s/json", id))
   257  	assert.NilError(c, err)
   258  	defer body.Close()
   259  	assert.Equal(c, resp.StatusCode, http.StatusOK)
   260  	err = json.NewDecoder(body).Decode(out)
   261  	assert.NilError(c, err)
   262  }
   263  
   264  func waitForExec(ctx context.Context, c *testing.T, id string) {
   265  	timeout := time.After(60 * time.Second)
   266  	var execJSON struct{ Running bool }
   267  	for {
   268  		select {
   269  		case <-timeout:
   270  			c.Fatal("timeout waiting for exec to start")
   271  		default:
   272  		}
   273  
   274  		inspectExec(ctx, c, id, &execJSON)
   275  		if !execJSON.Running {
   276  			break
   277  		}
   278  	}
   279  }
   280  
   281  func inspectContainer(ctx context.Context, c *testing.T, id string, out interface{}) {
   282  	resp, body, err := request.Get(ctx, "/containers/"+id+"/json")
   283  	assert.NilError(c, err)
   284  	defer body.Close()
   285  	assert.Equal(c, resp.StatusCode, http.StatusOK)
   286  	err = json.NewDecoder(body).Decode(out)
   287  	assert.NilError(c, err)
   288  }