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