Browse code

Let client print error when speicify wrong detach keys

Fix #21064

Let client print error message explicitly when user specifies wrong
detach keys.

Signed-off-by: Zhang Wei <zhangwei555@huawei.com>

Zhang Wei authored on 2016/03/23 20:34:47
Showing 10 changed files
... ...
@@ -3,6 +3,7 @@ package client
3 3
 import (
4 4
 	"fmt"
5 5
 	"io"
6
+	"net/http/httputil"
6 7
 
7 8
 	"golang.org/x/net/context"
8 9
 
... ...
@@ -66,9 +67,12 @@ func (cli *DockerCli) CmdAttach(args ...string) error {
66 66
 		defer signal.StopCatch(sigc)
67 67
 	}
68 68
 
69
-	resp, err := cli.client.ContainerAttach(context.Background(), options)
70
-	if err != nil {
71
-		return err
69
+	resp, errAttach := cli.client.ContainerAttach(context.Background(), options)
70
+	if errAttach != nil && errAttach != httputil.ErrPersistEOF {
71
+		// ContainerAttach returns an ErrPersistEOF (connection closed)
72
+		// means server met an error and put it in Hijacked connection
73
+		// keep the error and read detailed error message from hijacked connection later
74
+		return errAttach
72 75
 	}
73 76
 	defer resp.Close()
74 77
 
... ...
@@ -90,6 +94,10 @@ func (cli *DockerCli) CmdAttach(args ...string) error {
90 90
 		return err
91 91
 	}
92 92
 
93
+	if errAttach != nil {
94
+		return errAttach
95
+	}
96
+
93 97
 	_, status, err := getExitCode(cli, options.ContainerID)
94 98
 	if err != nil {
95 99
 		return err
... ...
@@ -3,6 +3,7 @@ package client
3 3
 import (
4 4
 	"fmt"
5 5
 	"io"
6
+	"net/http/httputil"
6 7
 	"os"
7 8
 	"runtime"
8 9
 	"strings"
... ...
@@ -205,19 +206,26 @@ func (cli *DockerCli) CmdRun(args ...string) error {
205 205
 			DetachKeys:  cli.configFile.DetachKeys,
206 206
 		}
207 207
 
208
-		resp, err := cli.client.ContainerAttach(context.Background(), options)
209
-		if err != nil {
210
-			return err
208
+		resp, errAttach := cli.client.ContainerAttach(context.Background(), options)
209
+		if errAttach != nil && errAttach != httputil.ErrPersistEOF {
210
+			// ContainerAttach returns an ErrPersistEOF (connection closed)
211
+			// means server met an error and put it in Hijacked connection
212
+			// keep the error and read detailed error message from hijacked connection later
213
+			return errAttach
211 214
 		}
212 215
 		ctx, cancelFun = context.WithCancel(context.Background())
213 216
 		errCh = promise.Go(func() error {
214
-			return cli.holdHijackedConnection(ctx, config.Tty, in, out, stderr, resp)
217
+			errHijack := cli.holdHijackedConnection(ctx, config.Tty, in, out, stderr, resp)
218
+			if errHijack == nil {
219
+				return errAttach
220
+			}
221
+			return errHijack
215 222
 		})
216 223
 	}
217 224
 
218 225
 	if *flAutoRemove {
219 226
 		defer func() {
220
-			if err := cli.removeContainer(createResponse.ID, true, false, false); err != nil {
227
+			if err := cli.removeContainer(createResponse.ID, true, false, true); err != nil {
221 228
 				fmt.Fprintf(cli.err, "%v\n", err)
222 229
 			}
223 230
 		}()
... ...
@@ -3,6 +3,7 @@ package client
3 3
 import (
4 4
 	"fmt"
5 5
 	"io"
6
+	"net/http/httputil"
6 7
 	"os"
7 8
 	"strings"
8 9
 
... ...
@@ -94,14 +95,21 @@ func (cli *DockerCli) CmdStart(args ...string) error {
94 94
 			in = cli.in
95 95
 		}
96 96
 
97
-		resp, err := cli.client.ContainerAttach(context.Background(), options)
98
-		if err != nil {
99
-			return err
97
+		resp, errAttach := cli.client.ContainerAttach(context.Background(), options)
98
+		if errAttach != nil && errAttach != httputil.ErrPersistEOF {
99
+			// ContainerAttach return an ErrPersistEOF (connection closed)
100
+			// means server met an error and put it in Hijacked connection
101
+			// keep the error and read detailed error message from hijacked connection
102
+			return errAttach
100 103
 		}
101 104
 		defer resp.Close()
102 105
 		ctx, cancelFun := context.WithCancel(context.Background())
103 106
 		cErr := promise.Go(func() error {
104
-			return cli.holdHijackedConnection(ctx, c.Config.Tty, in, cli.out, cli.err, resp)
107
+			errHijack := cli.holdHijackedConnection(ctx, c.Config.Tty, in, cli.out, cli.err, resp)
108
+			if errHijack == nil {
109
+				return errAttach
110
+			}
111
+			return errHijack
105 112
 		})
106 113
 
107 114
 		// 3. Start the container.
... ...
@@ -24,11 +24,11 @@ type inputValidationError interface {
24 24
 	IsValidationError() bool
25 25
 }
26 26
 
27
-// WriteError decodes a specific docker error and sends it in the response.
28
-func WriteError(w http.ResponseWriter, err error) {
29
-	if err == nil || w == nil {
30
-		logrus.WithFields(logrus.Fields{"error": err, "writer": w}).Error("unexpected HTTP error handling")
31
-		return
27
+// GetHTTPErrorStatusCode retrieve status code from error message
28
+func GetHTTPErrorStatusCode(err error) int {
29
+	if err == nil {
30
+		logrus.WithFields(logrus.Fields{"error": err}).Error("unexpected HTTP error handling")
31
+		return http.StatusInternalServerError
32 32
 	}
33 33
 
34 34
 	var statusCode int
... ...
@@ -65,5 +65,16 @@ func WriteError(w http.ResponseWriter, err error) {
65 65
 		statusCode = http.StatusInternalServerError
66 66
 	}
67 67
 
68
-	http.Error(w, errMsg, statusCode)
68
+	return statusCode
69
+}
70
+
71
+// WriteError decodes a specific docker error and sends it in the response.
72
+func WriteError(w http.ResponseWriter, err error) {
73
+	if err == nil || w == nil {
74
+		logrus.WithFields(logrus.Fields{"error": err, "writer": w}).Error("unexpected HTTP error handling")
75
+		return
76
+	}
77
+
78
+	statusCode := GetHTTPErrorStatusCode(err)
79
+	http.Error(w, err.Error(), statusCode)
69 80
 }
... ...
@@ -15,7 +15,6 @@ import (
15 15
 	"github.com/docker/docker/api/types/backend"
16 16
 	"github.com/docker/docker/pkg/ioutils"
17 17
 	"github.com/docker/docker/pkg/signal"
18
-	"github.com/docker/docker/pkg/term"
19 18
 	"github.com/docker/engine-api/types"
20 19
 	"github.com/docker/engine-api/types/container"
21 20
 	"github.com/docker/engine-api/types/filters"
... ...
@@ -408,15 +407,7 @@ func (s *containerRouter) postContainersAttach(ctx context.Context, w http.Respo
408 408
 	containerName := vars["name"]
409 409
 
410 410
 	_, upgrade := r.Header["Upgrade"]
411
-
412
-	keys := []byte{}
413 411
 	detachKeys := r.FormValue("detachKeys")
414
-	if detachKeys != "" {
415
-		keys, err = term.ToBytes(detachKeys)
416
-		if err != nil {
417
-			logrus.Warnf("Invalid escape keys provided (%s) using default : ctrl-p ctrl-q", detachKeys)
418
-		}
419
-	}
420 412
 
421 413
 	hijacker, ok := w.(http.Hijacker)
422 414
 	if !ok {
... ...
@@ -452,11 +443,24 @@ func (s *containerRouter) postContainersAttach(ctx context.Context, w http.Respo
452 452
 		UseStderr:  httputils.BoolValue(r, "stderr"),
453 453
 		Logs:       httputils.BoolValue(r, "logs"),
454 454
 		Stream:     httputils.BoolValue(r, "stream"),
455
-		DetachKeys: keys,
455
+		DetachKeys: detachKeys,
456 456
 		MuxStreams: true,
457 457
 	}
458 458
 
459
-	return s.backend.ContainerAttach(containerName, attachConfig)
459
+	if err = s.backend.ContainerAttach(containerName, attachConfig); err != nil {
460
+		logrus.Errorf("Handler for %s %s returned error: %v", r.Method, r.URL.Path, err)
461
+		// Remember to close stream if error happens
462
+		conn, _, errHijack := hijacker.Hijack()
463
+		if errHijack == nil {
464
+			statusCode := httputils.GetHTTPErrorStatusCode(err)
465
+			statusText := http.StatusText(statusCode)
466
+			fmt.Fprintf(conn, "HTTP/1.1 %d %s\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\n%s\r\n", statusCode, statusText, err.Error())
467
+			httputils.CloseStreams(conn)
468
+		} else {
469
+			logrus.Errorf("Error Hijacking: %v", err)
470
+		}
471
+	}
472
+	return nil
460 473
 }
461 474
 
462 475
 func (s *containerRouter) wsContainersAttach(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
... ...
@@ -465,15 +469,8 @@ func (s *containerRouter) wsContainersAttach(ctx context.Context, w http.Respons
465 465
 	}
466 466
 	containerName := vars["name"]
467 467
 
468
-	var keys []byte
469 468
 	var err error
470 469
 	detachKeys := r.FormValue("detachKeys")
471
-	if detachKeys != "" {
472
-		keys, err = term.ToBytes(detachKeys)
473
-		if err != nil {
474
-			logrus.Warnf("Invalid escape keys provided (%s) using default : ctrl-p ctrl-q", detachKeys)
475
-		}
476
-	}
477 470
 
478 471
 	done := make(chan struct{})
479 472
 	started := make(chan struct{})
... ...
@@ -499,7 +496,7 @@ func (s *containerRouter) wsContainersAttach(ctx context.Context, w http.Respons
499 499
 		GetStreams: setupStreams,
500 500
 		Logs:       httputils.BoolValue(r, "logs"),
501 501
 		Stream:     httputils.BoolValue(r, "stream"),
502
-		DetachKeys: keys,
502
+		DetachKeys: detachKeys,
503 503
 		UseStdin:   true,
504 504
 		UseStdout:  true,
505 505
 		UseStderr:  true,
... ...
@@ -18,7 +18,7 @@ type ContainerAttachConfig struct {
18 18
 	UseStderr  bool
19 19
 	Logs       bool
20 20
 	Stream     bool
21
-	DetachKeys []byte
21
+	DetachKeys string
22 22
 
23 23
 	// Used to signify that streams are multiplexed and therefore need a StdWriter to encode stdout/sderr messages accordingly.
24 24
 	// TODO @cpuguy83: This shouldn't be needed. It was only added so that http and websocket endpoints can use the same function, and the websocket function was not using a stdwriter prior to this change...
... ...
@@ -11,10 +11,20 @@ import (
11 11
 	"github.com/docker/docker/daemon/logger"
12 12
 	"github.com/docker/docker/errors"
13 13
 	"github.com/docker/docker/pkg/stdcopy"
14
+	"github.com/docker/docker/pkg/term"
14 15
 )
15 16
 
16 17
 // ContainerAttach attaches to logs according to the config passed in. See ContainerAttachConfig.
17 18
 func (daemon *Daemon) ContainerAttach(prefixOrName string, c *backend.ContainerAttachConfig) error {
19
+	keys := []byte{}
20
+	var err error
21
+	if c.DetachKeys != "" {
22
+		keys, err = term.ToBytes(c.DetachKeys)
23
+		if err != nil {
24
+			return fmt.Errorf("Invalid escape keys (%s) provided", c.DetachKeys)
25
+		}
26
+	}
27
+
18 28
 	container, err := daemon.GetContainer(prefixOrName)
19 29
 	if err != nil {
20 30
 		return err
... ...
@@ -48,7 +58,7 @@ func (daemon *Daemon) ContainerAttach(prefixOrName string, c *backend.ContainerA
48 48
 		stderr = errStream
49 49
 	}
50 50
 
51
-	if err := daemon.containerAttach(container, stdin, stdout, stderr, c.Logs, c.Stream, c.DetachKeys); err != nil {
51
+	if err := daemon.containerAttach(container, stdin, stdout, stderr, c.Logs, c.Stream, keys); err != nil {
52 52
 		fmt.Fprintf(outStream, "Error attaching: %s\n", err)
53 53
 	}
54 54
 	return nil
... ...
@@ -101,7 +101,8 @@ func (d *Daemon) ContainerExecCreate(config *types.ExecConfig) (string, error) {
101 101
 	if config.DetachKeys != "" {
102 102
 		keys, err = term.ToBytes(config.DetachKeys)
103 103
 		if err != nil {
104
-			logrus.Warnf("Wrong escape keys provided (%s, error: %s) using default : ctrl-p ctrl-q", config.DetachKeys, err.Error())
104
+			err = fmt.Errorf("Invalid escape keys (%s) provided", config.DetachKeys)
105
+			return "", err
105 106
 		}
106 107
 	}
107 108
 
... ...
@@ -67,11 +67,18 @@ func (s *DockerSuite) TestGetContainersAttachWebsocket(c *check.C) {
67 67
 
68 68
 // regression gh14320
69 69
 func (s *DockerSuite) TestPostContainersAttachContainerNotFound(c *check.C) {
70
-	status, body, err := sockRequest("POST", "/containers/doesnotexist/attach", nil)
71
-	c.Assert(status, checker.Equals, http.StatusNotFound)
70
+	req, client, err := newRequestClient("POST", "/containers/doesnotexist/attach", nil, "")
72 71
 	c.Assert(err, checker.IsNil)
73
-	expected := "No such container: doesnotexist\n"
74
-	c.Assert(string(body), checker.Contains, expected)
72
+
73
+	resp, err := client.Do(req)
74
+	// connection will shutdown, err should be "persistent connection closed"
75
+	c.Assert(err, checker.NotNil) // Server shutdown connection
76
+
77
+	body, err := readBody(resp.Body)
78
+	c.Assert(err, checker.IsNil)
79
+	c.Assert(resp.StatusCode, checker.Equals, http.StatusNotFound)
80
+	expected := "No such container: doesnotexist\r\n"
81
+	c.Assert(string(body), checker.Equals, expected)
75 82
 }
76 83
 
77 84
 func (s *DockerSuite) TestGetContainersWsAttachContainerNotFound(c *check.C) {
... ...
@@ -197,6 +197,38 @@ func (s *DockerSuite) TestRunAttachDetachFromFlag(c *check.C) {
197 197
 	c.Assert(running, checker.Equals, "true", check.Commentf("expected container to still be running"))
198 198
 }
199 199
 
200
+// TestRunDetach checks attaching and detaching with the escape sequence specified via flags.
201
+func (s *DockerSuite) TestRunAttachDetachFromInvalidFlag(c *check.C) {
202
+	name := "attach-detach"
203
+	dockerCmd(c, "run", "--name", name, "-itd", "busybox", "top")
204
+	c.Assert(waitRun(name), check.IsNil)
205
+
206
+	// specify an invalid detach key, container will ignore it and use default
207
+	cmd := exec.Command(dockerBinary, "attach", "--detach-keys='ctrl-A,a'", name)
208
+	stdout, err := cmd.StdoutPipe()
209
+	if err != nil {
210
+		c.Fatal(err)
211
+	}
212
+	cpty, tty, err := pty.Open()
213
+	if err != nil {
214
+		c.Fatal(err)
215
+	}
216
+	defer cpty.Close()
217
+	cmd.Stdin = tty
218
+	if err := cmd.Start(); err != nil {
219
+		c.Fatal(err)
220
+	}
221
+
222
+	bufReader := bufio.NewReader(stdout)
223
+	out, err := bufReader.ReadString('\n')
224
+	if err != nil {
225
+		c.Fatal(err)
226
+	}
227
+	// it should print a warning to indicate the detach key flag is invalid
228
+	errStr := "Invalid escape keys (ctrl-A,a) provided"
229
+	c.Assert(strings.TrimSpace(out), checker.Equals, errStr)
230
+}
231
+
200 232
 // TestRunDetach checks attaching and detaching with the escape sequence specified via config file.
201 233
 func (s *DockerSuite) TestRunAttachDetachFromConfig(c *check.C) {
202 234
 	keyCtrlA := []byte{1}