diff --git a/api/client/commands.go b/api/client/commands.go index d588db8764..fefe778498 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -1965,9 +1965,112 @@ func (cli *DockerCli) pullImage(image string) error { return nil } -func (cli *DockerCli) CmdRun(args ...string) error { +type cidFile struct { + path string + file *os.File + written bool +} + +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 (cid *cidFile) Close() error { + cid.file.Close() + + if !cid.written { + if err := os.Remove(cid.path); err != nil { + return fmt.Errorf("failed to remove 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 (cli *DockerCli) createContainer(config *runconfig.Config, hostConfig *runconfig.HostConfig, cidfile, name string) (engine.Env, error) { + containerValues := url.Values{} + if name != "" { + containerValues.Set("name", name) + } + + var data interface{} + if hostConfig != nil { + data = runconfig.MergeConfigs(config, hostConfig) + } else { + data = config + } + + var containerIDFile *cidFile + if cidfile != "" { + var err error + if containerIDFile, err = newCIDFile(cidfile); err != nil { + return nil, err + } + defer containerIDFile.Close() + } + + //create the container + stream, statusCode, err := cli.call("POST", "/containers/create?"+containerValues.Encode(), data, false) + //if image not found try to pull it + if statusCode == 404 { + fmt.Fprintf(cli.err, "Unable to find image '%s' locally\n", config.Image) + + if err = cli.pullImage(config.Image); err != nil { + return nil, err + } + // Retry + if stream, _, err = cli.call("POST", "/containers/create?"+containerValues.Encode(), data, false); err != nil { + return nil, err + } + } else if err != nil { + return nil, err + } + + var result engine.Env + if err := result.Decode(stream); err != nil { + return nil, err + } + + for _, warning := range result.GetList("Warnings") { + fmt.Fprintf(cli.err, "WARNING: %s\n", warning) + } + + if containerIDFile != nil { + if err = containerIDFile.Write(result.Get("Id")); err != nil { + return nil, err + } + } + + return result, nil + +} + +func (cli *DockerCli) CmdCreate(args ...string) error { // FIXME: just use runconfig.Parse already - config, hostConfig, cmd, err := runconfig.ParseSubcommand(cli.Subcmd("run", "IMAGE [COMMAND] [ARG...]", "Run a command in a new container"), args, nil) + cmd := cli.Subcmd("create", "IMAGE [COMMAND] [ARG...]", "Create a new container") + + // These are flags not stored in Config/HostConfig + var ( + flName = cmd.String([]string{"-name"}, "", "Assign a name to the container") + ) + + config, hostConfig, cmd, err := runconfig.ParseSubcommand(cmd, args, nil) if err != nil { return err } @@ -1976,82 +2079,69 @@ func (cli *DockerCli) CmdRun(args ...string) error { return nil } - // Retrieve relevant client-side config + createResult, err := cli.createContainer(config, hostConfig, hostConfig.ContainerIDFile, *flName) + if err != nil { + return err + } + + fmt.Fprintf(cli.out, "%s\n", createResult.Get("Id")) + + return nil +} + +func (cli *DockerCli) CmdRun(args ...string) error { + // FIXME: just use runconfig.Parse already + cmd := cli.Subcmd("run", "IMAGE [COMMAND] [ARG...]", "Run a command in a new container") + + // These are flags not stored in Config/HostConfig var ( - flName = cmd.Lookup("name") - flRm = cmd.Lookup("rm") - flSigProxy = cmd.Lookup("sig-proxy") - autoRemove, _ = strconv.ParseBool(flRm.Value.String()) - sigProxy, _ = strconv.ParseBool(flSigProxy.Value.String()) + flAutoRemove = cmd.Bool([]string{"#rm", "-rm"}, false, "Automatically remove the container when it exits (incompatible with -d)") + flDetach = cmd.Bool([]string{"d", "-detach"}, false, "Detached mode: run container in the background and print new container ID") + flSigProxy = cmd.Bool([]string{"#sig-proxy", "-sig-proxy"}, true, "Proxy received signals to the process (even in non-TTY mode). SIGCHLD, SIGSTOP, and SIGKILL are not proxied.") + flName = cmd.String([]string{"#name", "-name"}, "", "Assign a name to the container") + + flAttach *opts.ListOpts + + ErrConflictAttachDetach = fmt.Errorf("Conflicting options: -a and -d") ) - // Disable sigProxy in case on TTY + config, hostConfig, cmd, err := runconfig.ParseSubcommand(cmd, args, nil) + if err != nil { + return err + } + if config.Image == "" { + cmd.Usage() + return nil + } + + if *flDetach { + if fl := cmd.Lookup("attach"); fl != nil { + flAttach = fl.Value.(*opts.ListOpts) + if flAttach.Len() != 0 { + return fmt.Errorf("Conflicting options: -a and -d") + } + } + if *flAutoRemove { + return fmt.Errorf("Conflicting options: --rm and -d") + } + + config.AttachStdin = false + config.AttachStdout = false + config.AttachStderr = false + config.StdinOnce = false + } + + // Disable flSigProxy in case on TTY + sigProxy := *flSigProxy if config.Tty { sigProxy = false } - var containerIDFile io.WriteCloser - if len(hostConfig.ContainerIDFile) > 0 { - if _, err := os.Stat(hostConfig.ContainerIDFile); err == nil { - return fmt.Errorf("Container ID file found, make sure the other container isn't running or delete %s", hostConfig.ContainerIDFile) - } - if containerIDFile, err = os.Create(hostConfig.ContainerIDFile); err != nil { - return fmt.Errorf("Failed to create the container ID file: %s", err) - } - defer func() { - containerIDFile.Close() - var ( - cidFileInfo os.FileInfo - err error - ) - if cidFileInfo, err = os.Stat(hostConfig.ContainerIDFile); err != nil { - return - } - if cidFileInfo.Size() == 0 { - if err := os.Remove(hostConfig.ContainerIDFile); err != nil { - fmt.Printf("failed to remove Container ID file '%s': %s \n", hostConfig.ContainerIDFile, err) - } - } - }() - } - - containerValues := url.Values{} - if name := flName.Value.String(); name != "" { - containerValues.Set("name", name) - } - - //create the container - stream, statusCode, err := cli.call("POST", "/containers/create?"+containerValues.Encode(), config, false) - //if image not found try to pull it - if statusCode == 404 { - fmt.Fprintf(cli.err, "Unable to find image '%s' locally\n", config.Image) - - if err = cli.pullImage(config.Image); err != nil { - return err - } - // Retry - if stream, _, err = cli.call("POST", "/containers/create?"+containerValues.Encode(), config, false); err != nil { - return err - } - } else if err != nil { + runResult, err := cli.createContainer(config, nil, hostConfig.ContainerIDFile, *flName) + if err != nil { return err } - var runResult engine.Env - if err := runResult.Decode(stream); err != nil { - return err - } - - for _, warning := range runResult.GetList("Warnings") { - fmt.Fprintf(cli.err, "WARNING: %s\n", warning) - } - - if len(hostConfig.ContainerIDFile) > 0 { - if _, err = containerIDFile.Write([]byte(runResult.Get("Id"))); err != nil { - return fmt.Errorf("Failed to write the container ID to the file: %s", err) - } - } - if sigProxy { sigc := cli.forwardAllSignals(runResult.Get("Id")) defer signal.StopCatch(sigc) @@ -2158,7 +2248,7 @@ func (cli *DockerCli) CmdRun(args ...string) error { var status int // Attached mode - if autoRemove { + if *flAutoRemove { // Autoremove: wait for the container to finish, retrieve // the exit code and remove the container if _, _, err := readBody(cli.call("POST", "/containers/"+runResult.Get("Id")+"/wait", nil, false)); err != nil { diff --git a/daemon/create.go b/daemon/create.go index 3c6827eeec..4fb58480b1 100644 --- a/daemon/create.go +++ b/daemon/create.go @@ -50,6 +50,14 @@ func (daemon *Daemon) ContainerCreate(job *engine.Job) engine.Status { for _, warning := range buildWarnings { job.Errorf("%s\n", warning) } + + if job.EnvExists("HostConfig") { + hostConfig := runconfig.ContainerHostConfigFromJob(job) + if err := daemon.setHostConfig(container, hostConfig); err != nil { + return job.Error(err) + } + } + return engine.StatusOK } diff --git a/docker/flags.go b/docker/flags.go index bb5b427d63..44b727511f 100644 --- a/docker/flags.go +++ b/docker/flags.go @@ -53,6 +53,7 @@ func init() { {"build", "Build an image from a Dockerfile"}, {"commit", "Create a new image from a container's changes"}, {"cp", "Copy files/folders from a container's filesystem to the host path"}, + {"create", "Create a new container"}, {"diff", "Inspect changes on a container's filesystem"}, {"events", "Get real time events from the server"}, {"export", "Stream the contents of a container as a tar archive"}, diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 0920917f93..57c7d528b0 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -370,6 +370,63 @@ path. Paths are relative to the root of the filesystem. Copy files/folders from the PATH to the HOSTPATH + +## create + +Creates a new container. + + Usage: docker create [OPTIONS] IMAGE[:TAG] [COMMAND] [ARG...] + + + -a, --attach=[] Attach to stdin, stdout or stderr. + -c, --cpu-shares=0 CPU shares (relative weight) + --cidfile="" Write the container ID to the file + --dns=[] Set custom dns servers + --dns-search=[] Set custom dns search domains + -e, --env=[] Set environment variables + --entrypoint="" Overwrite the default entrypoint of the image + --env-file=[] Read in a line delimited file of ENV variables + --expose=[] Expose a port from the container without publishing it to your host + -h, --hostname="" Container host name + -i, --interactive=false Keep stdin open even if not attached + --link=[] Add link to another container (name:alias) + --lxc-conf=[] (lxc exec-driver only) Add custom lxc options --lxc-conf="lxc.cgroup.cpuset.cpus = 0,1" + -m, --memory="" Memory limit (format: , where unit = b, k, m or g) + --name="" Assign a name to the container + --net="bridge" Set the Network mode for the container + 'bridge': creates a new network stack for the container on the docker bridge + 'none': no networking for this container + 'container:': reuses another container network stack + 'host': use the host network stack inside the contaner + -p, --publish=[] Publish a container's port to the host + format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort + (use 'docker port' to see the actual mapping) + -P, --publish-all=false Publish all exposed ports to the host interfaces + --privileged=false Give extended privileges to this container + -t, --tty=false Allocate a pseudo-tty + -u, --user="" Username or UID + -v, --volume=[] Bind mount a volume (e.g. from the host: -v /host:/container, from docker: -v /container) + --volumes-from=[] Mount volumes from the specified container(s) + -w, --workdir="" Working directory inside the container + + +The `docker create` command `creates` a writeable container layer over +the specified image, and prepares it for running the specified +command. The container id is then printed to stdout. This is similar +to what `docker run -d `, does except the container is never +started. You can then use the `docker start ` command to +start the container at any point. + +This is useful when you want to set up a container configuration ahead +of time, so that it is ready to start when you need it. + +### Example: + + $ sudo docker create -t -i fedora bash + 6d8af538ec541dd581ebc2a24153a28329acb5268abe5ef868c1f1a261221752 + $ sudo docker start -a -i 6d8af538ec5 + bash-4.2# + ## diff List the changed files and directories in a container᾿s filesystem diff --git a/runconfig/hostconfig.go b/runconfig/hostconfig.go index 2c6b9feb9f..451870394f 100644 --- a/runconfig/hostconfig.go +++ b/runconfig/hostconfig.go @@ -57,7 +57,27 @@ type HostConfig struct { RestartPolicy RestartPolicy } +// This is used by the create command when you want to both set the +// Config and the HostConfig in the same call +type ConfigAndHostConfig struct { + Config + HostConfig HostConfig +} + +func MergeConfigs(config *Config, hostConfig *HostConfig) *ConfigAndHostConfig { + return &ConfigAndHostConfig{ + *config, + *hostConfig, + } +} + func ContainerHostConfigFromJob(job *engine.Job) *HostConfig { + if job.EnvExists("HostConfig") { + hostConfig := HostConfig{} + job.GetenvJson("HostConfig", &hostConfig) + return &hostConfig + } + hostConfig := &HostConfig{ ContainerIDFile: job.Getenv("ContainerIDFile"), Privileged: job.GetenvBool("Privileged"), diff --git a/runconfig/parse.go b/runconfig/parse.go index 7431b7611d..419e7d4dcf 100644 --- a/runconfig/parse.go +++ b/runconfig/parse.go @@ -18,7 +18,6 @@ import ( var ( ErrInvalidWorkingDirectory = fmt.Errorf("The working directory is invalid. It needs to be an absolute path.") - ErrConflictAttachDetach = fmt.Errorf("Conflicting options: -a and -d") ErrConflictContainerNetworkAndLinks = fmt.Errorf("Conflicting options: --net=container can't be used with links. This would result in undefined behavior.") ErrConflictContainerNetworkAndDns = fmt.Errorf("Conflicting options: --net=container can't be used with --dns. This configuration is invalid.") ErrConflictDetachAutoRemove = fmt.Errorf("Conflicting options: --rm and -d") @@ -28,7 +27,7 @@ var ( ErrConflictRestartPolicyAndAutoRemove = fmt.Errorf("Conflicting options: --restart and --rm") ) -//FIXME Only used in tests +// FIXME Only used in tests func Parse(args []string, sysInfo *sysinfo.SysInfo) (*Config, *HostConfig, *flag.FlagSet, error) { cmd := flag.NewFlagSet("run", flag.ContinueOnError) cmd.SetOutput(ioutil.Discard) @@ -60,8 +59,6 @@ func parseRun(cmd *flag.FlagSet, args []string, sysInfo *sysinfo.SysInfo) (*Conf flCapAdd = opts.NewListOpts(nil) flCapDrop = opts.NewListOpts(nil) - flAutoRemove = cmd.Bool([]string{"#rm", "-rm"}, false, "Automatically remove the container when it exits (incompatible with -d)") - flDetach = cmd.Bool([]string{"d", "-detach"}, false, "Detached mode: run container in the background and print new container ID") flNetwork = cmd.Bool([]string{"#n", "#-networking"}, true, "Enable networking for this container") flPrivileged = cmd.Bool([]string{"#privileged", "-privileged"}, false, "Give extended privileges to this container") flPublishAll = cmd.Bool([]string{"P", "-publish-all"}, false, "Publish all exposed ports to the host interfaces") @@ -77,15 +74,13 @@ func parseRun(cmd *flag.FlagSet, args []string, sysInfo *sysinfo.SysInfo) (*Conf flCpuset = cmd.String([]string{"-cpuset"}, "", "CPUs in which to allow execution (0-3, 0,1)") flNetMode = cmd.String([]string{"-net"}, "bridge", "Set the Network mode for the container\n'bridge': creates a new network stack for the container on the docker bridge\n'none': no networking for this container\n'container:': reuses another container network stack\n'host': use the host network stack inside the container. Note: the host mode gives the container full access to local system services such as D-bus and is therefore considered insecure.") flRestartPolicy = cmd.String([]string{"-restart"}, "", "Restart policy to apply when a container exits (no, on-failure[:max-retry], always)") - // For documentation purpose - _ = cmd.Bool([]string{"#sig-proxy", "-sig-proxy"}, true, "Proxy received signals to the process (even in non-TTY mode). SIGCHLD, SIGSTOP, and SIGKILL are not proxied.") - _ = cmd.String([]string{"#name", "-name"}, "", "Assign a name to the container") ) cmd.Var(&flAttach, []string{"a", "-attach"}, "Attach to STDIN, STDOUT or STDERR.") cmd.Var(&flVolumes, []string{"v", "-volume"}, "Bind mount a volume (e.g., from the host: -v /host:/container, from Docker: -v /container)") cmd.Var(&flLinks, []string{"#link", "-link"}, "Add link to another container in the form of name:alias") cmd.Var(&flDevices, []string{"-device"}, "Add a host device to the container (e.g. --device=/dev/sdc:/dev/xvdc)") + cmd.Var(&flEnv, []string{"e", "-env"}, "Set environment variables") cmd.Var(&flEnvFile, []string{"-env-file"}, "Read in a line delimited file of environment variables") @@ -109,15 +104,15 @@ func parseRun(cmd *flag.FlagSet, args []string, sysInfo *sysinfo.SysInfo) (*Conf } // Validate input params - if *flDetach && flAttach.Len() > 0 { - return nil, nil, cmd, ErrConflictAttachDetach - } if *flWorkingDir != "" && !path.IsAbs(*flWorkingDir) { return nil, nil, cmd, ErrInvalidWorkingDirectory } - if *flDetach && *flAutoRemove { - return nil, nil, cmd, ErrConflictDetachAutoRemove - } + + var ( + attachStdin = flAttach.Get("stdin") + attachStdout = flAttach.Get("stdout") + attachStderr = flAttach.Get("stderr") + ) if *flNetMode != "bridge" && *flNetMode != "none" && *flHostname != "" { return nil, nil, cmd, ErrConflictNetworkHostname @@ -140,13 +135,11 @@ func parseRun(cmd *flag.FlagSet, args []string, sysInfo *sysinfo.SysInfo) (*Conf } // If neither -d or -a are set, attach to everything by default - if flAttach.Len() == 0 && !*flDetach { - if !*flDetach { - flAttach.Set("stdout") - flAttach.Set("stderr") - if *flStdin { - flAttach.Set("stdin") - } + if flAttach.Len() == 0 { + attachStdout = true + attachStderr = true + if *flStdin { + attachStdin = true } } @@ -270,9 +263,9 @@ func parseRun(cmd *flag.FlagSet, args []string, sysInfo *sysinfo.SysInfo) (*Conf Memory: flMemory, CpuShares: *flCpuShares, Cpuset: *flCpuset, - AttachStdin: flAttach.Get("stdin"), - AttachStdout: flAttach.Get("stdout"), - AttachStderr: flAttach.Get("stderr"), + AttachStdin: attachStdin, + AttachStdout: attachStdout, + AttachStderr: attachStderr, Env: envVariables, Cmd: runCmd, Image: image,