From f3c767d798f945192d32441cf624bdd54e746b74 Mon Sep 17 00:00:00 2001 From: Vishnu Kannan Date: Thu, 4 Sep 2014 05:29:19 +0000 Subject: [PATCH 01/12] Adding Exec method to native execdriver. Modified Attach() method to support docker exec. Docker-DCO-1.1-Signed-off-by: Vishnu Kannan (github: vishh) --- builder/internals.go | 2 +- daemon/attach.go | 32 +++++++++------ daemon/execdriver/driver.go | 2 + daemon/execdriver/lxc/driver.go | 4 ++ daemon/execdriver/native/exec.go | 68 +++++++++++++++++++++++++++++++ daemon/execdriver/native/utils.go | 40 ++++++++++++++++++ 6 files changed, 135 insertions(+), 13 deletions(-) create mode 100644 daemon/execdriver/native/exec.go create mode 100644 daemon/execdriver/native/utils.go diff --git a/builder/internals.go b/builder/internals.go index 0655942b3c..4fe1500358 100644 --- a/builder/internals.go +++ b/builder/internals.go @@ -407,7 +407,7 @@ func (b *Builder) run(c *daemon.Container) error { // FIXME (LK4D4): Also, maybe makes sense to call "logs" job, it is like attach // but without hijacking for stdin. Also, with attach there can be race // condition because of some output already was printed before it. - return <-b.Daemon.Attach(c, nil, nil, b.OutStream, b.ErrStream) + return <-b.Daemon.Attach(c, c.Config.OpenStdin, c.Config.StdinOnce, c.Config.Tty, nil, nil, b.OutStream, b.ErrStream) }) } diff --git a/daemon/attach.go b/daemon/attach.go index c015ee7258..ade4b89738 100644 --- a/daemon/attach.go +++ b/daemon/attach.go @@ -8,9 +8,9 @@ import ( "time" "github.com/docker/docker/engine" - "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/jsonlog" "github.com/docker/docker/pkg/log" + "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/utils" ) @@ -103,7 +103,7 @@ func (daemon *Daemon) ContainerAttach(job *engine.Job) engine.Status { cStderr = job.Stderr } - <-daemon.Attach(container, cStdin, cStdinCloser, cStdout, cStderr) + <-daemon.Attach(container, container.Config.OpenStdin, container.Config.StdinOnce, container.Config.Tty, cStdin, cStdinCloser, cStdout, cStderr) // If we are in stdinonce mode, wait for the process to end // otherwise, simply return if container.Config.StdinOnce && !container.Config.Tty { @@ -119,15 +119,17 @@ func (daemon *Daemon) ContainerAttach(job *engine.Job) engine.Status { // Attach and ContainerAttach. // // This method is in use by builder/builder.go. -func (daemon *Daemon) Attach(container *Container, stdin io.ReadCloser, stdinCloser io.Closer, stdout io.Writer, stderr io.Writer) chan error { +func (daemon *Daemon) Attach(container *Container, openStdin, stdinOnce, tty bool, stdin io.ReadCloser, stdinCloser io.Closer, stdout io.Writer, stderr io.Writer) chan error { var ( cStdout, cStderr io.ReadCloser nJobs int errors = make(chan error, 3) ) - if stdin != nil && container.Config.OpenStdin { - nJobs++ + // Connect stdin of container to the http conn. + if stdin != nil && openStdin { + nJobs += 1 + // Get the stdin pipe. if cStdin, err := container.StdinPipe(); err != nil { errors <- err } else { @@ -135,7 +137,7 @@ func (daemon *Daemon) Attach(container *Container, stdin io.ReadCloser, stdinClo log.Debugf("attach: stdin: begin") defer log.Debugf("attach: stdin: end") // No matter what, when stdin is closed (io.Copy unblock), close stdout and stderr - if container.Config.StdinOnce && !container.Config.Tty { + if stdinOnce && !tty { defer cStdin.Close() } else { defer func() { @@ -147,10 +149,11 @@ func (daemon *Daemon) Attach(container *Container, stdin io.ReadCloser, stdinClo } }() } - if container.Config.Tty { + if tty { _, err = utils.CopyEscapable(cStdin, stdin) } else { _, err = io.Copy(cStdin, stdin) + } if err == io.ErrClosedPipe { err = nil @@ -163,7 +166,8 @@ func (daemon *Daemon) Attach(container *Container, stdin io.ReadCloser, stdinClo } } if stdout != nil { - nJobs++ + nJobs += 1 + // Get a reader end of a pipe that is attached as stdout to the container. if p, err := container.StdoutPipe(); err != nil { errors <- err } else { @@ -172,7 +176,7 @@ func (daemon *Daemon) Attach(container *Container, stdin io.ReadCloser, stdinClo log.Debugf("attach: stdout: begin") defer log.Debugf("attach: stdout: end") // If we are in StdinOnce mode, then close stdin - if container.Config.StdinOnce && stdin != nil { + if stdinOnce && stdin != nil { defer stdin.Close() } if stdinCloser != nil { @@ -189,6 +193,7 @@ func (daemon *Daemon) Attach(container *Container, stdin io.ReadCloser, stdinClo }() } } else { + // Point stdout of container to a no-op writer. go func() { if stdinCloser != nil { defer stdinCloser.Close() @@ -201,7 +206,7 @@ func (daemon *Daemon) Attach(container *Container, stdin io.ReadCloser, stdinClo }() } if stderr != nil { - nJobs++ + nJobs += 1 if p, err := container.StderrPipe(); err != nil { errors <- err } else { @@ -210,7 +215,8 @@ func (daemon *Daemon) Attach(container *Container, stdin io.ReadCloser, stdinClo log.Debugf("attach: stderr: begin") defer log.Debugf("attach: stderr: end") // If we are in StdinOnce mode, then close stdin - if container.Config.StdinOnce && stdin != nil { + // Why are we closing stdin here and above while handling stdout? + if stdinOnce && stdin != nil { defer stdin.Close() } if stdinCloser != nil { @@ -223,10 +229,12 @@ func (daemon *Daemon) Attach(container *Container, stdin io.ReadCloser, stdinClo if err != nil { log.Errorf("attach: stderr: %s", err) } + log.Debugf("stdout attach end") errors <- err }() } } else { + // Point stderr at a no-op writer. go func() { if stdinCloser != nil { defer stdinCloser.Close() @@ -252,7 +260,7 @@ func (daemon *Daemon) Attach(container *Container, stdin io.ReadCloser, stdinClo // FIXME: how to clean up the stdin goroutine without the unwanted side effect // of closing the passed stdin? Add an intermediary io.Pipe? - for i := 0; i < nJobs; i++ { + for i := 0; i < nJobs; i += 1 { log.Debugf("attach: waiting for job %d/%d", i+1, nJobs) if err := <-errors; err != nil { log.Errorf("attach: job %d returned error %s, aborting all jobs", i+1, err) diff --git a/daemon/execdriver/driver.go b/daemon/execdriver/driver.go index 94cb1632ab..dc57791bf4 100644 --- a/daemon/execdriver/driver.go +++ b/daemon/execdriver/driver.go @@ -42,6 +42,8 @@ type TtyTerminal interface { type Driver interface { Run(c *Command, pipes *Pipes, startCallback StartCallback) (int, error) // Run executes the process and blocks until the process exits and returns the exit code + // Exec executes the process in an existing container, blocks until the process exits and returns the exit code + Exec(c *Command, processConfig *ProcessConfig, pipes *Pipes, startCallback StartCallback) (int, error) Kill(c *Command, sig int) error Pause(c *Command) error Unpause(c *Command) error diff --git a/daemon/execdriver/lxc/driver.go b/daemon/execdriver/lxc/driver.go index 023f4b4d7d..13884fc699 100644 --- a/daemon/execdriver/lxc/driver.go +++ b/daemon/execdriver/lxc/driver.go @@ -527,3 +527,7 @@ func (t *TtyConsole) Close() error { t.SlavePty.Close() return t.MasterPty.Close() } + +func (d *driver) Exec(c *execdriver.Command, processConfig *execdriver.ProcessConfig, pipes *execdriver.Pipes, startCallback execdriver.StartCallback) (int, error) { + return -1, fmt.Errorf("Unsupported: Exec is not supported by the lxc driver") +} diff --git a/daemon/execdriver/native/exec.go b/daemon/execdriver/native/exec.go new file mode 100644 index 0000000000..2aa786eb17 --- /dev/null +++ b/daemon/execdriver/native/exec.go @@ -0,0 +1,68 @@ +// +build linux + +package native + +import ( + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "runtime" + + "github.com/docker/libcontainer" + "github.com/docker/libcontainer/namespaces" + "github.com/docker/docker/daemon/execdriver" + "github.com/docker/docker/reexec" +) + +const commandName = "nsenter-exec" + +func init() { + reexec.Register(commandName, nsenterExec) +} + +func nsenterExec() { + runtime.LockOSThread() + + userArgs := findUserArgs() + + config, err := loadConfigFromFd() + if err != nil { + log.Fatalf("docker-exec: unable to receive config from sync pipe: %s", err) + } + + if err := namespaces.FinalizeSetns(config, userArgs); err != nil { + log.Fatalf("docker-exec: failed to exec: %s", err) + } +} + +func (d *driver) Exec(c *execdriver.Command, processConfig *execdriver.ProcessConfig, pipes *execdriver.Pipes, startCallback execdriver.StartCallback) (int, error) { + active := d.activeContainers[c.ID] + if active == nil { + return -1, fmt.Errorf("No active container exists with ID %s", c.ID) + } + state, err := libcontainer.GetState(filepath.Join(d.root, c.ID)) + if err != nil { + return -1, fmt.Errorf("State unavailable for container with ID %s. The container may have been cleaned up already. Error: %s", c.ID, err) + } + + var term execdriver.Terminal + + if processConfig.Tty { + term, err = NewTtyConsole(processConfig, pipes) + } else { + term, err = execdriver.NewStdConsole(processConfig, pipes) + } + + processConfig.Terminal = term + + args := append([]string{processConfig.Entrypoint}, processConfig.Arguments...) + + return namespaces.ExecIn(active.container, state, args, os.Args[0], "exec", processConfig.Stdin, processConfig.Stdout, processConfig.Stderr, processConfig.Console, + func(cmd *exec.Cmd) { + if startCallback != nil { + startCallback(&c.ProcessConfig, cmd.Process.Pid) + } + }) +} diff --git a/daemon/execdriver/native/utils.go b/daemon/execdriver/native/utils.go new file mode 100644 index 0000000000..05266ea144 --- /dev/null +++ b/daemon/execdriver/native/utils.go @@ -0,0 +1,40 @@ +// +build linux + +package native + +import ( + "os" + + "github.com/docker/libcontainer" + "github.com/docker/libcontainer/syncpipe" +) + +func findUserArgs() []string { + i := 0 + for _, a := range os.Args { + i++ + + if a == "--" { + break + } + } + + return os.Args[i:] +} + +// loadConfigFromFd loads a container's config from the sync pipe that is provided by +// fd 3 when running a process +func loadConfigFromFd() (*libcontainer.Config, error) { + syncPipe, err := syncpipe.NewSyncPipeFromFd(0, 3) + if err != nil { + return nil, err + } + + var config *libcontainer.Config + if err := syncPipe.ReadFromParent(&config); err != nil { + return nil, err + } + + return config, nil +} + From 5130fe5d38837302e72bdc5e4bd1f5fa1df72c7f Mon Sep 17 00:00:00 2001 From: Vishnu Kannan Date: Tue, 9 Sep 2014 04:19:32 +0000 Subject: [PATCH 02/12] Adding support for docker exec in daemon. Docker-DCO-1.1-Signed-off-by: Vishnu Kannan (github: vishh) --- builder/internals.go | 2 +- daemon/attach.go | 16 +-- daemon/daemon.go | 14 +-- daemon/exec.go | 180 ++++++++++++++++++++++++++++++ daemon/execdriver/native/exec.go | 6 +- daemon/execdriver/native/utils.go | 3 +- runconfig/exec.go | 78 +++++++++++++ 7 files changed, 278 insertions(+), 21 deletions(-) create mode 100644 daemon/exec.go create mode 100644 runconfig/exec.go diff --git a/builder/internals.go b/builder/internals.go index 4fe1500358..aa8ec5cf72 100644 --- a/builder/internals.go +++ b/builder/internals.go @@ -407,7 +407,7 @@ func (b *Builder) run(c *daemon.Container) error { // FIXME (LK4D4): Also, maybe makes sense to call "logs" job, it is like attach // but without hijacking for stdin. Also, with attach there can be race // condition because of some output already was printed before it. - return <-b.Daemon.Attach(c, c.Config.OpenStdin, c.Config.StdinOnce, c.Config.Tty, nil, nil, b.OutStream, b.ErrStream) + return <-b.Daemon.Attach(&c.StreamConfig, c.Config.OpenStdin, c.Config.StdinOnce, c.Config.Tty, nil, nil, b.OutStream, b.ErrStream) }) } diff --git a/daemon/attach.go b/daemon/attach.go index ade4b89738..88883e7a77 100644 --- a/daemon/attach.go +++ b/daemon/attach.go @@ -8,9 +8,9 @@ import ( "time" "github.com/docker/docker/engine" + "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/jsonlog" "github.com/docker/docker/pkg/log" - "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/utils" ) @@ -103,7 +103,7 @@ func (daemon *Daemon) ContainerAttach(job *engine.Job) engine.Status { cStderr = job.Stderr } - <-daemon.Attach(container, container.Config.OpenStdin, container.Config.StdinOnce, container.Config.Tty, cStdin, cStdinCloser, cStdout, cStderr) + <-daemon.Attach(&container.StreamConfig, container.Config.OpenStdin, container.Config.StdinOnce, container.Config.Tty, cStdin, cStdinCloser, cStdout, cStderr) // If we are in stdinonce mode, wait for the process to end // otherwise, simply return if container.Config.StdinOnce && !container.Config.Tty { @@ -119,7 +119,7 @@ func (daemon *Daemon) ContainerAttach(job *engine.Job) engine.Status { // Attach and ContainerAttach. // // This method is in use by builder/builder.go. -func (daemon *Daemon) Attach(container *Container, openStdin, stdinOnce, tty bool, stdin io.ReadCloser, stdinCloser io.Closer, stdout io.Writer, stderr io.Writer) chan error { +func (daemon *Daemon) Attach(streamConfig *StreamConfig, openStdin, stdinOnce, tty bool, stdin io.ReadCloser, stdinCloser io.Closer, stdout io.Writer, stderr io.Writer) chan error { var ( cStdout, cStderr io.ReadCloser nJobs int @@ -130,7 +130,7 @@ func (daemon *Daemon) Attach(container *Container, openStdin, stdinOnce, tty boo if stdin != nil && openStdin { nJobs += 1 // Get the stdin pipe. - if cStdin, err := container.StdinPipe(); err != nil { + if cStdin, err := streamConfig.StdinPipe(); err != nil { errors <- err } else { go func() { @@ -168,7 +168,7 @@ func (daemon *Daemon) Attach(container *Container, openStdin, stdinOnce, tty boo if stdout != nil { nJobs += 1 // Get a reader end of a pipe that is attached as stdout to the container. - if p, err := container.StdoutPipe(); err != nil { + if p, err := streamConfig.StdoutPipe(); err != nil { errors <- err } else { cStdout = p @@ -198,7 +198,7 @@ func (daemon *Daemon) Attach(container *Container, openStdin, stdinOnce, tty boo if stdinCloser != nil { defer stdinCloser.Close() } - if cStdout, err := container.StdoutPipe(); err != nil { + if cStdout, err := streamConfig.StdoutPipe(); err != nil { log.Errorf("attach: stdout pipe: %s", err) } else { io.Copy(&ioutils.NopWriter{}, cStdout) @@ -207,7 +207,7 @@ func (daemon *Daemon) Attach(container *Container, openStdin, stdinOnce, tty boo } if stderr != nil { nJobs += 1 - if p, err := container.StderrPipe(); err != nil { + if p, err := streamConfig.StderrPipe(); err != nil { errors <- err } else { cStderr = p @@ -240,7 +240,7 @@ func (daemon *Daemon) Attach(container *Container, openStdin, stdinOnce, tty boo defer stdinCloser.Close() } - if cStderr, err := container.StderrPipe(); err != nil { + if cStderr, err := streamConfig.StderrPipe(); err != nil { log.Errorf("attach: stdout pipe: %s", err) } else { io.Copy(&ioutils.NopWriter{}, cStderr) diff --git a/daemon/daemon.go b/daemon/daemon.go index b9c652cb4e..973efd6ed1 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -496,17 +496,17 @@ func (daemon *Daemon) generateHostname(id string, config *runconfig.Config) { } } -func (daemon *Daemon) getEntrypointAndArgs(config *runconfig.Config) (string, []string) { +func (daemon *Daemon) getEntrypointAndArgs(configEntrypoint, configCmd []string) (string, []string) { var ( entrypoint string args []string ) - if len(config.Entrypoint) != 0 { - entrypoint = config.Entrypoint[0] - args = append(config.Entrypoint[1:], config.Cmd...) + if len(configEntrypoint) != 0 { + entrypoint = configEntrypoint[0] + args = append(configEntrypoint[1:], configCmd...) } else { - entrypoint = config.Cmd[0] - args = config.Cmd[1:] + entrypoint = configCmd[0] + args = configCmd[1:] } return entrypoint, args } @@ -522,7 +522,7 @@ func (daemon *Daemon) newContainer(name string, config *runconfig.Config, img *i } daemon.generateHostname(id, config) - entrypoint, args := daemon.getEntrypointAndArgs(config) + entrypoint, args := daemon.getEntrypointAndArgs(config.Entrypoint, config.Cmd) container := &Container{ // FIXME: we should generate the ID here instead of receiving it as an argument diff --git a/daemon/exec.go b/daemon/exec.go new file mode 100644 index 0000000000..2bbc1965d7 --- /dev/null +++ b/daemon/exec.go @@ -0,0 +1,180 @@ +// build linux + +package daemon + +import ( + "fmt" + "io" + "io/ioutil" + + "github.com/docker/docker/daemon/execdriver" + "github.com/docker/docker/engine" + "github.com/docker/docker/pkg/broadcastwriter" + "github.com/docker/docker/pkg/ioutils" + "github.com/docker/docker/pkg/log" + "github.com/docker/docker/runconfig" + "github.com/docker/docker/utils" +) + +type ExecConfig struct { + ProcessConfig execdriver.ProcessConfig + StreamConfig StreamConfig + OpenStdin bool +} + +func (d *Daemon) ContainerExec(job *engine.Job) engine.Status { + if len(job.Args) != 1 { + return job.Errorf("Usage: %s container_id command", job.Name) + } + + var ( + cStdin io.ReadCloser + cStdout, cStderr io.Writer + cStdinCloser io.Closer + name = job.Args[0] + ) + + container := d.Get(name) + + if container == nil { + return job.Errorf("No such container: %s", name) + } + + if !container.State.IsRunning() { + return job.Errorf("Container %s is not not running", name) + } + + config := runconfig.ExecConfigFromJob(job) + + if config.AttachStdin { + r, w := io.Pipe() + go func() { + defer w.Close() + io.Copy(w, job.Stdin) + }() + cStdin = r + cStdinCloser = job.Stdin + } + if config.AttachStdout { + cStdout = job.Stdout + } + if config.AttachStderr { + cStderr = job.Stderr + } + + entrypoint, args := d.getEntrypointAndArgs(nil, config.Cmd) + + processConfig := execdriver.ProcessConfig{ + Privileged: config.Privileged, + User: config.User, + Tty: config.Tty, + Entrypoint: entrypoint, + Arguments: args, + } + + execConfig := &ExecConfig{ + OpenStdin: config.AttachStdin, + StreamConfig: StreamConfig{}, + ProcessConfig: processConfig, + } + + execConfig.StreamConfig.stderr = broadcastwriter.New() + execConfig.StreamConfig.stdout = broadcastwriter.New() + // Attach to stdin + if execConfig.OpenStdin { + execConfig.StreamConfig.stdin, execConfig.StreamConfig.stdinPipe = io.Pipe() + } else { + execConfig.StreamConfig.stdinPipe = ioutils.NopWriteCloser(ioutil.Discard) // Silently drop stdin + } + + var execErr, attachErr chan error + go func() { + attachErr = d.Attach(&execConfig.StreamConfig, config.AttachStdin, false, config.Tty, cStdin, cStdinCloser, cStdout, cStderr) + }() + + go func() { + err := container.Exec(execConfig) + if err != nil { + err = fmt.Errorf("Cannot run in container %s: %s", name, err) + } + execErr <- err + }() + + select { + case err := <-attachErr: + return job.Errorf("attach failed with error: %s", err) + case err := <-execErr: + return job.Error(err) + } + + return engine.StatusOK +} + +func (daemon *Daemon) Exec(c *Container, execConfig *ExecConfig, pipes *execdriver.Pipes, startCallback execdriver.StartCallback) (int, error) { + return daemon.execDriver.Exec(c.command, &execConfig.ProcessConfig, pipes, startCallback) +} + +func (container *Container) Exec(execConfig *ExecConfig) error { + container.Lock() + defer container.Unlock() + + waitStart := make(chan struct{}) + + callback := func(processConfig *execdriver.ProcessConfig, pid int) { + if processConfig.Tty { + // The callback is called after the process Start() + // so we are in the parent process. In TTY mode, stdin/out/err is the PtySlace + // which we close here. + if c, ok := processConfig.Stdout.(io.Closer); ok { + c.Close() + } + } + close(waitStart) + } + + // We use a callback here instead of a goroutine and an chan for + // syncronization purposes + cErr := utils.Go(func() error { return container.monitorExec(execConfig, callback) }) + + // Exec should not return until the process is actually running + select { + case <-waitStart: + case err := <-cErr: + return err + } + + return nil +} + +func (container *Container) monitorExec(execConfig *ExecConfig, callback execdriver.StartCallback) error { + var ( + err error + exitCode int + ) + + pipes := execdriver.NewPipes(execConfig.StreamConfig.stdin, execConfig.StreamConfig.stdout, execConfig.StreamConfig.stderr, execConfig.OpenStdin) + exitCode, err = container.daemon.Exec(container, execConfig, pipes, callback) + if err != nil { + log.Errorf("Error running command in existing container %s: %s", container.ID, err) + } + + log.Debugf("Exec task in container %s exited with code %d", container.ID, exitCode) + if execConfig.OpenStdin { + if err := execConfig.StreamConfig.stdin.Close(); err != nil { + log.Errorf("Error closing stdin while running in %s: %s", container.ID, err) + } + } + if err := execConfig.StreamConfig.stdout.Clean(); err != nil { + log.Errorf("Error closing stdout while running in %s: %s", container.ID, err) + } + if err := execConfig.StreamConfig.stderr.Clean(); err != nil { + log.Errorf("Error closing stderr while running in %s: %s", container.ID, err) + } + if execConfig.ProcessConfig.Terminal != nil { + if err := execConfig.ProcessConfig.Terminal.Close(); err != nil { + log.Errorf("Error closing terminal while running in container %s: %s", container.ID, err) + } + } + + return err +} diff --git a/daemon/execdriver/native/exec.go b/daemon/execdriver/native/exec.go index 2aa786eb17..c02819e4ac 100644 --- a/daemon/execdriver/native/exec.go +++ b/daemon/execdriver/native/exec.go @@ -10,10 +10,10 @@ import ( "path/filepath" "runtime" + "github.com/docker/docker/daemon/execdriver" + "github.com/docker/docker/reexec" "github.com/docker/libcontainer" "github.com/docker/libcontainer/namespaces" - "github.com/docker/docker/daemon/execdriver" - "github.com/docker/docker/reexec" ) const commandName = "nsenter-exec" @@ -59,7 +59,7 @@ func (d *driver) Exec(c *execdriver.Command, processConfig *execdriver.ProcessCo args := append([]string{processConfig.Entrypoint}, processConfig.Arguments...) - return namespaces.ExecIn(active.container, state, args, os.Args[0], "exec", processConfig.Stdin, processConfig.Stdout, processConfig.Stderr, processConfig.Console, + return namespaces.ExecIn(active.container, state, args, os.Args[0], "exec", processConfig.Stdin, processConfig.Stdout, processConfig.Stderr, processConfig.Console, func(cmd *exec.Cmd) { if startCallback != nil { startCallback(&c.ProcessConfig, cmd.Process.Pid) diff --git a/daemon/execdriver/native/utils.go b/daemon/execdriver/native/utils.go index 05266ea144..ee05b246e3 100644 --- a/daemon/execdriver/native/utils.go +++ b/daemon/execdriver/native/utils.go @@ -4,7 +4,7 @@ package native import ( "os" - + "github.com/docker/libcontainer" "github.com/docker/libcontainer/syncpipe" ) @@ -37,4 +37,3 @@ func loadConfigFromFd() (*libcontainer.Config, error) { return config, nil } - diff --git a/runconfig/exec.go b/runconfig/exec.go new file mode 100644 index 0000000000..c37ca715bd --- /dev/null +++ b/runconfig/exec.go @@ -0,0 +1,78 @@ +package runconfig + +import ( + "github.com/docker/docker/engine" + flag "github.com/docker/docker/pkg/mflag" +) + +type ExecConfig struct { + User string + Privileged bool + Tty bool + Container string + AttachStdin bool + AttachStderr bool + AttachStdout bool + Detach bool + Cmd []string + Hostname string +} + +func ExecConfigFromJob(job *engine.Job) *ExecConfig { + execConfig := &ExecConfig{ + User: job.Getenv("User"), + Privileged: job.GetenvBool("Privileged"), + Tty: job.GetenvBool("Tty"), + Container: job.Getenv("Container"), + AttachStdin: job.GetenvBool("AttachStdin"), + AttachStderr: job.GetenvBool("AttachStderr"), + AttachStdout: job.GetenvBool("AttachStdout"), + } + if Cmd := job.GetenvList("Cmd"); Cmd != nil { + execConfig.Cmd = Cmd + } + + return execConfig +} + +func ParseExec(cmd *flag.FlagSet, args []string) (*ExecConfig, error) { + var ( + flPrivileged = cmd.Bool([]string{"#privileged", "-privileged"}, false, "Give extended privileges to this container") + 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") + flHostname = cmd.String([]string{"h", "-hostname"}, "", "Container host name") + flUser = cmd.String([]string{"u", "-user"}, "", "Username or UID") + flDetach = cmd.Bool([]string{"d", "-detach"}, false, "Detached mode: run command in the background") + execCmd []string + container string + ) + if err := cmd.Parse(args); err != nil { + return nil, err + } + parsedArgs := cmd.Args() + if len(parsedArgs) > 1 { + container = cmd.Arg(0) + execCmd = parsedArgs[1:] + } + + execConfig := &ExecConfig{ + User: *flUser, + Privileged: *flPrivileged, + Tty: *flTty, + Cmd: execCmd, + Container: container, + Hostname: *flHostname, + Detach: *flDetach, + } + + // If -d is not set, attach to everything by default + if !*flDetach { + execConfig.AttachStdout = true + execConfig.AttachStderr = true + if *flStdin { + execConfig.AttachStdin = true + } + } + + return execConfig, nil +} From 985d57958675ee11e23070e009605c8968d547cf Mon Sep 17 00:00:00 2001 From: Vishnu Kannan Date: Tue, 9 Sep 2014 05:51:53 +0000 Subject: [PATCH 03/12] Adding 'exec' command to remote API and CLI. Docker-DCO-1.1-Signed-off-by: Vishnu Kannan (github: vishh) --- api/client/commands.go | 80 ++++++++++++++++++++++++++++++++++++++++-- api/client/hijack.go | 8 +++-- api/client/utils.go | 15 +++++--- api/server/server.go | 60 +++++++++++++++++++++++++++++++ daemon/daemon.go | 1 + daemon/exec.go | 1 + 6 files changed, 156 insertions(+), 9 deletions(-) diff --git a/api/client/commands.go b/api/client/commands.go index c47f0bfd16..01e7e6d743 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -625,7 +625,7 @@ func (cli *DockerCli) CmdStart(args ...string) error { v.Set("stderr", "1") cErr = utils.Go(func() error { - return cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?"+v.Encode(), tty, in, cli.out, cli.err, nil) + return cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?"+v.Encode(), tty, in, cli.out, cli.err, nil, nil) }) } @@ -1827,7 +1827,7 @@ func (cli *DockerCli) CmdAttach(args ...string) error { defer signal.StopCatch(sigc) } - if err := cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?"+v.Encode(), tty, in, cli.out, cli.err, nil); err != nil { + if err := cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?"+v.Encode(), tty, in, cli.out, cli.err, nil, nil); err != nil { return err } @@ -2109,7 +2109,7 @@ func (cli *DockerCli) CmdRun(args ...string) error { } errCh = utils.Go(func() error { - return cli.hijack("POST", "/containers/"+runResult.Get("Id")+"/attach?"+v.Encode(), config.Tty, in, out, stderr, hijacked) + return cli.hijack("POST", "/containers/"+runResult.Get("Id")+"/attach?"+v.Encode(), config.Tty, in, out, stderr, hijacked, nil) }) } else { close(hijacked) @@ -2299,3 +2299,77 @@ func (cli *DockerCli) CmdLoad(args ...string) error { } return nil } + +func (cli *DockerCli) CmdExec(args ...string) error { + cmd := cli.Subcmd("exec", "[OPTIONS] CONTAINER COMMAND [ARG...]", "Run a command in an existing container") + + execConfig, err := runconfig.ParseExec(cmd, args) + if err != nil { + return err + } + if execConfig.Container == "" { + cmd.Usage() + return nil + } + + if execConfig.Detach { + _, _, err := cli.call("POST", "/containers/"+execConfig.Container+"/exec", execConfig, false) + return err + } + var ( + out, stderr io.Writer + in io.ReadCloser + // We need to instanciate the chan because the select needs it. It can + // be closed but can't be uninitialized. + hijacked = make(chan io.Closer) + errCh chan error + ) + // Block the return until the chan gets closed + defer func() { + log.Debugf("End of CmdExec(), Waiting for hijack to finish.") + if _, ok := <-hijacked; ok { + log.Errorf("Hijack did not finish (chan still open)") + } + }() + + if execConfig.AttachStdin { + in = cli.in + } + if execConfig.AttachStdout { + out = cli.out + } + if execConfig.AttachStderr { + if execConfig.Tty { + stderr = cli.out + } else { + stderr = cli.err + } + } + errCh = utils.Go(func() error { + return cli.hijack("POST", "/containers/"+execConfig.Container+"/exec?", execConfig.Tty, in, out, stderr, hijacked, execConfig) + }) + + // Acknowledge the hijack before starting + select { + case closer := <-hijacked: + // Make sure that hijack gets closed when returning. (result + // in closing hijack chan and freeing server's goroutines. + if closer != nil { + defer closer.Close() + } + case err := <-errCh: + if err != nil { + log.Debugf("Error hijack: %s", err) + return err + } + } + // TODO(vishh): Enable tty size monitoring once the daemon can support that. + if errCh != nil { + if err := <-errCh; err != nil { + log.Debugf("Error hijack: %s", err) + return err + } + } + + return nil +} diff --git a/api/client/hijack.go b/api/client/hijack.go index ba6ebfb0d8..63fd8edb75 100644 --- a/api/client/hijack.go +++ b/api/client/hijack.go @@ -25,14 +25,18 @@ func (cli *DockerCli) dial() (net.Conn, error) { return net.Dial(cli.proto, cli.addr) } -func (cli *DockerCli) hijack(method, path string, setRawTerminal bool, in io.ReadCloser, stdout, stderr io.Writer, started chan io.Closer) error { +func (cli *DockerCli) hijack(method, path string, setRawTerminal bool, in io.ReadCloser, stdout, stderr io.Writer, started chan io.Closer, body interface{}) error { defer func() { if started != nil { close(started) } }() - req, err := http.NewRequest(method, fmt.Sprintf("/v%s%s", api.APIVERSION, path), nil) + params, err := cli.getUrlBody(body) + if err != nil { + return err + } + req, err := http.NewRequest(method, fmt.Sprintf("/v%s%s", api.APIVERSION, path), params) if err != nil { return err } diff --git a/api/client/utils.go b/api/client/utils.go index e4ef8d3875..e1d829e6da 100644 --- a/api/client/utils.go +++ b/api/client/utils.go @@ -40,24 +40,31 @@ func (cli *DockerCli) HTTPClient() *http.Client { return &http.Client{Transport: tr} } -func (cli *DockerCli) call(method, path string, data interface{}, passAuthInfo bool) (io.ReadCloser, int, error) { +func (cli *DockerCli) getUrlBody(data interface{}) (*bytes.Buffer, error) { params := bytes.NewBuffer(nil) if data != nil { if env, ok := data.(engine.Env); ok { if err := env.Encode(params); err != nil { - return nil, -1, err + return nil, err } } else { buf, err := json.Marshal(data) if err != nil { - return nil, -1, err + return nil, err } if _, err := params.Write(buf); err != nil { - return nil, -1, err + return nil, err } } } + return params, nil +} +func (cli *DockerCli) call(method, path string, data interface{}, passAuthInfo bool) (io.ReadCloser, int, error) { + params, err := cli.getUrlBody(data) + if err != nil { + return nil, -1, err + } req, err := http.NewRequest(method, fmt.Sprintf("/v%s%s", api.APIVERSION, path), params) if err != nil { return nil, -1, err diff --git a/api/server/server.go b/api/server/server.go index ea708bc6e9..6201b35d5e 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -1025,6 +1025,65 @@ func postContainersCopy(eng *engine.Engine, version version.Version, w http.Resp return nil } +func postContainersExec(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := parseForm(r); err != nil { + return nil + } + var ( + name = vars["name"] + job = eng.Job("exec", name) + ) + if err := job.DecodeEnv(r.Body); err != nil { + return err + } + var errOut io.Writer = os.Stderr + + if !job.GetenvBool("Detach") { + // Setting up the streaming http interface. + inStream, outStream, err := hijackServer(w) + if err != nil { + return err + } + + defer func() { + if tcpc, ok := inStream.(*net.TCPConn); ok { + tcpc.CloseWrite() + } else { + inStream.Close() + } + }() + defer func() { + if tcpc, ok := outStream.(*net.TCPConn); ok { + tcpc.CloseWrite() + } else if closer, ok := outStream.(io.Closer); ok { + closer.Close() + } + }() + + var errStream io.Writer + + fmt.Fprintf(outStream, "HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\n") + if !job.GetenvBool("Tty") && version.GreaterThanOrEqualTo("1.6") { + errStream = utils.NewStdWriter(outStream, utils.Stderr) + outStream = utils.NewStdWriter(outStream, utils.Stdout) + } else { + errStream = outStream + } + job.Stdin.Add(inStream) + job.Stdout.Add(outStream) + job.Stderr.Set(errStream) + errOut = outStream + } + // Now run the user process in container. + if err := job.Run(); err != nil { + fmt.Fprintf(errOut, "Error running in container %s: %s\n", name, err) + return err + } + w.WriteHeader(http.StatusNoContent) + + return nil +} + func optionsHandler(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { w.WriteHeader(http.StatusOK) return nil @@ -1147,6 +1206,7 @@ func createRouter(eng *engine.Engine, logging, enableCors bool, dockerVersion st "/containers/{name:.*}/resize": postContainersResize, "/containers/{name:.*}/attach": postContainersAttach, "/containers/{name:.*}/copy": postContainersCopy, + "/containers/{name:.*}/exec": postContainersExec, }, "DELETE": { "/containers/{name:.*}": deleteContainers, diff --git a/daemon/daemon.go b/daemon/daemon.go index 973efd6ed1..36e6bac58b 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -122,6 +122,7 @@ func (daemon *Daemon) Install(eng *engine.Engine) error { "unpause": daemon.ContainerUnpause, "wait": daemon.ContainerWait, "image_delete": daemon.ImageDelete, // FIXME: see above + "exec": daemon.ContainerExec, } { if err := eng.Register(name, method); err != nil { return err diff --git a/daemon/exec.go b/daemon/exec.go index 2bbc1965d7..263d1d0fd9 100644 --- a/daemon/exec.go +++ b/daemon/exec.go @@ -92,6 +92,7 @@ func (d *Daemon) ContainerExec(job *engine.Job) engine.Status { attachErr = d.Attach(&execConfig.StreamConfig, config.AttachStdin, false, config.Tty, cStdin, cStdinCloser, cStdout, cStderr) }() + log.Debugf("Exec Config is %+v\n", execConfig) go func() { err := container.Exec(execConfig) if err != nil { From e1cf95b593a57e0c8f15d50bb3e5e8ccfb55defa Mon Sep 17 00:00:00 2001 From: Vishnu Kannan Date: Tue, 9 Sep 2014 17:36:13 +0000 Subject: [PATCH 04/12] Import nsenter in docker. Docker-DCO-1.1-Signed-off-by: Vishnu Kannan (github: vishh) --- daemon/exec.go | 5 ++--- daemon/execdriver/native/driver.go | 1 + daemon/execdriver/native/exec.go | 1 + runconfig/exec.go | 21 +++++++++------------ 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/daemon/exec.go b/daemon/exec.go index 263d1d0fd9..c27171b4a1 100644 --- a/daemon/exec.go +++ b/daemon/exec.go @@ -18,8 +18,8 @@ import ( type ExecConfig struct { ProcessConfig execdriver.ProcessConfig - StreamConfig StreamConfig - OpenStdin bool + StreamConfig + OpenStdin bool } func (d *Daemon) ContainerExec(job *engine.Job) engine.Status { @@ -92,7 +92,6 @@ func (d *Daemon) ContainerExec(job *engine.Job) engine.Status { attachErr = d.Attach(&execConfig.StreamConfig, config.AttachStdin, false, config.Tty, cStdin, cStdinCloser, cStdout, cStderr) }() - log.Debugf("Exec Config is %+v\n", execConfig) go func() { err := container.Exec(execConfig) if err != nil { diff --git a/daemon/execdriver/native/driver.go b/daemon/execdriver/native/driver.go index 1d20de73ea..ef3dc95818 100644 --- a/daemon/execdriver/native/driver.go +++ b/daemon/execdriver/native/driver.go @@ -22,6 +22,7 @@ import ( "github.com/docker/libcontainer/cgroups/systemd" consolepkg "github.com/docker/libcontainer/console" "github.com/docker/libcontainer/namespaces" + _ "github.com/docker/libcontainer/namespaces/nsenter" "github.com/docker/libcontainer/system" ) diff --git a/daemon/execdriver/native/exec.go b/daemon/execdriver/native/exec.go index c02819e4ac..02d627d3ed 100644 --- a/daemon/execdriver/native/exec.go +++ b/daemon/execdriver/native/exec.go @@ -37,6 +37,7 @@ func nsenterExec() { } } +// TODO(vishh): Add support for running in priviledged mode and running as a different user. func (d *driver) Exec(c *execdriver.Command, processConfig *execdriver.ProcessConfig, pipes *execdriver.Pipes, startCallback execdriver.StartCallback) (int, error) { active := d.activeContainers[c.ID] if active == nil { diff --git a/runconfig/exec.go b/runconfig/exec.go index c37ca715bd..abbb985d7c 100644 --- a/runconfig/exec.go +++ b/runconfig/exec.go @@ -15,7 +15,6 @@ type ExecConfig struct { AttachStdout bool Detach bool Cmd []string - Hostname string } func ExecConfigFromJob(job *engine.Job) *ExecConfig { @@ -37,14 +36,11 @@ func ExecConfigFromJob(job *engine.Job) *ExecConfig { func ParseExec(cmd *flag.FlagSet, args []string) (*ExecConfig, error) { var ( - flPrivileged = cmd.Bool([]string{"#privileged", "-privileged"}, false, "Give extended privileges to this container") - 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") - flHostname = cmd.String([]string{"h", "-hostname"}, "", "Container host name") - flUser = cmd.String([]string{"u", "-user"}, "", "Username or UID") - flDetach = cmd.Bool([]string{"d", "-detach"}, false, "Detached mode: run command in the background") - execCmd []string - container string + 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") + flDetach = cmd.Bool([]string{"d", "-detach"}, false, "Detached mode: run command in the background") + execCmd []string + container string ) if err := cmd.Parse(args); err != nil { return nil, err @@ -56,12 +52,13 @@ func ParseExec(cmd *flag.FlagSet, args []string) (*ExecConfig, error) { } execConfig := &ExecConfig{ - User: *flUser, - Privileged: *flPrivileged, + // TODO(vishh): Expose '-u' flag once it is supported. + User: "", + // TODO(vishh): Expose '-p' flag once it is supported. + Privileged: false, Tty: *flTty, Cmd: execCmd, Container: container, - Hostname: *flHostname, Detach: *flDetach, } From d130c10ab78417ebf64284c2a399304767446c88 Mon Sep 17 00:00:00 2001 From: Vishnu Kannan Date: Wed, 10 Sep 2014 07:20:05 +0000 Subject: [PATCH 05/12] Fix bug in attach handling for docker exec. Add docs for 'docker exec' feature. Docker-DCO-1.1-Signed-off-by: Vishnu Kannan (github: vishh) --- daemon/exec.go | 14 +++++++------- docs/man/docker-exec.md | 29 +++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 docs/man/docker-exec.md diff --git a/daemon/exec.go b/daemon/exec.go index c27171b4a1..44bdbfe1d3 100644 --- a/daemon/exec.go +++ b/daemon/exec.go @@ -87,22 +87,22 @@ func (d *Daemon) ContainerExec(job *engine.Job) engine.Status { execConfig.StreamConfig.stdinPipe = ioutils.NopWriteCloser(ioutil.Discard) // Silently drop stdin } - var execErr, attachErr chan error - go func() { - attachErr = d.Attach(&execConfig.StreamConfig, config.AttachStdin, false, config.Tty, cStdin, cStdinCloser, cStdout, cStderr) - }() + attachErr := d.Attach(&execConfig.StreamConfig, config.AttachStdin, false, config.Tty, cStdin, cStdinCloser, cStdout, cStderr) + execErr := make(chan error) go func() { err := container.Exec(execConfig) if err != nil { - err = fmt.Errorf("Cannot run in container %s: %s", name, err) + execErr <- fmt.Errorf("Cannot run in container %s: %s", name, err) } - execErr <- err }() select { case err := <-attachErr: - return job.Errorf("attach failed with error: %s", err) + if err != nil { + return job.Errorf("attach failed with error: %s", err) + } + break case err := <-execErr: return job.Error(err) } diff --git a/docs/man/docker-exec.md b/docs/man/docker-exec.md new file mode 100644 index 0000000000..2a6979545b --- /dev/null +++ b/docs/man/docker-exec.md @@ -0,0 +1,29 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% SEPT 2014 +# NAME +docker-exec - Run a command in an existing container + +# SYNOPSIS +**docker exec** +[**-d**|**--detach**[=*false*]] +[**-i**|**--interactive**[=*false*]] +[**-t**|**--tty**[=*false*]] + CONTAINER COMMAND [ARG...] + +# DESCRIPTION + +Run a process in an existing container. The existing CONTAINER needs is active. + +# Options + +**-d**, **--detach**=*true*|*false* + Detached mode. This runs the new process in the background. + +**-i**, **--interactive**=*true*|*false* + When set to true, keep stdin open even if not attached. The default is false. + +**-t**, **--tty**=*true*|*false* + When set to true Docker can allocate a pseudo-tty and attach to the standard +input of the process. This can be used, for example, to run a throwaway +interactive shell. The default is value is false. From e1818d2a69e691acc9fa8d05ee87c07b2e474a0c Mon Sep 17 00:00:00 2001 From: Vishnu Kannan Date: Wed, 10 Sep 2014 07:39:20 +0000 Subject: [PATCH 06/12] Update cli.md to include 'docker exec' feature. Docker-DCO-1.1-Signed-off-by: Vishnu Kannan (github: vishh) --- docs/sources/reference/commandline/cli.md | 30 +++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 4fc63040c2..ff3fd3a949 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -1295,6 +1295,36 @@ It is even useful to cherry-pick particular tags of an image repository $ sudo docker save -o ubuntu.tar ubuntu:lucid ubuntu:saucy +## exec + + Usage: docker exec CONTAINER COMMAND [ARG...] + + Run a command in an existing container + + -d, --detach=false Detached mode: run the process in the background and exit + -i, --interactive=false Keep STDIN open even if not attached + -t, --tty=false Allocate a pseudo-TTY + +The `docker exec` command runs a user specified command as a new process in an existing +user specified container. The container needs to be active. + +The `docker exec` command will typically be used after `docker run`. + +### Examples: + + $ sudo docker run --name ubuntu_bash --rm -i -t ubuntu bash + +This will create a container named 'ubuntu_bash' and start a bash session. + + $ sudo docker exec -d ubuntu_bash touch /tmp/execWorks + +This will create a new file '/tmp/execWorks' inside the existing and active container +'ubuntu_bash', in the background. + + $ sudo docker exec ubuntu_bash -it bash + +This will create a new bash session in the container 'ubuntu_bash'. + ## search From d980589de6e086d1b4c62cc7495bd952c394890b Mon Sep 17 00:00:00 2001 From: Vishnu Kannan Date: Wed, 10 Sep 2014 23:49:18 +0000 Subject: [PATCH 07/12] Adding integration tests for docker exec feature. Docker-DCO-1.1-Signed-off-by: Vishnu Kannan (github: vishh) --- integration-cli/docker_cli_run_test.go | 75 ++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 21aa383f40..c2c3703cfe 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -1916,3 +1916,78 @@ func TestRunPortInUse(t *testing.T) { deleteAllContainers() logDone("run - fail if port already in use") } + +// "test" should be printed by docker exec +func TestDockerExec(t *testing.T) { + runCmd := exec.Command(dockerBinary, "run", "-d", "--name", "testing", "busybox", "sh", "-c", "echo test > /tmp/file && sleep 100") + out, _, _, err := runCommandWithStdoutStderr(runCmd) + errorOut(err, t, out) + + execCmd := exec.Command(dockerBinary, "exec", "testing", "cat", "/tmp/file") + + out, _, err = runCommandWithOutput(execCmd) + errorOut(err, t, out) + + out = strings.Trim(out, "\r\n") + + if expected := "test"; out != expected { + t.Errorf("container exec should've printed %q but printed %q", expected, out) + } + + deleteAllContainers() + + logDone("exec - basic test") +} + +// "test" should be printed by docker exec +func TestDockerExecInteractive(t *testing.T) { + runCmd := exec.Command(dockerBinary, "run", "-d", "--name", "testing", "busybox", "sh", "-c", "echo test > /tmp/file && sleep 100") + out, _, _, err := runCommandWithStdoutStderr(runCmd) + errorOut(err, t, out) + + execCmd := exec.Command(dockerBinary, "exec", "-i", "testing", "sh") + stdin, err := execCmd.StdinPipe() + if err != nil { + t.Fatal(err) + } + stdout, err := execCmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + + if err := execCmd.Start(); err != nil { + t.Fatal(err) + } + if _, err := stdin.Write([]byte("cat /tmp/file\n")); err != nil { + t.Fatal(err) + } + + r := bufio.NewReader(stdout) + line, err := r.ReadString('\n') + if err != nil { + t.Fatal(err) + } + line = strings.TrimSpace(line) + if line != "test" { + t.Fatalf("Output should be 'test', got '%q'", line) + } + if err := stdin.Close(); err != nil { + t.Fatal(err) + } + finish := make(chan struct{}) + go func() { + if err := execCmd.Wait(); err != nil { + t.Fatal(err) + } + close(finish) + }() + select { + case <-finish: + case <-time.After(1 * time.Second): + t.Fatal("docker exec failed to exit on stdin close") + } + + deleteAllContainers() + + logDone("exec - Interactive test") +} From 669561c2aa8966f9327eca4304a06168bcf5bc49 Mon Sep 17 00:00:00 2001 From: Vishnu Kannan Date: Thu, 11 Sep 2014 00:06:59 +0000 Subject: [PATCH 08/12] Address review comments. Docker-DCO-1.1-Signed-off-by: Vishnu Kannan (github: vishh) --- api/client/hijack.go | 4 ++-- api/client/utils.go | 4 ++-- daemon/attach.go | 6 +++--- daemon/exec.go | 4 ++-- daemon/execdriver/native/exec.go | 5 +++-- daemon/execdriver/native/utils.go | 10 +++------- docs/sources/reference/commandline/cli.md | 2 +- runconfig/exec.go | 4 ++-- 8 files changed, 18 insertions(+), 21 deletions(-) diff --git a/api/client/hijack.go b/api/client/hijack.go index 63fd8edb75..a14376c79e 100644 --- a/api/client/hijack.go +++ b/api/client/hijack.go @@ -25,14 +25,14 @@ func (cli *DockerCli) dial() (net.Conn, error) { return net.Dial(cli.proto, cli.addr) } -func (cli *DockerCli) hijack(method, path string, setRawTerminal bool, in io.ReadCloser, stdout, stderr io.Writer, started chan io.Closer, body interface{}) error { +func (cli *DockerCli) hijack(method, path string, setRawTerminal bool, in io.ReadCloser, stdout, stderr io.Writer, started chan io.Closer, data interface{}) error { defer func() { if started != nil { close(started) } }() - params, err := cli.getUrlBody(body) + params, err := cli.encodeData(data) if err != nil { return err } diff --git a/api/client/utils.go b/api/client/utils.go index e1d829e6da..7bbcf06cf6 100644 --- a/api/client/utils.go +++ b/api/client/utils.go @@ -40,7 +40,7 @@ func (cli *DockerCli) HTTPClient() *http.Client { return &http.Client{Transport: tr} } -func (cli *DockerCli) getUrlBody(data interface{}) (*bytes.Buffer, error) { +func (cli *DockerCli) encodeData(data interface{}) (*bytes.Buffer, error) { params := bytes.NewBuffer(nil) if data != nil { if env, ok := data.(engine.Env); ok { @@ -61,7 +61,7 @@ func (cli *DockerCli) getUrlBody(data interface{}) (*bytes.Buffer, error) { } func (cli *DockerCli) call(method, path string, data interface{}, passAuthInfo bool) (io.ReadCloser, int, error) { - params, err := cli.getUrlBody(data) + params, err := cli.encodeData(data) if err != nil { return nil, -1, err } diff --git a/daemon/attach.go b/daemon/attach.go index 88883e7a77..c6163494c5 100644 --- a/daemon/attach.go +++ b/daemon/attach.go @@ -128,7 +128,7 @@ func (daemon *Daemon) Attach(streamConfig *StreamConfig, openStdin, stdinOnce, t // Connect stdin of container to the http conn. if stdin != nil && openStdin { - nJobs += 1 + nJobs++ // Get the stdin pipe. if cStdin, err := streamConfig.StdinPipe(); err != nil { errors <- err @@ -166,7 +166,7 @@ func (daemon *Daemon) Attach(streamConfig *StreamConfig, openStdin, stdinOnce, t } } if stdout != nil { - nJobs += 1 + nJobs++ // Get a reader end of a pipe that is attached as stdout to the container. if p, err := streamConfig.StdoutPipe(); err != nil { errors <- err @@ -260,7 +260,7 @@ func (daemon *Daemon) Attach(streamConfig *StreamConfig, openStdin, stdinOnce, t // FIXME: how to clean up the stdin goroutine without the unwanted side effect // of closing the passed stdin? Add an intermediary io.Pipe? - for i := 0; i < nJobs; i += 1 { + for i := 0; i < nJobs; i++ { log.Debugf("attach: waiting for job %d/%d", i+1, nJobs) if err := <-errors; err != nil { log.Errorf("attach: job %d returned error %s, aborting all jobs", i+1, err) diff --git a/daemon/exec.go b/daemon/exec.go index 44bdbfe1d3..da7faa27cc 100644 --- a/daemon/exec.go +++ b/daemon/exec.go @@ -24,7 +24,7 @@ type ExecConfig struct { func (d *Daemon) ContainerExec(job *engine.Job) engine.Status { if len(job.Args) != 1 { - return job.Errorf("Usage: %s container_id command", job.Name) + return job.Errorf("Usage: %s [options] container command [args]", job.Name) } var ( @@ -40,7 +40,7 @@ func (d *Daemon) ContainerExec(job *engine.Job) engine.Status { return job.Errorf("No such container: %s", name) } - if !container.State.IsRunning() { + if !container.IsRunning() { return job.Errorf("Container %s is not not running", name) } diff --git a/daemon/execdriver/native/exec.go b/daemon/execdriver/native/exec.go index 02d627d3ed..0f7e5c07bd 100644 --- a/daemon/execdriver/native/exec.go +++ b/daemon/execdriver/native/exec.go @@ -16,15 +16,16 @@ import ( "github.com/docker/libcontainer/namespaces" ) -const commandName = "nsenter-exec" +const execCommandName = "nsenter-exec" func init() { - reexec.Register(commandName, nsenterExec) + reexec.Register(execCommandName, nsenterExec) } func nsenterExec() { runtime.LockOSThread() + // User args are passed after '--' in the command line. userArgs := findUserArgs() config, err := loadConfigFromFd() diff --git a/daemon/execdriver/native/utils.go b/daemon/execdriver/native/utils.go index ee05b246e3..e337cf4316 100644 --- a/daemon/execdriver/native/utils.go +++ b/daemon/execdriver/native/utils.go @@ -10,16 +10,12 @@ import ( ) func findUserArgs() []string { - i := 0 - for _, a := range os.Args { - i++ - + for i, a := range os.Args { if a == "--" { - break + return os.Args[i+1:] } } - - return os.Args[i:] + return []string{} } // loadConfigFromFd loads a container's config from the sync pipe that is provided by diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index ff3fd3a949..7399585783 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -1297,7 +1297,7 @@ It is even useful to cherry-pick particular tags of an image repository ## exec - Usage: docker exec CONTAINER COMMAND [ARG...] + Usage: docker exec [OPTIONS] CONTAINER COMMAND [ARG...] Run a command in an existing container diff --git a/runconfig/exec.go b/runconfig/exec.go index abbb985d7c..07de3e43bc 100644 --- a/runconfig/exec.go +++ b/runconfig/exec.go @@ -27,8 +27,8 @@ func ExecConfigFromJob(job *engine.Job) *ExecConfig { AttachStderr: job.GetenvBool("AttachStderr"), AttachStdout: job.GetenvBool("AttachStdout"), } - if Cmd := job.GetenvList("Cmd"); Cmd != nil { - execConfig.Cmd = Cmd + if cmd := job.GetenvList("Cmd"); cmd != nil { + execConfig.Cmd = cmd } return execConfig From 0029180f7feb59fd0e4fede0bb839e2aa4968811 Mon Sep 17 00:00:00 2001 From: Vishnu Kannan Date: Mon, 15 Sep 2014 17:06:07 +0000 Subject: [PATCH 09/12] Removing 'exec' feature from the CLI until the docker daemon supports resizing of tty sessions for exec'ed commands. Docker-DCO-1.1-Signed-off-by: Vishnu Kannan (github: vishh) --- api/client/commands.go | 74 ----------------------- docs/man/docker-exec.md | 29 --------- docs/sources/reference/commandline/cli.md | 31 ---------- 3 files changed, 134 deletions(-) delete mode 100644 docs/man/docker-exec.md diff --git a/api/client/commands.go b/api/client/commands.go index 01e7e6d743..d3e975739a 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -2299,77 +2299,3 @@ func (cli *DockerCli) CmdLoad(args ...string) error { } return nil } - -func (cli *DockerCli) CmdExec(args ...string) error { - cmd := cli.Subcmd("exec", "[OPTIONS] CONTAINER COMMAND [ARG...]", "Run a command in an existing container") - - execConfig, err := runconfig.ParseExec(cmd, args) - if err != nil { - return err - } - if execConfig.Container == "" { - cmd.Usage() - return nil - } - - if execConfig.Detach { - _, _, err := cli.call("POST", "/containers/"+execConfig.Container+"/exec", execConfig, false) - return err - } - var ( - out, stderr io.Writer - in io.ReadCloser - // We need to instanciate the chan because the select needs it. It can - // be closed but can't be uninitialized. - hijacked = make(chan io.Closer) - errCh chan error - ) - // Block the return until the chan gets closed - defer func() { - log.Debugf("End of CmdExec(), Waiting for hijack to finish.") - if _, ok := <-hijacked; ok { - log.Errorf("Hijack did not finish (chan still open)") - } - }() - - if execConfig.AttachStdin { - in = cli.in - } - if execConfig.AttachStdout { - out = cli.out - } - if execConfig.AttachStderr { - if execConfig.Tty { - stderr = cli.out - } else { - stderr = cli.err - } - } - errCh = utils.Go(func() error { - return cli.hijack("POST", "/containers/"+execConfig.Container+"/exec?", execConfig.Tty, in, out, stderr, hijacked, execConfig) - }) - - // Acknowledge the hijack before starting - select { - case closer := <-hijacked: - // Make sure that hijack gets closed when returning. (result - // in closing hijack chan and freeing server's goroutines. - if closer != nil { - defer closer.Close() - } - case err := <-errCh: - if err != nil { - log.Debugf("Error hijack: %s", err) - return err - } - } - // TODO(vishh): Enable tty size monitoring once the daemon can support that. - if errCh != nil { - if err := <-errCh; err != nil { - log.Debugf("Error hijack: %s", err) - return err - } - } - - return nil -} diff --git a/docs/man/docker-exec.md b/docs/man/docker-exec.md deleted file mode 100644 index 2a6979545b..0000000000 --- a/docs/man/docker-exec.md +++ /dev/null @@ -1,29 +0,0 @@ -% DOCKER(1) Docker User Manuals -% Docker Community -% SEPT 2014 -# NAME -docker-exec - Run a command in an existing container - -# SYNOPSIS -**docker exec** -[**-d**|**--detach**[=*false*]] -[**-i**|**--interactive**[=*false*]] -[**-t**|**--tty**[=*false*]] - CONTAINER COMMAND [ARG...] - -# DESCRIPTION - -Run a process in an existing container. The existing CONTAINER needs is active. - -# Options - -**-d**, **--detach**=*true*|*false* - Detached mode. This runs the new process in the background. - -**-i**, **--interactive**=*true*|*false* - When set to true, keep stdin open even if not attached. The default is false. - -**-t**, **--tty**=*true*|*false* - When set to true Docker can allocate a pseudo-tty and attach to the standard -input of the process. This can be used, for example, to run a throwaway -interactive shell. The default is value is false. diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 7399585783..fcd12ee08d 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -1295,37 +1295,6 @@ It is even useful to cherry-pick particular tags of an image repository $ sudo docker save -o ubuntu.tar ubuntu:lucid ubuntu:saucy -## exec - - Usage: docker exec [OPTIONS] CONTAINER COMMAND [ARG...] - - Run a command in an existing container - - -d, --detach=false Detached mode: run the process in the background and exit - -i, --interactive=false Keep STDIN open even if not attached - -t, --tty=false Allocate a pseudo-TTY - -The `docker exec` command runs a user specified command as a new process in an existing -user specified container. The container needs to be active. - -The `docker exec` command will typically be used after `docker run`. - -### Examples: - - $ sudo docker run --name ubuntu_bash --rm -i -t ubuntu bash - -This will create a container named 'ubuntu_bash' and start a bash session. - - $ sudo docker exec -d ubuntu_bash touch /tmp/execWorks - -This will create a new file '/tmp/execWorks' inside the existing and active container -'ubuntu_bash', in the background. - - $ sudo docker exec ubuntu_bash -it bash - -This will create a new bash session in the container 'ubuntu_bash'. - - ## search Search [Docker Hub](https://hub.docker.com) for images From bfebdfde78753f85d53d62634654de0b80f6872d Mon Sep 17 00:00:00 2001 From: Vishnu Kannan Date: Mon, 15 Sep 2014 22:56:47 +0000 Subject: [PATCH 10/12] Splitting the exec remote API into two separate APIs inorder to support resizing of tty sessions. 1. /container//exec - Creates a new exec command instance in the daemon and container ''. Returns an unique ID for each exec command. 2. /exec//start - Starts an existing exec command instance. Removes the exec command from the daemon once it completes. Adding /exec//resize to resize tty session of an exec command. Docker-DCO-1.1-Signed-off-by: Vishnu Kannan (github: vishh) --- api/server/server.go | 57 ++++++++++++-- daemon/container.go | 5 +- daemon/daemon.go | 7 +- daemon/exec.go | 182 +++++++++++++++++++++++++++++++++---------- daemon/resize.go | 24 ++++++ 5 files changed, 225 insertions(+), 50 deletions(-) diff --git a/api/server/server.go b/api/server/server.go index 6201b35d5e..3df32955f2 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -793,7 +793,7 @@ func postContainersResize(eng *engine.Engine, version version.Version, w http.Re if vars == nil { return fmt.Errorf("Missing parameter") } - if err := eng.Job("resize", vars["name"], r.Form.Get("h"), r.Form.Get("w")).Run(); err != nil { + if err := eng.Job("resize", vars["name"], r.Form.Get("h"), r.Form.Get("w"), r.Form.Get("exec")).Run(); err != nil { return err } return nil @@ -1025,18 +1025,45 @@ func postContainersCopy(eng *engine.Engine, version version.Version, w http.Resp return nil } -func postContainersExec(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { +func postContainerExecCreate(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return nil } var ( - name = vars["name"] - job = eng.Job("exec", name) + out engine.Env + name = vars["name"] + job = eng.Job("execCreate", name) + stdoutBuffer = bytes.NewBuffer(nil) ) if err := job.DecodeEnv(r.Body); err != nil { return err } - var errOut io.Writer = os.Stderr + + job.Stdout.Add(stdoutBuffer) + // Register an instance of Exec in container. + if err := job.Run(); err != nil { + fmt.Fprintf(os.Stderr, "Error setting up exec command in container %s: %s\n", name, err) + return err + } + // Return the ID + out.Set("Id", engine.Tail(stdoutBuffer, 1)) + + return writeJSON(w, http.StatusCreated, out) +} + +func postContainerExecStart(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := parseForm(r); err != nil { + return nil + } + var ( + name = vars["name"] + job = eng.Job("execStart", name) + errOut io.Writer = os.Stderr + ) + + if err := job.DecodeEnv(r.Body); err != nil { + return err + } if !job.GetenvBool("Detach") { // Setting up the streaming http interface. @@ -1076,7 +1103,7 @@ func postContainersExec(eng *engine.Engine, version version.Version, w http.Resp } // Now run the user process in container. if err := job.Run(); err != nil { - fmt.Fprintf(errOut, "Error running in container %s: %s\n", name, err) + fmt.Fprintf(errOut, "Error starting exec command in container %s: %s\n", name, err) return err } w.WriteHeader(http.StatusNoContent) @@ -1084,6 +1111,19 @@ func postContainersExec(eng *engine.Engine, version version.Version, w http.Resp return nil } +func postContainerExecResize(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := parseForm(r); err != nil { + return err + } + if vars == nil { + return fmt.Errorf("Missing parameter") + } + if err := eng.Job("execResize", vars["name"], r.Form.Get("h"), r.Form.Get("w")).Run(); err != nil { + return err + } + return nil +} + func optionsHandler(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { w.WriteHeader(http.StatusOK) return nil @@ -1206,7 +1246,9 @@ func createRouter(eng *engine.Engine, logging, enableCors bool, dockerVersion st "/containers/{name:.*}/resize": postContainersResize, "/containers/{name:.*}/attach": postContainersAttach, "/containers/{name:.*}/copy": postContainersCopy, - "/containers/{name:.*}/exec": postContainersExec, + "/containers/{name:.*}/exec": postContainerExecCreate, + "/exec/{name:.*}/start": postContainerExecStart, + "/exec/{name:.*}/resize": postContainerExecResize, }, "DELETE": { "/containers/{name:.*}": deleteContainers, @@ -1393,6 +1435,7 @@ func ListenAndServe(proto, addr string, job *engine.Job) error { return err } } + } if err := os.Chmod(addr, 0660); err != nil { return err diff --git a/daemon/container.go b/daemon/container.go index e45ec68d52..014899fc3c 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -86,8 +86,9 @@ type Container struct { VolumesRW map[string]bool hostConfig *runconfig.HostConfig - activeLinks map[string]*links.Link - monitor *containerMonitor + activeLinks map[string]*links.Link + monitor *containerMonitor + execCommands *execStore } func (container *Container) FromDisk() error { diff --git a/daemon/daemon.go b/daemon/daemon.go index 36e6bac58b..7ee7948a28 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -85,6 +85,7 @@ type Daemon struct { repository string sysInitPath string containers *contStore + execCommands *execStore graph *graph.Graph repositories *graph.TagStore idIndex *truncindex.TruncIndex @@ -122,7 +123,9 @@ func (daemon *Daemon) Install(eng *engine.Engine) error { "unpause": daemon.ContainerUnpause, "wait": daemon.ContainerWait, "image_delete": daemon.ImageDelete, // FIXME: see above - "exec": daemon.ContainerExec, + "execCreate": daemon.ContainerExecCreate, + "execStart": daemon.ContainerExecStart, + "execResize": daemon.ContainerExecResize, } { if err := eng.Register(name, method); err != nil { return err @@ -539,6 +542,7 @@ func (daemon *Daemon) newContainer(name string, config *runconfig.Config, img *i Driver: daemon.driver.String(), ExecDriver: daemon.execDriver.Name(), State: NewState(), + execCommands: newExecStore(), } container.root = daemon.containerRoot(container.ID) @@ -847,6 +851,7 @@ func NewDaemonFromDirectory(config *Config, eng *engine.Engine) (*Daemon, error) daemon := &Daemon{ repository: daemonRepo, containers: &contStore{s: make(map[string]*Container)}, + execCommands: newExecStore(), graph: g, repositories: repositories, idIndex: truncindex.NewTruncIndex([]string{}), diff --git a/daemon/exec.go b/daemon/exec.go index da7faa27cc..d32930d3ec 100644 --- a/daemon/exec.go +++ b/daemon/exec.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "io/ioutil" + "sync" "github.com/docker/docker/daemon/execdriver" "github.com/docker/docker/engine" @@ -16,52 +17,99 @@ import ( "github.com/docker/docker/utils" ) -type ExecConfig struct { +type execConfig struct { + ID string ProcessConfig execdriver.ProcessConfig StreamConfig - OpenStdin bool + OpenStdin bool + OpenStderr bool + OpenStdout bool + Container *Container } -func (d *Daemon) ContainerExec(job *engine.Job) engine.Status { +type execStore struct { + s map[string]*execConfig + sync.Mutex +} + +func newExecStore() *execStore { + return &execStore{s: make(map[string]*execConfig, 0)} +} + +func (e *execStore) Add(id string, execConfig *execConfig) { + e.Lock() + e.s[id] = execConfig + e.Unlock() +} + +func (e *execStore) Get(id string) *execConfig { + e.Lock() + res := e.s[id] + e.Unlock() + return res +} + +func (e *execStore) Delete(id string) { + e.Lock() + delete(e.s, id) + e.Unlock() +} + +func (execConfig *execConfig) Resize(h, w int) error { + return execConfig.ProcessConfig.Terminal.Resize(h, w) +} + +func (d *Daemon) registerExecCommand(execConfig *execConfig) { + // Storing execs in container inorder to kill them gracefully whenever the container is stopped or removed. + execConfig.Container.execCommands.Add(execConfig.ID, execConfig) + // Storing execs in daemon for easy access via remote API. + d.execCommands.Add(execConfig.ID, execConfig) +} + +func (d *Daemon) getExecConfig(name string) (*execConfig, error) { + if execConfig := d.execCommands.Get(name); execConfig != nil { + if !execConfig.Container.IsRunning() { + return nil, fmt.Errorf("Container %s is not not running", execConfig.Container.ID) + } + return execConfig, nil + } + + return nil, fmt.Errorf("No exec '%s' in found in daemon", name) +} + +func (d *Daemon) unregisterExecCommand(execConfig *execConfig) { + execConfig.Container.execCommands.Delete(execConfig.ID) + d.execCommands.Delete(execConfig.ID) +} + +func (d *Daemon) getActiveContainer(name string) (*Container, error) { + container := d.Get(name) + + if container == nil { + return nil, fmt.Errorf("No such container: %s", name) + } + + if !container.IsRunning() { + return nil, fmt.Errorf("Container %s is not not running", name) + } + + return container, nil +} + +func (d *Daemon) ContainerExecCreate(job *engine.Job) engine.Status { if len(job.Args) != 1 { return job.Errorf("Usage: %s [options] container command [args]", job.Name) } - var ( - cStdin io.ReadCloser - cStdout, cStderr io.Writer - cStdinCloser io.Closer - name = job.Args[0] - ) + var name = job.Args[0] - container := d.Get(name) - - if container == nil { - return job.Errorf("No such container: %s", name) - } - - if !container.IsRunning() { - return job.Errorf("Container %s is not not running", name) + container, err := d.getActiveContainer(name) + if err != nil { + return job.Error(err) } config := runconfig.ExecConfigFromJob(job) - if config.AttachStdin { - r, w := io.Pipe() - go func() { - defer w.Close() - io.Copy(w, job.Stdin) - }() - cStdin = r - cStdinCloser = job.Stdin - } - if config.AttachStdout { - cStdout = job.Stdout - } - if config.AttachStderr { - cStderr = job.Stderr - } - entrypoint, args := d.getEntrypointAndArgs(nil, config.Cmd) processConfig := execdriver.ProcessConfig{ @@ -72,10 +120,60 @@ func (d *Daemon) ContainerExec(job *engine.Job) engine.Status { Arguments: args, } - execConfig := &ExecConfig{ + execConfig := &execConfig{ + ID: utils.GenerateRandomID(), OpenStdin: config.AttachStdin, + OpenStdout: config.AttachStdout, + OpenStderr: config.AttachStderr, StreamConfig: StreamConfig{}, ProcessConfig: processConfig, + Container: container, + } + + d.registerExecCommand(execConfig) + + job.Printf("%s\n", execConfig.ID) + + return engine.StatusOK +} + +func (d *Daemon) ContainerExecStart(job *engine.Job) engine.Status { + if len(job.Args) != 2 { + return job.Errorf("Usage: %s [options] container exec", job.Name) + } + + var ( + cStdin io.ReadCloser + cStdout, cStderr io.Writer + cStdinCloser io.Closer + execName = job.Args[0] + ) + + if execName == "" { + return job.Errorf("ExecName not specified. Cannot start exec command") + } + + execConfig, err := d.getExecConfig(execName) + if err != nil { + return job.Error(err) + } + + container := execConfig.Container + + if execConfig.OpenStdin { + r, w := io.Pipe() + go func() { + defer w.Close() + io.Copy(w, job.Stdin) + }() + cStdin = r + cStdinCloser = job.Stdin + } + if execConfig.OpenStdout { + cStdout = job.Stdout + } + if execConfig.OpenStderr { + cStderr = job.Stderr } execConfig.StreamConfig.stderr = broadcastwriter.New() @@ -87,13 +185,17 @@ func (d *Daemon) ContainerExec(job *engine.Job) engine.Status { execConfig.StreamConfig.stdinPipe = ioutils.NopWriteCloser(ioutil.Discard) // Silently drop stdin } - attachErr := d.Attach(&execConfig.StreamConfig, config.AttachStdin, false, config.Tty, cStdin, cStdinCloser, cStdout, cStderr) + attachErr := d.Attach(&execConfig.StreamConfig, execConfig.OpenStdin, false, execConfig.ProcessConfig.Tty, cStdin, cStdinCloser, cStdout, cStderr) execErr := make(chan error) + + // Remove exec from daemon and container. + defer d.unregisterExecCommand(execConfig) + go func() { err := container.Exec(execConfig) if err != nil { - execErr <- fmt.Errorf("Cannot run in container %s: %s", name, err) + execErr <- fmt.Errorf("Cannot run exec command %s in container %s: %s", execName, container.ID, err) } }() @@ -110,11 +212,11 @@ func (d *Daemon) ContainerExec(job *engine.Job) engine.Status { return engine.StatusOK } -func (daemon *Daemon) Exec(c *Container, execConfig *ExecConfig, pipes *execdriver.Pipes, startCallback execdriver.StartCallback) (int, error) { - return daemon.execDriver.Exec(c.command, &execConfig.ProcessConfig, pipes, startCallback) +func (d *Daemon) Exec(c *Container, execConfig *execConfig, pipes *execdriver.Pipes, startCallback execdriver.StartCallback) (int, error) { + return d.execDriver.Exec(c.command, &execConfig.ProcessConfig, pipes, startCallback) } -func (container *Container) Exec(execConfig *ExecConfig) error { +func (container *Container) Exec(execConfig *execConfig) error { container.Lock() defer container.Unlock() @@ -146,7 +248,7 @@ func (container *Container) Exec(execConfig *ExecConfig) error { return nil } -func (container *Container) monitorExec(execConfig *ExecConfig, callback execdriver.StartCallback) error { +func (container *Container) monitorExec(execConfig *execConfig, callback execdriver.StartCallback) error { var ( err error exitCode int diff --git a/daemon/resize.go b/daemon/resize.go index dd196ff6c4..68c070370a 100644 --- a/daemon/resize.go +++ b/daemon/resize.go @@ -19,6 +19,7 @@ func (daemon *Daemon) ContainerResize(job *engine.Job) engine.Status { if err != nil { return job.Error(err) } + if container := daemon.Get(name); container != nil { if err := container.Resize(height, width); err != nil { return job.Error(err) @@ -27,3 +28,26 @@ func (daemon *Daemon) ContainerResize(job *engine.Job) engine.Status { } return job.Errorf("No such container: %s", name) } + +func (daemon *Daemon) ContainerExecResize(job *engine.Job) engine.Status { + if len(job.Args) != 3 { + return job.Errorf("Not enough arguments. Usage: %s EXEC HEIGHT WIDTH\n", job.Name) + } + name := job.Args[0] + height, err := strconv.Atoi(job.Args[1]) + if err != nil { + return job.Error(err) + } + width, err := strconv.Atoi(job.Args[2]) + if err != nil { + return job.Error(err) + } + execConfig, err := daemon.getExecConfig(name) + if err != nil { + return job.Error(err) + } + if err := execConfig.Resize(height, width); err != nil { + return job.Error(err) + } + return engine.StatusOK +} From 39030382c4809029cc3897e59f97948b00d57ad1 Mon Sep 17 00:00:00 2001 From: Vishnu Kannan Date: Mon, 15 Sep 2014 23:14:04 +0000 Subject: [PATCH 11/12] Adding state to exec commands to prevent multiple starts of a single exec command. Docker-DCO-1.1-Signed-off-by: Vishnu Kannan (github: vishh) --- daemon/exec.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/daemon/exec.go b/daemon/exec.go index d32930d3ec..80c6a9e6d1 100644 --- a/daemon/exec.go +++ b/daemon/exec.go @@ -18,7 +18,9 @@ import ( ) type execConfig struct { + sync.Mutex ID string + Running bool ProcessConfig execdriver.ProcessConfig StreamConfig OpenStdin bool @@ -128,6 +130,7 @@ func (d *Daemon) ContainerExecCreate(job *engine.Job) engine.Status { StreamConfig: StreamConfig{}, ProcessConfig: processConfig, Container: container, + Running: false, } d.registerExecCommand(execConfig) @@ -149,11 +152,20 @@ func (d *Daemon) ContainerExecStart(job *engine.Job) engine.Status { execName = job.Args[0] ) - if execName == "" { - return job.Errorf("ExecName not specified. Cannot start exec command") + execConfig, err := d.getExecConfig(execName) + if err != nil { + return job.Error(err) } - execConfig, err := d.getExecConfig(execName) + func() { + execConfig.Lock() + defer execConfig.Unlock() + if execConfig.Running { + err = fmt.Errorf("Error: Exec command %s is already running", execName) + } + execConfig.Running = true + }() + if err != nil { return job.Error(err) } From c786a8ee5e9db8f5f609cf8721bd1e1513fb0043 Mon Sep 17 00:00:00 2001 From: Vishnu Kannan Date: Tue, 16 Sep 2014 06:43:43 +0000 Subject: [PATCH 12/12] Adding docker exec support in CLI. Fixed a bug in daemon that resulted in accessing of a closed pipe. Docker-DCO-1.1-Signed-off-by: Vishnu Kannan (github: vishh) --- api/client/commands.go | 104 +++++++++++++++++++++- api/client/utils.go | 19 ++-- api/server/server.go | 7 +- daemon/attach.go | 3 +- daemon/exec.go | 10 +-- docs/man/docker-exec.md | 29 ++++++ docs/sources/reference/commandline/cli.md | 30 +++++++ engine/engine.go | 15 ++-- engine/job.go | 26 ++++-- 9 files changed, 208 insertions(+), 35 deletions(-) create mode 100644 docs/man/docker-exec.md diff --git a/api/client/commands.go b/api/client/commands.go index d3e975739a..f4ab5e2f14 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -653,7 +653,7 @@ func (cli *DockerCli) CmdStart(args ...string) error { if *openStdin || *attach { if tty && cli.isTerminal { - if err := cli.monitorTtySize(cmd.Arg(0)); err != nil { + if err := cli.monitorTtySize(cmd.Arg(0), false); err != nil { log.Errorf("Error monitoring TTY size: %s", err) } } @@ -1805,7 +1805,7 @@ func (cli *DockerCli) CmdAttach(args ...string) error { ) if tty && cli.isTerminal { - if err := cli.monitorTtySize(cmd.Arg(0)); err != nil { + if err := cli.monitorTtySize(cmd.Arg(0), false); err != nil { log.Debugf("Error monitoring TTY size: %s", err) } } @@ -2136,7 +2136,7 @@ func (cli *DockerCli) CmdRun(args ...string) error { } if (config.AttachStdin || config.AttachStdout || config.AttachStderr) && config.Tty && cli.isTerminal { - if err := cli.monitorTtySize(runResult.Get("Id")); err != nil { + if err := cli.monitorTtySize(runResult.Get("Id"), false); err != nil { log.Errorf("Error monitoring TTY size: %s", err) } } @@ -2299,3 +2299,101 @@ func (cli *DockerCli) CmdLoad(args ...string) error { } return nil } + +func (cli *DockerCli) CmdExec(args ...string) error { + cmd := cli.Subcmd("exec", "CONTAINER COMMAND [ARG...]", "Run a command in an existing container") + + execConfig, err := runconfig.ParseExec(cmd, args) + if err != nil { + return err + } + if execConfig.Container == "" { + cmd.Usage() + return nil + } + + stream, _, err := cli.call("POST", "/containers/"+execConfig.Container+"/exec", execConfig, false) + if err != nil { + return err + } + + var execResult engine.Env + if err := execResult.Decode(stream); err != nil { + return err + } + + execID := execResult.Get("Id") + + if execID == "" { + fmt.Fprintf(cli.out, "exec ID empty") + return nil + } + + if execConfig.Detach { + if _, _, err := readBody(cli.call("POST", "/exec/"+execID+"/start", execConfig, false)); err != nil { + return err + } + return nil + } + + // Interactive exec requested. + var ( + out, stderr io.Writer + in io.ReadCloser + hijacked = make(chan io.Closer) + errCh chan error + ) + + // Block the return until the chan gets closed + defer func() { + log.Debugf("End of CmdExec(), Waiting for hijack to finish.") + if _, ok := <-hijacked; ok { + log.Errorf("Hijack did not finish (chan still open)") + } + }() + + if execConfig.AttachStdin { + in = cli.in + } + if execConfig.AttachStdout { + out = cli.out + } + if execConfig.AttachStderr { + if execConfig.Tty { + stderr = cli.out + } else { + stderr = cli.err + } + } + errCh = utils.Go(func() error { + return cli.hijack("POST", "/exec/"+execID+"/start", execConfig.Tty, in, out, stderr, hijacked, execConfig) + }) + + // Acknowledge the hijack before starting + select { + case closer := <-hijacked: + // Make sure that hijack gets closed when returning. (result + // in closing hijack chan and freeing server's goroutines. + if closer != nil { + defer closer.Close() + } + case err := <-errCh: + if err != nil { + log.Debugf("Error hijack: %s", err) + return err + } + } + + if execConfig.Tty && cli.isTerminal { + if err := cli.monitorTtySize(execID, true); err != nil { + log.Errorf("Error monitoring TTY size: %s", err) + } + } + + if err := <-errCh; err != nil { + log.Debugf("Error hijack: %s", err) + return err + } + + return nil +} diff --git a/api/client/utils.go b/api/client/utils.go index 7bbcf06cf6..dfff9717d3 100644 --- a/api/client/utils.go +++ b/api/client/utils.go @@ -115,6 +115,7 @@ func (cli *DockerCli) call(method, path string, data interface{}, passAuthInfo b } return nil, resp.StatusCode, fmt.Errorf("Error response from daemon: %s", bytes.TrimSpace(body)) } + return resp.Body, resp.StatusCode, nil } @@ -179,7 +180,7 @@ func (cli *DockerCli) streamHelper(method, path string, setRawTerminal bool, in return nil } -func (cli *DockerCli) resizeTty(id string) { +func (cli *DockerCli) resizeTty(id string, isExec bool) { height, width := cli.getTtySize() if height == 0 && width == 0 { return @@ -187,7 +188,15 @@ func (cli *DockerCli) resizeTty(id string) { v := url.Values{} v.Set("h", strconv.Itoa(height)) v.Set("w", strconv.Itoa(width)) - if _, _, err := readBody(cli.call("POST", "/containers/"+id+"/resize?"+v.Encode(), nil, false)); err != nil { + + path := "" + if !isExec { + path = "/containers/" + id + "/resize?" + } else { + path = "/exec/" + id + "/resize?" + } + + if _, _, err := readBody(cli.call("POST", path+v.Encode(), nil, false)); err != nil { log.Debugf("Error resize: %s", err) } } @@ -226,14 +235,14 @@ func getExitCode(cli *DockerCli, containerId string) (bool, int, error) { return state.GetBool("Running"), state.GetInt("ExitCode"), nil } -func (cli *DockerCli) monitorTtySize(id string) error { - cli.resizeTty(id) +func (cli *DockerCli) monitorTtySize(id string, isExec bool) error { + cli.resizeTty(id, isExec) sigchan := make(chan os.Signal, 1) gosignal.Notify(sigchan, syscall.SIGWINCH) go func() { for _ = range sigchan { - cli.resizeTty(id) + cli.resizeTty(id, isExec) } }() return nil diff --git a/api/server/server.go b/api/server/server.go index 3df32955f2..2eee20d26b 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -663,6 +663,7 @@ func postContainersCreate(eng *engine.Engine, version version.Version, w http.Re } out.Set("Id", engine.Tail(stdoutBuffer, 1)) out.SetList("Warnings", outWarnings) + return writeJSON(w, http.StatusCreated, out) } @@ -793,7 +794,7 @@ func postContainersResize(eng *engine.Engine, version version.Version, w http.Re if vars == nil { return fmt.Errorf("Missing parameter") } - if err := eng.Job("resize", vars["name"], r.Form.Get("h"), r.Form.Get("w"), r.Form.Get("exec")).Run(); err != nil { + if err := eng.Job("resize", vars["name"], r.Form.Get("h"), r.Form.Get("w")).Run(); err != nil { return err } return nil @@ -1060,11 +1061,9 @@ func postContainerExecStart(eng *engine.Engine, version version.Version, w http. job = eng.Job("execStart", name) errOut io.Writer = os.Stderr ) - if err := job.DecodeEnv(r.Body); err != nil { return err } - if !job.GetenvBool("Detach") { // Setting up the streaming http interface. inStream, outStream, err := hijackServer(w) @@ -1102,12 +1101,12 @@ func postContainerExecStart(eng *engine.Engine, version version.Version, w http. errOut = outStream } // Now run the user process in container. + job.SetCloseIO(false) if err := job.Run(); err != nil { fmt.Fprintf(errOut, "Error starting exec command in container %s: %s\n", name, err) return err } w.WriteHeader(http.StatusNoContent) - return nil } diff --git a/daemon/attach.go b/daemon/attach.go index c6163494c5..e257375e23 100644 --- a/daemon/attach.go +++ b/daemon/attach.go @@ -206,7 +206,7 @@ func (daemon *Daemon) Attach(streamConfig *StreamConfig, openStdin, stdinOnce, t }() } if stderr != nil { - nJobs += 1 + nJobs++ if p, err := streamConfig.StderrPipe(); err != nil { errors <- err } else { @@ -229,7 +229,6 @@ func (daemon *Daemon) Attach(streamConfig *StreamConfig, openStdin, stdinOnce, t if err != nil { log.Errorf("attach: stderr: %s", err) } - log.Debugf("stdout attach end") errors <- err }() } diff --git a/daemon/exec.go b/daemon/exec.go index 80c6a9e6d1..02f4804605 100644 --- a/daemon/exec.go +++ b/daemon/exec.go @@ -20,7 +20,7 @@ import ( type execConfig struct { sync.Mutex ID string - Running bool + Running bool ProcessConfig execdriver.ProcessConfig StreamConfig OpenStdin bool @@ -130,7 +130,7 @@ func (d *Daemon) ContainerExecCreate(job *engine.Job) engine.Status { StreamConfig: StreamConfig{}, ProcessConfig: processConfig, Container: container, - Running: false, + Running: false, } d.registerExecCommand(execConfig) @@ -141,8 +141,8 @@ func (d *Daemon) ContainerExecCreate(job *engine.Job) engine.Status { } func (d *Daemon) ContainerExecStart(job *engine.Job) engine.Status { - if len(job.Args) != 2 { - return job.Errorf("Usage: %s [options] container exec", job.Name) + if len(job.Args) != 1 { + return job.Errorf("Usage: %s [options] exec", job.Name) } var ( @@ -165,11 +165,11 @@ func (d *Daemon) ContainerExecStart(job *engine.Job) engine.Status { } execConfig.Running = true }() - if err != nil { return job.Error(err) } + log.Debugf("starting exec command %s in container %s", execConfig.ID, execConfig.Container.ID) container := execConfig.Container if execConfig.OpenStdin { diff --git a/docs/man/docker-exec.md b/docs/man/docker-exec.md new file mode 100644 index 0000000000..23c6a1dbb3 --- /dev/null +++ b/docs/man/docker-exec.md @@ -0,0 +1,29 @@ +% DOCKER(1) Docker User Manuals +% Docker Community +% SEPT 2014 +# NAME +docker-exec - Run a command in an existing container + +# SYNOPSIS +**docker exec** +[**-d**|**--detach**[=*false*]] +[**-i**|**--interactive**[=*false*]] +[**-t**|**--tty**[=*false*]] + CONTAINER COMMAND [ARG...] + +# DESCRIPTION + +Run a process in an existing container. The existing CONTAINER needs to be active. + +# Options + +**-d**, **--detach**=*true*|*false* + Detached mode. This runs the new process in the background. + +**-i**, **--interactive**=*true*|*false* + When set to true, keep STDIN open even if not attached. The default is false. + +**-t**, **--tty**=*true*|*false* + When set to true Docker can allocate a pseudo-tty and attach to the standard +input of the process. This can be used, for example, to run a throwaway +interactive shell. The default value is false. diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index fcd12ee08d..cae002994d 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -1295,6 +1295,36 @@ It is even useful to cherry-pick particular tags of an image repository $ sudo docker save -o ubuntu.tar ubuntu:lucid ubuntu:saucy +## exec + + Usage: docker exec CONTAINER COMMAND [ARG...] + + Run a command in an existing container + + -d, --detach=false Detached mode: run the process in the background and exit + -i, --interactive=false Keep STDIN open even if not attached + -t, --tty=false Allocate a pseudo-TTY + +The `docker exec` command runs a user specified command as a new process in an existing +user specified container. The container needs to be active. + +The `docker exec` command will typically be used after `docker run`. + +### Examples: + + $ sudo docker run --name ubuntu_bash --rm -i -t ubuntu bash + +This will create a container named 'ubuntu_bash' and start a bash session. + + $ sudo docker exec -d ubuntu_bash touch /tmp/execWorks + +This will create a new file '/tmp/execWorks' inside the existing and active container +'ubuntu_bash', in the background. + + $ sudo docker exec ubuntu_bash -it bash + +This will create a new bash session in the container 'ubuntu_bash'. + ## search Search [Docker Hub](https://hub.docker.com) for images diff --git a/engine/engine.go b/engine/engine.go index e35acbbaf3..5c708d405f 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -115,13 +115,14 @@ func (eng *Engine) commands() []string { // This function mimics `Command` from the standard os/exec package. func (eng *Engine) Job(name string, args ...string) *Job { job := &Job{ - Eng: eng, - Name: name, - Args: args, - Stdin: NewInput(), - Stdout: NewOutput(), - Stderr: NewOutput(), - env: &Env{}, + Eng: eng, + Name: name, + Args: args, + Stdin: NewInput(), + Stdout: NewOutput(), + Stderr: NewOutput(), + env: &Env{}, + closeIO: true, } if eng.Logging { job.Stderr.Add(ioutils.NopWriteCloser(eng.Stderr)) diff --git a/engine/job.go b/engine/job.go index 5b0080167d..d032ff0215 100644 --- a/engine/job.go +++ b/engine/job.go @@ -31,6 +31,7 @@ type Job struct { handler Handler status Status end time.Time + closeIO bool } type Status int @@ -78,19 +79,22 @@ func (job *Job) Run() error { job.status = job.handler(job) job.end = time.Now() } - // Wait for all background tasks to complete - if err := job.Stdout.Close(); err != nil { - return err - } - if err := job.Stderr.Close(); err != nil { - return err - } - if err := job.Stdin.Close(); err != nil { - return err + if job.closeIO { + // Wait for all background tasks to complete + if err := job.Stdout.Close(); err != nil { + return err + } + if err := job.Stderr.Close(); err != nil { + return err + } + if err := job.Stdin.Close(); err != nil { + return err + } } if job.status != 0 { return fmt.Errorf("%s", Tail(errorMessage, 1)) } + return nil } @@ -228,3 +232,7 @@ func (job *Job) Error(err error) Status { func (job *Job) StatusCode() int { return int(job.status) } + +func (job *Job) SetCloseIO(val bool) { + job.closeIO = val +}