github.1git.de/docker/cli@v26.1.3+incompatible/cli/command/container/exec.go (about)

     1  package container
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  
     9  	"github.com/docker/cli/cli"
    10  	"github.com/docker/cli/cli/command"
    11  	"github.com/docker/cli/cli/command/completion"
    12  	"github.com/docker/cli/cli/config/configfile"
    13  	"github.com/docker/cli/opts"
    14  	"github.com/docker/docker/api/types"
    15  	apiclient "github.com/docker/docker/client"
    16  	"github.com/pkg/errors"
    17  	"github.com/sirupsen/logrus"
    18  	"github.com/spf13/cobra"
    19  )
    20  
    21  // ExecOptions group options for `exec` command
    22  type ExecOptions struct {
    23  	DetachKeys  string
    24  	Interactive bool
    25  	TTY         bool
    26  	Detach      bool
    27  	User        string
    28  	Privileged  bool
    29  	Env         opts.ListOpts
    30  	Workdir     string
    31  	Command     []string
    32  	EnvFile     opts.ListOpts
    33  }
    34  
    35  // NewExecOptions creates a new ExecOptions
    36  func NewExecOptions() ExecOptions {
    37  	return ExecOptions{
    38  		Env:     opts.NewListOpts(opts.ValidateEnv),
    39  		EnvFile: opts.NewListOpts(nil),
    40  	}
    41  }
    42  
    43  // NewExecCommand creates a new cobra.Command for `docker exec`
    44  func NewExecCommand(dockerCli command.Cli) *cobra.Command {
    45  	options := NewExecOptions()
    46  	var container string
    47  
    48  	cmd := &cobra.Command{
    49  		Use:   "exec [OPTIONS] CONTAINER COMMAND [ARG...]",
    50  		Short: "Execute a command in a running container",
    51  		Args:  cli.RequiresMinArgs(2),
    52  		RunE: func(cmd *cobra.Command, args []string) error {
    53  			container = args[0]
    54  			options.Command = args[1:]
    55  			return RunExec(cmd.Context(), dockerCli, container, options)
    56  		},
    57  		ValidArgsFunction: completion.ContainerNames(dockerCli, false, func(container types.Container) bool {
    58  			return container.State != "paused"
    59  		}),
    60  		Annotations: map[string]string{
    61  			"category-top": "2",
    62  			"aliases":      "docker container exec, docker exec",
    63  		},
    64  	}
    65  
    66  	flags := cmd.Flags()
    67  	flags.SetInterspersed(false)
    68  
    69  	flags.StringVar(&options.DetachKeys, "detach-keys", "", "Override the key sequence for detaching a container")
    70  	flags.BoolVarP(&options.Interactive, "interactive", "i", false, "Keep STDIN open even if not attached")
    71  	flags.BoolVarP(&options.TTY, "tty", "t", false, "Allocate a pseudo-TTY")
    72  	flags.BoolVarP(&options.Detach, "detach", "d", false, "Detached mode: run command in the background")
    73  	flags.StringVarP(&options.User, "user", "u", "", `Username or UID (format: "<name|uid>[:<group|gid>]")`)
    74  	flags.BoolVar(&options.Privileged, "privileged", false, "Give extended privileges to the command")
    75  	flags.VarP(&options.Env, "env", "e", "Set environment variables")
    76  	flags.SetAnnotation("env", "version", []string{"1.25"})
    77  	flags.Var(&options.EnvFile, "env-file", "Read in a file of environment variables")
    78  	flags.SetAnnotation("env-file", "version", []string{"1.25"})
    79  	flags.StringVarP(&options.Workdir, "workdir", "w", "", "Working directory inside the container")
    80  	flags.SetAnnotation("workdir", "version", []string{"1.35"})
    81  
    82  	cmd.RegisterFlagCompletionFunc(
    83  		"env",
    84  		func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
    85  			return os.Environ(), cobra.ShellCompDirectiveNoFileComp
    86  		},
    87  	)
    88  	cmd.RegisterFlagCompletionFunc(
    89  		"env-file",
    90  		func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
    91  			return nil, cobra.ShellCompDirectiveDefault // _filedir
    92  		},
    93  	)
    94  
    95  	return cmd
    96  }
    97  
    98  // RunExec executes an `exec` command
    99  func RunExec(ctx context.Context, dockerCli command.Cli, container string, options ExecOptions) error {
   100  	execConfig, err := parseExec(options, dockerCli.ConfigFile())
   101  	if err != nil {
   102  		return err
   103  	}
   104  
   105  	client := dockerCli.Client()
   106  
   107  	// We need to check the tty _before_ we do the ContainerExecCreate, because
   108  	// otherwise if we error out we will leak execIDs on the server (and
   109  	// there's no easy way to clean those up). But also in order to make "not
   110  	// exist" errors take precedence we do a dummy inspect first.
   111  	if _, err := client.ContainerInspect(ctx, container); err != nil {
   112  		return err
   113  	}
   114  	if !execConfig.Detach {
   115  		if err := dockerCli.In().CheckTty(execConfig.AttachStdin, execConfig.Tty); err != nil {
   116  			return err
   117  		}
   118  	}
   119  
   120  	fillConsoleSize(execConfig, dockerCli)
   121  
   122  	response, err := client.ContainerExecCreate(ctx, container, *execConfig)
   123  	if err != nil {
   124  		return err
   125  	}
   126  
   127  	execID := response.ID
   128  	if execID == "" {
   129  		return errors.New("exec ID empty")
   130  	}
   131  
   132  	if execConfig.Detach {
   133  		execStartCheck := types.ExecStartCheck{
   134  			Detach:      execConfig.Detach,
   135  			Tty:         execConfig.Tty,
   136  			ConsoleSize: execConfig.ConsoleSize,
   137  		}
   138  		return client.ContainerExecStart(ctx, execID, execStartCheck)
   139  	}
   140  	return interactiveExec(ctx, dockerCli, execConfig, execID)
   141  }
   142  
   143  func fillConsoleSize(execConfig *types.ExecConfig, dockerCli command.Cli) {
   144  	if execConfig.Tty {
   145  		height, width := dockerCli.Out().GetTtySize()
   146  		execConfig.ConsoleSize = &[2]uint{height, width}
   147  	}
   148  }
   149  
   150  func interactiveExec(ctx context.Context, dockerCli command.Cli, execConfig *types.ExecConfig, execID string) error {
   151  	// Interactive exec requested.
   152  	var (
   153  		out, stderr io.Writer
   154  		in          io.ReadCloser
   155  	)
   156  
   157  	if execConfig.AttachStdin {
   158  		in = dockerCli.In()
   159  	}
   160  	if execConfig.AttachStdout {
   161  		out = dockerCli.Out()
   162  	}
   163  	if execConfig.AttachStderr {
   164  		if execConfig.Tty {
   165  			stderr = dockerCli.Out()
   166  		} else {
   167  			stderr = dockerCli.Err()
   168  		}
   169  	}
   170  	fillConsoleSize(execConfig, dockerCli)
   171  
   172  	client := dockerCli.Client()
   173  	execStartCheck := types.ExecStartCheck{
   174  		Tty:         execConfig.Tty,
   175  		ConsoleSize: execConfig.ConsoleSize,
   176  	}
   177  	resp, err := client.ContainerExecAttach(ctx, execID, execStartCheck)
   178  	if err != nil {
   179  		return err
   180  	}
   181  	defer resp.Close()
   182  
   183  	errCh := make(chan error, 1)
   184  
   185  	go func() {
   186  		defer close(errCh)
   187  		errCh <- func() error {
   188  			streamer := hijackedIOStreamer{
   189  				streams:      dockerCli,
   190  				inputStream:  in,
   191  				outputStream: out,
   192  				errorStream:  stderr,
   193  				resp:         resp,
   194  				tty:          execConfig.Tty,
   195  				detachKeys:   execConfig.DetachKeys,
   196  			}
   197  
   198  			return streamer.stream(ctx)
   199  		}()
   200  	}()
   201  
   202  	if execConfig.Tty && dockerCli.In().IsTerminal() {
   203  		if err := MonitorTtySize(ctx, dockerCli, execID, true); err != nil {
   204  			fmt.Fprintln(dockerCli.Err(), "Error monitoring TTY size:", err)
   205  		}
   206  	}
   207  
   208  	if err := <-errCh; err != nil {
   209  		logrus.Debugf("Error hijack: %s", err)
   210  		return err
   211  	}
   212  
   213  	return getExecExitStatus(ctx, client, execID)
   214  }
   215  
   216  func getExecExitStatus(ctx context.Context, client apiclient.ContainerAPIClient, execID string) error {
   217  	resp, err := client.ContainerExecInspect(ctx, execID)
   218  	if err != nil {
   219  		// If we can't connect, then the daemon probably died.
   220  		if !apiclient.IsErrConnectionFailed(err) {
   221  			return err
   222  		}
   223  		return cli.StatusError{StatusCode: -1}
   224  	}
   225  	status := resp.ExitCode
   226  	if status != 0 {
   227  		return cli.StatusError{StatusCode: status}
   228  	}
   229  	return nil
   230  }
   231  
   232  // parseExec parses the specified args for the specified command and generates
   233  // an ExecConfig from it.
   234  func parseExec(execOpts ExecOptions, configFile *configfile.ConfigFile) (*types.ExecConfig, error) {
   235  	execConfig := &types.ExecConfig{
   236  		User:       execOpts.User,
   237  		Privileged: execOpts.Privileged,
   238  		Tty:        execOpts.TTY,
   239  		Cmd:        execOpts.Command,
   240  		Detach:     execOpts.Detach,
   241  		WorkingDir: execOpts.Workdir,
   242  	}
   243  
   244  	// collect all the environment variables for the container
   245  	var err error
   246  	if execConfig.Env, err = opts.ReadKVEnvStrings(execOpts.EnvFile.GetAll(), execOpts.Env.GetAll()); err != nil {
   247  		return nil, err
   248  	}
   249  
   250  	// If -d is not set, attach to everything by default
   251  	if !execOpts.Detach {
   252  		execConfig.AttachStdout = true
   253  		execConfig.AttachStderr = true
   254  		if execOpts.Interactive {
   255  			execConfig.AttachStdin = true
   256  		}
   257  	}
   258  
   259  	if execOpts.DetachKeys != "" {
   260  		execConfig.DetachKeys = execOpts.DetachKeys
   261  	} else {
   262  		execConfig.DetachKeys = configFile.DetachKeys
   263  	}
   264  	return execConfig, nil
   265  }