github.com/devdivbcp/moby@v17.12.0-ce-rc1.0.20200726071732-2d4bfdc789ad+incompatible/integration-cli/docker_api_attach_test.go (about) 1 package main 2 3 import ( 4 "bufio" 5 "bytes" 6 "context" 7 "fmt" 8 "io" 9 "net" 10 "net/http" 11 "net/http/httputil" 12 "strings" 13 "testing" 14 "time" 15 16 "github.com/docker/docker/api/types" 17 "github.com/docker/docker/client" 18 "github.com/docker/docker/internal/test/request" 19 "github.com/docker/docker/pkg/stdcopy" 20 "github.com/pkg/errors" 21 "golang.org/x/net/websocket" 22 "gotest.tools/assert" 23 is "gotest.tools/assert/cmp" 24 ) 25 26 func (s *DockerSuite) TestGetContainersAttachWebsocket(c *testing.T) { 27 testRequires(c, DaemonIsLinux) 28 out, _ := dockerCmd(c, "run", "-dit", "busybox", "cat") 29 30 rwc, err := request.SockConn(10*time.Second, request.DaemonHost()) 31 assert.NilError(c, err) 32 33 cleanedContainerID := strings.TrimSpace(out) 34 config, err := websocket.NewConfig( 35 "/containers/"+cleanedContainerID+"/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) 48 go func() { 49 _, err := io.ReadFull(ws, actual) 50 outChan <- err 51 close(outChan) 52 }() 53 54 inChan := make(chan error) 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 *DockerSuite) TestPostContainersAttachContainerNotFound(c *testing.T) { 80 resp, _, err := request.Post("/containers/doesnotexist/attach") 81 assert.NilError(c, err) 82 // connection will shutdown, err should be "persistent connection closed" 83 assert.Equal(c, resp.StatusCode, http.StatusNotFound) 84 content, err := request.ReadBody(resp.Body) 85 assert.NilError(c, err) 86 expected := "No such container: doesnotexist\r\n" 87 assert.Equal(c, string(content), expected) 88 } 89 90 func (s *DockerSuite) TestGetContainersWsAttachContainerNotFound(c *testing.T) { 91 res, body, err := request.Get("/containers/doesnotexist/attach/ws") 92 assert.Equal(c, res.StatusCode, http.StatusNotFound) 93 assert.NilError(c, err) 94 b, err := request.ReadBody(body) 95 assert.NilError(c, err) 96 expected := "No such container: doesnotexist" 97 assert.Assert(c, strings.Contains(getErrorMessage(c, b), expected)) 98 } 99 100 func (s *DockerSuite) TestPostContainersAttach(c *testing.T) { 101 testRequires(c, DaemonIsLinux) 102 103 expectSuccess := func(conn net.Conn, br *bufio.Reader, stream string, tty bool) { 104 defer conn.Close() 105 expected := []byte("success") 106 _, err := conn.Write(expected) 107 assert.NilError(c, err) 108 109 conn.SetReadDeadline(time.Now().Add(time.Second)) 110 lenHeader := 0 111 if !tty { 112 lenHeader = 8 113 } 114 actual := make([]byte, len(expected)+lenHeader) 115 _, err = io.ReadFull(br, actual) 116 assert.NilError(c, err) 117 if !tty { 118 fdMap := map[string]byte{ 119 "stdin": 0, 120 "stdout": 1, 121 "stderr": 2, 122 } 123 assert.Equal(c, actual[0], fdMap[stream]) 124 } 125 assert.Assert(c, is.DeepEqual(actual[lenHeader:], expected), "Attach didn't return the expected data from %s", stream) 126 } 127 128 expectTimeout := func(conn net.Conn, br *bufio.Reader, stream string) { 129 defer conn.Close() 130 _, err := conn.Write([]byte{'t'}) 131 assert.NilError(c, err) 132 133 conn.SetReadDeadline(time.Now().Add(time.Second)) 134 actual := make([]byte, 1) 135 _, err = io.ReadFull(br, actual) 136 opErr, ok := err.(*net.OpError) 137 assert.Assert(c, ok, "Error is expected to be *net.OpError, got %v", err) 138 assert.Assert(c, opErr.Timeout(), "Read from %s is expected to timeout", stream) 139 } 140 141 // Create a container that only emits stdout. 142 cid, _ := dockerCmd(c, "run", "-di", "busybox", "cat") 143 cid = strings.TrimSpace(cid) 144 // Attach to the container's stdout stream. 145 conn, br, err := sockRequestHijack("POST", "/containers/"+cid+"/attach?stream=1&stdin=1&stdout=1", nil, "text/plain", request.DaemonHost()) 146 assert.NilError(c, err) 147 // Check if the data from stdout can be received. 148 expectSuccess(conn, br, "stdout", false) 149 // Attach to the container's stderr stream. 150 conn, br, err = sockRequestHijack("POST", "/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(conn, br, "stdout") 154 155 // Test the similar functions of the stderr stream. 156 cid, _ = dockerCmd(c, "run", "-di", "busybox", "/bin/sh", "-c", "cat >&2") 157 cid = strings.TrimSpace(cid) 158 conn, br, err = sockRequestHijack("POST", "/containers/"+cid+"/attach?stream=1&stdin=1&stderr=1", nil, "text/plain", request.DaemonHost()) 159 assert.NilError(c, err) 160 expectSuccess(conn, br, "stderr", false) 161 conn, br, err = sockRequestHijack("POST", "/containers/"+cid+"/attach?stream=1&stdin=1&stdout=1", nil, "text/plain", request.DaemonHost()) 162 assert.NilError(c, err) 163 expectTimeout(conn, br, "stderr") 164 165 // Test with tty. 166 cid, _ = dockerCmd(c, "run", "-dit", "busybox", "/bin/sh", "-c", "cat >&2") 167 cid = strings.TrimSpace(cid) 168 // Attach to stdout only. 169 conn, br, err = sockRequestHijack("POST", "/containers/"+cid+"/attach?stream=1&stdin=1&stdout=1", nil, "text/plain", request.DaemonHost()) 170 assert.NilError(c, err) 171 expectSuccess(conn, br, "stdout", true) 172 173 // Attach without stdout stream. 174 conn, br, err = sockRequestHijack("POST", "/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(conn, br, "stdout") 179 180 // Test the client API 181 client, err := client.NewClientWithOpts(client.FromEnv) 182 assert.NilError(c, err) 183 defer client.Close() 184 185 cid, _ = dockerCmd(c, "run", "-di", "busybox", "/bin/sh", "-c", "echo hello; cat") 186 cid = strings.TrimSpace(cid) 187 188 // Make sure we don't see "hello" if Logs is false 189 attachOpts := types.ContainerAttachOptions{ 190 Stream: true, 191 Stdin: true, 192 Stdout: true, 193 Stderr: true, 194 Logs: false, 195 } 196 197 resp, err := client.ContainerAttach(context.Background(), cid, attachOpts) 198 assert.NilError(c, err) 199 expectSuccess(resp.Conn, resp.Reader, "stdout", false) 200 201 // Make sure we do see "hello" if Logs is true 202 attachOpts.Logs = true 203 resp, err = client.ContainerAttach(context.Background(), cid, attachOpts) 204 assert.NilError(c, err) 205 206 defer resp.Conn.Close() 207 resp.Conn.SetReadDeadline(time.Now().Add(time.Second)) 208 209 _, err = resp.Conn.Write([]byte("success")) 210 assert.NilError(c, err) 211 212 var outBuf, errBuf bytes.Buffer 213 _, err = stdcopy.StdCopy(&outBuf, &errBuf, resp.Reader) 214 if err != nil && errors.Cause(err).(net.Error).Timeout() { 215 // ignore the timeout error as it is expected 216 err = nil 217 } 218 assert.NilError(c, err) 219 assert.Equal(c, errBuf.String(), "") 220 assert.Equal(c, outBuf.String(), "hello\nsuccess") 221 } 222 223 // SockRequestHijack creates a connection to specified host (with method, contenttype, …) and returns a hijacked connection 224 // and the output as a `bufio.Reader` 225 func sockRequestHijack(method, endpoint string, data io.Reader, ct string, daemon string, modifiers ...func(*http.Request)) (net.Conn, *bufio.Reader, error) { 226 req, client, err := newRequestClient(method, endpoint, data, ct, daemon, modifiers...) 227 if err != nil { 228 return nil, nil, err 229 } 230 231 client.Do(req) 232 conn, br := client.Hijack() 233 return conn, br, nil 234 } 235 236 // FIXME(vdemeester) httputil.ClientConn is deprecated, use http.Client instead (closer to actual client) 237 // Deprecated: Use New instead of NewRequestClient 238 // Deprecated: use request.Do (or Get, Delete, Post) instead 239 func newRequestClient(method, endpoint string, data io.Reader, ct, daemon string, modifiers ...func(*http.Request)) (*http.Request, *httputil.ClientConn, error) { 240 c, err := request.SockConn(10*time.Second, daemon) 241 if err != nil { 242 return nil, nil, fmt.Errorf("could not dial docker daemon: %v", err) 243 } 244 245 client := httputil.NewClientConn(c, nil) 246 247 req, err := http.NewRequest(method, endpoint, data) 248 if err != nil { 249 client.Close() 250 return nil, nil, fmt.Errorf("could not create new request: %v", err) 251 } 252 253 for _, opt := range modifiers { 254 opt(req) 255 } 256 257 if ct != "" { 258 req.Header.Set("Content-Type", ct) 259 } 260 return req, client, nil 261 }