Skip to content

Commit 93e9b78

Browse files
committed
publish RunExec for use by docker/compose
Signed-off-by: Nicolas De Loof <[email protected]>
1 parent cf8c4ba commit 93e9b78

3 files changed

Lines changed: 123 additions & 113 deletions

File tree

cli/command/container/exec.go

Lines changed: 47 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -16,62 +16,65 @@ import (
1616
"github.com/spf13/cobra"
1717
)
1818

19-
type execOptions struct {
20-
detachKeys string
21-
interactive bool
22-
tty bool
23-
detach bool
24-
user string
25-
privileged bool
26-
env opts.ListOpts
27-
workdir string
28-
container string
29-
command []string
30-
envFile opts.ListOpts
19+
// ExecOptions group options for `exec` command
20+
type ExecOptions struct {
21+
DetachKeys string
22+
Interactive bool
23+
TTY bool
24+
Detach bool
25+
User string
26+
Privileged bool
27+
Env opts.ListOpts
28+
Workdir string
29+
Container string
30+
Command []string
31+
EnvFile opts.ListOpts
3132
}
3233

33-
func newExecOptions() execOptions {
34-
return execOptions{
35-
env: opts.NewListOpts(opts.ValidateEnv),
36-
envFile: opts.NewListOpts(nil),
34+
// NewExecOptions initialize an ExecOptions
35+
func NewExecOptions() ExecOptions {
36+
return ExecOptions{
37+
Env: opts.NewListOpts(opts.ValidateEnv),
38+
EnvFile: opts.NewListOpts(nil),
3739
}
3840
}
3941

4042
// NewExecCommand creates a new cobra.Command for `docker exec`
4143
func NewExecCommand(dockerCli command.Cli) *cobra.Command {
42-
options := newExecOptions()
44+
options := NewExecOptions()
4345

4446
cmd := &cobra.Command{
4547
Use: "exec [OPTIONS] CONTAINER COMMAND [ARG...]",
4648
Short: "Run a command in a running container",
4749
Args: cli.RequiresMinArgs(2),
4850
RunE: func(cmd *cobra.Command, args []string) error {
49-
options.container = args[0]
50-
options.command = args[1:]
51-
return runExec(dockerCli, options)
51+
options.Container = args[0]
52+
options.Command = args[1:]
53+
return RunExec(dockerCli, options)
5254
},
5355
}
5456

5557
flags := cmd.Flags()
5658
flags.SetInterspersed(false)
5759

58-
flags.StringVarP(&options.detachKeys, "detach-keys", "", "", "Override the key sequence for detaching a container")
59-
flags.BoolVarP(&options.interactive, "interactive", "i", false, "Keep STDIN open even if not attached")
60-
flags.BoolVarP(&options.tty, "tty", "t", false, "Allocate a pseudo-TTY")
61-
flags.BoolVarP(&options.detach, "detach", "d", false, "Detached mode: run command in the background")
62-
flags.StringVarP(&options.user, "user", "u", "", "Username or UID (format: <name|uid>[:<group|gid>])")
63-
flags.BoolVarP(&options.privileged, "privileged", "", false, "Give extended privileges to the command")
64-
flags.VarP(&options.env, "env", "e", "Set environment variables")
60+
flags.StringVarP(&options.DetachKeys, "detach-keys", "", "", "Override the key sequence for detaching a container")
61+
flags.BoolVarP(&options.Interactive, "interactive", "i", false, "Keep STDIN open even if not attached")
62+
flags.BoolVarP(&options.TTY, "tty", "t", false, "Allocate a pseudo-TTY")
63+
flags.BoolVarP(&options.Detach, "detach", "d", false, "Detached mode: run command in the background")
64+
flags.StringVarP(&options.User, "user", "u", "", "Username or UID (format: <name|uid>[:<group|gid>])")
65+
flags.BoolVarP(&options.Privileged, "privileged", "", false, "Give extended privileges to the command")
66+
flags.VarP(&options.Env, "env", "e", "Set environment variables")
6567
flags.SetAnnotation("env", "version", []string{"1.25"})
66-
flags.Var(&options.envFile, "env-file", "Read in a file of environment variables")
68+
flags.Var(&options.EnvFile, "env-file", "Read in a file of environment variables")
6769
flags.SetAnnotation("env-file", "version", []string{"1.25"})
68-
flags.StringVarP(&options.workdir, "workdir", "w", "", "Working directory inside the container")
70+
flags.StringVarP(&options.Workdir, "workdir", "w", "", "Working directory inside the container")
6971
flags.SetAnnotation("workdir", "version", []string{"1.35"})
7072

7173
return cmd
7274
}
7375

74-
func runExec(dockerCli command.Cli, options execOptions) error {
76+
// RunExec executes an `exec` command
77+
func RunExec(dockerCli command.Cli, options ExecOptions) error {
7578
execConfig, err := parseExec(options, dockerCli.ConfigFile())
7679
if err != nil {
7780
return err
@@ -84,7 +87,7 @@ func runExec(dockerCli command.Cli, options execOptions) error {
8487
// otherwise if we error out we will leak execIDs on the server (and
8588
// there's no easy way to clean those up). But also in order to make "not
8689
// exist" errors take precedence we do a dummy inspect first.
87-
if _, err := client.ContainerInspect(ctx, options.container); err != nil {
90+
if _, err := client.ContainerInspect(ctx, options.Container); err != nil {
8891
return err
8992
}
9093
if !execConfig.Detach {
@@ -93,7 +96,7 @@ func runExec(dockerCli command.Cli, options execOptions) error {
9396
}
9497
}
9598

96-
response, err := client.ContainerExecCreate(ctx, options.container, *execConfig)
99+
response, err := client.ContainerExecCreate(ctx, options.Container, *execConfig)
97100
if err != nil {
98101
return err
99102
}
@@ -195,33 +198,33 @@ func getExecExitStatus(ctx context.Context, client apiclient.ContainerAPIClient,
195198

196199
// parseExec parses the specified args for the specified command and generates
197200
// an ExecConfig from it.
198-
func parseExec(execOpts execOptions, configFile *configfile.ConfigFile) (*types.ExecConfig, error) {
201+
func parseExec(execOpts ExecOptions, configFile *configfile.ConfigFile) (*types.ExecConfig, error) {
199202
execConfig := &types.ExecConfig{
200-
User: execOpts.user,
201-
Privileged: execOpts.privileged,
202-
Tty: execOpts.tty,
203-
Cmd: execOpts.command,
204-
Detach: execOpts.detach,
205-
WorkingDir: execOpts.workdir,
203+
User: execOpts.User,
204+
Privileged: execOpts.Privileged,
205+
Tty: execOpts.TTY,
206+
Cmd: execOpts.Command,
207+
Detach: execOpts.Detach,
208+
WorkingDir: execOpts.Workdir,
206209
}
207210

208211
// collect all the environment variables for the container
209212
var err error
210-
if execConfig.Env, err = opts.ReadKVEnvStrings(execOpts.envFile.GetAll(), execOpts.env.GetAll()); err != nil {
213+
if execConfig.Env, err = opts.ReadKVEnvStrings(execOpts.EnvFile.GetAll(), execOpts.Env.GetAll()); err != nil {
211214
return nil, err
212215
}
213216

214217
// If -d is not set, attach to everything by default
215-
if !execOpts.detach {
218+
if !execOpts.Detach {
216219
execConfig.AttachStdout = true
217220
execConfig.AttachStderr = true
218-
if execOpts.interactive {
221+
if execOpts.Interactive {
219222
execConfig.AttachStdin = true
220223
}
221224
}
222225

223-
if execOpts.detachKeys != "" {
224-
execConfig.DetachKeys = execOpts.detachKeys
226+
if execOpts.DetachKeys != "" {
227+
execConfig.DetachKeys = execOpts.DetachKeys
225228
} else {
226229
execConfig.DetachKeys = configFile.DetachKeys
227230
}

cli/command/container/exec_test.go

Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ import (
1717
"gotest.tools/v3/fs"
1818
)
1919

20-
func withDefaultOpts(options execOptions) execOptions {
21-
options.env = opts.NewListOpts(opts.ValidateEnv)
22-
options.envFile = opts.NewListOpts(nil)
23-
if len(options.command) == 0 {
24-
options.command = []string{"command"}
20+
func withDefaultOpts(options ExecOptions) ExecOptions {
21+
options.Env = opts.NewListOpts(opts.ValidateEnv)
22+
options.EnvFile = opts.NewListOpts(nil)
23+
if len(options.Command) == 0 {
24+
options.Command = []string{"command"}
2525
}
2626
return options
2727
}
@@ -35,7 +35,7 @@ TWO=2
3535
defer tmpFile.Remove()
3636

3737
testcases := []struct {
38-
options execOptions
38+
options ExecOptions
3939
configFile configfile.ConfigFile
4040
expected types.ExecConfig
4141
}{
@@ -45,23 +45,23 @@ TWO=2
4545
AttachStdout: true,
4646
AttachStderr: true,
4747
},
48-
options: withDefaultOpts(execOptions{}),
48+
options: withDefaultOpts(ExecOptions{}),
4949
},
5050
{
5151
expected: types.ExecConfig{
5252
Cmd: []string{"command1", "command2"},
5353
AttachStdout: true,
5454
AttachStderr: true,
5555
},
56-
options: withDefaultOpts(execOptions{
57-
command: []string{"command1", "command2"},
56+
options: withDefaultOpts(ExecOptions{
57+
Command: []string{"command1", "command2"},
5858
}),
5959
},
6060
{
61-
options: withDefaultOpts(execOptions{
62-
interactive: true,
63-
tty: true,
64-
user: "uid",
61+
options: withDefaultOpts(ExecOptions{
62+
Interactive: true,
63+
TTY: true,
64+
User: "uid",
6565
}),
6666
expected: types.ExecConfig{
6767
User: "uid",
@@ -73,17 +73,17 @@ TWO=2
7373
},
7474
},
7575
{
76-
options: withDefaultOpts(execOptions{detach: true}),
76+
options: withDefaultOpts(ExecOptions{Detach: true}),
7777
expected: types.ExecConfig{
7878
Detach: true,
7979
Cmd: []string{"command"},
8080
},
8181
},
8282
{
83-
options: withDefaultOpts(execOptions{
84-
tty: true,
85-
interactive: true,
86-
detach: true,
83+
options: withDefaultOpts(ExecOptions{
84+
TTY: true,
85+
Interactive: true,
86+
Detach: true,
8787
}),
8888
expected: types.ExecConfig{
8989
Detach: true,
@@ -92,7 +92,7 @@ TWO=2
9292
},
9393
},
9494
{
95-
options: withDefaultOpts(execOptions{detach: true}),
95+
options: withDefaultOpts(ExecOptions{Detach: true}),
9696
configFile: configfile.ConfigFile{DetachKeys: "de"},
9797
expected: types.ExecConfig{
9898
Cmd: []string{"command"},
@@ -101,9 +101,9 @@ TWO=2
101101
},
102102
},
103103
{
104-
options: withDefaultOpts(execOptions{
105-
detach: true,
106-
detachKeys: "ab",
104+
options: withDefaultOpts(ExecOptions{
105+
Detach: true,
106+
DetachKeys: "ab",
107107
}),
108108
configFile: configfile.ConfigFile{DetachKeys: "de"},
109109
expected: types.ExecConfig{
@@ -119,9 +119,9 @@ TWO=2
119119
AttachStderr: true,
120120
Env: []string{"ONE=1", "TWO=2"},
121121
},
122-
options: func() execOptions {
123-
o := withDefaultOpts(execOptions{})
124-
o.envFile.Set(tmpFile.Path())
122+
options: func() ExecOptions {
123+
o := withDefaultOpts(ExecOptions{})
124+
o.EnvFile.Set(tmpFile.Path())
125125
return o
126126
}(),
127127
},
@@ -132,10 +132,10 @@ TWO=2
132132
AttachStderr: true,
133133
Env: []string{"ONE=1", "TWO=2", "ONE=override"},
134134
},
135-
options: func() execOptions {
136-
o := withDefaultOpts(execOptions{})
137-
o.envFile.Set(tmpFile.Path())
138-
o.env.Set("ONE=override")
135+
options: func() ExecOptions {
136+
o := withDefaultOpts(ExecOptions{})
137+
o.EnvFile.Set(tmpFile.Path())
138+
o.Env.Set("ONE=override")
139139
return o
140140
}(),
141141
},
@@ -149,8 +149,8 @@ TWO=2
149149
}
150150

151151
func TestParseExecNoSuchFile(t *testing.T) {
152-
execOpts := withDefaultOpts(execOptions{})
153-
execOpts.envFile.Set("no-such-env-file")
152+
execOpts := withDefaultOpts(ExecOptions{})
153+
execOpts.EnvFile.Set("no-such-env-file")
154154
execConfig, err := parseExec(execOpts, &configfile.ConfigFile{})
155155
assert.ErrorContains(t, err, "no-such-env-file")
156156
assert.Check(t, os.IsNotExist(err))
@@ -160,23 +160,23 @@ func TestParseExecNoSuchFile(t *testing.T) {
160160
func TestRunExec(t *testing.T) {
161161
var testcases = []struct {
162162
doc string
163-
options execOptions
163+
options ExecOptions
164164
client fakeClient
165165
expectedError string
166166
expectedOut string
167167
expectedErr string
168168
}{
169169
{
170170
doc: "successful detach",
171-
options: withDefaultOpts(execOptions{
172-
container: "thecontainer",
173-
detach: true,
171+
options: withDefaultOpts(ExecOptions{
172+
Container: "thecontainer",
173+
Detach: true,
174174
}),
175175
client: fakeClient{execCreateFunc: execCreateWithID},
176176
},
177177
{
178178
doc: "inspect error",
179-
options: newExecOptions(),
179+
options: NewExecOptions(),
180180
client: fakeClient{
181181
inspectFunc: func(string) (types.ContainerJSON, error) {
182182
return types.ContainerJSON{}, errors.New("failed inspect")
@@ -186,7 +186,7 @@ func TestRunExec(t *testing.T) {
186186
},
187187
{
188188
doc: "missing exec ID",
189-
options: newExecOptions(),
189+
options: NewExecOptions(),
190190
expectedError: "exec ID empty",
191191
},
192192
}
@@ -195,7 +195,7 @@ func TestRunExec(t *testing.T) {
195195
t.Run(testcase.doc, func(t *testing.T) {
196196
cli := test.NewFakeCli(&testcase.client)
197197

198-
err := runExec(cli, testcase.options)
198+
err := RunExec(cli, testcase.options)
199199
if testcase.expectedError != "" {
200200
assert.ErrorContains(t, err, testcase.expectedError)
201201
} else {

0 commit comments

Comments
 (0)