github.com/ali-iotechsys/cli@v20.10.0+incompatible/cli/command/container/run.go (about)

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