github.com/thajeztah/cli@v0.0.0-20240223162942-dc6bfac81a8b/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  	statusChan := waitExitOrRemoved(ctx, apiClient, containerID, copts.autoRemove)
   190  
   191  	// start the container
   192  	if err := apiClient.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil {
   193  		// If we have hijackedIOStreamer, we should notify
   194  		// hijackedIOStreamer we are going to exit and wait
   195  		// to avoid the terminal are not restored.
   196  		if attach {
   197  			cancelFun()
   198  			<-errCh
   199  		}
   200  
   201  		reportError(stderr, "run", err.Error(), false)
   202  		if copts.autoRemove {
   203  			// wait container to be removed
   204  			<-statusChan
   205  		}
   206  		return runStartContainerErr(err)
   207  	}
   208  
   209  	if (config.AttachStdin || config.AttachStdout || config.AttachStderr) && config.Tty && dockerCli.Out().IsTerminal() {
   210  		if err := MonitorTtySize(ctx, dockerCli, containerID, false); err != nil {
   211  			_, _ = fmt.Fprintln(stderr, "Error monitoring TTY size:", err)
   212  		}
   213  	}
   214  
   215  	if errCh != nil {
   216  		if err := <-errCh; err != nil {
   217  			if _, ok := err.(term.EscapeError); ok {
   218  				// The user entered the detach escape sequence.
   219  				return nil
   220  			}
   221  
   222  			logrus.Debugf("Error hijack: %s", err)
   223  			return err
   224  		}
   225  	}
   226  
   227  	// Detached mode: wait for the id to be displayed and return.
   228  	if !config.AttachStdout && !config.AttachStderr {
   229  		// Detached mode
   230  		<-waitDisplayID
   231  		return nil
   232  	}
   233  
   234  	status := <-statusChan
   235  	if status != 0 {
   236  		return cli.StatusError{StatusCode: status}
   237  	}
   238  	return nil
   239  }
   240  
   241  func attachContainer(ctx context.Context, dockerCli command.Cli, containerID string, errCh *chan error, config *container.Config, options container.AttachOptions) (func(), error) {
   242  	resp, errAttach := dockerCli.Client().ContainerAttach(ctx, containerID, options)
   243  	if errAttach != nil {
   244  		return nil, errAttach
   245  	}
   246  
   247  	var (
   248  		out, cerr io.Writer
   249  		in        io.ReadCloser
   250  	)
   251  	if options.Stdin {
   252  		in = dockerCli.In()
   253  	}
   254  	if options.Stdout {
   255  		out = dockerCli.Out()
   256  	}
   257  	if options.Stderr {
   258  		if config.Tty {
   259  			cerr = dockerCli.Out()
   260  		} else {
   261  			cerr = dockerCli.Err()
   262  		}
   263  	}
   264  
   265  	ch := make(chan error, 1)
   266  	*errCh = ch
   267  
   268  	go func() {
   269  		ch <- func() error {
   270  			streamer := hijackedIOStreamer{
   271  				streams:      dockerCli,
   272  				inputStream:  in,
   273  				outputStream: out,
   274  				errorStream:  cerr,
   275  				resp:         resp,
   276  				tty:          config.Tty,
   277  				detachKeys:   options.DetachKeys,
   278  			}
   279  
   280  			if errHijack := streamer.stream(ctx); errHijack != nil {
   281  				return errHijack
   282  			}
   283  			return errAttach
   284  		}()
   285  	}()
   286  	return resp.Close, nil
   287  }
   288  
   289  // reportError is a utility method that prints a user-friendly message
   290  // containing the error that occurred during parsing and a suggestion to get help
   291  func reportError(stderr io.Writer, name string, str string, withHelp bool) {
   292  	str = strings.TrimSuffix(str, ".") + "."
   293  	if withHelp {
   294  		str += "\nSee 'docker " + name + " --help'."
   295  	}
   296  	_, _ = fmt.Fprintln(stderr, "docker:", str)
   297  }
   298  
   299  // if container start fails with 'not found'/'no such' error, return 127
   300  // if container start fails with 'permission denied' error, return 126
   301  // return 125 for generic docker daemon failures
   302  func runStartContainerErr(err error) error {
   303  	trimmedErr := strings.TrimPrefix(err.Error(), "Error response from daemon: ")
   304  	statusError := cli.StatusError{StatusCode: 125}
   305  	if strings.Contains(trimmedErr, "executable file not found") ||
   306  		strings.Contains(trimmedErr, "no such file or directory") ||
   307  		strings.Contains(trimmedErr, "system cannot find the file specified") {
   308  		statusError = cli.StatusError{StatusCode: 127}
   309  	} else if strings.Contains(trimmedErr, syscall.EACCES.Error()) ||
   310  		strings.Contains(trimmedErr, syscall.EISDIR.Error()) {
   311  		statusError = cli.StatusError{StatusCode: 126}
   312  	}
   313  
   314  	return statusError
   315  }