Skip to content

Commit 9d9dff3

Browse files
committed
Migrate exec command to cobra
Signed-off-by: Akihiro Suda <[email protected]>
1 parent 498cd17 commit 9d9dff3

9 files changed

Lines changed: 208 additions & 194 deletions

File tree

api/client/cli.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,12 @@ func (cli *DockerCli) ConfigFile() *configfile.ConfigFile {
8787
return cli.configFile
8888
}
8989

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
9196
func (cli *DockerCli) IsTerminalOut() bool {
9297
return cli.isTerminalOut
9398
}

api/client/commands.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package client
33
// Command returns a cli command handler if one exists
44
func (cli *DockerCli) Command(name string) func(...string) error {
55
return map[string]func(...string) error{
6-
"exec": cli.CmdExec,
76
"inspect": cli.CmdInspect,
87
}[name]
98
}

api/client/container/exec.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package container
2+
3+
import (
4+
"fmt"
5+
"io"
6+
7+
"golang.org/x/net/context"
8+
9+
"github.com/Sirupsen/logrus"
10+
"github.com/docker/docker/api/client"
11+
"github.com/docker/docker/cli"
12+
"github.com/docker/docker/pkg/promise"
13+
"github.com/docker/engine-api/types"
14+
"github.com/spf13/cobra"
15+
)
16+
17+
type execOptions struct {
18+
detachKeys string
19+
interactive bool
20+
tty bool
21+
detach bool
22+
user string
23+
privileged bool
24+
}
25+
26+
// NewExecCommand creats a new cobra.Command for `docker exec`
27+
func NewExecCommand(dockerCli *client.DockerCli) *cobra.Command {
28+
var opts execOptions
29+
30+
cmd := &cobra.Command{
31+
Use: "exec CONTAINER COMMAND [ARG...]",
32+
Short: "Run a command in a running container",
33+
Args: cli.RequiresMinArgs(2),
34+
RunE: func(cmd *cobra.Command, args []string) error {
35+
container := args[0]
36+
execCmd := args[1:]
37+
return runExec(dockerCli, &opts, container, execCmd)
38+
},
39+
}
40+
41+
flags := cmd.Flags()
42+
flags.SetInterspersed(false)
43+
44+
flags.StringVarP(&opts.detachKeys, "detach-keys", "", "", "Override the key sequence for detaching a container")
45+
flags.BoolVarP(&opts.interactive, "interactive", "i", false, "Keep STDIN open even if not attached")
46+
flags.BoolVarP(&opts.tty, "tty", "t", false, "Allocate a pseudo-TTY")
47+
flags.BoolVarP(&opts.detach, "detach", "d", false, "Detached mode: run command in the background")
48+
flags.StringVarP(&opts.user, "user", "u", "", "Username or UID (format: <name|uid>[:<group|gid>])")
49+
flags.BoolVarP(&opts.privileged, "privileged", "", false, "Give extended privileges to the command")
50+
51+
return cmd
52+
}
53+
54+
func runExec(dockerCli *client.DockerCli, opts *execOptions, container string, execCmd []string) error {
55+
execConfig, err := parseExec(opts, container, execCmd)
56+
// just in case the ParseExec does not exit
57+
if container == "" || err != nil {
58+
return cli.StatusError{StatusCode: 1}
59+
}
60+
61+
if opts.detachKeys != "" {
62+
dockerCli.ConfigFile().DetachKeys = opts.detachKeys
63+
}
64+
65+
// Send client escape keys
66+
execConfig.DetachKeys = dockerCli.ConfigFile().DetachKeys
67+
68+
ctx := context.Background()
69+
70+
response, err := dockerCli.Client().ContainerExecCreate(ctx, container, *execConfig)
71+
if err != nil {
72+
return err
73+
}
74+
75+
execID := response.ID
76+
if execID == "" {
77+
fmt.Fprintf(dockerCli.Out(), "exec ID empty")
78+
return nil
79+
}
80+
81+
//Temp struct for execStart so that we don't need to transfer all the execConfig
82+
if !execConfig.Detach {
83+
if err := dockerCli.CheckTtyInput(execConfig.AttachStdin, execConfig.Tty); err != nil {
84+
return err
85+
}
86+
} else {
87+
execStartCheck := types.ExecStartCheck{
88+
Detach: execConfig.Detach,
89+
Tty: execConfig.Tty,
90+
}
91+
92+
if err := dockerCli.Client().ContainerExecStart(ctx, execID, execStartCheck); err != nil {
93+
return err
94+
}
95+
// For now don't print this - wait for when we support exec wait()
96+
// fmt.Fprintf(dockerCli.Out(), "%s\n", execID)
97+
return nil
98+
}
99+
100+
// Interactive exec requested.
101+
var (
102+
out, stderr io.Writer
103+
in io.ReadCloser
104+
errCh chan error
105+
)
106+
107+
if execConfig.AttachStdin {
108+
in = dockerCli.In()
109+
}
110+
if execConfig.AttachStdout {
111+
out = dockerCli.Out()
112+
}
113+
if execConfig.AttachStderr {
114+
if execConfig.Tty {
115+
stderr = dockerCli.Out()
116+
} else {
117+
stderr = dockerCli.Err()
118+
}
119+
}
120+
121+
resp, err := dockerCli.Client().ContainerExecAttach(ctx, execID, *execConfig)
122+
if err != nil {
123+
return err
124+
}
125+
defer resp.Close()
126+
errCh = promise.Go(func() error {
127+
return dockerCli.HoldHijackedConnection(ctx, execConfig.Tty, in, out, stderr, resp)
128+
})
129+
130+
if execConfig.Tty && dockerCli.IsTerminalIn() {
131+
if err := dockerCli.MonitorTtySize(ctx, execID, true); err != nil {
132+
fmt.Fprintf(dockerCli.Err(), "Error monitoring TTY size: %s\n", err)
133+
}
134+
}
135+
136+
if err := <-errCh; err != nil {
137+
logrus.Debugf("Error hijack: %s", err)
138+
return err
139+
}
140+
141+
var status int
142+
if _, status, err = dockerCli.GetExecExitCode(ctx, execID); err != nil {
143+
return err
144+
}
145+
146+
if status != 0 {
147+
return cli.StatusError{StatusCode: status}
148+
}
149+
150+
return nil
151+
}
152+
153+
// parseExec parses the specified args for the specified command and generates
154+
// an ExecConfig from it.
155+
func parseExec(opts *execOptions, container string, execCmd []string) (*types.ExecConfig, error) {
156+
execConfig := &types.ExecConfig{
157+
User: opts.user,
158+
Privileged: opts.privileged,
159+
Tty: opts.tty,
160+
Cmd: execCmd,
161+
Detach: opts.detach,
162+
// container is not used here
163+
}
164+
165+
// If -d is not set, attach to everything by default
166+
if !opts.detach {
167+
execConfig.AttachStdout = true
168+
execConfig.AttachStderr = true
169+
if opts.interactive {
170+
execConfig.AttachStdin = true
171+
}
172+
}
173+
174+
return execConfig, nil
175+
}
Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,40 @@
1-
package client
1+
package container
22

33
import (
4-
"fmt"
5-
"io/ioutil"
64
"testing"
75

8-
flag "github.com/docker/docker/pkg/mflag"
96
"github.com/docker/engine-api/types"
107
)
118

129
type arguments struct {
13-
args []string
10+
options execOptions
11+
container string
12+
execCmd []string
1413
}
1514

1615
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-
}
2216
valids := map[*arguments]*types.ExecConfig{
2317
&arguments{
24-
[]string{"container", "command"},
18+
execCmd: []string{"command"},
2519
}: {
2620
Cmd: []string{"command"},
2721
AttachStdout: true,
2822
AttachStderr: true,
2923
},
3024
&arguments{
31-
[]string{"container", "command1", "command2"},
25+
execCmd: []string{"command1", "command2"},
3226
}: {
3327
Cmd: []string{"command1", "command2"},
3428
AttachStdout: true,
3529
AttachStderr: true,
3630
},
3731
&arguments{
38-
[]string{"-i", "-t", "-u", "uid", "container", "command"},
32+
options: execOptions{
33+
interactive: true,
34+
tty: true,
35+
user: "uid",
36+
},
37+
execCmd: []string{"command"},
3938
}: {
4039
User: "uid",
4140
AttachStdin: true,
@@ -45,7 +44,10 @@ func TestParseExec(t *testing.T) {
4544
Cmd: []string{"command"},
4645
},
4746
&arguments{
48-
[]string{"-d", "container", "command"},
47+
options: execOptions{
48+
detach: true,
49+
},
50+
execCmd: []string{"command"},
4951
}: {
5052
AttachStdin: false,
5153
AttachStdout: false,
@@ -54,7 +56,12 @@ func TestParseExec(t *testing.T) {
5456
Cmd: []string{"command"},
5557
},
5658
&arguments{
57-
[]string{"-t", "-i", "-d", "container", "command"},
59+
options: execOptions{
60+
tty: true,
61+
interactive: true,
62+
detach: true,
63+
},
64+
execCmd: []string{"command"},
5865
}: {
5966
AttachStdin: false,
6067
AttachStdout: false,
@@ -64,21 +71,9 @@ func TestParseExec(t *testing.T) {
6471
Cmd: []string{"command"},
6572
},
6673
}
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-
}
7574

76-
}
7775
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)
76+
execConfig, err := parseExec(&valid.options, valid.container, valid.execCmd)
8277
if err != nil {
8378
t.Fatal(err)
8479
}

0 commit comments

Comments
 (0)