Older versions of Go do not format these comments, so we can already
reformat them ahead of time to prevent gofmt linting failing once
we update to Go 1.19 or up.
Result of:
gofmt -s -w $(find . -type f -name '*.go' | grep -v "/vendor/")
With some manual adjusting.
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 82427d1a07)
Signed-off-by: Cory Snider <csnider@mirantis.com>
306 lines
8.6 KiB
Go
306 lines
8.6 KiB
Go
package container
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"runtime"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"github.com/docker/cli/cli"
|
|
"github.com/docker/cli/cli/command"
|
|
"github.com/docker/cli/opts"
|
|
"github.com/docker/docker/api/types"
|
|
"github.com/docker/docker/api/types/container"
|
|
"github.com/docker/docker/pkg/signal"
|
|
"github.com/moby/term"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/pflag"
|
|
)
|
|
|
|
type runOptions struct {
|
|
createOptions
|
|
detach bool
|
|
sigProxy bool
|
|
detachKeys string
|
|
}
|
|
|
|
// NewRunCommand create a new `docker run` command
|
|
func NewRunCommand(dockerCli command.Cli) *cobra.Command {
|
|
var opts runOptions
|
|
var copts *containerOptions
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "run [OPTIONS] IMAGE [COMMAND] [ARG...]",
|
|
Short: "Run a command in a new container",
|
|
Args: cli.RequiresMinArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
copts.Image = args[0]
|
|
if len(args) > 1 {
|
|
copts.Args = args[1:]
|
|
}
|
|
return runRun(dockerCli, cmd.Flags(), &opts, copts)
|
|
},
|
|
}
|
|
|
|
flags := cmd.Flags()
|
|
flags.SetInterspersed(false)
|
|
|
|
// These are flags not stored in Config/HostConfig
|
|
flags.BoolVarP(&opts.detach, "detach", "d", false, "Run container in background and print container ID")
|
|
flags.BoolVar(&opts.sigProxy, "sig-proxy", true, "Proxy received signals to the process")
|
|
flags.StringVar(&opts.name, "name", "", "Assign a name to the container")
|
|
flags.StringVar(&opts.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container")
|
|
flags.StringVar(&opts.createOptions.pull, "pull", PullImageMissing,
|
|
`Pull image before running ("`+PullImageAlways+`"|"`+PullImageMissing+`"|"`+PullImageNever+`")`)
|
|
|
|
// Add an explicit help that doesn't have a `-h` to prevent the conflict
|
|
// with hostname
|
|
flags.Bool("help", false, "Print usage")
|
|
|
|
command.AddPlatformFlag(flags, &opts.platform)
|
|
command.AddTrustVerificationFlags(flags, &opts.untrusted, dockerCli.ContentTrustEnabled())
|
|
copts = addFlags(flags)
|
|
return cmd
|
|
}
|
|
|
|
func runRun(dockerCli command.Cli, flags *pflag.FlagSet, ropts *runOptions, copts *containerOptions) error {
|
|
proxyConfig := dockerCli.ConfigFile().ParseProxyConfig(dockerCli.Client().DaemonHost(), opts.ConvertKVStringsToMapWithNil(copts.env.GetAll()))
|
|
newEnv := []string{}
|
|
for k, v := range proxyConfig {
|
|
if v == nil {
|
|
newEnv = append(newEnv, k)
|
|
} else {
|
|
newEnv = append(newEnv, fmt.Sprintf("%s=%s", k, *v))
|
|
}
|
|
}
|
|
copts.env = *opts.NewListOptsRef(&newEnv, nil)
|
|
containerConfig, err := parse(flags, copts, dockerCli.ServerInfo().OSType)
|
|
// just in case the parse does not exit
|
|
if err != nil {
|
|
reportError(dockerCli.Err(), "run", err.Error(), true)
|
|
return cli.StatusError{StatusCode: 125}
|
|
}
|
|
if err = validateAPIVersion(containerConfig, dockerCli.Client().ClientVersion()); err != nil {
|
|
reportError(dockerCli.Err(), "run", err.Error(), true)
|
|
return cli.StatusError{StatusCode: 125}
|
|
}
|
|
return runContainer(dockerCli, ropts, copts, containerConfig)
|
|
}
|
|
|
|
//nolint:gocyclo
|
|
func runContainer(dockerCli command.Cli, opts *runOptions, copts *containerOptions, containerConfig *containerConfig) error {
|
|
config := containerConfig.Config
|
|
hostConfig := containerConfig.HostConfig
|
|
stdout, stderr := dockerCli.Out(), dockerCli.Err()
|
|
client := dockerCli.Client()
|
|
|
|
config.ArgsEscaped = false
|
|
|
|
if !opts.detach {
|
|
if err := dockerCli.In().CheckTty(config.AttachStdin, config.Tty); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if copts.attach.Len() != 0 {
|
|
return errors.New("Conflicting options: -a and -d")
|
|
}
|
|
|
|
config.AttachStdin = false
|
|
config.AttachStdout = false
|
|
config.AttachStderr = false
|
|
config.StdinOnce = false
|
|
}
|
|
|
|
// Telling the Windows daemon the initial size of the tty during start makes
|
|
// a far better user experience rather than relying on subsequent resizes
|
|
// to cause things to catch up.
|
|
if runtime.GOOS == "windows" {
|
|
hostConfig.ConsoleSize[0], hostConfig.ConsoleSize[1] = dockerCli.Out().GetTtySize()
|
|
}
|
|
|
|
ctx, cancelFun := context.WithCancel(context.Background())
|
|
defer cancelFun()
|
|
|
|
createResponse, err := createContainer(ctx, dockerCli, containerConfig, &opts.createOptions)
|
|
if err != nil {
|
|
reportError(stderr, "run", err.Error(), true)
|
|
return runStartContainerErr(err)
|
|
}
|
|
if opts.sigProxy {
|
|
sigc := notfiyAllSignals()
|
|
go ForwardAllSignals(ctx, dockerCli, createResponse.ID, sigc)
|
|
defer signal.StopCatch(sigc)
|
|
}
|
|
|
|
var (
|
|
waitDisplayID chan struct{}
|
|
errCh chan error
|
|
)
|
|
if !config.AttachStdout && !config.AttachStderr {
|
|
// Make this asynchronous to allow the client to write to stdin before having to read the ID
|
|
waitDisplayID = make(chan struct{})
|
|
go func() {
|
|
defer close(waitDisplayID)
|
|
fmt.Fprintln(stdout, createResponse.ID)
|
|
}()
|
|
}
|
|
attach := config.AttachStdin || config.AttachStdout || config.AttachStderr
|
|
if attach {
|
|
if opts.detachKeys != "" {
|
|
dockerCli.ConfigFile().DetachKeys = opts.detachKeys
|
|
}
|
|
|
|
close, err := attachContainer(ctx, dockerCli, &errCh, config, createResponse.ID)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer close()
|
|
}
|
|
|
|
statusChan := waitExitOrRemoved(ctx, dockerCli, createResponse.ID, copts.autoRemove)
|
|
|
|
// start the container
|
|
if err := client.ContainerStart(ctx, createResponse.ID, types.ContainerStartOptions{}); err != nil {
|
|
// If we have hijackedIOStreamer, we should notify
|
|
// hijackedIOStreamer we are going to exit and wait
|
|
// to avoid the terminal are not restored.
|
|
if attach {
|
|
cancelFun()
|
|
<-errCh
|
|
}
|
|
|
|
reportError(stderr, "run", err.Error(), false)
|
|
if copts.autoRemove {
|
|
// wait container to be removed
|
|
<-statusChan
|
|
}
|
|
return runStartContainerErr(err)
|
|
}
|
|
|
|
if (config.AttachStdin || config.AttachStdout || config.AttachStderr) && config.Tty && dockerCli.Out().IsTerminal() {
|
|
if err := MonitorTtySize(ctx, dockerCli, createResponse.ID, false); err != nil {
|
|
fmt.Fprintln(stderr, "Error monitoring TTY size:", err)
|
|
}
|
|
}
|
|
|
|
if errCh != nil {
|
|
if err := <-errCh; err != nil {
|
|
if _, ok := err.(term.EscapeError); ok {
|
|
// The user entered the detach escape sequence.
|
|
return nil
|
|
}
|
|
|
|
logrus.Debugf("Error hijack: %s", err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Detached mode: wait for the id to be displayed and return.
|
|
if !config.AttachStdout && !config.AttachStderr {
|
|
// Detached mode
|
|
<-waitDisplayID
|
|
return nil
|
|
}
|
|
|
|
status := <-statusChan
|
|
if status != 0 {
|
|
return cli.StatusError{StatusCode: status}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func attachContainer(
|
|
ctx context.Context,
|
|
dockerCli command.Cli,
|
|
errCh *chan error,
|
|
config *container.Config,
|
|
containerID string,
|
|
) (func(), error) {
|
|
stdout, stderr := dockerCli.Out(), dockerCli.Err()
|
|
var (
|
|
out, cerr io.Writer
|
|
in io.ReadCloser
|
|
)
|
|
if config.AttachStdin {
|
|
in = dockerCli.In()
|
|
}
|
|
if config.AttachStdout {
|
|
out = stdout
|
|
}
|
|
if config.AttachStderr {
|
|
if config.Tty {
|
|
cerr = stdout
|
|
} else {
|
|
cerr = stderr
|
|
}
|
|
}
|
|
|
|
options := types.ContainerAttachOptions{
|
|
Stream: true,
|
|
Stdin: config.AttachStdin,
|
|
Stdout: config.AttachStdout,
|
|
Stderr: config.AttachStderr,
|
|
DetachKeys: dockerCli.ConfigFile().DetachKeys,
|
|
}
|
|
|
|
resp, errAttach := dockerCli.Client().ContainerAttach(ctx, containerID, options)
|
|
if errAttach != nil {
|
|
return nil, errAttach
|
|
}
|
|
|
|
ch := make(chan error, 1)
|
|
*errCh = ch
|
|
|
|
go func() {
|
|
ch <- func() error {
|
|
streamer := hijackedIOStreamer{
|
|
streams: dockerCli,
|
|
inputStream: in,
|
|
outputStream: out,
|
|
errorStream: cerr,
|
|
resp: resp,
|
|
tty: config.Tty,
|
|
detachKeys: options.DetachKeys,
|
|
}
|
|
|
|
if errHijack := streamer.stream(ctx); errHijack != nil {
|
|
return errHijack
|
|
}
|
|
return errAttach
|
|
}()
|
|
}()
|
|
return resp.Close, nil
|
|
}
|
|
|
|
// reportError is a utility method that prints a user-friendly message
|
|
// containing the error that occurred during parsing and a suggestion to get help
|
|
func reportError(stderr io.Writer, name string, str string, withHelp bool) {
|
|
str = strings.TrimSuffix(str, ".") + "."
|
|
if withHelp {
|
|
str += "\nSee 'docker " + name + " --help'."
|
|
}
|
|
fmt.Fprintln(stderr, "docker:", str)
|
|
}
|
|
|
|
// if container start fails with 'not found'/'no such' error, return 127
|
|
// if container start fails with 'permission denied' error, return 126
|
|
// return 125 for generic docker daemon failures
|
|
func runStartContainerErr(err error) error {
|
|
trimmedErr := strings.TrimPrefix(err.Error(), "Error response from daemon: ")
|
|
statusError := cli.StatusError{StatusCode: 125}
|
|
if strings.Contains(trimmedErr, "executable file not found") ||
|
|
strings.Contains(trimmedErr, "no such file or directory") ||
|
|
strings.Contains(trimmedErr, "system cannot find the file specified") {
|
|
statusError = cli.StatusError{StatusCode: 127}
|
|
} else if strings.Contains(trimmedErr, syscall.EACCES.Error()) {
|
|
statusError = cli.StatusError{StatusCode: 126}
|
|
}
|
|
|
|
return statusError
|
|
}
|