package container import ( "fmt" "io" "github.com/Sirupsen/logrus" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/opts" "github.com/docker/docker/api/types" apiclient "github.com/docker/docker/client" "github.com/docker/docker/pkg/promise" "github.com/spf13/cobra" "golang.org/x/net/context" ) type execOptions struct { detachKeys string interactive bool tty bool detach bool user string privileged bool env *opts.ListOpts } func newExecOptions() *execOptions { var values []string return &execOptions{ env: opts.NewListOptsRef(&values, opts.ValidateEnv), } } // NewExecCommand creates a new cobra.Command for `docker exec` func NewExecCommand(dockerCli *command.DockerCli) *cobra.Command { 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 { container := args[0] execCmd := args[1:] return runExec(dockerCli, options, container, execCmd) }, } 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.SetAnnotation("env", "version", []string{"1.25"}) return cmd } // nolint: gocyclo func runExec(dockerCli *command.DockerCli, options *execOptions, container string, execCmd []string) error { execConfig, err := parseExec(options, execCmd) // just in case the ParseExec does not exit if container == "" || err != nil { return cli.StatusError{StatusCode: 1} } if options.detachKeys != "" { dockerCli.ConfigFile().DetachKeys = options.detachKeys } // Send client escape keys execConfig.DetachKeys = dockerCli.ConfigFile().DetachKeys ctx := context.Background() client := dockerCli.Client() // We need to check the tty _before_ we do the ContainerExecCreate, because // 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, container); err != nil { return err } if !execConfig.Detach { if err := dockerCli.In().CheckTty(execConfig.AttachStdin, execConfig.Tty); err != nil { return err } } response, err := client.ContainerExecCreate(ctx, container, *execConfig) if err != nil { return err } execID := response.ID if execID == "" { fmt.Fprintln(dockerCli.Out(), "exec ID empty") return nil } // Temp struct for execStart so that we don't need to transfer all the execConfig. if execConfig.Detach { execStartCheck := types.ExecStartCheck{ Detach: execConfig.Detach, Tty: execConfig.Tty, } if err := client.ContainerExecStart(ctx, execID, execStartCheck); err != nil { return err } // For now don't print this - wait for when we support exec wait() // fmt.Fprintf(dockerCli.Out(), "%s\n", execID) return nil } // Interactive exec requested. var ( out, stderr io.Writer in io.ReadCloser errCh chan error ) if execConfig.AttachStdin { in = dockerCli.In() } if execConfig.AttachStdout { out = dockerCli.Out() } if execConfig.AttachStderr { if execConfig.Tty { stderr = dockerCli.Out() } else { stderr = dockerCli.Err() } } resp, err := client.ContainerExecAttach(ctx, execID, *execConfig) if err != nil { return err } defer resp.Close() errCh = promise.Go(func() error { return holdHijackedConnection(ctx, dockerCli, execConfig.Tty, in, out, stderr, resp) }) if execConfig.Tty && dockerCli.In().IsTerminal() { if err := MonitorTtySize(ctx, dockerCli, execID, true); err != nil { fmt.Fprintln(dockerCli.Err(), "Error monitoring TTY size:", err) } } if err := <-errCh; err != nil { logrus.Debugf("Error hijack: %s", err) return err } var status int if _, status, err = getExecExitCode(ctx, client, execID); err != nil { return err } if status != 0 { return cli.StatusError{StatusCode: status} } return nil } // getExecExitCode perform an inspect on the exec command. It returns // the running state and the exit code. func getExecExitCode(ctx context.Context, client apiclient.ContainerAPIClient, execID string) (bool, int, error) { resp, err := client.ContainerExecInspect(ctx, execID) if err != nil { // If we can't connect, then the daemon probably died. if !apiclient.IsErrConnectionFailed(err) { return false, -1, err } return false, -1, nil } return resp.Running, resp.ExitCode, nil } // parseExec parses the specified args for the specified command and generates // an ExecConfig from it. func parseExec(opts *execOptions, execCmd []string) (*types.ExecConfig, error) { execConfig := &types.ExecConfig{ User: opts.user, Privileged: opts.privileged, Tty: opts.tty, Cmd: execCmd, Detach: opts.detach, } // If -d is not set, attach to everything by default if !opts.detach { execConfig.AttachStdout = true execConfig.AttachStderr = true if opts.interactive { execConfig.AttachStdin = true } } if opts.env != nil { execConfig.Env = opts.env.GetAll() } return execConfig, nil }