From 5130fe5d38837302e72bdc5e4bd1f5fa1df72c7f Mon Sep 17 00:00:00 2001 From: Vishnu Kannan Date: Tue, 9 Sep 2014 04:19:32 +0000 Subject: [PATCH] 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 +}