Browse code

Migrate exec command to cobra

Signed-off-by: Akihiro Suda <suda.akihiro@lab.ntt.co.jp>

Akihiro Suda authored on 2016/06/20 22:27:56
Showing 10 changed files
... ...
@@ -87,7 +87,12 @@ func (cli *DockerCli) ConfigFile() *configfile.ConfigFile {
87 87
 	return cli.configFile
88 88
 }
89 89
 
90
-// IsTerminalOut returns true if the clients stdin is a TTY
90
+// IsTerminalIn returns true if the clients stdin is a TTY
91
+func (cli *DockerCli) IsTerminalIn() bool {
92
+	return cli.isTerminalIn
93
+}
94
+
95
+// IsTerminalOut returns true if the clients stdout is a TTY
91 96
 func (cli *DockerCli) IsTerminalOut() bool {
92 97
 	return cli.isTerminalOut
93 98
 }
... ...
@@ -3,7 +3,6 @@ package client
3 3
 // Command returns a cli command handler if one exists
4 4
 func (cli *DockerCli) Command(name string) func(...string) error {
5 5
 	return map[string]func(...string) error{
6
-		"exec":    cli.CmdExec,
7 6
 		"inspect": cli.CmdInspect,
8 7
 	}[name]
9 8
 }
10 9
new file mode 100644
... ...
@@ -0,0 +1,175 @@
0
+package container
1
+
2
+import (
3
+	"fmt"
4
+	"io"
5
+
6
+	"golang.org/x/net/context"
7
+
8
+	"github.com/Sirupsen/logrus"
9
+	"github.com/docker/docker/api/client"
10
+	"github.com/docker/docker/cli"
11
+	"github.com/docker/docker/pkg/promise"
12
+	"github.com/docker/engine-api/types"
13
+	"github.com/spf13/cobra"
14
+)
15
+
16
+type execOptions struct {
17
+	detachKeys  string
18
+	interactive bool
19
+	tty         bool
20
+	detach      bool
21
+	user        string
22
+	privileged  bool
23
+}
24
+
25
+// NewExecCommand creats a new cobra.Command for `docker exec`
26
+func NewExecCommand(dockerCli *client.DockerCli) *cobra.Command {
27
+	var opts execOptions
28
+
29
+	cmd := &cobra.Command{
30
+		Use:   "exec CONTAINER COMMAND [ARG...]",
31
+		Short: "Run a command in a running container",
32
+		Args:  cli.RequiresMinArgs(2),
33
+		RunE: func(cmd *cobra.Command, args []string) error {
34
+			container := args[0]
35
+			execCmd := args[1:]
36
+			return runExec(dockerCli, &opts, container, execCmd)
37
+		},
38
+	}
39
+
40
+	flags := cmd.Flags()
41
+	flags.SetInterspersed(false)
42
+
43
+	flags.StringVarP(&opts.detachKeys, "detach-keys", "", "", "Override the key sequence for detaching a container")
44
+	flags.BoolVarP(&opts.interactive, "interactive", "i", false, "Keep STDIN open even if not attached")
45
+	flags.BoolVarP(&opts.tty, "tty", "t", false, "Allocate a pseudo-TTY")
46
+	flags.BoolVarP(&opts.detach, "detach", "d", false, "Detached mode: run command in the background")
47
+	flags.StringVarP(&opts.user, "user", "u", "", "Username or UID (format: <name|uid>[:<group|gid>])")
48
+	flags.BoolVarP(&opts.privileged, "privileged", "", false, "Give extended privileges to the command")
49
+
50
+	return cmd
51
+}
52
+
53
+func runExec(dockerCli *client.DockerCli, opts *execOptions, container string, execCmd []string) error {
54
+	execConfig, err := parseExec(opts, container, execCmd)
55
+	// just in case the ParseExec does not exit
56
+	if container == "" || err != nil {
57
+		return cli.StatusError{StatusCode: 1}
58
+	}
59
+
60
+	if opts.detachKeys != "" {
61
+		dockerCli.ConfigFile().DetachKeys = opts.detachKeys
62
+	}
63
+
64
+	// Send client escape keys
65
+	execConfig.DetachKeys = dockerCli.ConfigFile().DetachKeys
66
+
67
+	ctx := context.Background()
68
+
69
+	response, err := dockerCli.Client().ContainerExecCreate(ctx, container, *execConfig)
70
+	if err != nil {
71
+		return err
72
+	}
73
+
74
+	execID := response.ID
75
+	if execID == "" {
76
+		fmt.Fprintf(dockerCli.Out(), "exec ID empty")
77
+		return nil
78
+	}
79
+
80
+	//Temp struct for execStart so that we don't need to transfer all the execConfig
81
+	if !execConfig.Detach {
82
+		if err := dockerCli.CheckTtyInput(execConfig.AttachStdin, execConfig.Tty); err != nil {
83
+			return err
84
+		}
85
+	} else {
86
+		execStartCheck := types.ExecStartCheck{
87
+			Detach: execConfig.Detach,
88
+			Tty:    execConfig.Tty,
89
+		}
90
+
91
+		if err := dockerCli.Client().ContainerExecStart(ctx, execID, execStartCheck); err != nil {
92
+			return err
93
+		}
94
+		// For now don't print this - wait for when we support exec wait()
95
+		// fmt.Fprintf(dockerCli.Out(), "%s\n", execID)
96
+		return nil
97
+	}
98
+
99
+	// Interactive exec requested.
100
+	var (
101
+		out, stderr io.Writer
102
+		in          io.ReadCloser
103
+		errCh       chan error
104
+	)
105
+
106
+	if execConfig.AttachStdin {
107
+		in = dockerCli.In()
108
+	}
109
+	if execConfig.AttachStdout {
110
+		out = dockerCli.Out()
111
+	}
112
+	if execConfig.AttachStderr {
113
+		if execConfig.Tty {
114
+			stderr = dockerCli.Out()
115
+		} else {
116
+			stderr = dockerCli.Err()
117
+		}
118
+	}
119
+
120
+	resp, err := dockerCli.Client().ContainerExecAttach(ctx, execID, *execConfig)
121
+	if err != nil {
122
+		return err
123
+	}
124
+	defer resp.Close()
125
+	errCh = promise.Go(func() error {
126
+		return dockerCli.HoldHijackedConnection(ctx, execConfig.Tty, in, out, stderr, resp)
127
+	})
128
+
129
+	if execConfig.Tty && dockerCli.IsTerminalIn() {
130
+		if err := dockerCli.MonitorTtySize(ctx, execID, true); err != nil {
131
+			fmt.Fprintf(dockerCli.Err(), "Error monitoring TTY size: %s\n", err)
132
+		}
133
+	}
134
+
135
+	if err := <-errCh; err != nil {
136
+		logrus.Debugf("Error hijack: %s", err)
137
+		return err
138
+	}
139
+
140
+	var status int
141
+	if _, status, err = dockerCli.GetExecExitCode(ctx, execID); err != nil {
142
+		return err
143
+	}
144
+
145
+	if status != 0 {
146
+		return cli.StatusError{StatusCode: status}
147
+	}
148
+
149
+	return nil
150
+}
151
+
152
+// parseExec parses the specified args for the specified command and generates
153
+// an ExecConfig from it.
154
+func parseExec(opts *execOptions, container string, execCmd []string) (*types.ExecConfig, error) {
155
+	execConfig := &types.ExecConfig{
156
+		User:       opts.user,
157
+		Privileged: opts.privileged,
158
+		Tty:        opts.tty,
159
+		Cmd:        execCmd,
160
+		Detach:     opts.detach,
161
+		// container is not used here
162
+	}
163
+
164
+	// If -d is not set, attach to everything by default
165
+	if !opts.detach {
166
+		execConfig.AttachStdout = true
167
+		execConfig.AttachStderr = true
168
+		if opts.interactive {
169
+			execConfig.AttachStdin = true
170
+		}
171
+	}
172
+
173
+	return execConfig, nil
174
+}
0 175
new file mode 100644
... ...
@@ -0,0 +1,117 @@
0
+package container
1
+
2
+import (
3
+	"testing"
4
+
5
+	"github.com/docker/engine-api/types"
6
+)
7
+
8
+type arguments struct {
9
+	options   execOptions
10
+	container string
11
+	execCmd   []string
12
+}
13
+
14
+func TestParseExec(t *testing.T) {
15
+	valids := map[*arguments]*types.ExecConfig{
16
+		&arguments{
17
+			execCmd: []string{"command"},
18
+		}: {
19
+			Cmd:          []string{"command"},
20
+			AttachStdout: true,
21
+			AttachStderr: true,
22
+		},
23
+		&arguments{
24
+			execCmd: []string{"command1", "command2"},
25
+		}: {
26
+			Cmd:          []string{"command1", "command2"},
27
+			AttachStdout: true,
28
+			AttachStderr: true,
29
+		},
30
+		&arguments{
31
+			options: execOptions{
32
+				interactive: true,
33
+				tty:         true,
34
+				user:        "uid",
35
+			},
36
+			execCmd: []string{"command"},
37
+		}: {
38
+			User:         "uid",
39
+			AttachStdin:  true,
40
+			AttachStdout: true,
41
+			AttachStderr: true,
42
+			Tty:          true,
43
+			Cmd:          []string{"command"},
44
+		},
45
+		&arguments{
46
+			options: execOptions{
47
+				detach: true,
48
+			},
49
+			execCmd: []string{"command"},
50
+		}: {
51
+			AttachStdin:  false,
52
+			AttachStdout: false,
53
+			AttachStderr: false,
54
+			Detach:       true,
55
+			Cmd:          []string{"command"},
56
+		},
57
+		&arguments{
58
+			options: execOptions{
59
+				tty:         true,
60
+				interactive: true,
61
+				detach:      true,
62
+			},
63
+			execCmd: []string{"command"},
64
+		}: {
65
+			AttachStdin:  false,
66
+			AttachStdout: false,
67
+			AttachStderr: false,
68
+			Detach:       true,
69
+			Tty:          true,
70
+			Cmd:          []string{"command"},
71
+		},
72
+	}
73
+
74
+	for valid, expectedExecConfig := range valids {
75
+		execConfig, err := parseExec(&valid.options, valid.container, valid.execCmd)
76
+		if err != nil {
77
+			t.Fatal(err)
78
+		}
79
+		if !compareExecConfig(expectedExecConfig, execConfig) {
80
+			t.Fatalf("Expected [%v] for %v, got [%v]", expectedExecConfig, valid, execConfig)
81
+		}
82
+	}
83
+}
84
+
85
+func compareExecConfig(config1 *types.ExecConfig, config2 *types.ExecConfig) bool {
86
+	if config1.AttachStderr != config2.AttachStderr {
87
+		return false
88
+	}
89
+	if config1.AttachStdin != config2.AttachStdin {
90
+		return false
91
+	}
92
+	if config1.AttachStdout != config2.AttachStdout {
93
+		return false
94
+	}
95
+	if config1.Detach != config2.Detach {
96
+		return false
97
+	}
98
+	if config1.Privileged != config2.Privileged {
99
+		return false
100
+	}
101
+	if config1.Tty != config2.Tty {
102
+		return false
103
+	}
104
+	if config1.User != config2.User {
105
+		return false
106
+	}
107
+	if len(config1.Cmd) != len(config2.Cmd) {
108
+		return false
109
+	}
110
+	for index, value := range config1.Cmd {
111
+		if value != config2.Cmd[index] {
112
+			return false
113
+		}
114
+	}
115
+	return true
116
+}
0 117
deleted file mode 100644
... ...
@@ -1,160 +0,0 @@
1
-package client
2
-
3
-import (
4
-	"fmt"
5
-	"io"
6
-
7
-	"golang.org/x/net/context"
8
-
9
-	"github.com/Sirupsen/logrus"
10
-	Cli "github.com/docker/docker/cli"
11
-	flag "github.com/docker/docker/pkg/mflag"
12
-	"github.com/docker/docker/pkg/promise"
13
-	"github.com/docker/engine-api/types"
14
-)
15
-
16
-// CmdExec runs a command in a running container.
17
-//
18
-// Usage: docker exec [OPTIONS] CONTAINER COMMAND [ARG...]
19
-func (cli *DockerCli) CmdExec(args ...string) error {
20
-	cmd := Cli.Subcmd("exec", []string{"[OPTIONS] CONTAINER COMMAND [ARG...]"}, Cli.DockerCommands["exec"].Description, true)
21
-	detachKeys := cmd.String([]string{"-detach-keys"}, "", "Override the key sequence for detaching a container")
22
-
23
-	execConfig, err := ParseExec(cmd, args)
24
-	container := cmd.Arg(0)
25
-	// just in case the ParseExec does not exit
26
-	if container == "" || err != nil {
27
-		return Cli.StatusError{StatusCode: 1}
28
-	}
29
-
30
-	if *detachKeys != "" {
31
-		cli.configFile.DetachKeys = *detachKeys
32
-	}
33
-
34
-	// Send client escape keys
35
-	execConfig.DetachKeys = cli.configFile.DetachKeys
36
-
37
-	ctx := context.Background()
38
-
39
-	response, err := cli.client.ContainerExecCreate(ctx, container, *execConfig)
40
-	if err != nil {
41
-		return err
42
-	}
43
-
44
-	execID := response.ID
45
-	if execID == "" {
46
-		fmt.Fprintf(cli.out, "exec ID empty")
47
-		return nil
48
-	}
49
-
50
-	//Temp struct for execStart so that we don't need to transfer all the execConfig
51
-	if !execConfig.Detach {
52
-		if err := cli.CheckTtyInput(execConfig.AttachStdin, execConfig.Tty); err != nil {
53
-			return err
54
-		}
55
-	} else {
56
-		execStartCheck := types.ExecStartCheck{
57
-			Detach: execConfig.Detach,
58
-			Tty:    execConfig.Tty,
59
-		}
60
-
61
-		if err := cli.client.ContainerExecStart(ctx, execID, execStartCheck); err != nil {
62
-			return err
63
-		}
64
-		// For now don't print this - wait for when we support exec wait()
65
-		// fmt.Fprintf(cli.out, "%s\n", execID)
66
-		return nil
67
-	}
68
-
69
-	// Interactive exec requested.
70
-	var (
71
-		out, stderr io.Writer
72
-		in          io.ReadCloser
73
-		errCh       chan error
74
-	)
75
-
76
-	if execConfig.AttachStdin {
77
-		in = cli.in
78
-	}
79
-	if execConfig.AttachStdout {
80
-		out = cli.out
81
-	}
82
-	if execConfig.AttachStderr {
83
-		if execConfig.Tty {
84
-			stderr = cli.out
85
-		} else {
86
-			stderr = cli.err
87
-		}
88
-	}
89
-
90
-	resp, err := cli.client.ContainerExecAttach(ctx, execID, *execConfig)
91
-	if err != nil {
92
-		return err
93
-	}
94
-	defer resp.Close()
95
-	errCh = promise.Go(func() error {
96
-		return cli.HoldHijackedConnection(ctx, execConfig.Tty, in, out, stderr, resp)
97
-	})
98
-
99
-	if execConfig.Tty && cli.isTerminalIn {
100
-		if err := cli.MonitorTtySize(ctx, execID, true); err != nil {
101
-			fmt.Fprintf(cli.err, "Error monitoring TTY size: %s\n", err)
102
-		}
103
-	}
104
-
105
-	if err := <-errCh; err != nil {
106
-		logrus.Debugf("Error hijack: %s", err)
107
-		return err
108
-	}
109
-
110
-	var status int
111
-	if _, status, err = cli.getExecExitCode(ctx, execID); err != nil {
112
-		return err
113
-	}
114
-
115
-	if status != 0 {
116
-		return Cli.StatusError{StatusCode: status}
117
-	}
118
-
119
-	return nil
120
-}
121
-
122
-// ParseExec parses the specified args for the specified command and generates
123
-// an ExecConfig from it.
124
-// If the minimal number of specified args is not right or if specified args are
125
-// not valid, it will return an error.
126
-func ParseExec(cmd *flag.FlagSet, args []string) (*types.ExecConfig, error) {
127
-	var (
128
-		flStdin      = cmd.Bool([]string{"i", "-interactive"}, false, "Keep STDIN open even if not attached")
129
-		flTty        = cmd.Bool([]string{"t", "-tty"}, false, "Allocate a pseudo-TTY")
130
-		flDetach     = cmd.Bool([]string{"d", "-detach"}, false, "Detached mode: run command in the background")
131
-		flUser       = cmd.String([]string{"u", "-user"}, "", "Username or UID (format: <name|uid>[:<group|gid>])")
132
-		flPrivileged = cmd.Bool([]string{"-privileged"}, false, "Give extended privileges to the command")
133
-		execCmd      []string
134
-	)
135
-	cmd.Require(flag.Min, 2)
136
-	if err := cmd.ParseFlags(args, true); err != nil {
137
-		return nil, err
138
-	}
139
-	parsedArgs := cmd.Args()
140
-	execCmd = parsedArgs[1:]
141
-
142
-	execConfig := &types.ExecConfig{
143
-		User:       *flUser,
144
-		Privileged: *flPrivileged,
145
-		Tty:        *flTty,
146
-		Cmd:        execCmd,
147
-		Detach:     *flDetach,
148
-	}
149
-
150
-	// If -d is not set, attach to everything by default
151
-	if !*flDetach {
152
-		execConfig.AttachStdout = true
153
-		execConfig.AttachStderr = true
154
-		if *flStdin {
155
-			execConfig.AttachStdin = true
156
-		}
157
-	}
158
-
159
-	return execConfig, nil
160
-}
161 1
deleted file mode 100644
... ...
@@ -1,122 +0,0 @@
1
-package client
2
-
3
-import (
4
-	"fmt"
5
-	"io/ioutil"
6
-	"testing"
7
-
8
-	flag "github.com/docker/docker/pkg/mflag"
9
-	"github.com/docker/engine-api/types"
10
-)
11
-
12
-type arguments struct {
13
-	args []string
14
-}
15
-
16
-func TestParseExec(t *testing.T) {
17
-	invalids := map[*arguments]error{
18
-		&arguments{[]string{"-unknown"}}: fmt.Errorf("flag provided but not defined: -unknown"),
19
-		&arguments{[]string{"-u"}}:       fmt.Errorf("flag needs an argument: -u"),
20
-		&arguments{[]string{"--user"}}:   fmt.Errorf("flag needs an argument: --user"),
21
-	}
22
-	valids := map[*arguments]*types.ExecConfig{
23
-		&arguments{
24
-			[]string{"container", "command"},
25
-		}: {
26
-			Cmd:          []string{"command"},
27
-			AttachStdout: true,
28
-			AttachStderr: true,
29
-		},
30
-		&arguments{
31
-			[]string{"container", "command1", "command2"},
32
-		}: {
33
-			Cmd:          []string{"command1", "command2"},
34
-			AttachStdout: true,
35
-			AttachStderr: true,
36
-		},
37
-		&arguments{
38
-			[]string{"-i", "-t", "-u", "uid", "container", "command"},
39
-		}: {
40
-			User:         "uid",
41
-			AttachStdin:  true,
42
-			AttachStdout: true,
43
-			AttachStderr: true,
44
-			Tty:          true,
45
-			Cmd:          []string{"command"},
46
-		},
47
-		&arguments{
48
-			[]string{"-d", "container", "command"},
49
-		}: {
50
-			AttachStdin:  false,
51
-			AttachStdout: false,
52
-			AttachStderr: false,
53
-			Detach:       true,
54
-			Cmd:          []string{"command"},
55
-		},
56
-		&arguments{
57
-			[]string{"-t", "-i", "-d", "container", "command"},
58
-		}: {
59
-			AttachStdin:  false,
60
-			AttachStdout: false,
61
-			AttachStderr: false,
62
-			Detach:       true,
63
-			Tty:          true,
64
-			Cmd:          []string{"command"},
65
-		},
66
-	}
67
-	for invalid, expectedError := range invalids {
68
-		cmd := flag.NewFlagSet("exec", flag.ContinueOnError)
69
-		cmd.ShortUsage = func() {}
70
-		cmd.SetOutput(ioutil.Discard)
71
-		_, err := ParseExec(cmd, invalid.args)
72
-		if err == nil || err.Error() != expectedError.Error() {
73
-			t.Fatalf("Expected an error [%v] for %v, got %v", expectedError, invalid, err)
74
-		}
75
-
76
-	}
77
-	for valid, expectedExecConfig := range valids {
78
-		cmd := flag.NewFlagSet("exec", flag.ContinueOnError)
79
-		cmd.ShortUsage = func() {}
80
-		cmd.SetOutput(ioutil.Discard)
81
-		execConfig, err := ParseExec(cmd, valid.args)
82
-		if err != nil {
83
-			t.Fatal(err)
84
-		}
85
-		if !compareExecConfig(expectedExecConfig, execConfig) {
86
-			t.Fatalf("Expected [%v] for %v, got [%v]", expectedExecConfig, valid, execConfig)
87
-		}
88
-	}
89
-}
90
-
91
-func compareExecConfig(config1 *types.ExecConfig, config2 *types.ExecConfig) bool {
92
-	if config1.AttachStderr != config2.AttachStderr {
93
-		return false
94
-	}
95
-	if config1.AttachStdin != config2.AttachStdin {
96
-		return false
97
-	}
98
-	if config1.AttachStdout != config2.AttachStdout {
99
-		return false
100
-	}
101
-	if config1.Detach != config2.Detach {
102
-		return false
103
-	}
104
-	if config1.Privileged != config2.Privileged {
105
-		return false
106
-	}
107
-	if config1.Tty != config2.Tty {
108
-		return false
109
-	}
110
-	if config1.User != config2.User {
111
-		return false
112
-	}
113
-	if len(config1.Cmd) != len(config2.Cmd) {
114
-		return false
115
-	}
116
-	for index, value := range config1.Cmd {
117
-		if value != config2.Cmd[index] {
118
-			return false
119
-		}
120
-	}
121
-	return true
122
-}
... ...
@@ -49,9 +49,9 @@ func (cli *DockerCli) ResizeTtyTo(ctx context.Context, id string, height, width
49 49
 	}
50 50
 }
51 51
 
52
-// getExecExitCode perform an inspect on the exec command. It returns
52
+// GetExecExitCode perform an inspect on the exec command. It returns
53 53
 // the running state and the exit code.
54
-func (cli *DockerCli) getExecExitCode(ctx context.Context, execID string) (bool, int, error) {
54
+func (cli *DockerCli) GetExecExitCode(ctx context.Context, execID string) (bool, int, error) {
55 55
 	resp, err := cli.client.ContainerExecInspect(ctx, execID)
56 56
 	if err != nil {
57 57
 		// If we can't connect, then the daemon probably died.
... ...
@@ -52,6 +52,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor {
52 52
 		container.NewCopyCommand(dockerCli),
53 53
 		container.NewCreateCommand(dockerCli),
54 54
 		container.NewDiffCommand(dockerCli),
55
+		container.NewExecCommand(dockerCli),
55 56
 		container.NewExportCommand(dockerCli),
56 57
 		container.NewKillCommand(dockerCli),
57 58
 		container.NewLogsCommand(dockerCli),
... ...
@@ -8,7 +8,6 @@ type Command struct {
8 8
 
9 9
 // DockerCommandUsage lists the top level docker commands and their short usage
10 10
 var DockerCommandUsage = []Command{
11
-	{"exec", "Run a command in a running container"},
12 11
 	{"inspect", "Return low-level information on a container, image or task"},
13 12
 }
14 13
 
... ...
@@ -214,7 +214,7 @@ func (s *DockerSuite) TestExecParseError(c *check.C) {
214 214
 	cmd := exec.Command(dockerBinary, "exec", "top")
215 215
 	_, stderr, _, err := runCommandWithStdoutStderr(cmd)
216 216
 	c.Assert(err, checker.NotNil)
217
-	c.Assert(stderr, checker.Contains, "See '"+dockerBinary+" exec --help'")
217
+	c.Assert(stderr, checker.Contains, "See 'docker exec --help'")
218 218
 }
219 219
 
220 220
 func (s *DockerSuite) TestExecStopNotHanging(c *check.C) {