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 }