Docker-DCO-1.1-Signed-off-by: Vishnu Kannan <vishnuk@google.com> (github: vishh)
| ... | ... |
@@ -407,7 +407,7 @@ func (b *Builder) run(c *daemon.Container) error {
|
| 407 | 407 |
// FIXME (LK4D4): Also, maybe makes sense to call "logs" job, it is like attach |
| 408 | 408 |
// but without hijacking for stdin. Also, with attach there can be race |
| 409 | 409 |
// condition because of some output already was printed before it. |
| 410 |
- return <-b.Daemon.Attach(c, c.Config.OpenStdin, c.Config.StdinOnce, c.Config.Tty, nil, nil, b.OutStream, b.ErrStream) |
|
| 410 |
+ return <-b.Daemon.Attach(&c.StreamConfig, c.Config.OpenStdin, c.Config.StdinOnce, c.Config.Tty, nil, nil, b.OutStream, b.ErrStream) |
|
| 411 | 411 |
}) |
| 412 | 412 |
} |
| 413 | 413 |
|
| ... | ... |
@@ -8,9 +8,9 @@ import ( |
| 8 | 8 |
"time" |
| 9 | 9 |
|
| 10 | 10 |
"github.com/docker/docker/engine" |
| 11 |
+ "github.com/docker/docker/pkg/ioutils" |
|
| 11 | 12 |
"github.com/docker/docker/pkg/jsonlog" |
| 12 | 13 |
"github.com/docker/docker/pkg/log" |
| 13 |
- "github.com/docker/docker/pkg/ioutils" |
|
| 14 | 14 |
"github.com/docker/docker/utils" |
| 15 | 15 |
) |
| 16 | 16 |
|
| ... | ... |
@@ -103,7 +103,7 @@ func (daemon *Daemon) ContainerAttach(job *engine.Job) engine.Status {
|
| 103 | 103 |
cStderr = job.Stderr |
| 104 | 104 |
} |
| 105 | 105 |
|
| 106 |
- <-daemon.Attach(container, container.Config.OpenStdin, container.Config.StdinOnce, container.Config.Tty, cStdin, cStdinCloser, cStdout, cStderr) |
|
| 106 |
+ <-daemon.Attach(&container.StreamConfig, container.Config.OpenStdin, container.Config.StdinOnce, container.Config.Tty, cStdin, cStdinCloser, cStdout, cStderr) |
|
| 107 | 107 |
// If we are in stdinonce mode, wait for the process to end |
| 108 | 108 |
// otherwise, simply return |
| 109 | 109 |
if container.Config.StdinOnce && !container.Config.Tty {
|
| ... | ... |
@@ -119,7 +119,7 @@ func (daemon *Daemon) ContainerAttach(job *engine.Job) engine.Status {
|
| 119 | 119 |
// Attach and ContainerAttach. |
| 120 | 120 |
// |
| 121 | 121 |
// This method is in use by builder/builder.go. |
| 122 |
-func (daemon *Daemon) Attach(container *Container, openStdin, stdinOnce, tty bool, stdin io.ReadCloser, stdinCloser io.Closer, stdout io.Writer, stderr io.Writer) chan error {
|
|
| 122 |
+func (daemon *Daemon) Attach(streamConfig *StreamConfig, openStdin, stdinOnce, tty bool, stdin io.ReadCloser, stdinCloser io.Closer, stdout io.Writer, stderr io.Writer) chan error {
|
|
| 123 | 123 |
var ( |
| 124 | 124 |
cStdout, cStderr io.ReadCloser |
| 125 | 125 |
nJobs int |
| ... | ... |
@@ -130,7 +130,7 @@ func (daemon *Daemon) Attach(container *Container, openStdin, stdinOnce, tty boo |
| 130 | 130 |
if stdin != nil && openStdin {
|
| 131 | 131 |
nJobs += 1 |
| 132 | 132 |
// Get the stdin pipe. |
| 133 |
- if cStdin, err := container.StdinPipe(); err != nil {
|
|
| 133 |
+ if cStdin, err := streamConfig.StdinPipe(); err != nil {
|
|
| 134 | 134 |
errors <- err |
| 135 | 135 |
} else {
|
| 136 | 136 |
go func() {
|
| ... | ... |
@@ -168,7 +168,7 @@ func (daemon *Daemon) Attach(container *Container, openStdin, stdinOnce, tty boo |
| 168 | 168 |
if stdout != nil {
|
| 169 | 169 |
nJobs += 1 |
| 170 | 170 |
// Get a reader end of a pipe that is attached as stdout to the container. |
| 171 |
- if p, err := container.StdoutPipe(); err != nil {
|
|
| 171 |
+ if p, err := streamConfig.StdoutPipe(); err != nil {
|
|
| 172 | 172 |
errors <- err |
| 173 | 173 |
} else {
|
| 174 | 174 |
cStdout = p |
| ... | ... |
@@ -198,7 +198,7 @@ func (daemon *Daemon) Attach(container *Container, openStdin, stdinOnce, tty boo |
| 198 | 198 |
if stdinCloser != nil {
|
| 199 | 199 |
defer stdinCloser.Close() |
| 200 | 200 |
} |
| 201 |
- if cStdout, err := container.StdoutPipe(); err != nil {
|
|
| 201 |
+ if cStdout, err := streamConfig.StdoutPipe(); err != nil {
|
|
| 202 | 202 |
log.Errorf("attach: stdout pipe: %s", err)
|
| 203 | 203 |
} else {
|
| 204 | 204 |
io.Copy(&ioutils.NopWriter{}, cStdout)
|
| ... | ... |
@@ -207,7 +207,7 @@ func (daemon *Daemon) Attach(container *Container, openStdin, stdinOnce, tty boo |
| 207 | 207 |
} |
| 208 | 208 |
if stderr != nil {
|
| 209 | 209 |
nJobs += 1 |
| 210 |
- if p, err := container.StderrPipe(); err != nil {
|
|
| 210 |
+ if p, err := streamConfig.StderrPipe(); err != nil {
|
|
| 211 | 211 |
errors <- err |
| 212 | 212 |
} else {
|
| 213 | 213 |
cStderr = p |
| ... | ... |
@@ -240,7 +240,7 @@ func (daemon *Daemon) Attach(container *Container, openStdin, stdinOnce, tty boo |
| 240 | 240 |
defer stdinCloser.Close() |
| 241 | 241 |
} |
| 242 | 242 |
|
| 243 |
- if cStderr, err := container.StderrPipe(); err != nil {
|
|
| 243 |
+ if cStderr, err := streamConfig.StderrPipe(); err != nil {
|
|
| 244 | 244 |
log.Errorf("attach: stdout pipe: %s", err)
|
| 245 | 245 |
} else {
|
| 246 | 246 |
io.Copy(&ioutils.NopWriter{}, cStderr)
|
| ... | ... |
@@ -496,17 +496,17 @@ func (daemon *Daemon) generateHostname(id string, config *runconfig.Config) {
|
| 496 | 496 |
} |
| 497 | 497 |
} |
| 498 | 498 |
|
| 499 |
-func (daemon *Daemon) getEntrypointAndArgs(config *runconfig.Config) (string, []string) {
|
|
| 499 |
+func (daemon *Daemon) getEntrypointAndArgs(configEntrypoint, configCmd []string) (string, []string) {
|
|
| 500 | 500 |
var ( |
| 501 | 501 |
entrypoint string |
| 502 | 502 |
args []string |
| 503 | 503 |
) |
| 504 |
- if len(config.Entrypoint) != 0 {
|
|
| 505 |
- entrypoint = config.Entrypoint[0] |
|
| 506 |
- args = append(config.Entrypoint[1:], config.Cmd...) |
|
| 504 |
+ if len(configEntrypoint) != 0 {
|
|
| 505 |
+ entrypoint = configEntrypoint[0] |
|
| 506 |
+ args = append(configEntrypoint[1:], configCmd...) |
|
| 507 | 507 |
} else {
|
| 508 |
- entrypoint = config.Cmd[0] |
|
| 509 |
- args = config.Cmd[1:] |
|
| 508 |
+ entrypoint = configCmd[0] |
|
| 509 |
+ args = configCmd[1:] |
|
| 510 | 510 |
} |
| 511 | 511 |
return entrypoint, args |
| 512 | 512 |
} |
| ... | ... |
@@ -522,7 +522,7 @@ func (daemon *Daemon) newContainer(name string, config *runconfig.Config, img *i |
| 522 | 522 |
} |
| 523 | 523 |
|
| 524 | 524 |
daemon.generateHostname(id, config) |
| 525 |
- entrypoint, args := daemon.getEntrypointAndArgs(config) |
|
| 525 |
+ entrypoint, args := daemon.getEntrypointAndArgs(config.Entrypoint, config.Cmd) |
|
| 526 | 526 |
|
| 527 | 527 |
container := &Container{
|
| 528 | 528 |
// FIXME: we should generate the ID here instead of receiving it as an argument |
| 529 | 529 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,180 @@ |
| 0 |
+// build linux |
|
| 1 |
+ |
|
| 2 |
+package daemon |
|
| 3 |
+ |
|
| 4 |
+import ( |
|
| 5 |
+ "fmt" |
|
| 6 |
+ "io" |
|
| 7 |
+ "io/ioutil" |
|
| 8 |
+ |
|
| 9 |
+ "github.com/docker/docker/daemon/execdriver" |
|
| 10 |
+ "github.com/docker/docker/engine" |
|
| 11 |
+ "github.com/docker/docker/pkg/broadcastwriter" |
|
| 12 |
+ "github.com/docker/docker/pkg/ioutils" |
|
| 13 |
+ "github.com/docker/docker/pkg/log" |
|
| 14 |
+ "github.com/docker/docker/runconfig" |
|
| 15 |
+ "github.com/docker/docker/utils" |
|
| 16 |
+) |
|
| 17 |
+ |
|
| 18 |
+type ExecConfig struct {
|
|
| 19 |
+ ProcessConfig execdriver.ProcessConfig |
|
| 20 |
+ StreamConfig StreamConfig |
|
| 21 |
+ OpenStdin bool |
|
| 22 |
+} |
|
| 23 |
+ |
|
| 24 |
+func (d *Daemon) ContainerExec(job *engine.Job) engine.Status {
|
|
| 25 |
+ if len(job.Args) != 1 {
|
|
| 26 |
+ return job.Errorf("Usage: %s container_id command", job.Name)
|
|
| 27 |
+ } |
|
| 28 |
+ |
|
| 29 |
+ var ( |
|
| 30 |
+ cStdin io.ReadCloser |
|
| 31 |
+ cStdout, cStderr io.Writer |
|
| 32 |
+ cStdinCloser io.Closer |
|
| 33 |
+ name = job.Args[0] |
|
| 34 |
+ ) |
|
| 35 |
+ |
|
| 36 |
+ container := d.Get(name) |
|
| 37 |
+ |
|
| 38 |
+ if container == nil {
|
|
| 39 |
+ return job.Errorf("No such container: %s", name)
|
|
| 40 |
+ } |
|
| 41 |
+ |
|
| 42 |
+ if !container.State.IsRunning() {
|
|
| 43 |
+ return job.Errorf("Container %s is not not running", name)
|
|
| 44 |
+ } |
|
| 45 |
+ |
|
| 46 |
+ config := runconfig.ExecConfigFromJob(job) |
|
| 47 |
+ |
|
| 48 |
+ if config.AttachStdin {
|
|
| 49 |
+ r, w := io.Pipe() |
|
| 50 |
+ go func() {
|
|
| 51 |
+ defer w.Close() |
|
| 52 |
+ io.Copy(w, job.Stdin) |
|
| 53 |
+ }() |
|
| 54 |
+ cStdin = r |
|
| 55 |
+ cStdinCloser = job.Stdin |
|
| 56 |
+ } |
|
| 57 |
+ if config.AttachStdout {
|
|
| 58 |
+ cStdout = job.Stdout |
|
| 59 |
+ } |
|
| 60 |
+ if config.AttachStderr {
|
|
| 61 |
+ cStderr = job.Stderr |
|
| 62 |
+ } |
|
| 63 |
+ |
|
| 64 |
+ entrypoint, args := d.getEntrypointAndArgs(nil, config.Cmd) |
|
| 65 |
+ |
|
| 66 |
+ processConfig := execdriver.ProcessConfig{
|
|
| 67 |
+ Privileged: config.Privileged, |
|
| 68 |
+ User: config.User, |
|
| 69 |
+ Tty: config.Tty, |
|
| 70 |
+ Entrypoint: entrypoint, |
|
| 71 |
+ Arguments: args, |
|
| 72 |
+ } |
|
| 73 |
+ |
|
| 74 |
+ execConfig := &ExecConfig{
|
|
| 75 |
+ OpenStdin: config.AttachStdin, |
|
| 76 |
+ StreamConfig: StreamConfig{},
|
|
| 77 |
+ ProcessConfig: processConfig, |
|
| 78 |
+ } |
|
| 79 |
+ |
|
| 80 |
+ execConfig.StreamConfig.stderr = broadcastwriter.New() |
|
| 81 |
+ execConfig.StreamConfig.stdout = broadcastwriter.New() |
|
| 82 |
+ // Attach to stdin |
|
| 83 |
+ if execConfig.OpenStdin {
|
|
| 84 |
+ execConfig.StreamConfig.stdin, execConfig.StreamConfig.stdinPipe = io.Pipe() |
|
| 85 |
+ } else {
|
|
| 86 |
+ execConfig.StreamConfig.stdinPipe = ioutils.NopWriteCloser(ioutil.Discard) // Silently drop stdin |
|
| 87 |
+ } |
|
| 88 |
+ |
|
| 89 |
+ var execErr, attachErr chan error |
|
| 90 |
+ go func() {
|
|
| 91 |
+ attachErr = d.Attach(&execConfig.StreamConfig, config.AttachStdin, false, config.Tty, cStdin, cStdinCloser, cStdout, cStderr) |
|
| 92 |
+ }() |
|
| 93 |
+ |
|
| 94 |
+ go func() {
|
|
| 95 |
+ err := container.Exec(execConfig) |
|
| 96 |
+ if err != nil {
|
|
| 97 |
+ err = fmt.Errorf("Cannot run in container %s: %s", name, err)
|
|
| 98 |
+ } |
|
| 99 |
+ execErr <- err |
|
| 100 |
+ }() |
|
| 101 |
+ |
|
| 102 |
+ select {
|
|
| 103 |
+ case err := <-attachErr: |
|
| 104 |
+ return job.Errorf("attach failed with error: %s", err)
|
|
| 105 |
+ case err := <-execErr: |
|
| 106 |
+ return job.Error(err) |
|
| 107 |
+ } |
|
| 108 |
+ |
|
| 109 |
+ return engine.StatusOK |
|
| 110 |
+} |
|
| 111 |
+ |
|
| 112 |
+func (daemon *Daemon) Exec(c *Container, execConfig *ExecConfig, pipes *execdriver.Pipes, startCallback execdriver.StartCallback) (int, error) {
|
|
| 113 |
+ return daemon.execDriver.Exec(c.command, &execConfig.ProcessConfig, pipes, startCallback) |
|
| 114 |
+} |
|
| 115 |
+ |
|
| 116 |
+func (container *Container) Exec(execConfig *ExecConfig) error {
|
|
| 117 |
+ container.Lock() |
|
| 118 |
+ defer container.Unlock() |
|
| 119 |
+ |
|
| 120 |
+ waitStart := make(chan struct{})
|
|
| 121 |
+ |
|
| 122 |
+ callback := func(processConfig *execdriver.ProcessConfig, pid int) {
|
|
| 123 |
+ if processConfig.Tty {
|
|
| 124 |
+ // The callback is called after the process Start() |
|
| 125 |
+ // so we are in the parent process. In TTY mode, stdin/out/err is the PtySlace |
|
| 126 |
+ // which we close here. |
|
| 127 |
+ if c, ok := processConfig.Stdout.(io.Closer); ok {
|
|
| 128 |
+ c.Close() |
|
| 129 |
+ } |
|
| 130 |
+ } |
|
| 131 |
+ close(waitStart) |
|
| 132 |
+ } |
|
| 133 |
+ |
|
| 134 |
+ // We use a callback here instead of a goroutine and an chan for |
|
| 135 |
+ // syncronization purposes |
|
| 136 |
+ cErr := utils.Go(func() error { return container.monitorExec(execConfig, callback) })
|
|
| 137 |
+ |
|
| 138 |
+ // Exec should not return until the process is actually running |
|
| 139 |
+ select {
|
|
| 140 |
+ case <-waitStart: |
|
| 141 |
+ case err := <-cErr: |
|
| 142 |
+ return err |
|
| 143 |
+ } |
|
| 144 |
+ |
|
| 145 |
+ return nil |
|
| 146 |
+} |
|
| 147 |
+ |
|
| 148 |
+func (container *Container) monitorExec(execConfig *ExecConfig, callback execdriver.StartCallback) error {
|
|
| 149 |
+ var ( |
|
| 150 |
+ err error |
|
| 151 |
+ exitCode int |
|
| 152 |
+ ) |
|
| 153 |
+ |
|
| 154 |
+ pipes := execdriver.NewPipes(execConfig.StreamConfig.stdin, execConfig.StreamConfig.stdout, execConfig.StreamConfig.stderr, execConfig.OpenStdin) |
|
| 155 |
+ exitCode, err = container.daemon.Exec(container, execConfig, pipes, callback) |
|
| 156 |
+ if err != nil {
|
|
| 157 |
+ log.Errorf("Error running command in existing container %s: %s", container.ID, err)
|
|
| 158 |
+ } |
|
| 159 |
+ |
|
| 160 |
+ log.Debugf("Exec task in container %s exited with code %d", container.ID, exitCode)
|
|
| 161 |
+ if execConfig.OpenStdin {
|
|
| 162 |
+ if err := execConfig.StreamConfig.stdin.Close(); err != nil {
|
|
| 163 |
+ log.Errorf("Error closing stdin while running in %s: %s", container.ID, err)
|
|
| 164 |
+ } |
|
| 165 |
+ } |
|
| 166 |
+ if err := execConfig.StreamConfig.stdout.Clean(); err != nil {
|
|
| 167 |
+ log.Errorf("Error closing stdout while running in %s: %s", container.ID, err)
|
|
| 168 |
+ } |
|
| 169 |
+ if err := execConfig.StreamConfig.stderr.Clean(); err != nil {
|
|
| 170 |
+ log.Errorf("Error closing stderr while running in %s: %s", container.ID, err)
|
|
| 171 |
+ } |
|
| 172 |
+ if execConfig.ProcessConfig.Terminal != nil {
|
|
| 173 |
+ if err := execConfig.ProcessConfig.Terminal.Close(); err != nil {
|
|
| 174 |
+ log.Errorf("Error closing terminal while running in container %s: %s", container.ID, err)
|
|
| 175 |
+ } |
|
| 176 |
+ } |
|
| 177 |
+ |
|
| 178 |
+ return err |
|
| 179 |
+} |
| ... | ... |
@@ -10,10 +10,10 @@ import ( |
| 10 | 10 |
"path/filepath" |
| 11 | 11 |
"runtime" |
| 12 | 12 |
|
| 13 |
+ "github.com/docker/docker/daemon/execdriver" |
|
| 14 |
+ "github.com/docker/docker/reexec" |
|
| 13 | 15 |
"github.com/docker/libcontainer" |
| 14 | 16 |
"github.com/docker/libcontainer/namespaces" |
| 15 |
- "github.com/docker/docker/daemon/execdriver" |
|
| 16 |
- "github.com/docker/docker/reexec" |
|
| 17 | 17 |
) |
| 18 | 18 |
|
| 19 | 19 |
const commandName = "nsenter-exec" |
| ... | ... |
@@ -59,7 +59,7 @@ func (d *driver) Exec(c *execdriver.Command, processConfig *execdriver.ProcessCo |
| 59 | 59 |
|
| 60 | 60 |
args := append([]string{processConfig.Entrypoint}, processConfig.Arguments...)
|
| 61 | 61 |
|
| 62 |
- return namespaces.ExecIn(active.container, state, args, os.Args[0], "exec", processConfig.Stdin, processConfig.Stdout, processConfig.Stderr, processConfig.Console, |
|
| 62 |
+ return namespaces.ExecIn(active.container, state, args, os.Args[0], "exec", processConfig.Stdin, processConfig.Stdout, processConfig.Stderr, processConfig.Console, |
|
| 63 | 63 |
func(cmd *exec.Cmd) {
|
| 64 | 64 |
if startCallback != nil {
|
| 65 | 65 |
startCallback(&c.ProcessConfig, cmd.Process.Pid) |
| 41 | 40 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,78 @@ |
| 0 |
+package runconfig |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "github.com/docker/docker/engine" |
|
| 4 |
+ flag "github.com/docker/docker/pkg/mflag" |
|
| 5 |
+) |
|
| 6 |
+ |
|
| 7 |
+type ExecConfig struct {
|
|
| 8 |
+ User string |
|
| 9 |
+ Privileged bool |
|
| 10 |
+ Tty bool |
|
| 11 |
+ Container string |
|
| 12 |
+ AttachStdin bool |
|
| 13 |
+ AttachStderr bool |
|
| 14 |
+ AttachStdout bool |
|
| 15 |
+ Detach bool |
|
| 16 |
+ Cmd []string |
|
| 17 |
+ Hostname string |
|
| 18 |
+} |
|
| 19 |
+ |
|
| 20 |
+func ExecConfigFromJob(job *engine.Job) *ExecConfig {
|
|
| 21 |
+ execConfig := &ExecConfig{
|
|
| 22 |
+ User: job.Getenv("User"),
|
|
| 23 |
+ Privileged: job.GetenvBool("Privileged"),
|
|
| 24 |
+ Tty: job.GetenvBool("Tty"),
|
|
| 25 |
+ Container: job.Getenv("Container"),
|
|
| 26 |
+ AttachStdin: job.GetenvBool("AttachStdin"),
|
|
| 27 |
+ AttachStderr: job.GetenvBool("AttachStderr"),
|
|
| 28 |
+ AttachStdout: job.GetenvBool("AttachStdout"),
|
|
| 29 |
+ } |
|
| 30 |
+ if Cmd := job.GetenvList("Cmd"); Cmd != nil {
|
|
| 31 |
+ execConfig.Cmd = Cmd |
|
| 32 |
+ } |
|
| 33 |
+ |
|
| 34 |
+ return execConfig |
|
| 35 |
+} |
|
| 36 |
+ |
|
| 37 |
+func ParseExec(cmd *flag.FlagSet, args []string) (*ExecConfig, error) {
|
|
| 38 |
+ var ( |
|
| 39 |
+ flPrivileged = cmd.Bool([]string{"#privileged", "-privileged"}, false, "Give extended privileges to this container")
|
|
| 40 |
+ flStdin = cmd.Bool([]string{"i", "-interactive"}, false, "Keep STDIN open even if not attached")
|
|
| 41 |
+ flTty = cmd.Bool([]string{"t", "-tty"}, false, "Allocate a pseudo-TTY")
|
|
| 42 |
+ flHostname = cmd.String([]string{"h", "-hostname"}, "", "Container host name")
|
|
| 43 |
+ flUser = cmd.String([]string{"u", "-user"}, "", "Username or UID")
|
|
| 44 |
+ flDetach = cmd.Bool([]string{"d", "-detach"}, false, "Detached mode: run command in the background")
|
|
| 45 |
+ execCmd []string |
|
| 46 |
+ container string |
|
| 47 |
+ ) |
|
| 48 |
+ if err := cmd.Parse(args); err != nil {
|
|
| 49 |
+ return nil, err |
|
| 50 |
+ } |
|
| 51 |
+ parsedArgs := cmd.Args() |
|
| 52 |
+ if len(parsedArgs) > 1 {
|
|
| 53 |
+ container = cmd.Arg(0) |
|
| 54 |
+ execCmd = parsedArgs[1:] |
|
| 55 |
+ } |
|
| 56 |
+ |
|
| 57 |
+ execConfig := &ExecConfig{
|
|
| 58 |
+ User: *flUser, |
|
| 59 |
+ Privileged: *flPrivileged, |
|
| 60 |
+ Tty: *flTty, |
|
| 61 |
+ Cmd: execCmd, |
|
| 62 |
+ Container: container, |
|
| 63 |
+ Hostname: *flHostname, |
|
| 64 |
+ Detach: *flDetach, |
|
| 65 |
+ } |
|
| 66 |
+ |
|
| 67 |
+ // If -d is not set, attach to everything by default |
|
| 68 |
+ if !*flDetach {
|
|
| 69 |
+ execConfig.AttachStdout = true |
|
| 70 |
+ execConfig.AttachStderr = true |
|
| 71 |
+ if *flStdin {
|
|
| 72 |
+ execConfig.AttachStdin = true |
|
| 73 |
+ } |
|
| 74 |
+ } |
|
| 75 |
+ |
|
| 76 |
+ return execConfig, nil |
|
| 77 |
+} |