github.com/Prakhar-Agarwal-byte/moby@v0.0.0-20231027092010-a14e3e8ab87e/integration-cli/docker_api_attach_test.go (about)

     1  package main
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"io"
     7  	"net"
     8  	"net/http"
     9  	"strings"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/Prakhar-Agarwal-byte/moby/api/types"
    14  	"github.com/Prakhar-Agarwal-byte/moby/api/types/container"
    15  	"github.com/Prakhar-Agarwal-byte/moby/client"
    16  	"github.com/Prakhar-Agarwal-byte/moby/integration-cli/cli"
    17  	"github.com/Prakhar-Agarwal-byte/moby/pkg/stdcopy"
    18  	"github.com/Prakhar-Agarwal-byte/moby/testutil"
    19  	"github.com/Prakhar-Agarwal-byte/moby/testutil/request"
    20  	"github.com/docker/go-connections/sockets"
    21  	"github.com/pkg/errors"
    22  	"golang.org/x/net/websocket"
    23  	"gotest.tools/v3/assert"
    24  	is "gotest.tools/v3/assert/cmp"
    25  )
    26  
    27  func (s *DockerAPISuite) TestGetContainersAttachWebsocket(c *testing.T) {
    28  	cid := cli.DockerCmd(c, "run", "-di", "busybox", "cat").Stdout()
    29  	cid = strings.TrimSpace(cid)
    30  
    31  	rwc, err := request.SockConn(10*time.Second, request.DaemonHost())
    32  	assert.NilError(c, err)
    33  
    34  	config, err := websocket.NewConfig(
    35  		"/containers/"+cid+"/attach/ws?stream=1&stdin=1&stdout=1&stderr=1",
    36  		"http://localhost",
    37  	)
    38  	assert.NilError(c, err)
    39  
    40  	ws, err := websocket.NewClient(config, rwc)
    41  	assert.NilError(c, err)
    42  	defer ws.Close()
    43  
    44  	expected := []byte("hello")
    45  	actual := make([]byte, len(expected))
    46  
    47  	outChan := make(chan error, 1)
    48  	go func() {
    49  		_, err := io.ReadFull(ws, actual)
    50  		outChan <- err
    51  		close(outChan)
    52  	}()
    53  
    54  	inChan := make(chan error, 1)
    55  	go func() {
    56  		_, err := ws.Write(expected)
    57  		inChan <- err
    58  		close(inChan)
    59  	}()
    60  
    61  	select {
    62  	case err := <-inChan:
    63  		assert.NilError(c, err)
    64  	case <-time.After(5 * time.Second):
    65  		c.Fatal("Timeout writing to ws")
    66  	}
    67  
    68  	select {
    69  	case err := <-outChan:
    70  		assert.NilError(c, err)
    71  	case <-time.After(5 * time.Second):
    72  		c.Fatal("Timeout reading from ws")
    73  	}
    74  
    75  	assert.Assert(c, is.DeepEqual(actual, expected), "Websocket didn't return the expected data")
    76  }
    77  
    78  // regression gh14320
    79  func (s *DockerAPISuite) TestPostContainersAttachContainerNotFound(c *testing.T) {
    80  	ctx := testutil.GetContext(c)
    81  	resp, _, err := request.Post(ctx, "/containers/doesnotexist/attach")
    82  	assert.NilError(c, err)
    83  	// connection will shutdown, err should be "persistent connection closed"
    84  	assert.Equal(c, resp.StatusCode, http.StatusNotFound)
    85  	content, err := request.ReadBody(resp.Body)
    86  	assert.NilError(c, err)
    87  	expected := "No such container: doesnotexist\r\n"
    88  	assert.Equal(c, string(content), expected)
    89  }
    90  
    91  func (s *DockerAPISuite) TestGetContainersWsAttachContainerNotFound(c *testing.T) {
    92  	ctx := testutil.GetContext(c)
    93  	res, body, err := request.Get(ctx, "/containers/doesnotexist/attach/ws")
    94  	assert.Equal(c, res.StatusCode, http.StatusNotFound)
    95  	assert.NilError(c, err)
    96  	b, err := request.ReadBody(body)
    97  	assert.NilError(c, err)
    98  	expected := "No such container: doesnotexist"
    99  	assert.Assert(c, strings.Contains(getErrorMessage(c, b), expected))
   100  }
   101  
   102  func (s *DockerAPISuite) TestPostContainersAttach(c *testing.T) {
   103  	testRequires(c, DaemonIsLinux)
   104  
   105  	expectSuccess := func(wc io.WriteCloser, br *bufio.Reader, stream string, tty bool) {
   106  		defer wc.Close()
   107  		expected := []byte("success")
   108  		_, err := wc.Write(expected)
   109  		assert.NilError(c, err)
   110  
   111  		lenHeader := 0
   112  		if !tty {
   113  			lenHeader = 8
   114  		}
   115  		actual := make([]byte, len(expected)+lenHeader)
   116  		_, err = readTimeout(br, actual, time.Second)
   117  		assert.NilError(c, err)
   118  		if !tty {
   119  			fdMap := map[string]byte{
   120  				"stdin":  0,
   121  				"stdout": 1,
   122  				"stderr": 2,
   123  			}
   124  			assert.Equal(c, actual[0], fdMap[stream])
   125  		}
   126  		assert.Assert(c, is.DeepEqual(actual[lenHeader:], expected), "Attach didn't return the expected data from %s", stream)
   127  	}
   128  
   129  	expectTimeout := func(wc io.WriteCloser, br *bufio.Reader, stream string) {
   130  		defer wc.Close()
   131  		_, err := wc.Write([]byte{'t'})
   132  		assert.NilError(c, err)
   133  
   134  		actual := make([]byte, 1)
   135  		_, err = readTimeout(br, actual, time.Second)
   136  		assert.Assert(c, err.Error() == "Timeout", "Read from %s is expected to timeout", stream)
   137  	}
   138  
   139  	// Create a container that only emits stdout.
   140  	cid := cli.DockerCmd(c, "run", "-di", "busybox", "cat").Stdout()
   141  	cid = strings.TrimSpace(cid)
   142  
   143  	// Attach to the container's stdout stream.
   144  	wc, br, err := requestHijack(http.MethodPost, "/containers/"+cid+"/attach?stream=1&stdin=1&stdout=1", nil, "text/plain", request.DaemonHost())
   145  	assert.NilError(c, err)
   146  	// Check if the data from stdout can be received.
   147  	expectSuccess(wc, br, "stdout", false)
   148  
   149  	// Attach to the container's stderr stream.
   150  	wc, br, err = requestHijack(http.MethodPost, "/containers/"+cid+"/attach?stream=1&stdin=1&stderr=1", nil, "text/plain", request.DaemonHost())
   151  	assert.NilError(c, err)
   152  	// Since the container only emits stdout, attaching to stderr should return nothing.
   153  	expectTimeout(wc, br, "stdout")
   154  
   155  	// Test the similar functions of the stderr stream.
   156  	cid = cli.DockerCmd(c, "run", "-di", "busybox", "/bin/sh", "-c", "cat >&2").Stdout()
   157  	cid = strings.TrimSpace(cid)
   158  	wc, br, err = requestHijack(http.MethodPost, "/containers/"+cid+"/attach?stream=1&stdin=1&stderr=1", nil, "text/plain", request.DaemonHost())
   159  	assert.NilError(c, err)
   160  	expectSuccess(wc, br, "stderr", false)
   161  	wc, br, err = requestHijack(http.MethodPost, "/containers/"+cid+"/attach?stream=1&stdin=1&stdout=1", nil, "text/plain", request.DaemonHost())
   162  	assert.NilError(c, err)
   163  	expectTimeout(wc, br, "stderr")
   164  
   165  	// Test with tty.
   166  	cid = cli.DockerCmd(c, "run", "-dit", "busybox", "/bin/sh", "-c", "cat >&2").Stdout()
   167  	cid = strings.TrimSpace(cid)
   168  	// Attach to stdout only.
   169  	wc, br, err = requestHijack(http.MethodPost, "/containers/"+cid+"/attach?stream=1&stdin=1&stdout=1", nil, "text/plain", request.DaemonHost())
   170  	assert.NilError(c, err)
   171  	expectSuccess(wc, br, "stdout", true)
   172  
   173  	// Attach without stdout stream.
   174  	wc, br, err = requestHijack(http.MethodPost, "/containers/"+cid+"/attach?stream=1&stdin=1&stderr=1", nil, "text/plain", request.DaemonHost())
   175  	assert.NilError(c, err)
   176  	// Nothing should be received because both the stdout and stderr of the container will be
   177  	// sent to the client as stdout when tty is enabled.
   178  	expectTimeout(wc, br, "stdout")
   179  
   180  	// Test the client API
   181  	apiClient, err := client.NewClientWithOpts(client.FromEnv)
   182  	assert.NilError(c, err)
   183  	defer apiClient.Close()
   184  
   185  	cid = cli.DockerCmd(c, "run", "-di", "busybox", "/bin/sh", "-c", "echo hello; cat").Stdout()
   186  	cid = strings.TrimSpace(cid)
   187  
   188  	// Make sure we don't see "hello" if Logs is false
   189  	attachOpts := container.AttachOptions{
   190  		Stream: true,
   191  		Stdin:  true,
   192  		Stdout: true,
   193  		Stderr: true,
   194  		Logs:   false,
   195  	}
   196  
   197  	resp, err := apiClient.ContainerAttach(testutil.GetContext(c), cid, attachOpts)
   198  	assert.NilError(c, err)
   199  	mediaType, b := resp.MediaType()
   200  	assert.Check(c, b)
   201  	assert.Equal(c, mediaType, types.MediaTypeMultiplexedStream)
   202  	expectSuccess(resp.Conn, resp.Reader, "stdout", false)
   203  
   204  	// Make sure we do see "hello" if Logs is true
   205  	attachOpts.Logs = true
   206  	resp, err = apiClient.ContainerAttach(testutil.GetContext(c), cid, attachOpts)
   207  	assert.NilError(c, err)
   208  
   209  	defer resp.Conn.Close()
   210  	resp.Conn.SetReadDeadline(time.Now().Add(time.Second))
   211  
   212  	_, err = resp.Conn.Write([]byte("success"))
   213  	assert.NilError(c, err)
   214  
   215  	var outBuf, errBuf bytes.Buffer
   216  	var nErr net.Error
   217  	_, err = stdcopy.StdCopy(&outBuf, &errBuf, resp.Reader)
   218  	if errors.As(err, &nErr) && nErr.Timeout() {
   219  		// ignore the timeout error as it is expected
   220  		err = nil
   221  	}
   222  	assert.NilError(c, err)
   223  	assert.Equal(c, errBuf.String(), "")
   224  	assert.Equal(c, outBuf.String(), "hello\nsuccess")
   225  }
   226  
   227  // requestHijack create a http requst to specified host with `Upgrade` header (with method
   228  // , contenttype, …), if receive a successful "101 Switching Protocols" response return
   229  // a `io.WriteCloser` and `bufio.Reader`
   230  func requestHijack(method, endpoint string, data io.Reader, ct, daemon string, modifiers ...func(*http.Request)) (io.WriteCloser, *bufio.Reader, error) {
   231  	hostURL, err := client.ParseHostURL(daemon)
   232  	if err != nil {
   233  		return nil, nil, errors.Wrap(err, "parse daemon host error")
   234  	}
   235  
   236  	req, err := http.NewRequest(method, endpoint, data)
   237  	if err != nil {
   238  		return nil, nil, errors.Wrap(err, "could not create new request")
   239  	}
   240  	req.URL.Scheme = "http"
   241  	req.URL.Host = hostURL.Host
   242  
   243  	if hostURL.Scheme == "unix" || hostURL.Scheme == "npipe" {
   244  		// Override host header for non-tcp connections.
   245  		req.Host = client.DummyHost
   246  	}
   247  
   248  	for _, opt := range modifiers {
   249  		opt(req)
   250  	}
   251  
   252  	if ct != "" {
   253  		req.Header.Set("Content-Type", ct)
   254  	}
   255  
   256  	// must have Upgrade header
   257  	// server api return 101 Switching Protocols
   258  	req.Header.Set("Upgrade", "tcp")
   259  
   260  	// new client
   261  	// FIXME use testutil/request newHTTPClient
   262  	transport := &http.Transport{}
   263  	err = sockets.ConfigureTransport(transport, hostURL.Scheme, hostURL.Host)
   264  	if err != nil {
   265  		return nil, nil, errors.Wrap(err, "configure Transport error")
   266  	}
   267  
   268  	c := http.Client{
   269  		Transport: transport,
   270  	}
   271  
   272  	resp, err := c.Do(req)
   273  	if err != nil {
   274  		return nil, nil, errors.Wrap(err, "client.Do")
   275  	}
   276  
   277  	if !bodyIsWritable(resp) {
   278  		return nil, nil, errors.New("response.Body not writable")
   279  	}
   280  
   281  	return resp.Body.(io.WriteCloser), bufio.NewReader(resp.Body), nil
   282  }
   283  
   284  // bodyIsWritable check Response.Body is writable
   285  func bodyIsWritable(r *http.Response) bool {
   286  	_, ok := r.Body.(io.Writer)
   287  	return ok
   288  }
   289  
   290  // readTimeout read from io.Reader with timeout
   291  func readTimeout(r io.Reader, buf []byte, timeout time.Duration) (n int, err error) {
   292  	ch := make(chan bool, 1)
   293  	go func() {
   294  		n, err = io.ReadFull(r, buf)
   295  		ch <- true
   296  	}()
   297  	select {
   298  	case <-ch:
   299  		return
   300  	case <-time.After(timeout):
   301  		return 0, errors.New("Timeout")
   302  	}
   303  }