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

     1  package container
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"strings"
     9  	"syscall"
    10  
    11  	"github.com/docker/cli/cli"
    12  	"github.com/docker/cli/cli/command"
    13  	"github.com/docker/cli/cli/command/completion"
    14  	"github.com/docker/cli/opts"
    15  	"github.com/docker/docker/api/types/container"
    16  	"github.com/moby/sys/signal"
    17  	"github.com/moby/term"
    18  	"github.com/pkg/errors"
    19  	"github.com/sirupsen/logrus"
    20  	"github.com/spf13/cobra"
    21  	"github.com/spf13/pflag"
    22  )
    23  
    24  type runOptions struct {
    25  	createOptions
    26  	detach     bool
    27  	sigProxy   bool
    28  	detachKeys string
    29  }
    30  
    31  // NewRunCommand create a new `docker run` command
    32  func NewRunCommand(dockerCli command.Cli) *cobra.Command {
    33  	var options runOptions
    34  	var copts *containerOptions
    35  
    36  	cmd := &cobra.Command{
    37  		Use:   "run [OPTIONS] IMAGE [COMMAND] [ARG...]",
    38  		Short: "Create and run a new container from an image",
    39  		Args:  cli.RequiresMinArgs(1),
    40  		RunE: func(cmd *cobra.Command, args []string) error {
    41  			copts.Image = args[0]
    42  			if len(args) > 1 {
    43  				copts.Args = args[1:]
    44  			}
    45  			return runRun(cmd.Context(), dockerCli, cmd.Flags(), &options, copts)
    46  		},
    47  		ValidArgsFunction: completion.ImageNames(dockerCli),
    48  		Annotations: map[string]string{
    49  			"category-top": "1",
    50  			"aliases":      "docker container run, docker run",
    51  		},
    52  	}
    53  
    54  	flags := cmd.Flags()
    55  	flags.SetInterspersed(false)
    56  
    57  	// These are flags not stored in Config/HostConfig
    58  	flags.BoolVarP(&options.detach, "detach", "d", false, "Run container in background and print container ID")
    59  	flags.BoolVar(&options.sigProxy, "sig-proxy", true, "Proxy received signals to the process")
    60  	flags.StringVar(&options.name, "name", "", "Assign a name to the container")
    61  	flags.StringVar(&options.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container")
    62  	flags.StringVar(&options.pull, "pull", PullImageMissing, `Pull image before running ("`+PullImageAlways+`", "`+PullImageMissing+`", "`+PullImageNever+`")`)
    63  	flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the pull output")
    64  
    65  	// Add an explicit help that doesn't have a `-h` to prevent the conflict
    66  	// with hostname
    67  	flags.Bool("help", false, "Print usage")
    68  
    69  	command.AddPlatformFlag(flags, &options.platform)
    70  	command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled())
    71  	copts = addFlags(flags)
    72  
    73  	cmd.RegisterFlagCompletionFunc(
    74  		"env",
    75  		func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
    76  			return os.Environ(), cobra.ShellCompDirectiveNoFileComp
    77  		},
    78  	)
    79  	cmd.RegisterFlagCompletionFunc(
    80  		"env-file",
    81  		func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
    82  			return nil, cobra.ShellCompDirectiveDefault
    83  		},
    84  	)
    85  	cmd.RegisterFlagCompletionFunc(
    86  		"network",
    87  		completion.NetworkNames(dockerCli),
    88  	)
    89  	return cmd
    90  }
    91  
    92  func runRun(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, ropts *runOptions, copts *containerOptions) error {
    93  	if err := validatePullOpt(ropts.pull); err != nil {
    94  		reportError(dockerCli.Err(), "run", err.Error(), true)
    95  		return cli.StatusError{StatusCode: 125}
    96  	}
    97  	proxyConfig := dockerCli.ConfigFile().ParseProxyConfig(dockerCli.Client().DaemonHost(), opts.ConvertKVStringsToMapWithNil(copts.env.GetAll()))
    98  	newEnv := []string{}
    99  	for k, v := range proxyConfig {
   100  		if v == nil {
   101  			newEnv = append(newEnv, k)
   102  		} else {
   103  			newEnv = append(newEnv, k+"="+*v)
   104  		}
   105  	}
   106  	copts.env = *opts.NewListOptsRef(&newEnv, nil)
   107  	containerCfg, err := parse(flags, copts, dockerCli.ServerInfo().OSType)
   108  	// just in case the parse does not exit
   109  	if err != nil {
   110  		reportError(dockerCli.Err(), "run", err.Error(), true)
   111  		return cli.StatusError{StatusCode: 125}
   112  	}
   113  	if err = validateAPIVersion(containerCfg, dockerCli.CurrentVersion()); err != nil {
   114  		reportError(dockerCli.Err(), "run", err.Error(), true)
   115  		return cli.StatusError{StatusCode: 125}
   116  	}
   117  	return runContainer(ctx, dockerCli, ropts, copts, containerCfg)
   118  }
   119  
   120  //nolint:gocyclo
   121  func runContainer(ctx context.Context, dockerCli command.Cli, runOpts *runOptions, copts *containerOptions, containerCfg *containerConfig) error {
   122  	config := containerCfg.Config
   123  	stdout, stderr := dockerCli.Out(), dockerCli.Err()
   124  	apiClient := dockerCli.Client()
   125  
   126  	config.ArgsEscaped = false
   127  
   128  	if !runOpts.detach {
   129  		if err := dockerCli.In().CheckTty(config.AttachStdin, config.Tty); err != nil {
   130  			return err
   131  		}
   132  	} else {
   133  		if copts.attach.Len() != 0 {
   134  			return errors.New("Conflicting options: -a and -d")
   135  		}
   136  
   137  		config.AttachStdin = false
   138  		config.AttachStdout = false
   139  		config.AttachStderr = false
   140  		config.StdinOnce = false
   141  	}
   142  
   143  	ctx, cancelFun := context.WithCancel(ctx)
   144  	defer cancelFun()
   145  
   146  	containerID, err := createContainer(ctx, dockerCli, containerCfg, &runOpts.createOptions)
   147  	if err != nil {
   148  		reportError(stderr, "run", err.Error(), true)
   149  		return runStartContainerErr(err)
   150  	}
   151  	if runOpts.sigProxy {
   152  		sigc := notifyAllSignals()
   153  		go ForwardAllSignals(ctx, apiClient, containerID, sigc)
   154  		defer signal.StopCatch(sigc)
   155  	}
   156  
   157  	var (
   158  		waitDisplayID chan struct{}
   159  		errCh         chan error
   160  	)
   161  	if !config.AttachStdout && !config.AttachStderr {
   162  		// Make this asynchronous to allow the client to write to stdin before having to read the ID
   163  		waitDisplayID = make(chan struct{})
   164  		go func() {
   165  			defer close(waitDisplayID)
   166  			_, _ = fmt.Fprintln(stdout, containerID)
   167  		}()
   168  	}
   169  	attach := config.AttachStdin || config.AttachStdout || config.AttachStderr
   170  	if attach {
   171  		detachKeys := dockerCli.ConfigFile().DetachKeys
   172  		if runOpts.detachKeys != "" {
   173  			detachKeys = runOpts.detachKeys
   174  		}
   175  
   176  		closeFn, err := attachContainer(ctx, dockerCli, containerID, &errCh, config, container.AttachOptions{
   177  			Stream:     true,
   178  			Stdin:      config.AttachStdin,
   179  			Stdout:     config.AttachStdout,
   180  			Stderr:     config.AttachStderr,
   181  			DetachKeys: detachKeys,
   182  		})
   183  		if err != nil {
   184  			return err
   185  		}
   186  		defer closeFn()
   187  	}
   188  
   189  	// New context here because we don't to cancel waiting on container exit/remove
   190  	// when we cancel attach, etc.
   191  	statusCtx, cancelStatusCtx := context.WithCancel(context.WithoutCancel(ctx))
   192  	defer cancelStatusCtx()
   193  	statusChan := waitExitOrRemoved(statusCtx, apiClient, containerID, copts.autoRemove)
   194  
   195  	// start the container
   196  	if err := apiClient.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil {
   197  		// If we have hijackedIOStreamer, we should notify
   198  		// hijackedIOStreamer we are going to exit and wait
   199  		// to avoid the terminal are not restored.
   200  		if attach {
   201  			cancelFun()
   202  			<-errCh
   203  		}
   204  
   205  		reportError(stderr, "run", err.Error(), false)
   206  		if copts.autoRemove {
   207  			// wait container to be removed
   208  			<-statusChan
   209  		}
   210  		return runStartContainerErr(err)
   211  	}
   212  
   213  	if (config.AttachStdin || config.AttachStdout || config.AttachStderr) && config.Tty && dockerCli.Out().IsTerminal() {
   214  		if err := MonitorTtySize(ctx, dockerCli, containerID, false); err != nil {
   215  			_, _ = fmt.Fprintln(stderr, "Error monitoring TTY size:", err)
   216  		}
   217  	}
   218  
   219  	if errCh != nil {
   220  		if err := <-errCh; err != nil {
   221  			if _, ok := err.(term.EscapeError); ok {
   222  				// The user entered the detach escape sequence.
   223  				return nil
   224  			}
   225  
   226  			logrus.Debugf("Error hijack: %s", err)
   227  			return err
   228  		}
   229  	}
   230  
   231  	// Detached mode: wait for the id to be displayed and return.
   232  	if !config.AttachStdout && !config.AttachStderr {
   233  		// Detached mode
   234  		<-waitDisplayID
   235  		return nil
   236  	}
   237  
   238  	status := <-statusChan
   239  	if status != 0 {
   240  		return cli.StatusError{StatusCode: status}
   241  	}
   242  	return nil
   243  }
   244  
   245  func attachContainer(ctx context.Context, dockerCli command.Cli, containerID string, errCh *chan error, config *container.Config, options container.AttachOptions) (func(), error) {
   246  	resp, errAttach := dockerCli.Client().ContainerAttach(ctx, containerID, options)
   247  	if errAttach != nil {
   248  		return nil, errAttach
   249  	}
   250  
   251  	var (
   252  		out, cerr io.Writer
   253  		in        io.ReadCloser
   254  	)
   255  	if options.Stdin {
   256  		in = dockerCli.In()
   257  	}
   258  	if options.Stdout {
   259  		out = dockerCli.Out()
   260  	}
   261  	if options.Stderr {
   262  		if config.Tty {
   263  			cerr = dockerCli.Out()
   264  		} else {
   265  			cerr = dockerCli.Err()
   266  		}
   267  	}
   268  
   269  	ch := make(chan error, 1)
   270  	*errCh = ch
   271  
   272  	go func() {
   273  		ch <- func() error {
   274  			streamer := hijackedIOStreamer{
   275  				streams:      dockerCli,
   276  				inputStream:  in,
   277  				outputStream: out,
   278  				errorStream:  cerr,
   279  				resp:         resp,
   280  				tty:          config.Tty,
   281  				detachKeys:   options.DetachKeys,
   282  			}
   283  
   284  			if errHijack := streamer.stream(ctx); errHijack != nil {
   285  				return errHijack
   286  			}
   287  			return errAttach
   288  		}()
   289  	}()
   290  	return resp.Close, nil
   291  }
   292  
   293  // reportError is a utility method that prints a user-friendly message
   294  // containing the error that occurred during parsing and a suggestion to get help
   295  func reportError(stderr io.Writer, name string, str string, withHelp bool) {
   296  	str = strings.TrimSuffix(str, ".") + "."
   297  	if withHelp {
   298  		str += "\nSee 'docker " + name + " --help'."
   299  	}
   300  	_, _ = fmt.Fprintln(stderr, "docker:", str)
   301  }
   302  
   303  // if container start fails with 'not found'/'no such' error, return 127
   304  // if container start fails with 'permission denied' error, return 126
   305  // return 125 for generic docker daemon failures
   306  func runStartContainerErr(err error) error {
   307  	trimmedErr := strings.TrimPrefix(err.Error(), "Error response from daemon: ")
   308  	statusError := cli.StatusError{StatusCode: 125}
   309  	if strings.Contains(trimmedErr, "executable file not found") ||
   310  		strings.Contains(trimmedErr, "no such file or directory") ||
   311  		strings.Contains(trimmedErr, "system cannot find the file specified") {
   312  		statusError = cli.StatusError{StatusCode: 127}
   313  	} else if strings.Contains(trimmedErr, syscall.EACCES.Error()) ||
   314  		strings.Contains(trimmedErr, syscall.EISDIR.Error()) {
   315  		statusError = cli.StatusError{StatusCode: 126}
   316  	}
   317  
   318  	return statusError
   319  }