diff --git a/cli/command/container/exec.go b/cli/command/container/exec.go index c96f405594..b62b92569d 100644 --- a/cli/command/container/exec.go +++ b/cli/command/container/exec.go @@ -27,10 +27,14 @@ type execOptions struct { workdir string container string command []string + envFile opts.ListOpts } func newExecOptions() execOptions { - return execOptions{env: opts.NewListOpts(opts.ValidateEnv)} + return execOptions{ + env: opts.NewListOpts(opts.ValidateEnv), + envFile: opts.NewListOpts(nil), + } } // NewExecCommand creates a new cobra.Command for `docker exec` @@ -59,6 +63,8 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command { flags.BoolVarP(&options.privileged, "privileged", "", false, "Give extended privileges to the command") flags.VarP(&options.env, "env", "e", "Set environment variables") flags.SetAnnotation("env", "version", []string{"1.25"}) + flags.Var(&options.envFile, "env-file", "Read in a file of environment variables") + flags.SetAnnotation("env-file", "version", []string{"1.25"}) flags.StringVarP(&options.workdir, "workdir", "w", "", "Working directory inside the container") flags.SetAnnotation("workdir", "version", []string{"1.35"}) @@ -66,7 +72,11 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command { } func runExec(dockerCli command.Cli, options execOptions) error { - execConfig := parseExec(options, dockerCli.ConfigFile()) + execConfig, err := parseExec(options, dockerCli.ConfigFile()) + if err != nil { + return err + } + ctx := context.Background() client := dockerCli.Client() @@ -185,30 +195,35 @@ func getExecExitStatus(ctx context.Context, client apiclient.ContainerAPIClient, // parseExec parses the specified args for the specified command and generates // an ExecConfig from it. -func parseExec(opts execOptions, configFile *configfile.ConfigFile) *types.ExecConfig { +func parseExec(execOpts execOptions, configFile *configfile.ConfigFile) (*types.ExecConfig, error) { execConfig := &types.ExecConfig{ - User: opts.user, - Privileged: opts.privileged, - Tty: opts.tty, - Cmd: opts.command, - Detach: opts.detach, - Env: opts.env.GetAll(), - WorkingDir: opts.workdir, + User: execOpts.user, + Privileged: execOpts.privileged, + Tty: execOpts.tty, + Cmd: execOpts.command, + Detach: execOpts.detach, + WorkingDir: execOpts.workdir, + } + + // collect all the environment variables for the container + var err error + if execConfig.Env, err = opts.ReadKVEnvStrings(execOpts.envFile.GetAll(), execOpts.env.GetAll()); err != nil { + return nil, err } // If -d is not set, attach to everything by default - if !opts.detach { + if !execOpts.detach { execConfig.AttachStdout = true execConfig.AttachStderr = true - if opts.interactive { + if execOpts.interactive { execConfig.AttachStdin = true } } - if opts.detachKeys != "" { - execConfig.DetachKeys = opts.detachKeys + if execOpts.detachKeys != "" { + execConfig.DetachKeys = execOpts.detachKeys } else { execConfig.DetachKeys = configFile.DetachKeys } - return execConfig + return execConfig, nil } diff --git a/cli/command/container/exec_test.go b/cli/command/container/exec_test.go index ce10b334e9..0a83a5f159 100644 --- a/cli/command/container/exec_test.go +++ b/cli/command/container/exec_test.go @@ -3,6 +3,7 @@ package container import ( "context" "io/ioutil" + "os" "testing" "github.com/docker/cli/cli" @@ -13,10 +14,12 @@ import ( "github.com/pkg/errors" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" + "gotest.tools/v3/fs" ) func withDefaultOpts(options execOptions) execOptions { options.env = opts.NewListOpts(opts.ValidateEnv) + options.envFile = opts.NewListOpts(nil) if len(options.command) == 0 { options.command = []string{"command"} } @@ -24,6 +27,13 @@ func withDefaultOpts(options execOptions) execOptions { } func TestParseExec(t *testing.T) { + content := `ONE=1 +TWO=2 + ` + + tmpFile := fs.NewFile(t, t.Name(), fs.WithContent(content)) + defer tmpFile.Remove() + testcases := []struct { options execOptions configFile configfile.ConfigFile @@ -102,14 +112,51 @@ func TestParseExec(t *testing.T) { Detach: true, }, }, + { + expected: types.ExecConfig{ + Cmd: []string{"command"}, + AttachStdout: true, + AttachStderr: true, + Env: []string{"ONE=1", "TWO=2"}, + }, + options: func() execOptions { + o := withDefaultOpts(execOptions{}) + o.envFile.Set(tmpFile.Path()) + return o + }(), + }, + { + expected: types.ExecConfig{ + Cmd: []string{"command"}, + AttachStdout: true, + AttachStderr: true, + Env: []string{"ONE=1", "TWO=2", "ONE=override"}, + }, + options: func() execOptions { + o := withDefaultOpts(execOptions{}) + o.envFile.Set(tmpFile.Path()) + o.env.Set("ONE=override") + return o + }(), + }, } for _, testcase := range testcases { - execConfig := parseExec(testcase.options, &testcase.configFile) + execConfig, err := parseExec(testcase.options, &testcase.configFile) + assert.NilError(t, err) assert.Check(t, is.DeepEqual(testcase.expected, *execConfig)) } } +func TestParseExecNoSuchFile(t *testing.T) { + execOpts := withDefaultOpts(execOptions{}) + execOpts.envFile.Set("no-such-env-file") + execConfig, err := parseExec(execOpts, &configfile.ConfigFile{}) + assert.ErrorContains(t, err, "no-such-env-file") + assert.Check(t, os.IsNotExist(err)) + assert.Check(t, execConfig == nil) +} + func TestRunExec(t *testing.T) { var testcases = []struct { doc string diff --git a/contrib/completion/bash/docker b/contrib/completion/bash/docker index 6a52983197..f86263ec78 100644 --- a/contrib/completion/bash/docker +++ b/contrib/completion/bash/docker @@ -1604,6 +1604,10 @@ _docker_container_exec() { __docker_nospace return ;; + --env-file) + _filedir + return + ;; --user|-u) __docker_complete_user_group return @@ -1615,7 +1619,7 @@ _docker_container_exec() { case "$cur" in -*) - COMPREPLY=( $( compgen -W "--detach -d --detach-keys --env -e --help --interactive -i --privileged -t --tty -u --user --workdir -w" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--detach -d --detach-keys --env -e --env-file --help --interactive -i --privileged -t --tty -u --user --workdir -w" -- "$cur" ) ) ;; *) __docker_complete_containers_running diff --git a/contrib/completion/zsh/_docker b/contrib/completion/zsh/_docker index 835c8d4592..c11342cee2 100644 --- a/contrib/completion/zsh/_docker +++ b/contrib/completion/zsh/_docker @@ -750,6 +750,7 @@ __docker_container_subcommand() { $opts_attach_exec_run_start \ "($help -d --detach)"{-d,--detach}"[Detached mode: leave the container running in the background]" \ "($help)*"{-e=,--env=}"[Set environment variables]:environment variable: " \ + "($help)*--env-file=[Read environment variables from a file]:environment file:_files" \ "($help -i --interactive)"{-i,--interactive}"[Keep stdin open even if not attached]" \ "($help)--privileged[Give extended Linux capabilities to the command]" \ "($help -t --tty)"{-t,--tty}"[Allocate a pseudo-tty]" \ diff --git a/docs/reference/commandline/exec.md b/docs/reference/commandline/exec.md index 5414b909ee..adfc6f7ea6 100644 --- a/docs/reference/commandline/exec.md +++ b/docs/reference/commandline/exec.md @@ -15,6 +15,7 @@ Options: -d, --detach Detached mode: run command in the background --detach-keys Override the key sequence for detaching a container -e, --env=[] Set environment variables + --env-file Read in a file of environment variables --help Print usage -i, --interactive Keep STDIN open even if not attached --privileged Give extended privileges to the command