From a77f2450c70312f8c26877a18bfe2baa44d4abb9 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 31 May 2016 16:49:32 -0700 Subject: [PATCH 1/2] Convert 'docker run' to a cobra command and to use pflags Move container options into a struct so that tests should pass. Remove unused FlagSet arg from Parse Disable interspersed args on docker run Signed-off-by: Daniel Nephin --- api/client/attach.go | 12 +- api/client/cli.go | 15 + api/client/commands.go | 1 - api/client/{ => container}/run.go | 216 +++++++----- api/client/create.go | 64 ++-- api/client/exec.go | 4 +- api/client/hijack.go | 3 +- api/client/rm.go | 7 +- api/client/start.go | 13 +- api/client/trust.go | 16 +- api/client/utils.go | 16 +- cli/cobraadaptor/adaptor.go | 6 +- cli/usage.go | 1 - runconfig/opts/parse.go | 569 +++++++++++++++++------------- runconfig/opts/parse_test.go | 71 ++-- runconfig/opts/throttledevice.go | 5 + runconfig/opts/ulimit.go | 5 + runconfig/opts/weightdevice.go | 5 + 18 files changed, 609 insertions(+), 420 deletions(-) rename api/client/{ => container}/run.go (51%) diff --git a/api/client/attach.go b/api/client/attach.go index aae639378d..3fc3a86c8a 100644 --- a/api/client/attach.go +++ b/api/client/attach.go @@ -66,7 +66,7 @@ func (cli *DockerCli) CmdAttach(args ...string) error { } if *proxy && !c.Config.Tty { - sigc := cli.forwardAllSignals(ctx, container) + sigc := cli.ForwardAllSignals(ctx, container) defer signal.StopCatch(sigc) } @@ -80,20 +80,20 @@ func (cli *DockerCli) CmdAttach(args ...string) error { defer resp.Close() if c.Config.Tty && cli.isTerminalOut { - height, width := cli.getTtySize() + height, width := cli.GetTtySize() // To handle the case where a user repeatedly attaches/detaches without resizing their // terminal, the only way to get the shell prompt to display for attaches 2+ is to artificially // resize it, then go back to normal. Without this, every attach after the first will // require the user to manually resize or hit enter. cli.resizeTtyTo(ctx, cmd.Arg(0), height+1, width+1, false) - // After the above resizing occurs, the call to monitorTtySize below will handle resetting back + // After the above resizing occurs, the call to MonitorTtySize below will handle resetting back // to the actual size. - if err := cli.monitorTtySize(ctx, cmd.Arg(0), false); err != nil { + if err := cli.MonitorTtySize(ctx, cmd.Arg(0), false); err != nil { logrus.Debugf("Error monitoring TTY size: %s", err) } } - if err := cli.holdHijackedConnection(ctx, c.Config.Tty, in, cli.out, cli.err, resp); err != nil { + if err := cli.HoldHijackedConnection(ctx, c.Config.Tty, in, cli.out, cli.err, resp); err != nil { return err } @@ -101,7 +101,7 @@ func (cli *DockerCli) CmdAttach(args ...string) error { return errAttach } - _, status, err := cli.getExitCode(ctx, container) + _, status, err := cli.GetExitCode(ctx, container) if err != nil { return err } diff --git a/api/client/cli.go b/api/client/cli.go index 6c3a8630d8..684ab5e186 100644 --- a/api/client/cli.go +++ b/api/client/cli.go @@ -75,6 +75,21 @@ func (cli *DockerCli) Err() io.Writer { return cli.err } +// In returns the reader used for stdin +func (cli *DockerCli) In() io.ReadCloser { + return cli.in +} + +// ConfigFile returns the ConfigFile +func (cli *DockerCli) ConfigFile() *configfile.ConfigFile { + return cli.configFile +} + +// IsTerminalOut returns true if the clients stdin is a TTY +func (cli *DockerCli) IsTerminalOut() bool { + return cli.isTerminalOut +} + // CheckTtyInput checks if we are trying to attach to a container tty // from a non-tty client input stream, and if so, returns an error. func (cli *DockerCli) CheckTtyInput(attachStdin, ttyMode bool) error { diff --git a/api/client/commands.go b/api/client/commands.go index 2296a4a4c3..bc18be6911 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -38,7 +38,6 @@ func (cli *DockerCli) Command(name string) func(...string) error { "restart": cli.CmdRestart, "rm": cli.CmdRm, "rmi": cli.CmdRmi, - "run": cli.CmdRun, "save": cli.CmdSave, "start": cli.CmdStart, "stats": cli.CmdStats, diff --git a/api/client/run.go b/api/client/container/run.go similarity index 51% rename from api/client/run.go rename to api/client/container/run.go index 2c322e0865..a5363841cb 100644 --- a/api/client/run.go +++ b/api/client/container/run.go @@ -1,4 +1,4 @@ -package client +package container import ( "fmt" @@ -11,13 +11,16 @@ import ( "golang.org/x/net/context" "github.com/Sirupsen/logrus" - Cli "github.com/docker/docker/cli" - "github.com/docker/docker/opts" + "github.com/docker/docker/api/client" + "github.com/docker/docker/cli" + opttypes "github.com/docker/docker/opts" "github.com/docker/docker/pkg/promise" "github.com/docker/docker/pkg/signal" runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/docker/engine-api/types" "github.com/docker/libnetwork/resolvconf/dns" + "github.com/spf13/cobra" + "github.com/spf13/pflag" ) const ( @@ -25,74 +28,74 @@ const ( errCmdCouldNotBeInvoked = "could not be invoked" ) -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 +type runOptions struct { + autoRemove bool + detach bool + sigProxy bool + name string + detachKeys string } -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 -} +// NewRunCommand create a new `docker run` command +func NewRunCommand(dockerCli *client.DockerCli) *cobra.Command { + var opts runOptions + var copts *runconfigopts.ContainerOptions -// 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.TrimPrefix(err.Error(), "Error response from daemon: ") - statusError := Cli.StatusError{StatusCode: 125} - if strings.HasPrefix(trimmedErr, "Container command") { - if strings.Contains(trimmedErr, errCmdNotFound) { - statusError = Cli.StatusError{StatusCode: 127} - } else if strings.Contains(trimmedErr, errCmdCouldNotBeInvoked) { - statusError = Cli.StatusError{StatusCode: 126} - } + 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) + }, } - 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) + flags := cmd.Flags() + flags.SetInterspersed(false) // 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") - flDetachKeys = cmd.String([]string{"-detach-keys"}, "", "Override the key sequence for detaching a container") - flAttach *opts.ListOpts + flags.BoolVar(&opts.autoRemove, "rm", false, "Automatically remove the container when it exits") + 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") + // Add an explicit help that doesn't have a `-h` to prevent the conflict + // with hostname + flags.Bool("help", false, "Print usage") + + client.AddTrustedFlags(flags, true) + copts = runconfigopts.AddFlags(flags) + return cmd +} + +func runRun(dockerCli *client.DockerCli, flags *pflag.FlagSet, opts *runOptions, copts *runconfigopts.ContainerOptions) error { + stdout, stderr, stdin := dockerCli.Out(), dockerCli.Err(), dockerCli.In() + client := dockerCli.Client() + // TODO: pass this as an argument + cmdPath := "run" + + var ( + flAttach *opttypes.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, networkingConfig, cmd, err := runconfigopts.Parse(cmd, args) + config, hostConfig, networkingConfig, err := runconfigopts.Parse(flags, copts) // just in case the Parse does not exit if err != nil { - cmd.ReportError(err.Error(), true) + reportError(stderr, cmdPath, err.Error(), true) os.Exit(125) } if hostConfig.OomKillDisable != nil && *hostConfig.OomKillDisable && hostConfig.Memory == 0 { - fmt.Fprintf(cli.err, "WARNING: Disabling the OOM killer on containers without setting a '-m/--memory' limit may be dangerous.\n") + fmt.Fprintf(stderr, "WARNING: Disabling the OOM killer on containers without setting a '-m/--memory' limit may be dangerous.\n") } if len(hostConfig.DNS) > 0 { @@ -101,30 +104,26 @@ func (cli *DockerCli) CmdRun(args ...string) error { // 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) + fmt.Fprintf(stderr, "WARNING: Localhost DNS setting (--dns=%s) may fail in containers.\n", dnsIP) break } } } - if config.Image == "" { - cmd.Usage() - return nil - } config.ArgsEscaped = false - if !*flDetach { - if err := cli.CheckTtyInput(config.AttachStdin, config.Tty); err != nil { + if !opts.detach { + if err := dockerCli.CheckTtyInput(config.AttachStdin, config.Tty); err != nil { return err } } else { - if fl := cmd.Lookup("-attach"); fl != nil { - flAttach = fl.Value.(*opts.ListOpts) + if fl := flags.Lookup("attach"); fl != nil { + flAttach = fl.Value.(*opttypes.ListOpts) if flAttach.Len() != 0 { return ErrConflictAttachDetach } } - if *flAutoRemove { + if opts.autoRemove { return ErrConflictDetachAutoRemove } @@ -134,28 +133,27 @@ func (cli *DockerCli) CmdRun(args ...string) error { config.StdinOnce = false } - // Disable flSigProxy when in TTY mode - sigProxy := *flSigProxy + // Disable sigProxy when in TTY mode if config.Tty { - sigProxy = false + opts.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() + hostConfig.ConsoleSize[0], hostConfig.ConsoleSize[1] = dockerCli.GetTtySize() } ctx, cancelFun := context.WithCancel(context.Background()) - createResponse, err := cli.createContainer(ctx, config, hostConfig, networkingConfig, hostConfig.ContainerIDFile, *flName) + createResponse, err := dockerCli.CreateContainer(ctx, config, hostConfig, networkingConfig, hostConfig.ContainerIDFile, opts.name) if err != nil { - cmd.ReportError(err.Error(), true) + reportError(stderr, cmdPath, err.Error(), true) return runStartContainerErr(err) } - if sigProxy { - sigc := cli.forwardAllSignals(ctx, createResponse.ID) + if opts.sigProxy { + sigc := dockerCli.ForwardAllSignals(ctx, createResponse.ID) defer signal.StopCatch(sigc) } var ( @@ -167,34 +165,34 @@ func (cli *DockerCli) CmdRun(args ...string) error { waitDisplayID = make(chan struct{}) go func() { defer close(waitDisplayID) - fmt.Fprintf(cli.out, "%s\n", createResponse.ID) + fmt.Fprintf(stdout, "%s\n", createResponse.ID) }() } - if *flAutoRemove && (hostConfig.RestartPolicy.IsAlways() || hostConfig.RestartPolicy.IsOnFailure()) { + if opts.autoRemove && (hostConfig.RestartPolicy.IsAlways() || hostConfig.RestartPolicy.IsOnFailure()) { return ErrConflictRestartPolicyAndAutoRemove } attach := config.AttachStdin || config.AttachStdout || config.AttachStderr if attach { var ( - out, stderr io.Writer - in io.ReadCloser + out, cerr io.Writer + in io.ReadCloser ) if config.AttachStdin { - in = cli.in + in = stdin } if config.AttachStdout { - out = cli.out + out = stdout } if config.AttachStderr { if config.Tty { - stderr = cli.out + cerr = stdout } else { - stderr = cli.err + cerr = stderr } } - if *flDetachKeys != "" { - cli.configFile.DetachKeys = *flDetachKeys + if opts.detachKeys != "" { + dockerCli.ConfigFile().DetachKeys = opts.detachKeys } options := types.ContainerAttachOptions{ @@ -202,10 +200,10 @@ func (cli *DockerCli) CmdRun(args ...string) error { Stdin: config.AttachStdin, Stdout: config.AttachStdout, Stderr: config.AttachStderr, - DetachKeys: cli.configFile.DetachKeys, + DetachKeys: dockerCli.ConfigFile().DetachKeys, } - resp, errAttach := cli.client.ContainerAttach(ctx, createResponse.ID, options) + resp, errAttach := client.ContainerAttach(ctx, createResponse.ID, options) if errAttach != nil && errAttach != httputil.ErrPersistEOF { // ContainerAttach returns an ErrPersistEOF (connection closed) // means server met an error and put it in Hijacked connection @@ -215,7 +213,7 @@ func (cli *DockerCli) CmdRun(args ...string) error { defer resp.Close() errCh = promise.Go(func() error { - errHijack := cli.holdHijackedConnection(ctx, config.Tty, in, out, stderr, resp) + errHijack := dockerCli.HoldHijackedConnection(ctx, config.Tty, in, out, cerr, resp) if errHijack == nil { return errAttach } @@ -223,18 +221,18 @@ func (cli *DockerCli) CmdRun(args ...string) error { }) } - if *flAutoRemove { + if opts.autoRemove { defer func() { // Explicitly not sharing the context as it could be "Done" (by calling cancelFun) // and thus the container would not be removed. - if err := cli.removeContainer(context.Background(), createResponse.ID, true, false, true); err != nil { - fmt.Fprintf(cli.err, "%v\n", err) + if err := dockerCli.RemoveContainer(context.Background(), createResponse.ID, true, false, true); err != nil { + fmt.Fprintf(stderr, "%v\n", err) } }() } //start the container - if err := cli.client.ContainerStart(ctx, createResponse.ID, types.ContainerStartOptions{}); err != nil { + if err := client.ContainerStart(ctx, createResponse.ID, types.ContainerStartOptions{}); err != nil { // If we have holdHijackedConnection, we should notify // holdHijackedConnection we are going to exit and wait // to avoid the terminal are not restored. @@ -243,13 +241,13 @@ func (cli *DockerCli) CmdRun(args ...string) error { <-errCh } - cmd.ReportError(err.Error(), false) + reportError(stderr, cmdPath, err.Error(), false) return runStartContainerErr(err) } - if (config.AttachStdin || config.AttachStdout || config.AttachStderr) && config.Tty && cli.isTerminalOut { - if err := cli.monitorTtySize(ctx, createResponse.ID, false); err != nil { - fmt.Fprintf(cli.err, "Error monitoring TTY size: %s\n", err) + if (config.AttachStdin || config.AttachStdout || config.AttachStderr) && config.Tty && dockerCli.IsTerminalOut() { + if err := dockerCli.MonitorTtySize(ctx, createResponse.ID, false); err != nil { + fmt.Fprintf(stderr, "Error monitoring TTY size: %s\n", err) } } @@ -270,32 +268,58 @@ func (cli *DockerCli) CmdRun(args ...string) error { var status int // Attached mode - if *flAutoRemove { + if opts.autoRemove { // Autoremove: wait for the container to finish, retrieve // the exit code and remove the container - if status, err = cli.client.ContainerWait(ctx, createResponse.ID); err != nil { + if status, err = client.ContainerWait(ctx, createResponse.ID); err != nil { return runStartContainerErr(err) } - if _, status, err = cli.getExitCode(ctx, createResponse.ID); err != nil { + if _, status, err = dockerCli.GetExitCode(ctx, 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 = cli.client.ContainerWait(ctx, createResponse.ID); err != nil { + if status, err = client.ContainerWait(ctx, 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 = cli.getExitCode(ctx, createResponse.ID); err != nil { + if _, status, err = dockerCli.GetExitCode(ctx, createResponse.ID); err != nil { return err } } } if status != 0 { - return Cli.StatusError{StatusCode: status} + return cli.StatusError{StatusCode: status} } return 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) { + if withHelp { + str += ".\nSee '" + os.Args[0] + " " + name + " --help'" + } + fmt.Fprintf(stderr, "%s: %s.\n", os.Args[0], str) +} + +// 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.TrimPrefix(err.Error(), "Error response from daemon: ") + statusError := cli.StatusError{StatusCode: 125} + if strings.HasPrefix(trimmedErr, "Container command") { + if strings.Contains(trimmedErr, errCmdNotFound) { + statusError = cli.StatusError{StatusCode: 127} + } else if strings.Contains(trimmedErr, errCmdCouldNotBeInvoked) { + statusError = cli.StatusError{StatusCode: 126} + } + } + + return statusError +} diff --git a/api/client/create.go b/api/client/create.go index c81d6fb3c1..eac1a7f2a9 100644 --- a/api/client/create.go +++ b/api/client/create.go @@ -12,7 +12,7 @@ import ( // FIXME migrate to docker/distribution/reference "github.com/docker/docker/reference" "github.com/docker/docker/registry" - runconfigopts "github.com/docker/docker/runconfig/opts" + //runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/docker/engine-api/client" "github.com/docker/engine-api/types" "github.com/docker/engine-api/types/container" @@ -56,6 +56,26 @@ type cidFile struct { written bool } +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 +} + func newCIDFile(path string) (*cidFile, error) { if _, err := os.Stat(path); err == nil { return nil, fmt.Errorf("Container ID file found, make sure the other container isn't running or delete %s", path) @@ -69,7 +89,10 @@ func newCIDFile(path string) (*cidFile, error) { return &cidFile{path: path, file: f}, nil } -func (cli *DockerCli) createContainer(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *networktypes.NetworkingConfig, cidfile, name string) (*types.ContainerCreateResponse, error) { +// CreateContainer creates a container from a config +// TODO: this can be unexported again once all container commands are under +// api/client/container +func (cli *DockerCli) CreateContainer(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *networktypes.NetworkingConfig, cidfile, name string) (*types.ContainerCreateResponse, error) { var containerIDFile *cidFile if cidfile != "" { var err error @@ -143,25 +166,26 @@ func (cli *DockerCli) CmdCreate(args ...string) error { cmd := Cli.Subcmd("create", []string{"IMAGE [COMMAND] [ARG...]"}, Cli.DockerCommands["create"].Description, true) addTrustedFlags(cmd, true) + // TODO: tmp disable for PoC, convert to cobra and pflag later // These are flags not stored in Config/HostConfig - var ( - flName = cmd.String([]string{"-name"}, "", "Assign a name to the container") - ) + // var ( + // flName = cmd.String([]string{"-name"}, "", "Assign a name to the container") + // ) - config, hostConfig, networkingConfig, cmd, err := runconfigopts.Parse(cmd, args) - - if err != nil { - cmd.ReportError(err.Error(), true) - os.Exit(1) - } - if config.Image == "" { - cmd.Usage() - return nil - } - response, err := cli.createContainer(context.Background(), config, hostConfig, networkingConfig, hostConfig.ContainerIDFile, *flName) - if err != nil { - return err - } - fmt.Fprintf(cli.out, "%s\n", response.ID) + // config, hostConfig, networkingConfig, cmd, err := runconfigopts.Parse(cmd, args) + // + // if err != nil { + // cmd.ReportError(err.Error(), true) + // os.Exit(1) + // } + // if config.Image == "" { + // cmd.Usage() + // return nil + // } + // response, err := cli.CreateContainer(context.Background(), config, hostConfig, networkingConfig, hostConfig.ContainerIDFile, *flName) + // if err != nil { + // return err + // } + // fmt.Fprintf(cli.out, "%s\n", response.ID) return nil } diff --git a/api/client/exec.go b/api/client/exec.go index dd74780898..2aac0d606a 100644 --- a/api/client/exec.go +++ b/api/client/exec.go @@ -93,11 +93,11 @@ func (cli *DockerCli) CmdExec(args ...string) error { } defer resp.Close() errCh = promise.Go(func() error { - return cli.holdHijackedConnection(ctx, execConfig.Tty, in, out, stderr, resp) + return cli.HoldHijackedConnection(ctx, execConfig.Tty, in, out, stderr, resp) }) if execConfig.Tty && cli.isTerminalIn { - if err := cli.monitorTtySize(ctx, execID, true); err != nil { + if err := cli.MonitorTtySize(ctx, execID, true); err != nil { fmt.Fprintf(cli.err, "Error monitoring TTY size: %s\n", err) } } diff --git a/api/client/hijack.go b/api/client/hijack.go index 38bd6a7afb..7a7703d584 100644 --- a/api/client/hijack.go +++ b/api/client/hijack.go @@ -11,7 +11,8 @@ import ( "github.com/docker/engine-api/types" ) -func (cli *DockerCli) holdHijackedConnection(ctx context.Context, tty bool, inputStream io.ReadCloser, outputStream, errorStream io.Writer, resp types.HijackedResponse) error { +// HoldHijackedConnection ... TODO docstring +func (cli *DockerCli) HoldHijackedConnection(ctx context.Context, tty bool, inputStream io.ReadCloser, outputStream, errorStream io.Writer, resp types.HijackedResponse) error { var ( err error restoreOnce sync.Once diff --git a/api/client/rm.go b/api/client/rm.go index 77d70d5159..f94460d964 100644 --- a/api/client/rm.go +++ b/api/client/rm.go @@ -32,7 +32,7 @@ func (cli *DockerCli) CmdRm(args ...string) error { } name = strings.Trim(name, "/") - if err := cli.removeContainer(ctx, name, *v, *link, *force); err != nil { + if err := cli.RemoveContainer(ctx, name, *v, *link, *force); err != nil { errs = append(errs, err.Error()) } else { fmt.Fprintf(cli.out, "%s\n", name) @@ -44,7 +44,10 @@ func (cli *DockerCli) CmdRm(args ...string) error { return nil } -func (cli *DockerCli) removeContainer(ctx context.Context, container string, removeVolumes, removeLinks, force bool) error { +// RemoveContainer removes a container +// TODO: this can be unexported again once all container commands are under +// api/client/container +func (cli *DockerCli) RemoveContainer(ctx context.Context, container string, removeVolumes, removeLinks, force bool) error { options := types.ContainerRemoveOptions{ RemoveVolumes: removeVolumes, RemoveLinks: removeLinks, diff --git a/api/client/start.go b/api/client/start.go index 2625f75025..3b4dc08e5f 100644 --- a/api/client/start.go +++ b/api/client/start.go @@ -17,7 +17,10 @@ import ( "github.com/docker/engine-api/types" ) -func (cli *DockerCli) forwardAllSignals(ctx context.Context, cid string) chan os.Signal { +// ForwardAllSignals forwards signals to the contianer +// TODO: this can be unexported again once all container commands are under +// api/client/container +func (cli *DockerCli) ForwardAllSignals(ctx context.Context, cid string) chan os.Signal { sigc := make(chan os.Signal, 128) signal.CatchAll(sigc) go func() { @@ -74,7 +77,7 @@ func (cli *DockerCli) CmdStart(args ...string) error { } if !c.Config.Tty { - sigc := cli.forwardAllSignals(ctx, container) + sigc := cli.ForwardAllSignals(ctx, container) defer signal.StopCatch(sigc) } @@ -105,7 +108,7 @@ func (cli *DockerCli) CmdStart(args ...string) error { } defer resp.Close() cErr := promise.Go(func() error { - errHijack := cli.holdHijackedConnection(ctx, c.Config.Tty, in, cli.out, cli.err, resp) + errHijack := cli.HoldHijackedConnection(ctx, c.Config.Tty, in, cli.out, cli.err, resp) if errHijack == nil { return errAttach } @@ -121,14 +124,14 @@ func (cli *DockerCli) CmdStart(args ...string) error { // 4. Wait for attachment to break. if c.Config.Tty && cli.isTerminalOut { - if err := cli.monitorTtySize(ctx, container, false); err != nil { + if err := cli.MonitorTtySize(ctx, container, false); err != nil { fmt.Fprintf(cli.err, "Error monitoring TTY size: %s\n", err) } } if attchErr := <-cErr; attchErr != nil { return attchErr } - _, status, err := cli.getExitCode(ctx, container) + _, status, err := cli.GetExitCode(ctx, container) if err != nil { return err } diff --git a/api/client/trust.go b/api/client/trust.go index 87b7ce5658..6f2a3f6090 100644 --- a/api/client/trust.go +++ b/api/client/trust.go @@ -37,6 +37,7 @@ import ( "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/signed" "github.com/docker/notary/tuf/store" + "github.com/spf13/pflag" ) var ( @@ -44,7 +45,20 @@ var ( untrusted bool ) +// TODO: tmp workaround to get this PoC working, change everything to use +// exported version func addTrustedFlags(fs *flag.FlagSet, verify bool) { + trusted, message := setupTrustedFlag(verify) + fs.BoolVar(&untrusted, []string{"-disable-content-trust"}, !trusted, message) +} + +// AddTrustedFlags adds the trust flags to a FlagSet +func AddTrustedFlags(fs *pflag.FlagSet, verify bool) { + trusted, message := setupTrustedFlag(verify) + fs.BoolVar(&untrusted, "disable-content-trust", !trusted, message) +} + +func setupTrustedFlag(verify bool) (bool, string) { var trusted bool if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" { if t, err := strconv.ParseBool(e); t || err != nil { @@ -56,7 +70,7 @@ func addTrustedFlags(fs *flag.FlagSet, verify bool) { if verify { message = "Skip image verification" } - fs.BoolVar(&untrusted, []string{"-disable-content-trust"}, !trusted, message) + return trusted, message } func isTrusted() bool { diff --git a/api/client/utils.go b/api/client/utils.go index 455b5d1afd..5927499dd7 100644 --- a/api/client/utils.go +++ b/api/client/utils.go @@ -61,7 +61,7 @@ func (cli *DockerCli) RegistryAuthenticationPrivilegedFunc(index *registrytypes. } func (cli *DockerCli) resizeTty(ctx context.Context, id string, isExec bool) { - height, width := cli.getTtySize() + height, width := cli.GetTtySize() cli.resizeTtyTo(ctx, id, height, width, isExec) } @@ -87,9 +87,9 @@ func (cli *DockerCli) resizeTtyTo(ctx context.Context, id string, height, width } } -// getExitCode perform an inspect on the container. It returns +// GetExitCode perform an inspect on the container. It returns // the running state and the exit code. -func (cli *DockerCli) getExitCode(ctx context.Context, containerID string) (bool, int, error) { +func (cli *DockerCli) GetExitCode(ctx context.Context, containerID string) (bool, int, error) { c, err := cli.client.ContainerInspect(ctx, containerID) if err != nil { // If we can't connect, then the daemon probably died. @@ -117,15 +117,16 @@ func (cli *DockerCli) getExecExitCode(ctx context.Context, execID string) (bool, return resp.Running, resp.ExitCode, nil } -func (cli *DockerCli) monitorTtySize(ctx context.Context, id string, isExec bool) error { +// MonitorTtySize updates the container tty size when the terminal tty changes size +func (cli *DockerCli) MonitorTtySize(ctx context.Context, id string, isExec bool) error { cli.resizeTty(ctx, id, isExec) if runtime.GOOS == "windows" { go func() { - prevH, prevW := cli.getTtySize() + prevH, prevW := cli.GetTtySize() for { time.Sleep(time.Millisecond * 250) - h, w := cli.getTtySize() + h, w := cli.GetTtySize() if prevW != w || prevH != h { cli.resizeTty(ctx, id, isExec) @@ -146,7 +147,8 @@ func (cli *DockerCli) monitorTtySize(ctx context.Context, id string, isExec bool return nil } -func (cli *DockerCli) getTtySize() (int, int) { +// GetTtySize returns the height and width in characters of the tty +func (cli *DockerCli) GetTtySize() (int, int) { if !cli.isTerminalOut { return 0, 0 } diff --git a/cli/cobraadaptor/adaptor.go b/cli/cobraadaptor/adaptor.go index 633844a12a..7e6327ac2d 100644 --- a/cli/cobraadaptor/adaptor.go +++ b/cli/cobraadaptor/adaptor.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/docker/docker/api/client" + "github.com/docker/docker/api/client/container" "github.com/docker/docker/api/client/image" "github.com/docker/docker/api/client/volume" "github.com/docker/docker/cli" @@ -34,8 +35,9 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { rootCmd.SetFlagErrorFunc(flagErrorFunc) rootCmd.SetOutput(stdout) rootCmd.AddCommand( - volume.NewVolumeCommand(dockerCli), + container.NewRunCommand(dockerCli), image.NewSearchCommand(dockerCli), + volume.NewVolumeCommand(dockerCli), ) rootCmd.PersistentFlags().BoolP("help", "h", false, "Print usage") @@ -52,7 +54,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { func (c CobraAdaptor) Usage() []cli.Command { cmds := []cli.Command{} for _, cmd := range c.rootCmd.Commands() { - cmds = append(cmds, cli.Command{Name: cmd.Use, Description: cmd.Short}) + cmds = append(cmds, cli.Command{Name: cmd.Name(), Description: cmd.Short}) } return cmds } diff --git a/cli/usage.go b/cli/usage.go index 98d7fdf441..324d1d92bf 100644 --- a/cli/usage.go +++ b/cli/usage.go @@ -37,7 +37,6 @@ var DockerCommandUsage = []Command{ {"restart", "Restart a container"}, {"rm", "Remove one or more containers"}, {"rmi", "Remove one or more images"}, - {"run", "Run a command in a new container"}, {"save", "Save one or more images to a tar archive"}, {"start", "Start one or more stopped containers"}, {"stats", "Display a live stream of container(s) resource usage statistics"}, diff --git a/runconfig/opts/parse.go b/runconfig/opts/parse.go index c2e009eaa6..160e9c681f 100644 --- a/runconfig/opts/parse.go +++ b/runconfig/opts/parse.go @@ -8,160 +8,245 @@ import ( "path" "strconv" "strings" + "time" "github.com/docker/docker/opts" - flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/pkg/mount" "github.com/docker/docker/pkg/signal" "github.com/docker/engine-api/types/container" networktypes "github.com/docker/engine-api/types/network" "github.com/docker/engine-api/types/strslice" "github.com/docker/go-connections/nat" - "github.com/docker/go-units" + units "github.com/docker/go-units" + "github.com/spf13/pflag" ) -// Parse parses the specified args for the specified command and generates a Config, -// a HostConfig and returns them with the specified command. -// If the specified args are not valid, it will return an error. -func Parse(cmd *flag.FlagSet, args []string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, *flag.FlagSet, error) { - var ( - // FIXME: use utils.ListOpts for attach and volumes? - flAttach = opts.NewListOpts(ValidateAttach) - flVolumes = opts.NewListOpts(nil) - flTmpfs = opts.NewListOpts(nil) - flBlkioWeightDevice = NewWeightdeviceOpt(ValidateWeightDevice) - flDeviceReadBps = NewThrottledeviceOpt(ValidateThrottleBpsDevice) - flDeviceWriteBps = NewThrottledeviceOpt(ValidateThrottleBpsDevice) - flLinks = opts.NewListOpts(ValidateLink) - flAliases = opts.NewListOpts(nil) - flDeviceReadIOps = NewThrottledeviceOpt(ValidateThrottleIOpsDevice) - flDeviceWriteIOps = NewThrottledeviceOpt(ValidateThrottleIOpsDevice) - flEnv = opts.NewListOpts(ValidateEnv) - flLabels = opts.NewListOpts(ValidateEnv) - flDevices = opts.NewListOpts(ValidateDevice) +// ContainerOptions is a data object with all the options for creating a container +// TODO: remove fl prefix +type ContainerOptions struct { + flAttach opts.ListOpts + flVolumes opts.ListOpts + flTmpfs opts.ListOpts + flBlkioWeightDevice WeightdeviceOpt + flDeviceReadBps ThrottledeviceOpt + flDeviceWriteBps ThrottledeviceOpt + flLinks opts.ListOpts + flAliases opts.ListOpts + flDeviceReadIOps ThrottledeviceOpt + flDeviceWriteIOps ThrottledeviceOpt + flEnv opts.ListOpts + flLabels opts.ListOpts + flDevices opts.ListOpts + flUlimits *UlimitOpt + flSysctls *opts.MapOpts + flPublish opts.ListOpts + flExpose opts.ListOpts + flDNS opts.ListOpts + flDNSSearch opts.ListOpts + flDNSOptions opts.ListOpts + flExtraHosts opts.ListOpts + flVolumesFrom opts.ListOpts + flEnvFile opts.ListOpts + flCapAdd opts.ListOpts + flCapDrop opts.ListOpts + flGroupAdd opts.ListOpts + flSecurityOpt opts.ListOpts + flStorageOpt opts.ListOpts + flLabelsFile opts.ListOpts + flLoggingOpts opts.ListOpts + flPrivileged *bool + flPidMode *string + flUTSMode *string + flUsernsMode *string + flPublishAll *bool + flStdin *bool + flTty *bool + flOomKillDisable *bool + flOomScoreAdj *int + flContainerIDFile *string + flEntrypoint *string + flHostname *string + flMemoryString *string + flMemoryReservation *string + flMemorySwap *string + flKernelMemory *string + flUser *string + flWorkingDir *string + flCPUShares *int64 + flCPUPercent *int64 + flCPUPeriod *int64 + flCPUQuota *int64 + flCpusetCpus *string + flCpusetMems *string + flBlkioWeight *uint16 + flIOMaxBandwidth *string + flIOMaxIOps *uint64 + flSwappiness *int64 + flNetMode *string + flMacAddress *string + flIPv4Address *string + flIPv6Address *string + flIpcMode *string + flPidsLimit *int64 + flRestartPolicy *string + flReadonlyRootfs *bool + flLoggingDriver *string + flCgroupParent *string + flVolumeDriver *string + flStopSignal *string + flIsolation *string + flShmSize *string + flNoHealthcheck *bool + flHealthCmd *string + flHealthInterval *time.Duration + flHealthTimeout *time.Duration + flHealthRetries *int - flUlimits = NewUlimitOpt(nil) - flSysctls = opts.NewMapOpts(nil, opts.ValidateSysctl) + Image string + Args []string +} - flPublish = opts.NewListOpts(nil) - flExpose = opts.NewListOpts(nil) - flDNS = opts.NewListOpts(opts.ValidateIPAddress) - flDNSSearch = opts.NewListOpts(opts.ValidateDNSSearch) - flDNSOptions = opts.NewListOpts(nil) - flExtraHosts = opts.NewListOpts(ValidateExtraHost) - flVolumesFrom = opts.NewListOpts(nil) - flEnvFile = opts.NewListOpts(nil) - flCapAdd = opts.NewListOpts(nil) - flCapDrop = opts.NewListOpts(nil) - flGroupAdd = opts.NewListOpts(nil) - flSecurityOpt = opts.NewListOpts(nil) - flStorageOpt = opts.NewListOpts(nil) - flLabelsFile = opts.NewListOpts(nil) - flLoggingOpts = opts.NewListOpts(nil) - flPrivileged = cmd.Bool([]string{"-privileged"}, false, "Give extended privileges to this container") - flPidMode = cmd.String([]string{"-pid"}, "", "PID namespace to use") - flUTSMode = cmd.String([]string{"-uts"}, "", "UTS namespace to use") - flUsernsMode = cmd.String([]string{"-userns"}, "", "User namespace to use") - flPublishAll = cmd.Bool([]string{"P", "-publish-all"}, false, "Publish all exposed ports to random ports") - flStdin = cmd.Bool([]string{"i", "-interactive"}, false, "Keep STDIN open even if not attached") - flTty = cmd.Bool([]string{"t", "-tty"}, false, "Allocate a pseudo-TTY") - flOomKillDisable = cmd.Bool([]string{"-oom-kill-disable"}, false, "Disable OOM Killer") - flOomScoreAdj = cmd.Int([]string{"-oom-score-adj"}, 0, "Tune host's OOM preferences (-1000 to 1000)") - flContainerIDFile = cmd.String([]string{"-cidfile"}, "", "Write the container ID to the file") - flEntrypoint = cmd.String([]string{"-entrypoint"}, "", "Overwrite the default ENTRYPOINT of the image") - flHostname = cmd.String([]string{"h", "-hostname"}, "", "Container host name") - flMemoryString = cmd.String([]string{"m", "-memory"}, "", "Memory limit") - flMemoryReservation = cmd.String([]string{"-memory-reservation"}, "", "Memory soft limit") - flMemorySwap = cmd.String([]string{"-memory-swap"}, "", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") - flKernelMemory = cmd.String([]string{"-kernel-memory"}, "", "Kernel memory limit") - flUser = cmd.String([]string{"u", "-user"}, "", "Username or UID (format: [:])") - flWorkingDir = cmd.String([]string{"w", "-workdir"}, "", "Working directory inside the container") - flCPUShares = cmd.Int64([]string{"c", "-cpu-shares"}, 0, "CPU shares (relative weight)") - flCPUPercent = cmd.Int64([]string{"-cpu-percent"}, 0, "CPU percent (Windows only)") - flCPUPeriod = cmd.Int64([]string{"-cpu-period"}, 0, "Limit CPU CFS (Completely Fair Scheduler) period") - flCPUQuota = cmd.Int64([]string{"-cpu-quota"}, 0, "Limit CPU CFS (Completely Fair Scheduler) quota") - flCpusetCpus = cmd.String([]string{"-cpuset-cpus"}, "", "CPUs in which to allow execution (0-3, 0,1)") - flCpusetMems = cmd.String([]string{"-cpuset-mems"}, "", "MEMs in which to allow execution (0-3, 0,1)") - flBlkioWeight = cmd.Uint16([]string{"-blkio-weight"}, 0, "Block IO (relative weight), between 10 and 1000") - flIOMaxBandwidth = cmd.String([]string{"-io-maxbandwidth"}, "", "Maximum IO bandwidth limit for the system drive (Windows only)") - flIOMaxIOps = cmd.Uint64([]string{"-io-maxiops"}, 0, "Maximum IOps limit for the system drive (Windows only)") - flSwappiness = cmd.Int64([]string{"-memory-swappiness"}, -1, "Tune container memory swappiness (0 to 100)") - flNetMode = cmd.String([]string{"-net"}, "default", "Connect a container to a network") - flMacAddress = cmd.String([]string{"-mac-address"}, "", "Container MAC address (e.g. 92:d0:c6:0a:29:33)") - flIPv4Address = cmd.String([]string{"-ip"}, "", "Container IPv4 address (e.g. 172.30.100.104)") - flIPv6Address = cmd.String([]string{"-ip6"}, "", "Container IPv6 address (e.g. 2001:db8::33)") - flIpcMode = cmd.String([]string{"-ipc"}, "", "IPC namespace to use") - flPidsLimit = cmd.Int64([]string{"-pids-limit"}, 0, "Tune container pids limit (set -1 for unlimited)") - flRestartPolicy = cmd.String([]string{"-restart"}, "no", "Restart policy to apply when a container exits") - flReadonlyRootfs = cmd.Bool([]string{"-read-only"}, false, "Mount the container's root filesystem as read only") - flLoggingDriver = cmd.String([]string{"-log-driver"}, "", "Logging driver for container") - flCgroupParent = cmd.String([]string{"-cgroup-parent"}, "", "Optional parent cgroup for the container") - flVolumeDriver = cmd.String([]string{"-volume-driver"}, "", "Optional volume driver for the container") - flStopSignal = cmd.String([]string{"-stop-signal"}, signal.DefaultStopSignal, fmt.Sprintf("Signal to stop a container, %v by default", signal.DefaultStopSignal)) - flIsolation = cmd.String([]string{"-isolation"}, "", "Container isolation technology") - flShmSize = cmd.String([]string{"-shm-size"}, "", "Size of /dev/shm, default value is 64MB") - // Healthcheck - flNoHealthcheck = cmd.Bool([]string{"-no-healthcheck"}, false, "Disable any container-specified HEALTHCHECK") - flHealthCmd = cmd.String([]string{"-health-cmd"}, "", "Command to run to check health") - flHealthInterval = cmd.Duration([]string{"-health-interval"}, 0, "Time between running the check") - flHealthTimeout = cmd.Duration([]string{"-health-timeout"}, 0, "Maximum time to allow one check to run") - flHealthRetries = cmd.Int([]string{"-health-retries"}, 0, "Consecutive failures needed to report unhealthy") - ) +// AddFlags adds all command line flags that will be used by Parse to the FlagSet +func AddFlags(flags *pflag.FlagSet) *ContainerOptions { + copts := &ContainerOptions{ + flAttach: opts.NewListOpts(ValidateAttach), + flVolumes: opts.NewListOpts(nil), + flTmpfs: opts.NewListOpts(nil), + flBlkioWeightDevice: NewWeightdeviceOpt(ValidateWeightDevice), + flDeviceReadBps: NewThrottledeviceOpt(ValidateThrottleBpsDevice), + flDeviceWriteBps: NewThrottledeviceOpt(ValidateThrottleBpsDevice), + flLinks: opts.NewListOpts(ValidateLink), + flAliases: opts.NewListOpts(nil), + flDeviceReadIOps: NewThrottledeviceOpt(ValidateThrottleIOpsDevice), + flDeviceWriteIOps: NewThrottledeviceOpt(ValidateThrottleIOpsDevice), + flEnv: opts.NewListOpts(ValidateEnv), + flLabels: opts.NewListOpts(ValidateEnv), + flDevices: opts.NewListOpts(ValidateDevice), - cmd.Var(&flAttach, []string{"a", "-attach"}, "Attach to STDIN, STDOUT or STDERR") - cmd.Var(&flBlkioWeightDevice, []string{"-blkio-weight-device"}, "Block IO weight (relative device weight)") - cmd.Var(&flDeviceReadBps, []string{"-device-read-bps"}, "Limit read rate (bytes per second) from a device") - cmd.Var(&flDeviceWriteBps, []string{"-device-write-bps"}, "Limit write rate (bytes per second) to a device") - cmd.Var(&flDeviceReadIOps, []string{"-device-read-iops"}, "Limit read rate (IO per second) from a device") - cmd.Var(&flDeviceWriteIOps, []string{"-device-write-iops"}, "Limit write rate (IO per second) to a device") - cmd.Var(&flVolumes, []string{"v", "-volume"}, "Bind mount a volume") - cmd.Var(&flTmpfs, []string{"-tmpfs"}, "Mount a tmpfs directory") - cmd.Var(&flLinks, []string{"-link"}, "Add link to another container") - cmd.Var(&flAliases, []string{"-net-alias"}, "Add network-scoped alias for the container") - cmd.Var(&flDevices, []string{"-device"}, "Add a host device to the container") - cmd.Var(&flLabels, []string{"l", "-label"}, "Set meta data on a container") - cmd.Var(&flLabelsFile, []string{"-label-file"}, "Read in a line delimited file of labels") - cmd.Var(&flEnv, []string{"e", "-env"}, "Set environment variables") - cmd.Var(&flEnvFile, []string{"-env-file"}, "Read in a file of environment variables") - cmd.Var(&flPublish, []string{"p", "-publish"}, "Publish a container's port(s) to the host") - cmd.Var(&flExpose, []string{"-expose"}, "Expose a port or a range of ports") - cmd.Var(&flDNS, []string{"-dns"}, "Set custom DNS servers") - cmd.Var(&flDNSSearch, []string{"-dns-search"}, "Set custom DNS search domains") - cmd.Var(&flDNSOptions, []string{"-dns-opt"}, "Set DNS options") - cmd.Var(&flExtraHosts, []string{"-add-host"}, "Add a custom host-to-IP mapping (host:ip)") - cmd.Var(&flVolumesFrom, []string{"-volumes-from"}, "Mount volumes from the specified container(s)") - cmd.Var(&flCapAdd, []string{"-cap-add"}, "Add Linux capabilities") - cmd.Var(&flCapDrop, []string{"-cap-drop"}, "Drop Linux capabilities") - cmd.Var(&flGroupAdd, []string{"-group-add"}, "Add additional groups to join") - cmd.Var(&flSecurityOpt, []string{"-security-opt"}, "Security Options") - cmd.Var(&flStorageOpt, []string{"-storage-opt"}, "Set storage driver options per container") - cmd.Var(flUlimits, []string{"-ulimit"}, "Ulimit options") - cmd.Var(flSysctls, []string{"-sysctl"}, "Sysctl options") - cmd.Var(&flLoggingOpts, []string{"-log-opt"}, "Log driver options") + flUlimits: NewUlimitOpt(nil), + flSysctls: opts.NewMapOpts(nil, opts.ValidateSysctl), - cmd.Require(flag.Min, 1) + flPublish: opts.NewListOpts(nil), + flExpose: opts.NewListOpts(nil), + flDNS: opts.NewListOpts(opts.ValidateIPAddress), + flDNSSearch: opts.NewListOpts(opts.ValidateDNSSearch), + flDNSOptions: opts.NewListOpts(nil), + flExtraHosts: opts.NewListOpts(ValidateExtraHost), + flVolumesFrom: opts.NewListOpts(nil), + flEnvFile: opts.NewListOpts(nil), + flCapAdd: opts.NewListOpts(nil), + flCapDrop: opts.NewListOpts(nil), + flGroupAdd: opts.NewListOpts(nil), + flSecurityOpt: opts.NewListOpts(nil), + flStorageOpt: opts.NewListOpts(nil), + flLabelsFile: opts.NewListOpts(nil), + flLoggingOpts: opts.NewListOpts(nil), - if err := cmd.ParseFlags(args, true); err != nil { - return nil, nil, nil, cmd, err + flPrivileged: flags.Bool("privileged", false, "Give extended privileges to this container"), + flPidMode: flags.String("pid", "", "PID namespace to use"), + flUTSMode: flags.String("uts", "", "UTS namespace to use"), + flUsernsMode: flags.String("userns", "", "User namespace to use"), + flPublishAll: flags.BoolP("publish-all", "P", false, "Publish all exposed ports to random ports"), + flStdin: flags.BoolP("interactive", "i", false, "Keep STDIN open even if not attached"), + flTty: flags.BoolP("tty", "t", false, "Allocate a pseudo-TTY"), + flOomKillDisable: flags.Bool("oom-kill-disable", false, "Disable OOM Killer"), + flOomScoreAdj: flags.Int("oom-score-adj", 0, "Tune host's OOM preferences (-1000 to 1000)"), + flContainerIDFile: flags.String("cidfile", "", "Write the container ID to the file"), + flEntrypoint: flags.String("entrypoint", "", "Overwrite the default ENTRYPOINT of the image"), + flHostname: flags.StringP("hostname", "h", "", "Container host name"), + flMemoryString: flags.StringP("memory", "m", "", "Memory limit"), + flMemoryReservation: flags.String("memory-reservation", "", "Memory soft limit"), + flMemorySwap: flags.String("memory-swap", "", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap"), + flKernelMemory: flags.String("kernel-memory", "", "Kernel memory limit"), + flUser: flags.StringP("user", "u", "", "Username or UID (format: [:])"), + flWorkingDir: flags.StringP("workdir", "w", "", "Working directory inside the container"), + flCPUShares: flags.Int64P("cpu-shares", "c", 0, "CPU shares (relative weight)"), + flCPUPercent: flags.Int64("cpu-percent", 0, "CPU percent (Windows only)"), + flCPUPeriod: flags.Int64("cpu-period", 0, "Limit CPU CFS (Completely Fair Scheduler) period"), + flCPUQuota: flags.Int64("cpu-quota", 0, "Limit CPU CFS (Completely Fair Scheduler) quota"), + flCpusetCpus: flags.String("cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)"), + flCpusetMems: flags.String("cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)"), + flBlkioWeight: flags.Uint16("blkio-weight", 0, "Block IO (relative weight), between 10 and 1000"), + flIOMaxBandwidth: flags.String("io-maxbandwidth", "", "Maximum IO bandwidth limit for the system drive (Windows only)"), + flIOMaxIOps: flags.Uint64("io-maxiops", 0, "Maximum IOps limit for the system drive (Windows only)"), + flSwappiness: flags.Int64("memory-swappiness", -1, "Tune container memory swappiness (0 to 100)"), + flNetMode: flags.String("net", "default", "Connect a container to a network"), + flMacAddress: flags.String("mac-address", "", "Container MAC address (e.g. 92:d0:c6:0a:29:33)"), + flIPv4Address: flags.String("ip", "", "Container IPv4 address (e.g. 172.30.100.104)"), + flIPv6Address: flags.String("ip6", "", "Container IPv6 address (e.g. 2001:db8::33)"), + flIpcMode: flags.String("ipc", "", "IPC namespace to use"), + flPidsLimit: flags.Int64("pids-limit", 0, "Tune container pids limit (set -1 for unlimited)"), + flRestartPolicy: flags.String("restart", "no", "Restart policy to apply when a container exits"), + flReadonlyRootfs: flags.Bool("read-only", false, "Mount the container's root filesystem as read only"), + flLoggingDriver: flags.String("log-driver", "", "Logging driver for container"), + flCgroupParent: flags.String("cgroup-parent", "", "Optional parent cgroup for the container"), + flVolumeDriver: flags.String("volume-driver", "", "Optional volume driver for the container"), + flStopSignal: flags.String("stop-signal", signal.DefaultStopSignal, fmt.Sprintf("Signal to stop a container, %v by default", signal.DefaultStopSignal)), + flIsolation: flags.String("isolation", "", "Container isolation technology"), + flShmSize: flags.String("shm-size", "", "Size of /dev/shm, default value is 64MB"), + flNoHealthcheck: cmd.Bool([]string{"-no-healthcheck"}, false, "Disable any container-specified HEALTHCHECK"), + flHealthCmd: cmd.String([]string{"-health-cmd"}, "", "Command to run to check health"), + flHealthInterval: cmd.Duration([]string{"-health-interval"}, 0, "Time between running the check"), + flHealthTimeout: cmd.Duration([]string{"-health-timeout"}, 0, "Maximum time to allow one check to run"), + flHealthRetries: cmd.Int([]string{"-health-retries"}, 0, "Consecutive failures needed to report unhealthy"), } + flags.VarP(&copts.flAttach, "attach", "a", "Attach to STDIN, STDOUT or STDERR") + flags.Var(&copts.flBlkioWeightDevice, "blkio-weight-device", "Block IO weight (relative device weight)") + flags.Var(&copts.flDeviceReadBps, "device-read-bps", "Limit read rate (bytes per second) from a device") + flags.Var(&copts.flDeviceWriteBps, "device-write-bps", "Limit write rate (bytes per second) to a device") + flags.Var(&copts.flDeviceReadIOps, "device-read-iops", "Limit read rate (IO per second) from a device") + flags.Var(&copts.flDeviceWriteIOps, "device-write-iops", "Limit write rate (IO per second) to a device") + flags.VarP(&copts.flVolumes, "volume", "v", "Bind mount a volume") + flags.Var(&copts.flTmpfs, "tmpfs", "Mount a tmpfs directory") + flags.Var(&copts.flLinks, "link", "Add link to another container") + flags.Var(&copts.flAliases, "net-alias", "Add network-scoped alias for the container") + flags.Var(&copts.flDevices, "device", "Add a host device to the container") + flags.VarP(&copts.flLabels, "label", "l", "Set meta data on a container") + flags.Var(&copts.flLabelsFile, "label-file", "Read in a line delimited file of labels") + flags.VarP(&copts.flEnv, "env", "e", "Set environment variables") + flags.Var(&copts.flEnvFile, "env-file", "Read in a file of environment variables") + flags.VarP(&copts.flPublish, "publish", "p", "Publish a container's port(s) to the host") + flags.Var(&copts.flExpose, "expose", "Expose a port or a range of ports") + flags.Var(&copts.flDNS, "dns", "Set custom DNS servers") + flags.Var(&copts.flDNSSearch, "dns-search", "Set custom DNS search domains") + flags.Var(&copts.flDNSOptions, "dns-opt", "Set DNS options") + flags.Var(&copts.flExtraHosts, "add-host", "Add a custom host-to-IP mapping (host:ip)") + flags.Var(&copts.flVolumesFrom, "volumes-from", "Mount volumes from the specified container(s)") + flags.Var(&copts.flCapAdd, "cap-add", "Add Linux capabilities") + flags.Var(&copts.flCapDrop, "cap-drop", "Drop Linux capabilities") + flags.Var(&copts.flGroupAdd, "group-add", "Add additional groups to join") + flags.Var(&copts.flSecurityOpt, "security-opt", "Security Options") + flags.Var(&copts.flStorageOpt, "storage-opt", "Set storage driver options per container") + flags.Var(copts.flUlimits, "ulimit", "Ulimit options") + flags.Var(copts.flSysctls, "sysctl", "Sysctl options") + flags.Var(&copts.flLoggingOpts, "log-opt", "Log driver options") + + return copts +} + +// Parse parses the args for the specified command and generates a Config, +// a HostConfig and returns them with the specified command. +// If the specified args are not valid, it will return an error. +func Parse(flags *pflag.FlagSet, copts *ContainerOptions) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, error) { + var ( - attachStdin = flAttach.Get("stdin") - attachStdout = flAttach.Get("stdout") - attachStderr = flAttach.Get("stderr") + attachStdin = copts.flAttach.Get("stdin") + attachStdout = copts.flAttach.Get("stdout") + attachStderr = copts.flAttach.Get("stderr") ) // Validate the input mac address - if *flMacAddress != "" { - if _, err := ValidateMACAddress(*flMacAddress); err != nil { - return nil, nil, nil, cmd, fmt.Errorf("%s is not a valid mac address", *flMacAddress) + if *copts.flMacAddress != "" { + if _, err := ValidateMACAddress(*copts.flMacAddress); err != nil { + return nil, nil, nil, fmt.Errorf("%s is not a valid mac address", *copts.flMacAddress) } } - if *flStdin { + if *copts.flStdin { attachStdin = true } // If -a is not set, attach to stdout and stderr - if flAttach.Len() == 0 { + if copts.flAttach.Len() == 0 { attachStdout = true attachStderr = true } @@ -169,83 +254,83 @@ func Parse(cmd *flag.FlagSet, args []string) (*container.Config, *container.Host var err error var flMemory int64 - if *flMemoryString != "" { - flMemory, err = units.RAMInBytes(*flMemoryString) + if *copts.flMemoryString != "" { + flMemory, err = units.RAMInBytes(*copts.flMemoryString) if err != nil { - return nil, nil, nil, cmd, err + return nil, nil, nil, err } } var MemoryReservation int64 - if *flMemoryReservation != "" { - MemoryReservation, err = units.RAMInBytes(*flMemoryReservation) + if *copts.flMemoryReservation != "" { + MemoryReservation, err = units.RAMInBytes(*copts.flMemoryReservation) if err != nil { - return nil, nil, nil, cmd, err + return nil, nil, nil, err } } var memorySwap int64 - if *flMemorySwap != "" { - if *flMemorySwap == "-1" { + if *copts.flMemorySwap != "" { + if *copts.flMemorySwap == "-1" { memorySwap = -1 } else { - memorySwap, err = units.RAMInBytes(*flMemorySwap) + memorySwap, err = units.RAMInBytes(*copts.flMemorySwap) if err != nil { - return nil, nil, nil, cmd, err + return nil, nil, nil, err } } } var KernelMemory int64 - if *flKernelMemory != "" { - KernelMemory, err = units.RAMInBytes(*flKernelMemory) + if *copts.flKernelMemory != "" { + KernelMemory, err = units.RAMInBytes(*copts.flKernelMemory) if err != nil { - return nil, nil, nil, cmd, err + return nil, nil, nil, err } } - swappiness := *flSwappiness + swappiness := *copts.flSwappiness if swappiness != -1 && (swappiness < 0 || swappiness > 100) { - return nil, nil, nil, cmd, fmt.Errorf("invalid value: %d. Valid memory swappiness range is 0-100", swappiness) + return nil, nil, nil, fmt.Errorf("invalid value: %d. Valid memory swappiness range is 0-100", swappiness) } var shmSize int64 - if *flShmSize != "" { - shmSize, err = units.RAMInBytes(*flShmSize) + if *copts.flShmSize != "" { + shmSize, err = units.RAMInBytes(*copts.flShmSize) if err != nil { - return nil, nil, nil, cmd, err + return nil, nil, nil, err } } // TODO FIXME units.RAMInBytes should have a uint64 version var maxIOBandwidth int64 - if *flIOMaxBandwidth != "" { - maxIOBandwidth, err = units.RAMInBytes(*flIOMaxBandwidth) + if *copts.flIOMaxBandwidth != "" { + maxIOBandwidth, err = units.RAMInBytes(*copts.flIOMaxBandwidth) if err != nil { - return nil, nil, nil, cmd, err + return nil, nil, nil, err } if maxIOBandwidth < 0 { - return nil, nil, nil, cmd, fmt.Errorf("invalid value: %s. Maximum IO Bandwidth must be positive", *flIOMaxBandwidth) + return nil, nil, nil, fmt.Errorf("invalid value: %s. Maximum IO Bandwidth must be positive", *copts.flIOMaxBandwidth) } } var binds []string // add any bind targets to the list of container volumes - for bind := range flVolumes.GetMap() { + for bind := range copts.flVolumes.GetMap() { if arr := volumeSplitN(bind, 2); len(arr) > 1 { - // after creating the bind mount we want to delete it from the flVolumes values because + // after creating the bind mount we want to delete it from the copts.flVolumes values because // we do not want bind mounts being committed to image configs binds = append(binds, bind) - flVolumes.Delete(bind) + copts.flVolumes.Delete(bind) } } // Can't evaluate options passed into --tmpfs until we actually mount tmpfs := make(map[string]string) - for _, t := range flTmpfs.GetAll() { + for _, t := range copts.flTmpfs.GetAll() { if arr := strings.SplitN(t, ":", 2); len(arr) > 1 { if _, _, err := mount.ParseTmpfsOptions(arr[1]); err != nil { - return nil, nil, nil, cmd, err + return nil, nil, nil, err } tmpfs[arr[0]] = arr[1] } else { @@ -254,27 +339,25 @@ func Parse(cmd *flag.FlagSet, args []string) (*container.Config, *container.Host } var ( - parsedArgs = cmd.Args() runCmd strslice.StrSlice entrypoint strslice.StrSlice - image = cmd.Arg(0) ) - if len(parsedArgs) > 1 { - runCmd = strslice.StrSlice(parsedArgs[1:]) + if len(copts.Args) > 0 { + runCmd = strslice.StrSlice(copts.Args) } - if *flEntrypoint != "" { - entrypoint = strslice.StrSlice{*flEntrypoint} + if *copts.flEntrypoint != "" { + entrypoint = strslice.StrSlice{*copts.flEntrypoint} } - ports, portBindings, err := nat.ParsePortSpecs(flPublish.GetAll()) + ports, portBindings, err := nat.ParsePortSpecs(copts.flPublish.GetAll()) if err != nil { - return nil, nil, nil, cmd, err + return nil, nil, nil, err } // Merge in exposed ports to the map of published ports - for _, e := range flExpose.GetAll() { + for _, e := range copts.flExpose.GetAll() { if strings.Contains(e, ":") { - return nil, nil, nil, cmd, fmt.Errorf("invalid port format for --expose: %s", e) + return nil, nil, nil, fmt.Errorf("invalid port format for --expose: %s", e) } //support two formats for expose, original format /[] or /[] proto, port := nat.SplitProtoPort(e) @@ -282,12 +365,12 @@ func Parse(cmd *flag.FlagSet, args []string) (*container.Config, *container.Host //if expose a port, the start and end port are the same start, end, err := nat.ParsePortRange(port) if err != nil { - return nil, nil, nil, cmd, fmt.Errorf("invalid range format for --expose: %s, error: %s", e, err) + return nil, nil, nil, fmt.Errorf("invalid range format for --expose: %s, error: %s", e, err) } for i := start; i <= end; i++ { p, err := nat.NewPort(proto, strconv.FormatUint(i, 10)) if err != nil { - return nil, nil, nil, cmd, err + return nil, nil, nil, err } if _, exists := ports[p]; !exists { ports[p] = struct{}{} @@ -297,64 +380,64 @@ func Parse(cmd *flag.FlagSet, args []string) (*container.Config, *container.Host // parse device mappings deviceMappings := []container.DeviceMapping{} - for _, device := range flDevices.GetAll() { + for _, device := range copts.flDevices.GetAll() { deviceMapping, err := ParseDevice(device) if err != nil { - return nil, nil, nil, cmd, err + return nil, nil, nil, err } deviceMappings = append(deviceMappings, deviceMapping) } // collect all the environment variables for the container - envVariables, err := readKVStrings(flEnvFile.GetAll(), flEnv.GetAll()) + envVariables, err := readKVStrings(copts.flEnvFile.GetAll(), copts.flEnv.GetAll()) if err != nil { - return nil, nil, nil, cmd, err + return nil, nil, nil, err } // collect all the labels for the container - labels, err := readKVStrings(flLabelsFile.GetAll(), flLabels.GetAll()) + labels, err := readKVStrings(copts.flLabelsFile.GetAll(), copts.flLabels.GetAll()) if err != nil { - return nil, nil, nil, cmd, err + return nil, nil, nil, err } - ipcMode := container.IpcMode(*flIpcMode) + ipcMode := container.IpcMode(*copts.flIpcMode) if !ipcMode.Valid() { - return nil, nil, nil, cmd, fmt.Errorf("--ipc: invalid IPC mode") + return nil, nil, nil, fmt.Errorf("--ipc: invalid IPC mode") } - pidMode := container.PidMode(*flPidMode) + pidMode := container.PidMode(*copts.flPidMode) if !pidMode.Valid() { - return nil, nil, nil, cmd, fmt.Errorf("--pid: invalid PID mode") + return nil, nil, nil, fmt.Errorf("--pid: invalid PID mode") } - utsMode := container.UTSMode(*flUTSMode) + utsMode := container.UTSMode(*copts.flUTSMode) if !utsMode.Valid() { - return nil, nil, nil, cmd, fmt.Errorf("--uts: invalid UTS mode") + return nil, nil, nil, fmt.Errorf("--uts: invalid UTS mode") } - usernsMode := container.UsernsMode(*flUsernsMode) + usernsMode := container.UsernsMode(*copts.flUsernsMode) if !usernsMode.Valid() { - return nil, nil, nil, cmd, fmt.Errorf("--userns: invalid USER mode") + return nil, nil, nil, fmt.Errorf("--userns: invalid USER mode") } - restartPolicy, err := ParseRestartPolicy(*flRestartPolicy) + restartPolicy, err := ParseRestartPolicy(*copts.flRestartPolicy) if err != nil { - return nil, nil, nil, cmd, err + return nil, nil, nil, err } - loggingOpts, err := parseLoggingOpts(*flLoggingDriver, flLoggingOpts.GetAll()) + loggingOpts, err := parseLoggingOpts(*copts.flLoggingDriver, copts.flLoggingOpts.GetAll()) if err != nil { - return nil, nil, nil, cmd, err + return nil, nil, nil, err } - securityOpts, err := parseSecurityOpts(flSecurityOpt.GetAll()) + securityOpts, err := parseSecurityOpts(copts.flSecurityOpt.GetAll()) if err != nil { - return nil, nil, nil, cmd, err + return nil, nil, nil, err } - storageOpts, err := parseStorageOpts(flStorageOpt.GetAll()) + storageOpts, err := parseStorageOpts(copts.flStorageOpt.GetAll()) if err != nil { - return nil, nil, nil, cmd, err + return nil, nil, nil, err } // Healthcheck @@ -391,96 +474,96 @@ func Parse(cmd *flag.FlagSet, args []string) (*container.Config, *container.Host } resources := container.Resources{ - CgroupParent: *flCgroupParent, + CgroupParent: *copts.flCgroupParent, Memory: flMemory, MemoryReservation: MemoryReservation, MemorySwap: memorySwap, - MemorySwappiness: flSwappiness, + MemorySwappiness: copts.flSwappiness, KernelMemory: KernelMemory, - OomKillDisable: flOomKillDisable, - CPUPercent: *flCPUPercent, - CPUShares: *flCPUShares, - CPUPeriod: *flCPUPeriod, - CpusetCpus: *flCpusetCpus, - CpusetMems: *flCpusetMems, - CPUQuota: *flCPUQuota, - PidsLimit: *flPidsLimit, - BlkioWeight: *flBlkioWeight, - BlkioWeightDevice: flBlkioWeightDevice.GetList(), - BlkioDeviceReadBps: flDeviceReadBps.GetList(), - BlkioDeviceWriteBps: flDeviceWriteBps.GetList(), - BlkioDeviceReadIOps: flDeviceReadIOps.GetList(), - BlkioDeviceWriteIOps: flDeviceWriteIOps.GetList(), - IOMaximumIOps: *flIOMaxIOps, + OomKillDisable: copts.flOomKillDisable, + CPUPercent: *copts.flCPUPercent, + CPUShares: *copts.flCPUShares, + CPUPeriod: *copts.flCPUPeriod, + CpusetCpus: *copts.flCpusetCpus, + CpusetMems: *copts.flCpusetMems, + CPUQuota: *copts.flCPUQuota, + PidsLimit: *copts.flPidsLimit, + BlkioWeight: *copts.flBlkioWeight, + BlkioWeightDevice: copts.flBlkioWeightDevice.GetList(), + BlkioDeviceReadBps: copts.flDeviceReadBps.GetList(), + BlkioDeviceWriteBps: copts.flDeviceWriteBps.GetList(), + BlkioDeviceReadIOps: copts.flDeviceReadIOps.GetList(), + BlkioDeviceWriteIOps: copts.flDeviceWriteIOps.GetList(), + IOMaximumIOps: *copts.flIOMaxIOps, IOMaximumBandwidth: uint64(maxIOBandwidth), - Ulimits: flUlimits.GetList(), + Ulimits: copts.flUlimits.GetList(), Devices: deviceMappings, } config := &container.Config{ - Hostname: *flHostname, + Hostname: *copts.flHostname, ExposedPorts: ports, - User: *flUser, - Tty: *flTty, + User: *copts.flUser, + Tty: *copts.flTty, // TODO: deprecated, it comes from -n, --networking // it's still needed internally to set the network to disabled // if e.g. bridge is none in daemon opts, and in inspect NetworkDisabled: false, - OpenStdin: *flStdin, + OpenStdin: *copts.flStdin, AttachStdin: attachStdin, AttachStdout: attachStdout, AttachStderr: attachStderr, Env: envVariables, Cmd: runCmd, - Image: image, - Volumes: flVolumes.GetMap(), - MacAddress: *flMacAddress, + Image: copts.Image, + Volumes: copts.flVolumes.GetMap(), + MacAddress: *copts.flMacAddress, Entrypoint: entrypoint, - WorkingDir: *flWorkingDir, + WorkingDir: *copts.flWorkingDir, Labels: ConvertKVStringsToMap(labels), Healthcheck: healthConfig, } - if cmd.IsSet("-stop-signal") { - config.StopSignal = *flStopSignal + if flags.Changed("stop-signal") { + config.StopSignal = *copts.flStopSignal } hostConfig := &container.HostConfig{ Binds: binds, - ContainerIDFile: *flContainerIDFile, - OomScoreAdj: *flOomScoreAdj, - Privileged: *flPrivileged, + ContainerIDFile: *copts.flContainerIDFile, + OomScoreAdj: *copts.flOomScoreAdj, + Privileged: *copts.flPrivileged, PortBindings: portBindings, - Links: flLinks.GetAll(), - PublishAllPorts: *flPublishAll, + Links: copts.flLinks.GetAll(), + PublishAllPorts: *copts.flPublishAll, // Make sure the dns fields are never nil. // New containers don't ever have those fields nil, // but pre created containers can still have those nil values. // See https://github.com/docker/docker/pull/17779 // for a more detailed explanation on why we don't want that. - DNS: flDNS.GetAllOrEmpty(), - DNSSearch: flDNSSearch.GetAllOrEmpty(), - DNSOptions: flDNSOptions.GetAllOrEmpty(), - ExtraHosts: flExtraHosts.GetAll(), - VolumesFrom: flVolumesFrom.GetAll(), - NetworkMode: container.NetworkMode(*flNetMode), + DNS: copts.flDNS.GetAllOrEmpty(), + DNSSearch: copts.flDNSSearch.GetAllOrEmpty(), + DNSOptions: copts.flDNSOptions.GetAllOrEmpty(), + ExtraHosts: copts.flExtraHosts.GetAll(), + VolumesFrom: copts.flVolumesFrom.GetAll(), + NetworkMode: container.NetworkMode(*copts.flNetMode), IpcMode: ipcMode, PidMode: pidMode, UTSMode: utsMode, UsernsMode: usernsMode, - CapAdd: strslice.StrSlice(flCapAdd.GetAll()), - CapDrop: strslice.StrSlice(flCapDrop.GetAll()), - GroupAdd: flGroupAdd.GetAll(), + CapAdd: strslice.StrSlice(copts.flCapAdd.GetAll()), + CapDrop: strslice.StrSlice(copts.flCapDrop.GetAll()), + GroupAdd: copts.flGroupAdd.GetAll(), RestartPolicy: restartPolicy, SecurityOpt: securityOpts, StorageOpt: storageOpts, - ReadonlyRootfs: *flReadonlyRootfs, - LogConfig: container.LogConfig{Type: *flLoggingDriver, Config: loggingOpts}, - VolumeDriver: *flVolumeDriver, - Isolation: container.Isolation(*flIsolation), + ReadonlyRootfs: *copts.flReadonlyRootfs, + LogConfig: container.LogConfig{Type: *copts.flLoggingDriver, Config: loggingOpts}, + VolumeDriver: *copts.flVolumeDriver, + Isolation: container.Isolation(*copts.flIsolation), ShmSize: shmSize, Resources: resources, Tmpfs: tmpfs, - Sysctls: flSysctls.GetAll(), + Sysctls: copts.flSysctls.GetAll(), } // When allocating stdin in attached mode, close stdin at client disconnect @@ -492,11 +575,11 @@ func Parse(cmd *flag.FlagSet, args []string) (*container.Config, *container.Host EndpointsConfig: make(map[string]*networktypes.EndpointSettings), } - if *flIPv4Address != "" || *flIPv6Address != "" { + if *copts.flIPv4Address != "" || *copts.flIPv6Address != "" { networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)] = &networktypes.EndpointSettings{ IPAMConfig: &networktypes.EndpointIPAMConfig{ - IPv4Address: *flIPv4Address, - IPv6Address: *flIPv6Address, + IPv4Address: *copts.flIPv4Address, + IPv6Address: *copts.flIPv6Address, }, } } @@ -511,17 +594,17 @@ func Parse(cmd *flag.FlagSet, args []string) (*container.Config, *container.Host networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)] = epConfig } - if flAliases.Len() > 0 { + if copts.flAliases.Len() > 0 { epConfig := networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)] if epConfig == nil { epConfig = &networktypes.EndpointSettings{} } - epConfig.Aliases = make([]string, flAliases.Len()) - copy(epConfig.Aliases, flAliases.GetAll()) + epConfig.Aliases = make([]string, copts.flAliases.Len()) + copy(epConfig.Aliases, copts.flAliases.GetAll()) networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)] = epConfig } - return config, hostConfig, networkingConfig, cmd, nil + return config, hostConfig, networkingConfig, nil } // reads a file of line terminated key=value pairs, and overrides any keys diff --git a/runconfig/opts/parse_test.go b/runconfig/opts/parse_test.go index ef4f9a5396..3dea6066a8 100644 --- a/runconfig/opts/parse_test.go +++ b/runconfig/opts/parse_test.go @@ -11,22 +11,27 @@ import ( "testing" "time" - flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/runconfig" "github.com/docker/engine-api/types/container" networktypes "github.com/docker/engine-api/types/network" "github.com/docker/go-connections/nat" + "github.com/spf13/pflag" ) -func parseRun(args []string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, *flag.FlagSet, error) { - cmd := flag.NewFlagSet("run", flag.ContinueOnError) - cmd.SetOutput(ioutil.Discard) - cmd.Usage = nil - return Parse(cmd, args) +// TODO: drop FlagSet from return value +func parseRun(args []string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, error) { + flags := pflag.NewFlagSet("run", pflag.ContinueOnError) + flags.SetOutput(ioutil.Discard) + flags.Usage = nil + copts := AddFlags(flags) + if err := flags.Parse(args); err != nil { + return nil, nil, nil, err + } + return Parse(flags, copts) } func parse(t *testing.T, args string) (*container.Config, *container.HostConfig, error) { - config, hostConfig, _, _, err := parseRun(strings.Split(args+" ubuntu bash", " ")) + config, hostConfig, _, err := parseRun(strings.Split(args+" ubuntu bash", " ")) return config, hostConfig, err } @@ -351,7 +356,7 @@ func setupPlatformVolume(u []string, w []string) ([]string, string) { func TestParseWithMacAddress(t *testing.T) { invalidMacAddress := "--mac-address=invalidMacAddress" validMacAddress := "--mac-address=92:d0:c6:0a:29:33" - if _, _, _, _, err := parseRun([]string{invalidMacAddress, "img", "cmd"}); err != nil && err.Error() != "invalidMacAddress is not a valid mac address" { + if _, _, _, err := parseRun([]string{invalidMacAddress, "img", "cmd"}); err != nil && err.Error() != "invalidMacAddress is not a valid mac address" { t.Fatalf("Expected an error with %v mac-address, got %v", invalidMacAddress, err) } if config, _ := mustParse(t, validMacAddress); config.MacAddress != "92:d0:c6:0a:29:33" { @@ -362,7 +367,7 @@ func TestParseWithMacAddress(t *testing.T) { func TestParseWithMemory(t *testing.T) { invalidMemory := "--memory=invalid" validMemory := "--memory=1G" - if _, _, _, _, err := parseRun([]string{invalidMemory, "img", "cmd"}); err != nil && err.Error() != "invalid size: 'invalid'" { + if _, _, _, err := parseRun([]string{invalidMemory, "img", "cmd"}); err != nil && err.Error() != "invalid size: 'invalid'" { t.Fatalf("Expected an error with '%v' Memory, got '%v'", invalidMemory, err) } if _, hostconfig := mustParse(t, validMemory); hostconfig.Memory != 1073741824 { @@ -374,7 +379,7 @@ func TestParseWithMemorySwap(t *testing.T) { invalidMemory := "--memory-swap=invalid" validMemory := "--memory-swap=1G" anotherValidMemory := "--memory-swap=-1" - if _, _, _, _, err := parseRun([]string{invalidMemory, "img", "cmd"}); err == nil || err.Error() != "invalid size: 'invalid'" { + if _, _, _, err := parseRun([]string{invalidMemory, "img", "cmd"}); err == nil || err.Error() != "invalid size: 'invalid'" { t.Fatalf("Expected an error with '%v' MemorySwap, got '%v'", invalidMemory, err) } if _, hostconfig := mustParse(t, validMemory); hostconfig.MemorySwap != 1073741824 { @@ -427,12 +432,12 @@ func TestParseWithExpose(t *testing.T) { "8080-8082/tcp": {"8080/tcp", "8081/tcp", "8082/tcp"}, } for expose, expectedError := range invalids { - if _, _, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}); err == nil || err.Error() != expectedError { + if _, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}); err == nil || err.Error() != expectedError { t.Fatalf("Expected error '%v' with '--expose=%v', got '%v'", expectedError, expose, err) } } for expose, exposedPorts := range valids { - config, _, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}) + config, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}) if err != nil { t.Fatal(err) } @@ -446,7 +451,7 @@ func TestParseWithExpose(t *testing.T) { } } // Merge with actual published port - config, _, _, _, err := parseRun([]string{"--publish=80", "--expose=80-81/tcp", "img", "cmd"}) + config, _, _, err := parseRun([]string{"--publish=80", "--expose=80-81/tcp", "img", "cmd"}) if err != nil { t.Fatal(err) } @@ -485,7 +490,7 @@ func TestParseDevice(t *testing.T) { }, } for device, deviceMapping := range valids { - _, hostconfig, _, _, err := parseRun([]string{fmt.Sprintf("--device=%v", device), "img", "cmd"}) + _, hostconfig, _, err := parseRun([]string{fmt.Sprintf("--device=%v", device), "img", "cmd"}) if err != nil { t.Fatal(err) } @@ -501,11 +506,11 @@ func TestParseDevice(t *testing.T) { func TestParseModes(t *testing.T) { // ipc ko - if _, _, _, _, err := parseRun([]string{"--ipc=container:", "img", "cmd"}); err == nil || err.Error() != "--ipc: invalid IPC mode" { + if _, _, _, err := parseRun([]string{"--ipc=container:", "img", "cmd"}); err == nil || err.Error() != "--ipc: invalid IPC mode" { t.Fatalf("Expected an error with message '--ipc: invalid IPC mode', got %v", err) } // ipc ok - _, hostconfig, _, _, err := parseRun([]string{"--ipc=host", "img", "cmd"}) + _, hostconfig, _, err := parseRun([]string{"--ipc=host", "img", "cmd"}) if err != nil { t.Fatal(err) } @@ -513,11 +518,11 @@ func TestParseModes(t *testing.T) { t.Fatalf("Expected a valid IpcMode, got %v", hostconfig.IpcMode) } // pid ko - if _, _, _, _, err := parseRun([]string{"--pid=container:", "img", "cmd"}); err == nil || err.Error() != "--pid: invalid PID mode" { + if _, _, _, err := parseRun([]string{"--pid=container:", "img", "cmd"}); err == nil || err.Error() != "--pid: invalid PID mode" { t.Fatalf("Expected an error with message '--pid: invalid PID mode', got %v", err) } // pid ok - _, hostconfig, _, _, err = parseRun([]string{"--pid=host", "img", "cmd"}) + _, hostconfig, _, err = parseRun([]string{"--pid=host", "img", "cmd"}) if err != nil { t.Fatal(err) } @@ -525,11 +530,11 @@ func TestParseModes(t *testing.T) { t.Fatalf("Expected a valid PidMode, got %v", hostconfig.PidMode) } // uts ko - if _, _, _, _, err := parseRun([]string{"--uts=container:", "img", "cmd"}); err == nil || err.Error() != "--uts: invalid UTS mode" { + if _, _, _, err := parseRun([]string{"--uts=container:", "img", "cmd"}); err == nil || err.Error() != "--uts: invalid UTS mode" { t.Fatalf("Expected an error with message '--uts: invalid UTS mode', got %v", err) } // uts ok - _, hostconfig, _, _, err = parseRun([]string{"--uts=host", "img", "cmd"}) + _, hostconfig, _, err = parseRun([]string{"--uts=host", "img", "cmd"}) if err != nil { t.Fatal(err) } @@ -537,11 +542,11 @@ func TestParseModes(t *testing.T) { t.Fatalf("Expected a valid UTSMode, got %v", hostconfig.UTSMode) } // shm-size ko - if _, _, _, _, err = parseRun([]string{"--shm-size=a128m", "img", "cmd"}); err == nil || err.Error() != "invalid size: 'a128m'" { + if _, _, _, err = parseRun([]string{"--shm-size=a128m", "img", "cmd"}); err == nil || err.Error() != "invalid size: 'a128m'" { t.Fatalf("Expected an error with message 'invalid size: a128m', got %v", err) } // shm-size ok - _, hostconfig, _, _, err = parseRun([]string{"--shm-size=128m", "img", "cmd"}) + _, hostconfig, _, err = parseRun([]string{"--shm-size=128m", "img", "cmd"}) if err != nil { t.Fatal(err) } @@ -570,12 +575,12 @@ func TestParseRestartPolicy(t *testing.T) { }, } for restart, expectedError := range invalids { - if _, _, _, _, err := parseRun([]string{fmt.Sprintf("--restart=%s", restart), "img", "cmd"}); err == nil || err.Error() != expectedError { + if _, _, _, err := parseRun([]string{fmt.Sprintf("--restart=%s", restart), "img", "cmd"}); err == nil || err.Error() != expectedError { t.Fatalf("Expected an error with message '%v' for %v, got %v", expectedError, restart, err) } } for restart, expected := range valids { - _, hostconfig, _, _, err := parseRun([]string{fmt.Sprintf("--restart=%v", restart), "img", "cmd"}) + _, hostconfig, _, err := parseRun([]string{fmt.Sprintf("--restart=%v", restart), "img", "cmd"}) if err != nil { t.Fatal(err) } @@ -626,11 +631,11 @@ func TestParseHealth(t *testing.T) { func TestParseLoggingOpts(t *testing.T) { // logging opts ko - if _, _, _, _, err := parseRun([]string{"--log-driver=none", "--log-opt=anything", "img", "cmd"}); err == nil || err.Error() != "invalid logging opts for driver none" { + if _, _, _, err := parseRun([]string{"--log-driver=none", "--log-opt=anything", "img", "cmd"}); err == nil || err.Error() != "invalid logging opts for driver none" { t.Fatalf("Expected an error with message 'invalid logging opts for driver none', got %v", err) } // logging opts ok - _, hostconfig, _, _, err := parseRun([]string{"--log-driver=syslog", "--log-opt=something", "img", "cmd"}) + _, hostconfig, _, err := parseRun([]string{"--log-driver=syslog", "--log-opt=something", "img", "cmd"}) if err != nil { t.Fatal(err) } @@ -645,18 +650,18 @@ func TestParseEnvfileVariables(t *testing.T) { e = "open nonexistent: The system cannot find the file specified." } // env ko - if _, _, _, _, err := parseRun([]string{"--env-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e { + if _, _, _, err := parseRun([]string{"--env-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e { t.Fatalf("Expected an error with message '%s', got %v", e, err) } // env ok - config, _, _, _, err := parseRun([]string{"--env-file=fixtures/valid.env", "img", "cmd"}) + config, _, _, err := parseRun([]string{"--env-file=fixtures/valid.env", "img", "cmd"}) if err != nil { t.Fatal(err) } if len(config.Env) != 1 || config.Env[0] != "ENV1=value1" { t.Fatalf("Expected a config with [ENV1=value1], got %v", config.Env) } - config, _, _, _, err = parseRun([]string{"--env-file=fixtures/valid.env", "--env=ENV2=value2", "img", "cmd"}) + config, _, _, err = parseRun([]string{"--env-file=fixtures/valid.env", "--env=ENV2=value2", "img", "cmd"}) if err != nil { t.Fatal(err) } @@ -671,18 +676,18 @@ func TestParseLabelfileVariables(t *testing.T) { e = "open nonexistent: The system cannot find the file specified." } // label ko - if _, _, _, _, err := parseRun([]string{"--label-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e { + if _, _, _, err := parseRun([]string{"--label-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e { t.Fatalf("Expected an error with message '%s', got %v", e, err) } // label ok - config, _, _, _, err := parseRun([]string{"--label-file=fixtures/valid.label", "img", "cmd"}) + config, _, _, err := parseRun([]string{"--label-file=fixtures/valid.label", "img", "cmd"}) if err != nil { t.Fatal(err) } if len(config.Labels) != 1 || config.Labels["LABEL1"] != "value1" { t.Fatalf("Expected a config with [LABEL1:value1], got %v", config.Labels) } - config, _, _, _, err = parseRun([]string{"--label-file=fixtures/valid.label", "--label=LABEL2=value2", "img", "cmd"}) + config, _, _, err = parseRun([]string{"--label-file=fixtures/valid.label", "--label=LABEL2=value2", "img", "cmd"}) if err != nil { t.Fatal(err) } @@ -692,7 +697,7 @@ func TestParseLabelfileVariables(t *testing.T) { } func TestParseEntryPoint(t *testing.T) { - config, _, _, _, err := parseRun([]string{"--entrypoint=anything", "cmd", "img"}) + config, _, _, err := parseRun([]string{"--entrypoint=anything", "cmd", "img"}) if err != nil { t.Fatal(err) } diff --git a/runconfig/opts/throttledevice.go b/runconfig/opts/throttledevice.go index acb00ed5f7..f69e74ecc7 100644 --- a/runconfig/opts/throttledevice.go +++ b/runconfig/opts/throttledevice.go @@ -106,3 +106,8 @@ func (opt *ThrottledeviceOpt) GetList() []*blkiodev.ThrottleDevice { return throttledevice } + +// Type returns the option type +func (opt *ThrottledeviceOpt) Type() string { + return "throttled-device" +} diff --git a/runconfig/opts/ulimit.go b/runconfig/opts/ulimit.go index 0aec91f170..5adfe30851 100644 --- a/runconfig/opts/ulimit.go +++ b/runconfig/opts/ulimit.go @@ -50,3 +50,8 @@ func (o *UlimitOpt) GetList() []*units.Ulimit { return ulimits } + +// Type returns the option type +func (o *UlimitOpt) Type() string { + return "ulimit" +} diff --git a/runconfig/opts/weightdevice.go b/runconfig/opts/weightdevice.go index bd3c0b178a..b3afd2213c 100644 --- a/runconfig/opts/weightdevice.go +++ b/runconfig/opts/weightdevice.go @@ -82,3 +82,8 @@ func (opt *WeightdeviceOpt) GetList() []*blkiodev.WeightDevice { return weightdevice } + +// Type returns the option type +func (opt *WeightdeviceOpt) Type() string { + return "weighted-device" +} From 5ab24342258c70438ab8edf708ebc466b1677f38 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 31 May 2016 22:19:13 -0700 Subject: [PATCH 2/2] Convert 'docker create' to use cobra and pflag Return the correct status code on flag parsins errors. Signed-off-by: Daniel Nephin --- api/client/build.go | 10 +- api/client/cli.go | 5 + api/client/commands.go | 1 - api/client/container/create.go | 216 ++++++++++++++++++++ api/client/container/run.go | 12 +- api/client/create.go | 191 ----------------- api/client/hijack.go | 3 +- api/client/pull.go | 2 +- api/client/push.go | 2 +- api/client/trust.go | 14 +- cli/cobraadaptor/adaptor.go | 19 +- cli/flagerrors.go | 21 ++ cli/usage.go | 1 - cmd/docker/docker.go | 4 + integration-cli/docker_cli_run_test.go | 6 +- integration-cli/docker_cli_run_unix_test.go | 2 +- runconfig/opts/parse.go | 40 ++-- runconfig/opts/parse_test.go | 5 +- 18 files changed, 301 insertions(+), 253 deletions(-) create mode 100644 api/client/container/create.go delete mode 100644 api/client/create.go create mode 100644 cli/flagerrors.go diff --git a/api/client/build.go b/api/client/build.go index f79aafcbf2..7d56144be5 100644 --- a/api/client/build.go +++ b/api/client/build.go @@ -172,10 +172,10 @@ func (cli *DockerCli) CmdBuild(args ...string) error { ctx := context.Background() var resolvedTags []*resolvedTag - if isTrusted() { + if IsTrusted() { // Wrap the tar archive to replace the Dockerfile entry with the rewritten // Dockerfile which uses trusted pulls. - buildCtx = replaceDockerfileTarWrapper(ctx, buildCtx, relDockerfile, cli.trustedReference, &resolvedTags) + buildCtx = replaceDockerfileTarWrapper(ctx, buildCtx, relDockerfile, cli.TrustedReference, &resolvedTags) } // Setup an upload progress bar @@ -269,11 +269,11 @@ func (cli *DockerCli) CmdBuild(args ...string) error { fmt.Fprintf(cli.out, "%s", buildBuff) } - if isTrusted() { + if IsTrusted() { // Since the build was successful, now we must tag any of the resolved // images from the above Dockerfile rewrite. for _, resolved := range resolvedTags { - if err := cli.tagTrusted(ctx, resolved.digestRef, resolved.tagRef); err != nil { + if err := cli.TagTrusted(ctx, resolved.digestRef, resolved.tagRef); err != nil { return err } } @@ -321,7 +321,7 @@ func rewriteDockerfileFrom(ctx context.Context, dockerfile io.Reader, translator return nil, nil, err } ref = reference.WithDefaultTag(ref) - if ref, ok := ref.(reference.NamedTagged); ok && isTrusted() { + if ref, ok := ref.(reference.NamedTagged); ok && IsTrusted() { trustedRef, err := translator(ctx, ref) if err != nil { return nil, nil, err diff --git a/api/client/cli.go b/api/client/cli.go index 684ab5e186..b2c06d0671 100644 --- a/api/client/cli.go +++ b/api/client/cli.go @@ -90,6 +90,11 @@ func (cli *DockerCli) IsTerminalOut() bool { return cli.isTerminalOut } +// OutFd returns the fd for the stdout stream +func (cli *DockerCli) OutFd() uintptr { + return cli.outFd +} + // CheckTtyInput checks if we are trying to attach to a container tty // from a non-tty client input stream, and if so, returns an error. func (cli *DockerCli) CheckTtyInput(attachStdin, ttyMode bool) error { diff --git a/api/client/commands.go b/api/client/commands.go index bc18be6911..3e72da50a6 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -7,7 +7,6 @@ func (cli *DockerCli) Command(name string) func(...string) error { "build": cli.CmdBuild, "commit": cli.CmdCommit, "cp": cli.CmdCp, - "create": cli.CmdCreate, "diff": cli.CmdDiff, "events": cli.CmdEvents, "exec": cli.CmdExec, diff --git a/api/client/container/create.go b/api/client/container/create.go new file mode 100644 index 0000000000..70290fcc51 --- /dev/null +++ b/api/client/container/create.go @@ -0,0 +1,216 @@ +package container + +import ( + "fmt" + "io" + "os" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/client" + "github.com/docker/docker/cli" + "github.com/docker/docker/pkg/jsonmessage" + // FIXME migrate to docker/distribution/reference + "github.com/docker/docker/reference" + "github.com/docker/docker/registry" + runconfigopts "github.com/docker/docker/runconfig/opts" + apiclient "github.com/docker/engine-api/client" + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/container" + networktypes "github.com/docker/engine-api/types/network" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +type createOptions struct { + name string +} + +// NewCreateCommand creats a new cobra.Command for `docker create` +func NewCreateCommand(dockerCli *client.DockerCli) *cobra.Command { + var opts createOptions + var copts *runconfigopts.ContainerOptions + + cmd := &cobra.Command{ + Use: "create [OPTIONS] IMAGE [COMMAND] [ARG...]", + Short: "Create 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 runCreate(dockerCli, cmd.Flags(), &opts, copts) + }, + } + cmd.SetFlagErrorFunc(flagErrorFunc) + + flags := cmd.Flags() + flags.StringVar(&opts.name, "name", "", "Assign a name to the container") + + // Add an explicit help that doesn't have a `-h` to prevent the conflict + // with hostname + flags.Bool("help", false, "Print usage") + + client.AddTrustedFlags(flags, true) + copts = runconfigopts.AddFlags(flags) + return cmd +} + +func runCreate(dockerCli *client.DockerCli, flags *pflag.FlagSet, opts *createOptions, copts *runconfigopts.ContainerOptions) error { + config, hostConfig, networkingConfig, err := runconfigopts.Parse(flags, copts) + if err != nil { + reportError(dockerCli.Err(), "create", err.Error(), true) + return cli.StatusError{StatusCode: 125} + } + response, err := createContainer(context.Background(), dockerCli, config, hostConfig, networkingConfig, hostConfig.ContainerIDFile, opts.name) + if err != nil { + return err + } + fmt.Fprintf(dockerCli.Out(), "%s\n", response.ID) + return nil +} + +func pullImage(ctx context.Context, dockerCli *client.DockerCli, image string, out io.Writer) error { + ref, err := reference.ParseNamed(image) + if err != nil { + return err + } + + // Resolve the Repository name from fqn to RepositoryInfo + repoInfo, err := registry.ParseRepositoryInfo(ref) + if err != nil { + return err + } + + authConfig := dockerCli.ResolveAuthConfig(ctx, repoInfo.Index) + encodedAuth, err := client.EncodeAuthToBase64(authConfig) + if err != nil { + return err + } + + options := types.ImageCreateOptions{ + RegistryAuth: encodedAuth, + } + + responseBody, err := dockerCli.Client().ImageCreate(ctx, image, options) + if err != nil { + return err + } + defer responseBody.Close() + + return jsonmessage.DisplayJSONMessagesStream( + responseBody, + out, + dockerCli.OutFd(), + dockerCli.IsTerminalOut(), + nil) +} + +type cidFile struct { + path string + file *os.File + written bool +} + +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 +} + +func newCIDFile(path string) (*cidFile, error) { + if _, err := os.Stat(path); err == nil { + return nil, fmt.Errorf("Container ID file found, make sure the other container isn't running or delete %s", path) + } + + f, err := os.Create(path) + if err != nil { + return nil, fmt.Errorf("Failed to create the container ID file: %s", err) + } + + return &cidFile{path: path, file: f}, nil +} + +func createContainer(ctx context.Context, dockerCli *client.DockerCli, config *container.Config, hostConfig *container.HostConfig, networkingConfig *networktypes.NetworkingConfig, cidfile, name string) (*types.ContainerCreateResponse, error) { + stderr := dockerCli.Err() + + var containerIDFile *cidFile + if cidfile != "" { + var err error + if containerIDFile, err = newCIDFile(cidfile); err != nil { + return nil, err + } + defer containerIDFile.Close() + } + + var trustedRef reference.Canonical + _, ref, err := reference.ParseIDOrReference(config.Image) + if err != nil { + return nil, err + } + if ref != nil { + ref = reference.WithDefaultTag(ref) + + if ref, ok := ref.(reference.NamedTagged); ok && client.IsTrusted() { + var err error + trustedRef, err = dockerCli.TrustedReference(ctx, ref) + if err != nil { + return nil, err + } + config.Image = trustedRef.String() + } + } + + //create the container + response, err := dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, name) + + //if image not found try to pull it + if err != nil { + if apiclient.IsErrImageNotFound(err) && ref != nil { + fmt.Fprintf(stderr, "Unable to find image '%s' locally\n", ref.String()) + + // we don't want to write to stdout anything apart from container.ID + if err = pullImage(ctx, dockerCli, config.Image, stderr); err != nil { + return nil, err + } + if ref, ok := ref.(reference.NamedTagged); ok && trustedRef != nil { + if err := dockerCli.TagTrusted(ctx, trustedRef, ref); err != nil { + return nil, err + } + } + // Retry + var retryErr error + response, retryErr = dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, name) + if retryErr != nil { + return nil, retryErr + } + } else { + return nil, err + } + } + + for _, warning := range response.Warnings { + fmt.Fprintf(stderr, "WARNING: %s\n", warning) + } + if containerIDFile != nil { + if err = containerIDFile.Write(response.ID); err != nil { + return nil, err + } + } + return &response, nil +} diff --git a/api/client/container/run.go b/api/client/container/run.go index a5363841cb..e0629d0621 100644 --- a/api/client/container/run.go +++ b/api/client/container/run.go @@ -53,6 +53,7 @@ func NewRunCommand(dockerCli *client.DockerCli) *cobra.Command { return runRun(dockerCli, cmd.Flags(), &opts, copts) }, } + cmd.SetFlagErrorFunc(flagErrorFunc) flags := cmd.Flags() flags.SetInterspersed(false) @@ -73,6 +74,13 @@ func NewRunCommand(dockerCli *client.DockerCli) *cobra.Command { return cmd } +func flagErrorFunc(cmd *cobra.Command, err error) error { + return cli.StatusError{ + Status: cli.FlagErrorFunc(cmd, err).Error(), + StatusCode: 125, + } +} + func runRun(dockerCli *client.DockerCli, flags *pflag.FlagSet, opts *runOptions, copts *runconfigopts.ContainerOptions) error { stdout, stderr, stdin := dockerCli.Out(), dockerCli.Err(), dockerCli.In() client := dockerCli.Client() @@ -91,7 +99,7 @@ func runRun(dockerCli *client.DockerCli, flags *pflag.FlagSet, opts *runOptions, // just in case the Parse does not exit if err != nil { reportError(stderr, cmdPath, err.Error(), true) - os.Exit(125) + return cli.StatusError{StatusCode: 125} } if hostConfig.OomKillDisable != nil && *hostConfig.OomKillDisable && hostConfig.Memory == 0 { @@ -147,7 +155,7 @@ func runRun(dockerCli *client.DockerCli, flags *pflag.FlagSet, opts *runOptions, ctx, cancelFun := context.WithCancel(context.Background()) - createResponse, err := dockerCli.CreateContainer(ctx, config, hostConfig, networkingConfig, hostConfig.ContainerIDFile, opts.name) + createResponse, err := createContainer(ctx, dockerCli, config, hostConfig, networkingConfig, hostConfig.ContainerIDFile, opts.name) if err != nil { reportError(stderr, cmdPath, err.Error(), true) return runStartContainerErr(err) diff --git a/api/client/create.go b/api/client/create.go deleted file mode 100644 index eac1a7f2a9..0000000000 --- a/api/client/create.go +++ /dev/null @@ -1,191 +0,0 @@ -package client - -import ( - "fmt" - "io" - "os" - - "golang.org/x/net/context" - - Cli "github.com/docker/docker/cli" - "github.com/docker/docker/pkg/jsonmessage" - // FIXME migrate to docker/distribution/reference - "github.com/docker/docker/reference" - "github.com/docker/docker/registry" - //runconfigopts "github.com/docker/docker/runconfig/opts" - "github.com/docker/engine-api/client" - "github.com/docker/engine-api/types" - "github.com/docker/engine-api/types/container" - networktypes "github.com/docker/engine-api/types/network" -) - -func (cli *DockerCli) pullImage(ctx context.Context, image string, out io.Writer) error { - ref, err := reference.ParseNamed(image) - if err != nil { - return err - } - - // Resolve the Repository name from fqn to RepositoryInfo - repoInfo, err := registry.ParseRepositoryInfo(ref) - if err != nil { - return err - } - - authConfig := cli.ResolveAuthConfig(ctx, repoInfo.Index) - encodedAuth, err := EncodeAuthToBase64(authConfig) - if err != nil { - return err - } - - options := types.ImageCreateOptions{ - RegistryAuth: encodedAuth, - } - - responseBody, err := cli.client.ImageCreate(ctx, image, options) - if err != nil { - return err - } - defer responseBody.Close() - - return jsonmessage.DisplayJSONMessagesStream(responseBody, out, cli.outFd, cli.isTerminalOut, nil) -} - -type cidFile struct { - path string - file *os.File - written bool -} - -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 -} - -func newCIDFile(path string) (*cidFile, error) { - if _, err := os.Stat(path); err == nil { - return nil, fmt.Errorf("Container ID file found, make sure the other container isn't running or delete %s", path) - } - - f, err := os.Create(path) - if err != nil { - return nil, fmt.Errorf("Failed to create the container ID file: %s", err) - } - - return &cidFile{path: path, file: f}, nil -} - -// CreateContainer creates a container from a config -// TODO: this can be unexported again once all container commands are under -// api/client/container -func (cli *DockerCli) CreateContainer(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *networktypes.NetworkingConfig, cidfile, name string) (*types.ContainerCreateResponse, error) { - var containerIDFile *cidFile - if cidfile != "" { - var err error - if containerIDFile, err = newCIDFile(cidfile); err != nil { - return nil, err - } - defer containerIDFile.Close() - } - - var trustedRef reference.Canonical - _, ref, err := reference.ParseIDOrReference(config.Image) - if err != nil { - return nil, err - } - if ref != nil { - ref = reference.WithDefaultTag(ref) - - if ref, ok := ref.(reference.NamedTagged); ok && isTrusted() { - var err error - trustedRef, err = cli.trustedReference(ctx, ref) - if err != nil { - return nil, err - } - config.Image = trustedRef.String() - } - } - - //create the container - response, err := cli.client.ContainerCreate(ctx, config, hostConfig, networkingConfig, name) - - //if image not found try to pull it - if err != nil { - if client.IsErrImageNotFound(err) && ref != nil { - fmt.Fprintf(cli.err, "Unable to find image '%s' locally\n", ref.String()) - - // we don't want to write to stdout anything apart from container.ID - if err = cli.pullImage(ctx, config.Image, cli.err); err != nil { - return nil, err - } - if ref, ok := ref.(reference.NamedTagged); ok && trustedRef != nil { - if err := cli.tagTrusted(ctx, trustedRef, ref); err != nil { - return nil, err - } - } - // Retry - var retryErr error - response, retryErr = cli.client.ContainerCreate(ctx, config, hostConfig, networkingConfig, name) - if retryErr != nil { - return nil, retryErr - } - } else { - return nil, err - } - } - - for _, warning := range response.Warnings { - fmt.Fprintf(cli.err, "WARNING: %s\n", warning) - } - if containerIDFile != nil { - if err = containerIDFile.Write(response.ID); err != nil { - return nil, err - } - } - return &response, nil -} - -// CmdCreate creates a new container from a given image. -// -// Usage: docker create [OPTIONS] IMAGE [COMMAND] [ARG...] -func (cli *DockerCli) CmdCreate(args ...string) error { - cmd := Cli.Subcmd("create", []string{"IMAGE [COMMAND] [ARG...]"}, Cli.DockerCommands["create"].Description, true) - addTrustedFlags(cmd, true) - - // TODO: tmp disable for PoC, convert to cobra and pflag later - // These are flags not stored in Config/HostConfig - // var ( - // flName = cmd.String([]string{"-name"}, "", "Assign a name to the container") - // ) - - // config, hostConfig, networkingConfig, cmd, err := runconfigopts.Parse(cmd, args) - // - // if err != nil { - // cmd.ReportError(err.Error(), true) - // os.Exit(1) - // } - // if config.Image == "" { - // cmd.Usage() - // return nil - // } - // response, err := cli.CreateContainer(context.Background(), config, hostConfig, networkingConfig, hostConfig.ContainerIDFile, *flName) - // if err != nil { - // return err - // } - // fmt.Fprintf(cli.out, "%s\n", response.ID) - return nil -} diff --git a/api/client/hijack.go b/api/client/hijack.go index 7a7703d584..79af30b93f 100644 --- a/api/client/hijack.go +++ b/api/client/hijack.go @@ -11,7 +11,8 @@ import ( "github.com/docker/engine-api/types" ) -// HoldHijackedConnection ... TODO docstring +// HoldHijackedConnection handles copying input to and output from streams to the +// connection func (cli *DockerCli) HoldHijackedConnection(ctx context.Context, tty bool, inputStream io.ReadCloser, outputStream, errorStream io.Writer, resp types.HijackedResponse) error { var ( err error diff --git a/api/client/pull.go b/api/client/pull.go index a3aca027be..364e513de1 100644 --- a/api/client/pull.go +++ b/api/client/pull.go @@ -60,7 +60,7 @@ func (cli *DockerCli) CmdPull(args ...string) error { authConfig := cli.ResolveAuthConfig(ctx, repoInfo.Index) requestPrivilege := cli.RegistryAuthenticationPrivilegedFunc(repoInfo.Index, "pull") - if isTrusted() && !registryRef.HasDigest() { + if IsTrusted() && !registryRef.HasDigest() { // Check if tag is digest return cli.trustedPull(ctx, repoInfo, registryRef, authConfig, requestPrivilege) } diff --git a/api/client/push.go b/api/client/push.go index 8dc695b542..7b2f42bc4f 100644 --- a/api/client/push.go +++ b/api/client/push.go @@ -40,7 +40,7 @@ func (cli *DockerCli) CmdPush(args ...string) error { authConfig := cli.ResolveAuthConfig(ctx, repoInfo.Index) requestPrivilege := cli.RegistryAuthenticationPrivilegedFunc(repoInfo.Index, "push") - if isTrusted() { + if IsTrusted() { return cli.trustedPush(ctx, repoInfo, ref, authConfig, requestPrivilege) } diff --git a/api/client/trust.go b/api/client/trust.go index 6f2a3f6090..d2c3479f56 100644 --- a/api/client/trust.go +++ b/api/client/trust.go @@ -45,8 +45,7 @@ var ( untrusted bool ) -// TODO: tmp workaround to get this PoC working, change everything to use -// exported version +// addTrustedFlags is the mflag version of AddTrustedFlags func addTrustedFlags(fs *flag.FlagSet, verify bool) { trusted, message := setupTrustedFlag(verify) fs.BoolVar(&untrusted, []string{"-disable-content-trust"}, !trusted, message) @@ -73,7 +72,8 @@ func setupTrustedFlag(verify bool) (bool, string) { return trusted, message } -func isTrusted() bool { +// IsTrusted returns true if content trust is enabled +func IsTrusted() bool { return !untrusted } @@ -243,7 +243,8 @@ func (cli *DockerCli) getPassphraseRetriever() passphrase.Retriever { } } -func (cli *DockerCli) trustedReference(ctx context.Context, ref reference.NamedTagged) (reference.Canonical, error) { +// TrustedReference returns the canonical trusted reference for an image reference +func (cli *DockerCli) TrustedReference(ctx context.Context, ref reference.NamedTagged) (reference.Canonical, error) { repoInfo, err := registry.ParseRepositoryInfo(ref) if err != nil { return nil, err @@ -276,7 +277,8 @@ func (cli *DockerCli) trustedReference(ctx context.Context, ref reference.NamedT return reference.WithDigest(ref, r.digest) } -func (cli *DockerCli) tagTrusted(ctx context.Context, trustedRef reference.Canonical, ref reference.NamedTagged) error { +// TagTrusted tags a trusted ref +func (cli *DockerCli) TagTrusted(ctx context.Context, trustedRef reference.Canonical, ref reference.NamedTagged) error { fmt.Fprintf(cli.out, "Tagging %s as %s\n", trustedRef.String(), ref.String()) return cli.client.ImageTag(ctx, trustedRef.String(), ref.String()) @@ -388,7 +390,7 @@ func (cli *DockerCli) trustedPull(ctx context.Context, repoInfo *registry.Reposi if err != nil { return err } - if err := cli.tagTrusted(ctx, trustedRef, tagged); err != nil { + if err := cli.TagTrusted(ctx, trustedRef, tagged); err != nil { return err } } diff --git a/cli/cobraadaptor/adaptor.go b/cli/cobraadaptor/adaptor.go index 7e6327ac2d..719eaf4e1d 100644 --- a/cli/cobraadaptor/adaptor.go +++ b/cli/cobraadaptor/adaptor.go @@ -1,8 +1,6 @@ package cobraadaptor import ( - "fmt" - "github.com/docker/docker/api/client" "github.com/docker/docker/api/client/container" "github.com/docker/docker/api/client/image" @@ -32,9 +30,10 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { } rootCmd.SetUsageTemplate(usageTemplate) rootCmd.SetHelpTemplate(helpTemplate) - rootCmd.SetFlagErrorFunc(flagErrorFunc) + rootCmd.SetFlagErrorFunc(cli.FlagErrorFunc) rootCmd.SetOutput(stdout) rootCmd.AddCommand( + container.NewCreateCommand(dockerCli), container.NewRunCommand(dockerCli), image.NewSearchCommand(dockerCli), volume.NewVolumeCommand(dockerCli), @@ -78,20 +77,6 @@ func (c CobraAdaptor) Command(name string) func(...string) error { return nil } -// flagErrorFunc prints an error messages which matches the format of the -// docker/docker/cli error messages -func flagErrorFunc(cmd *cobra.Command, err error) error { - if err == nil { - return err - } - - usage := "" - if cmd.HasSubCommands() { - usage = "\n\n" + cmd.UsageString() - } - return fmt.Errorf("%s\nSee '%s --help'.%s", err, cmd.CommandPath(), usage) -} - var usageTemplate = `Usage: {{if not .HasSubCommands}}{{if .HasLocalFlags}}{{appendIfNotPresent .UseLine "[OPTIONS]"}}{{else}}{{.UseLine}}{{end}}{{end}}{{if .HasSubCommands}}{{ .CommandPath}} COMMAND{{end}} {{with or .Long .Short }}{{. | trim}}{{end}}{{if gt .Aliases 0}} diff --git a/cli/flagerrors.go b/cli/flagerrors.go new file mode 100644 index 0000000000..aab8a98845 --- /dev/null +++ b/cli/flagerrors.go @@ -0,0 +1,21 @@ +package cli + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// FlagErrorFunc prints an error messages which matches the format of the +// docker/docker/cli error messages +func FlagErrorFunc(cmd *cobra.Command, err error) error { + if err == nil { + return err + } + + usage := "" + if cmd.HasSubCommands() { + usage = "\n\n" + cmd.UsageString() + } + return fmt.Errorf("%s\nSee '%s --help'.%s", err, cmd.CommandPath(), usage) +} diff --git a/cli/usage.go b/cli/usage.go index 324d1d92bf..d6c97c32f5 100644 --- a/cli/usage.go +++ b/cli/usage.go @@ -12,7 +12,6 @@ var DockerCommandUsage = []Command{ {"build", "Build an image from a Dockerfile"}, {"commit", "Create a new image from a container's changes"}, {"cp", "Copy files/folders between a container and the local filesystem"}, - {"create", "Create a new container"}, {"diff", "Inspect changes on a container's filesystem"}, {"events", "Get real time events from the server"}, {"exec", "Run a command in a running container"}, diff --git a/cmd/docker/docker.go b/cmd/docker/docker.go index 45de2e3fca..0c727e32c9 100644 --- a/cmd/docker/docker.go +++ b/cmd/docker/docker.go @@ -73,6 +73,10 @@ func main() { if sterr, ok := err.(cli.StatusError); ok { if sterr.Status != "" { fmt.Fprintln(stderr, sterr.Status) + } + // StatusError should only be used for errors, and all errors should + // have a non-zero exit status, so never exit with 0 + if sterr.StatusCode == 0 { os.Exit(1) } os.Exit(sterr.StatusCode) diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 7eca63c5cb..e5dbe0f77e 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -1455,7 +1455,7 @@ func (s *DockerSuite) TestRunResolvconfUpdate(c *check.C) { }() //1. test that a restarting container gets an updated resolv.conf - dockerCmd(c, "run", "--name='first'", "busybox", "true") + dockerCmd(c, "run", "--name=first", "busybox", "true") containerID1, err := getIDByName("first") if err != nil { c.Fatal(err) @@ -1485,7 +1485,7 @@ func (s *DockerSuite) TestRunResolvconfUpdate(c *check.C) { } */ //2. test that a restarting container does not receive resolv.conf updates // if it modified the container copy of the starting point resolv.conf - dockerCmd(c, "run", "--name='second'", "busybox", "sh", "-c", "echo 'search mylittlepony.com' >>/etc/resolv.conf") + dockerCmd(c, "run", "--name=second", "busybox", "sh", "-c", "echo 'search mylittlepony.com' >>/etc/resolv.conf") containerID2, err := getIDByName("second") if err != nil { c.Fatal(err) @@ -1574,7 +1574,7 @@ func (s *DockerSuite) TestRunResolvconfUpdate(c *check.C) { } // Run the container so it picks up the old settings - dockerCmd(c, "run", "--name='third'", "busybox", "true") + dockerCmd(c, "run", "--name=third", "busybox", "true") containerID3, err := getIDByName("third") if err != nil { c.Fatal(err) diff --git a/integration-cli/docker_cli_run_unix_test.go b/integration-cli/docker_cli_run_unix_test.go index 4af37cc230..0ede9b9fa5 100644 --- a/integration-cli/docker_cli_run_unix_test.go +++ b/integration-cli/docker_cli_run_unix_test.go @@ -894,7 +894,7 @@ func (s *DockerSuite) TestRunSysctls(c *check.C) { runCmd := exec.Command(dockerBinary, "run", "--sysctl", "kernel.foobar=1", "--name", "test2", "busybox", "cat", "/proc/sys/kernel/foobar") out, _, _ = runCommandWithOutput(runCmd) - if !strings.Contains(out, "invalid value") { + if !strings.Contains(out, "invalid argument") { c.Fatalf("expected --sysctl to fail, got %s", out) } } diff --git a/runconfig/opts/parse.go b/runconfig/opts/parse.go index 160e9c681f..77254e05f4 100644 --- a/runconfig/opts/parse.go +++ b/runconfig/opts/parse.go @@ -184,11 +184,11 @@ func AddFlags(flags *pflag.FlagSet) *ContainerOptions { flStopSignal: flags.String("stop-signal", signal.DefaultStopSignal, fmt.Sprintf("Signal to stop a container, %v by default", signal.DefaultStopSignal)), flIsolation: flags.String("isolation", "", "Container isolation technology"), flShmSize: flags.String("shm-size", "", "Size of /dev/shm, default value is 64MB"), - flNoHealthcheck: cmd.Bool([]string{"-no-healthcheck"}, false, "Disable any container-specified HEALTHCHECK"), - flHealthCmd: cmd.String([]string{"-health-cmd"}, "", "Command to run to check health"), - flHealthInterval: cmd.Duration([]string{"-health-interval"}, 0, "Time between running the check"), - flHealthTimeout: cmd.Duration([]string{"-health-timeout"}, 0, "Maximum time to allow one check to run"), - flHealthRetries: cmd.Int([]string{"-health-retries"}, 0, "Consecutive failures needed to report unhealthy"), + flNoHealthcheck: flags.Bool("no-healthcheck", false, "Disable any container-specified HEALTHCHECK"), + flHealthCmd: flags.String("health-cmd", "", "Command to run to check health"), + flHealthInterval: flags.Duration("health-interval", 0, "Time between running the check"), + flHealthTimeout: flags.Duration("health-timeout", 0, "Maximum time to allow one check to run"), + flHealthRetries: flags.Int("health-retries", 0, "Consecutive failures needed to report unhealthy"), } flags.VarP(&copts.flAttach, "attach", "a", "Attach to STDIN, STDOUT or STDERR") @@ -442,34 +442,34 @@ func Parse(flags *pflag.FlagSet, copts *ContainerOptions) (*container.Config, *c // Healthcheck var healthConfig *container.HealthConfig - haveHealthSettings := *flHealthCmd != "" || - *flHealthInterval != 0 || - *flHealthTimeout != 0 || - *flHealthRetries != 0 - if *flNoHealthcheck { + haveHealthSettings := *copts.flHealthCmd != "" || + *copts.flHealthInterval != 0 || + *copts.flHealthTimeout != 0 || + *copts.flHealthRetries != 0 + if *copts.flNoHealthcheck { if haveHealthSettings { - return nil, nil, nil, cmd, fmt.Errorf("--no-healthcheck conflicts with --health-* options") + return nil, nil, nil, fmt.Errorf("--no-healthcheck conflicts with --health-* options") } test := strslice.StrSlice{"NONE"} healthConfig = &container.HealthConfig{Test: test} } else if haveHealthSettings { var probe strslice.StrSlice - if *flHealthCmd != "" { - args := []string{"CMD-SHELL", *flHealthCmd} + if *copts.flHealthCmd != "" { + args := []string{"CMD-SHELL", *copts.flHealthCmd} probe = strslice.StrSlice(args) } - if *flHealthInterval < 0 { - return nil, nil, nil, cmd, fmt.Errorf("--health-interval cannot be negative") + if *copts.flHealthInterval < 0 { + return nil, nil, nil, fmt.Errorf("--health-interval cannot be negative") } - if *flHealthTimeout < 0 { - return nil, nil, nil, cmd, fmt.Errorf("--health-timeout cannot be negative") + if *copts.flHealthTimeout < 0 { + return nil, nil, nil, fmt.Errorf("--health-timeout cannot be negative") } healthConfig = &container.HealthConfig{ Test: probe, - Interval: *flHealthInterval, - Timeout: *flHealthTimeout, - Retries: *flHealthRetries, + Interval: *copts.flHealthInterval, + Timeout: *copts.flHealthTimeout, + Retries: *copts.flHealthRetries, } } diff --git a/runconfig/opts/parse_test.go b/runconfig/opts/parse_test.go index 3dea6066a8..d0acd8be87 100644 --- a/runconfig/opts/parse_test.go +++ b/runconfig/opts/parse_test.go @@ -18,7 +18,6 @@ import ( "github.com/spf13/pflag" ) -// TODO: drop FlagSet from return value func parseRun(args []string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, error) { flags := pflag.NewFlagSet("run", pflag.ContinueOnError) flags.SetOutput(ioutil.Discard) @@ -592,14 +591,14 @@ func TestParseRestartPolicy(t *testing.T) { func TestParseHealth(t *testing.T) { checkOk := func(args ...string) *container.HealthConfig { - config, _, _, _, err := parseRun(args) + config, _, _, err := parseRun(args) if err != nil { t.Fatalf("%#v: %v", args, err) } return config.Healthcheck } checkError := func(expected string, args ...string) { - config, _, _, _, err := parseRun(args) + config, _, _, err := parseRun(args) if err == nil { t.Fatalf("Expected error, but got %#v", config) }