From 2d268392d1354a3cca68dac91a0cf93638a55d0d Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Tue, 22 Feb 2022 10:36:43 +0100 Subject: [PATCH] publish RunExec for use by docker/compose Signed-off-by: Nicolas De Loof --- cli/command/container/exec.go | 91 +++++++++++++++--------------- cli/command/container/exec_test.go | 76 ++++++++++++------------- cli/command/container/start.go | 67 ++++++++++++---------- 3 files changed, 122 insertions(+), 112 deletions(-) diff --git a/cli/command/container/exec.go b/cli/command/container/exec.go index b62b92569d..3e78d8e506 100644 --- a/cli/command/container/exec.go +++ b/cli/command/container/exec.go @@ -16,62 +16,65 @@ import ( "github.com/spf13/cobra" ) -type execOptions struct { - detachKeys string - interactive bool - tty bool - detach bool - user string - privileged bool - env opts.ListOpts - workdir string - container string - command []string - envFile opts.ListOpts +// ExecOptions group options for `exec` command +type ExecOptions struct { + DetachKeys string + Interactive bool + TTY bool + Detach bool + User string + Privileged bool + Env opts.ListOpts + Workdir string + Container string + Command []string + EnvFile opts.ListOpts } -func newExecOptions() execOptions { - return execOptions{ - env: opts.NewListOpts(opts.ValidateEnv), - envFile: opts.NewListOpts(nil), +// NewExecOptions creates a new ExecOptions +func NewExecOptions() ExecOptions { + return ExecOptions{ + Env: opts.NewListOpts(opts.ValidateEnv), + EnvFile: opts.NewListOpts(nil), } } // NewExecCommand creates a new cobra.Command for `docker exec` func NewExecCommand(dockerCli command.Cli) *cobra.Command { - options := newExecOptions() + options := NewExecOptions() cmd := &cobra.Command{ Use: "exec [OPTIONS] CONTAINER COMMAND [ARG...]", Short: "Run a command in a running container", Args: cli.RequiresMinArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - options.container = args[0] - options.command = args[1:] - return runExec(dockerCli, options) + options.Container = args[0] + options.Command = args[1:] + return RunExec(dockerCli, options) }, } flags := cmd.Flags() flags.SetInterspersed(false) - flags.StringVarP(&options.detachKeys, "detach-keys", "", "", "Override the key sequence for detaching a container") - flags.BoolVarP(&options.interactive, "interactive", "i", false, "Keep STDIN open even if not attached") - flags.BoolVarP(&options.tty, "tty", "t", false, "Allocate a pseudo-TTY") - flags.BoolVarP(&options.detach, "detach", "d", false, "Detached mode: run command in the background") - flags.StringVarP(&options.user, "user", "u", "", "Username or UID (format: [:])") - flags.BoolVarP(&options.privileged, "privileged", "", false, "Give extended privileges to the command") - flags.VarP(&options.env, "env", "e", "Set environment variables") + flags.StringVarP(&options.DetachKeys, "detach-keys", "", "", "Override the key sequence for detaching a container") + flags.BoolVarP(&options.Interactive, "interactive", "i", false, "Keep STDIN open even if not attached") + flags.BoolVarP(&options.TTY, "tty", "t", false, "Allocate a pseudo-TTY") + flags.BoolVarP(&options.Detach, "detach", "d", false, "Detached mode: run command in the background") + flags.StringVarP(&options.User, "user", "u", "", "Username or UID (format: [:])") + 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.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.StringVarP(&options.Workdir, "workdir", "w", "", "Working directory inside the container") flags.SetAnnotation("workdir", "version", []string{"1.35"}) return cmd } -func runExec(dockerCli command.Cli, options execOptions) error { +// RunExec executes an `exec` command +func RunExec(dockerCli command.Cli, options ExecOptions) error { execConfig, err := parseExec(options, dockerCli.ConfigFile()) if err != nil { return err @@ -84,7 +87,7 @@ func runExec(dockerCli command.Cli, options execOptions) error { // otherwise if we error out we will leak execIDs on the server (and // there's no easy way to clean those up). But also in order to make "not // exist" errors take precedence we do a dummy inspect first. - if _, err := client.ContainerInspect(ctx, options.container); err != nil { + if _, err := client.ContainerInspect(ctx, options.Container); err != nil { return err } if !execConfig.Detach { @@ -93,7 +96,7 @@ func runExec(dockerCli command.Cli, options execOptions) error { } } - response, err := client.ContainerExecCreate(ctx, options.container, *execConfig) + response, err := client.ContainerExecCreate(ctx, options.Container, *execConfig) if err != nil { return err } @@ -195,33 +198,33 @@ 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(execOpts execOptions, configFile *configfile.ConfigFile) (*types.ExecConfig, error) { +func parseExec(execOpts ExecOptions, configFile *configfile.ConfigFile) (*types.ExecConfig, error) { execConfig := &types.ExecConfig{ - User: execOpts.user, - Privileged: execOpts.privileged, - Tty: execOpts.tty, - Cmd: execOpts.command, - Detach: execOpts.detach, - WorkingDir: execOpts.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 { + 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 !execOpts.detach { + if !execOpts.Detach { execConfig.AttachStdout = true execConfig.AttachStderr = true - if execOpts.interactive { + if execOpts.Interactive { execConfig.AttachStdin = true } } - if execOpts.detachKeys != "" { - execConfig.DetachKeys = execOpts.detachKeys + if execOpts.DetachKeys != "" { + execConfig.DetachKeys = execOpts.DetachKeys } else { execConfig.DetachKeys = configFile.DetachKeys } diff --git a/cli/command/container/exec_test.go b/cli/command/container/exec_test.go index 37caa09906..286841c325 100644 --- a/cli/command/container/exec_test.go +++ b/cli/command/container/exec_test.go @@ -17,11 +17,11 @@ import ( "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"} +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"} } return options } @@ -35,7 +35,7 @@ TWO=2 defer tmpFile.Remove() testcases := []struct { - options execOptions + options ExecOptions configFile configfile.ConfigFile expected types.ExecConfig }{ @@ -45,7 +45,7 @@ TWO=2 AttachStdout: true, AttachStderr: true, }, - options: withDefaultOpts(execOptions{}), + options: withDefaultOpts(ExecOptions{}), }, { expected: types.ExecConfig{ @@ -53,15 +53,15 @@ TWO=2 AttachStdout: true, AttachStderr: true, }, - options: withDefaultOpts(execOptions{ - command: []string{"command1", "command2"}, + options: withDefaultOpts(ExecOptions{ + Command: []string{"command1", "command2"}, }), }, { - options: withDefaultOpts(execOptions{ - interactive: true, - tty: true, - user: "uid", + options: withDefaultOpts(ExecOptions{ + Interactive: true, + TTY: true, + User: "uid", }), expected: types.ExecConfig{ User: "uid", @@ -73,17 +73,17 @@ TWO=2 }, }, { - options: withDefaultOpts(execOptions{detach: true}), + options: withDefaultOpts(ExecOptions{Detach: true}), expected: types.ExecConfig{ Detach: true, Cmd: []string{"command"}, }, }, { - options: withDefaultOpts(execOptions{ - tty: true, - interactive: true, - detach: true, + options: withDefaultOpts(ExecOptions{ + TTY: true, + Interactive: true, + Detach: true, }), expected: types.ExecConfig{ Detach: true, @@ -92,7 +92,7 @@ TWO=2 }, }, { - options: withDefaultOpts(execOptions{detach: true}), + options: withDefaultOpts(ExecOptions{Detach: true}), configFile: configfile.ConfigFile{DetachKeys: "de"}, expected: types.ExecConfig{ Cmd: []string{"command"}, @@ -101,9 +101,9 @@ TWO=2 }, }, { - options: withDefaultOpts(execOptions{ - detach: true, - detachKeys: "ab", + options: withDefaultOpts(ExecOptions{ + Detach: true, + DetachKeys: "ab", }), configFile: configfile.ConfigFile{DetachKeys: "de"}, expected: types.ExecConfig{ @@ -119,9 +119,9 @@ TWO=2 AttachStderr: true, Env: []string{"ONE=1", "TWO=2"}, }, - options: func() execOptions { - o := withDefaultOpts(execOptions{}) - o.envFile.Set(tmpFile.Path()) + options: func() ExecOptions { + o := withDefaultOpts(ExecOptions{}) + o.EnvFile.Set(tmpFile.Path()) return o }(), }, @@ -132,10 +132,10 @@ TWO=2 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") + options: func() ExecOptions { + o := withDefaultOpts(ExecOptions{}) + o.EnvFile.Set(tmpFile.Path()) + o.Env.Set("ONE=override") return o }(), }, @@ -149,8 +149,8 @@ TWO=2 } func TestParseExecNoSuchFile(t *testing.T) { - execOpts := withDefaultOpts(execOptions{}) - execOpts.envFile.Set("no-such-env-file") + 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)) @@ -160,7 +160,7 @@ func TestParseExecNoSuchFile(t *testing.T) { func TestRunExec(t *testing.T) { var testcases = []struct { doc string - options execOptions + options ExecOptions client fakeClient expectedError string expectedOut string @@ -168,15 +168,15 @@ func TestRunExec(t *testing.T) { }{ { doc: "successful detach", - options: withDefaultOpts(execOptions{ - container: "thecontainer", - detach: true, + options: withDefaultOpts(ExecOptions{ + Container: "thecontainer", + Detach: true, }), client: fakeClient{execCreateFunc: execCreateWithID}, }, { doc: "inspect error", - options: newExecOptions(), + options: NewExecOptions(), client: fakeClient{ inspectFunc: func(string) (types.ContainerJSON, error) { return types.ContainerJSON{}, errors.New("failed inspect") @@ -186,7 +186,7 @@ func TestRunExec(t *testing.T) { }, { doc: "missing exec ID", - options: newExecOptions(), + options: NewExecOptions(), expectedError: "exec ID empty", }, } @@ -195,7 +195,7 @@ func TestRunExec(t *testing.T) { t.Run(testcase.doc, func(t *testing.T) { cli := test.NewFakeCli(&testcase.client) - err := runExec(cli, testcase.options) + err := RunExec(cli, testcase.options) if testcase.expectedError != "" { assert.ErrorContains(t, err, testcase.expectedError) } else { diff --git a/cli/command/container/start.go b/cli/command/container/start.go index 9a8d72fb3f..b86ce53ad9 100644 --- a/cli/command/container/start.go +++ b/cli/command/container/start.go @@ -15,58 +15,65 @@ import ( "github.com/spf13/cobra" ) -type startOptions struct { - attach bool - openStdin bool - detachKeys string - checkpoint string - checkpointDir string +// StartOptions group options for `start` command +type StartOptions struct { + Attach bool + OpenStdin bool + DetachKeys string + Checkpoint string + CheckpointDir string - containers []string + Containers []string +} + +// NewStartOptions creates a new StartOptions +func NewStartOptions() StartOptions { + return StartOptions{} } // NewStartCommand creates a new cobra.Command for `docker start` func NewStartCommand(dockerCli command.Cli) *cobra.Command { - var opts startOptions + var opts StartOptions cmd := &cobra.Command{ Use: "start [OPTIONS] CONTAINER [CONTAINER...]", Short: "Start one or more stopped containers", Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts.containers = args - return runStart(dockerCli, &opts) + opts.Containers = args + return RunStart(dockerCli, &opts) }, } flags := cmd.Flags() - flags.BoolVarP(&opts.attach, "attach", "a", false, "Attach STDOUT/STDERR and forward signals") - flags.BoolVarP(&opts.openStdin, "interactive", "i", false, "Attach container's STDIN") - flags.StringVar(&opts.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container") + flags.BoolVarP(&opts.Attach, "attach", "a", false, "Attach STDOUT/STDERR and forward signals") + flags.BoolVarP(&opts.OpenStdin, "interactive", "i", false, "Attach container's STDIN") + flags.StringVar(&opts.DetachKeys, "detach-keys", "", "Override the key sequence for detaching a container") - flags.StringVar(&opts.checkpoint, "checkpoint", "", "Restore from this checkpoint") + flags.StringVar(&opts.Checkpoint, "checkpoint", "", "Restore from this checkpoint") flags.SetAnnotation("checkpoint", "experimental", nil) flags.SetAnnotation("checkpoint", "ostype", []string{"linux"}) - flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory") + flags.StringVar(&opts.CheckpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory") flags.SetAnnotation("checkpoint-dir", "experimental", nil) flags.SetAnnotation("checkpoint-dir", "ostype", []string{"linux"}) return cmd } +// RunStart executes a `start` command // nolint: gocyclo -func runStart(dockerCli command.Cli, opts *startOptions) error { +func RunStart(dockerCli command.Cli, opts *StartOptions) error { ctx, cancelFun := context.WithCancel(context.Background()) defer cancelFun() - if opts.attach || opts.openStdin { + if opts.Attach || opts.OpenStdin { // We're going to attach to a container. // 1. Ensure we only have one container. - if len(opts.containers) > 1 { + if len(opts.Containers) > 1 { return errors.New("you cannot start and attach multiple containers at once") } // 2. Attach to the container. - container := opts.containers[0] + container := opts.Containers[0] c, err := dockerCli.Client().ContainerInspect(ctx, container) if err != nil { return err @@ -79,13 +86,13 @@ func runStart(dockerCli command.Cli, opts *startOptions) error { defer signal.StopCatch(sigc) } - if opts.detachKeys != "" { - dockerCli.ConfigFile().DetachKeys = opts.detachKeys + if opts.DetachKeys != "" { + dockerCli.ConfigFile().DetachKeys = opts.DetachKeys } options := types.ContainerAttachOptions{ Stream: true, - Stdin: opts.openStdin && c.Config.OpenStdin, + Stdin: opts.OpenStdin && c.Config.OpenStdin, Stdout: true, Stderr: true, DetachKeys: dockerCli.ConfigFile().DetachKeys, @@ -129,8 +136,8 @@ func runStart(dockerCli command.Cli, opts *startOptions) error { // no matter it's detached, removed on daemon side(--rm) or exit normally. statusChan := waitExitOrRemoved(ctx, dockerCli, c.ID, c.HostConfig.AutoRemove) startOptions := types.ContainerStartOptions{ - CheckpointID: opts.checkpoint, - CheckpointDir: opts.checkpointDir, + CheckpointID: opts.Checkpoint, + CheckpointDir: opts.CheckpointDir, } // 4. Start the container. @@ -161,21 +168,21 @@ func runStart(dockerCli command.Cli, opts *startOptions) error { if status := <-statusChan; status != 0 { return cli.StatusError{StatusCode: status} } - } else if opts.checkpoint != "" { - if len(opts.containers) > 1 { + } else if opts.Checkpoint != "" { + if len(opts.Containers) > 1 { return errors.New("you cannot restore multiple containers at once") } - container := opts.containers[0] + container := opts.Containers[0] startOptions := types.ContainerStartOptions{ - CheckpointID: opts.checkpoint, - CheckpointDir: opts.checkpointDir, + CheckpointID: opts.Checkpoint, + CheckpointDir: opts.CheckpointDir, } return dockerCli.Client().ContainerStart(ctx, container, startOptions) } else { // We're not going to attach to anything. // Start as many containers as we want. - return startContainersWithoutAttachments(ctx, dockerCli, opts.containers) + return startContainersWithoutAttachments(ctx, dockerCli, opts.Containers) } return nil