mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
![Sally O'Malley](/assets/img/avatar_default.png)
The purpose of this PR is for users to distinguish Docker errors from contained command errors. This PR modifies 'docker run' exit codes to follow the chroot standard for exit codes. Exit status: 125 if 'docker run' itself fails 126 if contained command cannot be invoked 127 if contained command cannot be found the exit status otherwise Signed-off-by: Sally O'Malley <somalley@redhat.com>
284 lines
8.3 KiB
Go
284 lines
8.3 KiB
Go
package client
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"os"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/Sirupsen/logrus"
|
|
Cli "github.com/docker/docker/cli"
|
|
derr "github.com/docker/docker/errors"
|
|
"github.com/docker/docker/opts"
|
|
"github.com/docker/docker/pkg/promise"
|
|
"github.com/docker/docker/pkg/signal"
|
|
"github.com/docker/docker/runconfig"
|
|
"github.com/docker/libnetwork/resolvconf/dns"
|
|
)
|
|
|
|
func (cid *cidFile) Close() error {
|
|
cid.file.Close()
|
|
|
|
if !cid.written {
|
|
if err := os.Remove(cid.path); err != nil {
|
|
return fmt.Errorf("failed to remove the CID file '%s': %s \n", cid.path, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (cid *cidFile) Write(id string) error {
|
|
if _, err := cid.file.Write([]byte(id)); err != nil {
|
|
return fmt.Errorf("Failed to write the container ID to the file: %s", err)
|
|
}
|
|
cid.written = true
|
|
return nil
|
|
}
|
|
|
|
// if container start fails with 'command not found' error, return 127
|
|
// if container start fails with 'command cannot be invoked' error, return 126
|
|
// return 125 for generic docker daemon failures
|
|
func runStartContainerErr(err error) error {
|
|
trimmedErr := strings.Trim(err.Error(), "Error response from daemon: ")
|
|
statusError := Cli.StatusError{}
|
|
derrCmdNotFound := derr.ErrorCodeCmdNotFound.Message()
|
|
derrCouldNotInvoke := derr.ErrorCodeCmdCouldNotBeInvoked.Message()
|
|
derrNoSuchImage := derr.ErrorCodeNoSuchImageHash.Message()
|
|
derrNoSuchImageTag := derr.ErrorCodeNoSuchImageTag.Message()
|
|
switch trimmedErr {
|
|
case derrCmdNotFound:
|
|
statusError = Cli.StatusError{StatusCode: 127}
|
|
case derrCouldNotInvoke:
|
|
statusError = Cli.StatusError{StatusCode: 126}
|
|
case derrNoSuchImage, derrNoSuchImageTag:
|
|
statusError = Cli.StatusError{StatusCode: 125}
|
|
default:
|
|
statusError = Cli.StatusError{StatusCode: 125}
|
|
}
|
|
return statusError
|
|
}
|
|
|
|
// CmdRun runs a command in a new container.
|
|
//
|
|
// Usage: docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
|
|
func (cli *DockerCli) CmdRun(args ...string) error {
|
|
cmd := Cli.Subcmd("run", []string{"IMAGE [COMMAND] [ARG...]"}, Cli.DockerCommands["run"].Description, true)
|
|
addTrustedFlags(cmd, true)
|
|
|
|
// These are flags not stored in Config/HostConfig
|
|
var (
|
|
flAutoRemove = cmd.Bool([]string{"-rm"}, false, "Automatically remove the container when it exits")
|
|
flDetach = cmd.Bool([]string{"d", "-detach"}, false, "Run container in background and print container ID")
|
|
flSigProxy = cmd.Bool([]string{"-sig-proxy"}, true, "Proxy received signals to the process")
|
|
flName = cmd.String([]string{"-name"}, "", "Assign a name to the container")
|
|
flAttach *opts.ListOpts
|
|
|
|
ErrConflictAttachDetach = fmt.Errorf("Conflicting options: -a and -d")
|
|
ErrConflictRestartPolicyAndAutoRemove = fmt.Errorf("Conflicting options: --restart and --rm")
|
|
ErrConflictDetachAutoRemove = fmt.Errorf("Conflicting options: --rm and -d")
|
|
)
|
|
|
|
config, hostConfig, cmd, err := runconfig.Parse(cmd, args)
|
|
// just in case the Parse does not exit
|
|
if err != nil {
|
|
cmd.ReportError(err.Error(), true)
|
|
os.Exit(125)
|
|
}
|
|
|
|
if len(hostConfig.DNS) > 0 {
|
|
// check the DNS settings passed via --dns against
|
|
// localhost regexp to warn if they are trying to
|
|
// set a DNS to a localhost address
|
|
for _, dnsIP := range hostConfig.DNS {
|
|
if dns.IsLocalhost(dnsIP) {
|
|
fmt.Fprintf(cli.err, "WARNING: Localhost DNS setting (--dns=%s) may fail in containers.\n", dnsIP)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if config.Image == "" {
|
|
cmd.Usage()
|
|
return nil
|
|
}
|
|
|
|
if !*flDetach {
|
|
if err := cli.CheckTtyInput(config.AttachStdin, config.Tty); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if fl := cmd.Lookup("-attach"); fl != nil {
|
|
flAttach = fl.Value.(*opts.ListOpts)
|
|
if flAttach.Len() != 0 {
|
|
return ErrConflictAttachDetach
|
|
}
|
|
}
|
|
if *flAutoRemove {
|
|
return ErrConflictDetachAutoRemove
|
|
}
|
|
|
|
config.AttachStdin = false
|
|
config.AttachStdout = false
|
|
config.AttachStderr = false
|
|
config.StdinOnce = false
|
|
}
|
|
|
|
// Disable flSigProxy when in TTY mode
|
|
sigProxy := *flSigProxy
|
|
if config.Tty {
|
|
sigProxy = 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] = cli.getTtySize()
|
|
}
|
|
|
|
createResponse, err := cli.createContainer(config, hostConfig, hostConfig.ContainerIDFile, *flName)
|
|
if err != nil {
|
|
cmd.ReportError(err.Error(), true)
|
|
return runStartContainerErr(err)
|
|
}
|
|
if sigProxy {
|
|
sigc := cli.forwardAllSignals(createResponse.ID)
|
|
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.Fprintf(cli.out, "%s\n", createResponse.ID)
|
|
}()
|
|
}
|
|
if *flAutoRemove && (hostConfig.RestartPolicy.IsAlways() || hostConfig.RestartPolicy.IsOnFailure()) {
|
|
return ErrConflictRestartPolicyAndAutoRemove
|
|
}
|
|
// We need to instantiate the chan because the select needs it. It can
|
|
// be closed but can't be uninitialized.
|
|
hijacked := make(chan io.Closer)
|
|
// Block the return until the chan gets closed
|
|
defer func() {
|
|
logrus.Debugf("End of CmdRun(), Waiting for hijack to finish.")
|
|
if _, ok := <-hijacked; ok {
|
|
fmt.Fprintln(cli.err, "Hijack did not finish (chan still open)")
|
|
}
|
|
}()
|
|
if config.AttachStdin || config.AttachStdout || config.AttachStderr {
|
|
var (
|
|
out, stderr io.Writer
|
|
in io.ReadCloser
|
|
v = url.Values{}
|
|
)
|
|
v.Set("stream", "1")
|
|
if config.AttachStdin {
|
|
v.Set("stdin", "1")
|
|
in = cli.in
|
|
}
|
|
if config.AttachStdout {
|
|
v.Set("stdout", "1")
|
|
out = cli.out
|
|
}
|
|
if config.AttachStderr {
|
|
v.Set("stderr", "1")
|
|
if config.Tty {
|
|
stderr = cli.out
|
|
} else {
|
|
stderr = cli.err
|
|
}
|
|
}
|
|
errCh = promise.Go(func() error {
|
|
return cli.hijack("POST", "/containers/"+createResponse.ID+"/attach?"+v.Encode(), config.Tty, in, out, stderr, hijacked, nil)
|
|
})
|
|
} else {
|
|
close(hijacked)
|
|
}
|
|
// Acknowledge the hijack before starting
|
|
select {
|
|
case closer := <-hijacked:
|
|
// Make sure that the hijack gets closed when returning (results
|
|
// in closing the hijack chan and freeing server's goroutines)
|
|
if closer != nil {
|
|
defer closer.Close()
|
|
}
|
|
case err := <-errCh:
|
|
if err != nil {
|
|
logrus.Debugf("Error hijack: %s", err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
defer func() {
|
|
if *flAutoRemove {
|
|
if _, _, err = readBody(cli.call("DELETE", "/containers/"+createResponse.ID+"?v=1", nil, nil)); err != nil {
|
|
fmt.Fprintf(cli.err, "Error deleting container: %s\n", err)
|
|
}
|
|
}
|
|
}()
|
|
|
|
//start the container
|
|
if _, _, err := readBody(cli.call("POST", "/containers/"+createResponse.ID+"/start", nil, nil)); err != nil {
|
|
cmd.ReportError(err.Error(), false)
|
|
return runStartContainerErr(err)
|
|
}
|
|
|
|
if (config.AttachStdin || config.AttachStdout || config.AttachStderr) && config.Tty && cli.isTerminalOut {
|
|
if err := cli.monitorTtySize(createResponse.ID, false); err != nil {
|
|
fmt.Fprintf(cli.err, "Error monitoring TTY size: %s\n", err)
|
|
}
|
|
}
|
|
|
|
if errCh != nil {
|
|
if err := <-errCh; err != 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
|
|
}
|
|
|
|
var status int
|
|
|
|
// Attached mode
|
|
if *flAutoRemove {
|
|
// Autoremove: wait for the container to finish, retrieve
|
|
// the exit code and remove the container
|
|
if _, _, err := readBody(cli.call("POST", "/containers/"+createResponse.ID+"/wait", nil, nil)); err != nil {
|
|
return runStartContainerErr(err)
|
|
}
|
|
if _, status, err = getExitCode(cli, createResponse.ID); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// No Autoremove: Simply retrieve the exit code
|
|
if !config.Tty {
|
|
// In non-TTY mode, we can't detach, so we must wait for container exit
|
|
if status, err = waitForExit(cli, createResponse.ID); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// In TTY mode, there is a race: if the process dies too slowly, the state could
|
|
// be updated after the getExitCode call and result in the wrong exit code being reported
|
|
if _, status, err = getExitCode(cli, createResponse.ID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if status != 0 {
|
|
return Cli.StatusError{StatusCode: status}
|
|
}
|
|
return nil
|
|
}
|