github.com/justincormack/cli@v0.0.0-20201215022714-831ebeae9675/cli/command/container/exec.go (about)

     1  package container
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  
     8  	"github.com/docker/cli/cli"
     9  	"github.com/docker/cli/cli/command"
    10  	"github.com/docker/cli/cli/config/configfile"
    11  	"github.com/docker/cli/opts"
    12  	"github.com/docker/docker/api/types"
    13  	apiclient "github.com/docker/docker/client"
    14  	"github.com/pkg/errors"
    15  	"github.com/sirupsen/logrus"
    16  	"github.com/spf13/cobra"
    17  )
    18  
    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
    31  }
    32  
    33  func newExecOptions() execOptions {
    34  	return execOptions{
    35  		env:     opts.NewListOpts(opts.ValidateEnv),
    36  		envFile: opts.NewListOpts(nil),
    37  	}
    38  }
    39  
    40  // NewExecCommand creates a new cobra.Command for `docker exec`
    41  func NewExecCommand(dockerCli command.Cli) *cobra.Command {
    42  	options := newExecOptions()
    43  
    44  	cmd := &cobra.Command{
    45  		Use:   "exec [OPTIONS] CONTAINER COMMAND [ARG...]",
    46  		Short: "Run a command in a running container",
    47  		Args:  cli.RequiresMinArgs(2),
    48  		RunE: func(cmd *cobra.Command, args []string) error {
    49  			options.container = args[0]
    50  			options.command = args[1:]
    51  			return runExec(dockerCli, options)
    52  		},
    53  	}
    54  
    55  	flags := cmd.Flags()
    56  	flags.SetInterspersed(false)
    57  
    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")
    65  	flags.SetAnnotation("env", "version", []string{"1.25"})
    66  	flags.Var(&options.envFile, "env-file", "Read in a file of environment variables")
    67  	flags.SetAnnotation("env-file", "version", []string{"1.25"})
    68  	flags.StringVarP(&options.workdir, "workdir", "w", "", "Working directory inside the container")
    69  	flags.SetAnnotation("workdir", "version", []string{"1.35"})
    70  
    71  	return cmd
    72  }
    73  
    74  func runExec(dockerCli command.Cli, options execOptions) error {
    75  	execConfig, err := parseExec(options, dockerCli.ConfigFile())
    76  	if err != nil {
    77  		return err
    78  	}
    79  
    80  	ctx := context.Background()
    81  	client := dockerCli.Client()
    82  
    83  	// We need to check the tty _before_ we do the ContainerExecCreate, because
    84  	// otherwise if we error out we will leak execIDs on the server (and
    85  	// there's no easy way to clean those up). But also in order to make "not
    86  	// exist" errors take precedence we do a dummy inspect first.
    87  	if _, err := client.ContainerInspect(ctx, options.container); err != nil {
    88  		return err
    89  	}
    90  	if !execConfig.Detach {
    91  		if err := dockerCli.In().CheckTty(execConfig.AttachStdin, execConfig.Tty); err != nil {
    92  			return err
    93  		}
    94  	}
    95  
    96  	response, err := client.ContainerExecCreate(ctx, options.container, *execConfig)
    97  	if err != nil {
    98  		return err
    99  	}
   100  
   101  	execID := response.ID
   102  	if execID == "" {
   103  		return errors.New("exec ID empty")
   104  	}
   105  
   106  	if execConfig.Detach {
   107  		execStartCheck := types.ExecStartCheck{
   108  			Detach: execConfig.Detach,
   109  			Tty:    execConfig.Tty,
   110  		}
   111  		return client.ContainerExecStart(ctx, execID, execStartCheck)
   112  	}
   113  	return interactiveExec(ctx, dockerCli, execConfig, execID)
   114  }
   115  
   116  func interactiveExec(ctx context.Context, dockerCli command.Cli, execConfig *types.ExecConfig, execID string) error {
   117  	// Interactive exec requested.
   118  	var (
   119  		out, stderr io.Writer
   120  		in          io.ReadCloser
   121  	)
   122  
   123  	if execConfig.AttachStdin {
   124  		in = dockerCli.In()
   125  	}
   126  	if execConfig.AttachStdout {
   127  		out = dockerCli.Out()
   128  	}
   129  	if execConfig.AttachStderr {
   130  		if execConfig.Tty {
   131  			stderr = dockerCli.Out()
   132  		} else {
   133  			stderr = dockerCli.Err()
   134  		}
   135  	}
   136  
   137  	client := dockerCli.Client()
   138  	execStartCheck := types.ExecStartCheck{
   139  		Tty: execConfig.Tty,
   140  	}
   141  	resp, err := client.ContainerExecAttach(ctx, execID, execStartCheck)
   142  	if err != nil {
   143  		return err
   144  	}
   145  	defer resp.Close()
   146  
   147  	errCh := make(chan error, 1)
   148  
   149  	go func() {
   150  		defer close(errCh)
   151  		errCh <- func() error {
   152  			streamer := hijackedIOStreamer{
   153  				streams:      dockerCli,
   154  				inputStream:  in,
   155  				outputStream: out,
   156  				errorStream:  stderr,
   157  				resp:         resp,
   158  				tty:          execConfig.Tty,
   159  				detachKeys:   execConfig.DetachKeys,
   160  			}
   161  
   162  			return streamer.stream(ctx)
   163  		}()
   164  	}()
   165  
   166  	if execConfig.Tty && dockerCli.In().IsTerminal() {
   167  		if err := MonitorTtySize(ctx, dockerCli, execID, true); err != nil {
   168  			fmt.Fprintln(dockerCli.Err(), "Error monitoring TTY size:", err)
   169  		}
   170  	}
   171  
   172  	if err := <-errCh; err != nil {
   173  		logrus.Debugf("Error hijack: %s", err)
   174  		return err
   175  	}
   176  
   177  	return getExecExitStatus(ctx, client, execID)
   178  }
   179  
   180  func getExecExitStatus(ctx context.Context, client apiclient.ContainerAPIClient, execID string) error {
   181  	resp, err := client.ContainerExecInspect(ctx, execID)
   182  	if err != nil {
   183  		// If we can't connect, then the daemon probably died.
   184  		if !apiclient.IsErrConnectionFailed(err) {
   185  			return err
   186  		}
   187  		return cli.StatusError{StatusCode: -1}
   188  	}
   189  	status := resp.ExitCode
   190  	if status != 0 {
   191  		return cli.StatusError{StatusCode: status}
   192  	}
   193  	return nil
   194  }
   195  
   196  // parseExec parses the specified args for the specified command and generates
   197  // an ExecConfig from it.
   198  func parseExec(execOpts execOptions, configFile *configfile.ConfigFile) (*types.ExecConfig, error) {
   199  	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,
   206  	}
   207  
   208  	// collect all the environment variables for the container
   209  	var err error
   210  	if execConfig.Env, err = opts.ReadKVEnvStrings(execOpts.envFile.GetAll(), execOpts.env.GetAll()); err != nil {
   211  		return nil, err
   212  	}
   213  
   214  	// If -d is not set, attach to everything by default
   215  	if !execOpts.detach {
   216  		execConfig.AttachStdout = true
   217  		execConfig.AttachStderr = true
   218  		if execOpts.interactive {
   219  			execConfig.AttachStdin = true
   220  		}
   221  	}
   222  
   223  	if execOpts.detachKeys != "" {
   224  		execConfig.DetachKeys = execOpts.detachKeys
   225  	} else {
   226  		execConfig.DetachKeys = configFile.DetachKeys
   227  	}
   228  	return execConfig, nil
   229  }