From ef2d304762686cf2b7a5879f4c54571819d2bb65 Mon Sep 17 00:00:00 2001 From: Djordje Lukic Date: Wed, 8 Jul 2020 14:31:27 +0200 Subject: [PATCH] Allow non-interactive exec on ACI If the request is for a non-interactive exec we don't attach the stdin when executing. --- azure/aci.go | 45 +++++++++++++++++++---------------- azure/backend.go | 10 ++++---- cli/cmd/exec.go | 27 ++++++++++++++++----- containers/api.go | 12 +++++++++- example/backend.go | 5 ++-- local/backend.go | 8 +++---- server/proxy/containers.go | 7 +++++- tests/aci-e2e/e2e-aci_test.go | 3 +++ 8 files changed, 75 insertions(+), 42 deletions(-) diff --git a/azure/aci.go b/azure/aci.go index 990433e4..81d61bf5 100644 --- a/azure/aci.go +++ b/azure/aci.go @@ -36,6 +36,7 @@ import ( "github.com/docker/api/azure/convert" "github.com/docker/api/azure/login" + "github.com/docker/api/containers" "github.com/docker/api/context/store" "github.com/docker/api/progress" ) @@ -166,7 +167,7 @@ func getTermSize() (*int32, *int32) { return to.Int32Ptr(int32(rows)), to.Int32Ptr(int32(cols)) } -func exec(ctx context.Context, address string, password string, reader io.Reader, writer io.Writer) error { +func exec(ctx context.Context, address string, password string, request containers.ExecRequest) error { conn, _, _, err := ws.DefaultDialer.Dial(ctx, address) if err != nil { return err @@ -190,34 +191,36 @@ func exec(ctx context.Context, address string, password string, reader io.Reader downstreamChannel <- err return } - fmt.Fprint(writer, string(msg)) + fmt.Fprint(request.Stdout, string(msg)) } }() - go func() { - for { - // We send each byte, byte-per-byte over the - // websocket because the console is in raw mode - buffer := make([]byte, 1) - n, err := reader.Read(buffer) - if err != nil { - if err == io.EOF { - upstreamChannel <- nil - return - } - upstreamChannel <- err - return - } - - if n > 0 { - err := wsutil.WriteClientMessage(conn, ws.OpText, buffer) + if request.Interactive { + go func() { + for { + // We send each byte, byte-per-byte over the + // websocket because the console is in raw mode + buffer := make([]byte, 1) + n, err := request.Stdin.Read(buffer) if err != nil { + if err == io.EOF { + upstreamChannel <- nil + return + } upstreamChannel <- err return } + + if n > 0 { + err := wsutil.WriteClientMessage(conn, ws.OpText, buffer) + if err != nil { + upstreamChannel <- err + return + } + } } - } - }() + }() + } for { select { diff --git a/azure/backend.go b/azure/backend.go index 372522fc..10f5210a 100644 --- a/azure/backend.go +++ b/azure/backend.go @@ -19,7 +19,6 @@ package azure import ( "context" "fmt" - "io" "net/http" "strconv" "strings" @@ -192,13 +191,13 @@ func getGroupAndContainerName(containerID string) (groupName string, containerNa return groupName, containerName } -func (cs *aciContainerService) Exec(ctx context.Context, name string, command string, reader io.Reader, writer io.Writer) error { - err := verifyExecCommand(command) +func (cs *aciContainerService) Exec(ctx context.Context, name string, request containers.ExecRequest) error { + err := verifyExecCommand(request.Command) if err != nil { return err } groupName, containerAciName := getGroupAndContainerName(name) - containerExecResponse, err := execACIContainer(ctx, cs.ctx, command, groupName, containerAciName) + containerExecResponse, err := execACIContainer(ctx, cs.ctx, request.Command, groupName, containerAciName) if err != nil { return err } @@ -207,8 +206,7 @@ func (cs *aciContainerService) Exec(ctx context.Context, name string, command st context.Background(), *containerExecResponse.WebSocketURI, *containerExecResponse.Password, - reader, - writer, + request, ) } diff --git a/cli/cmd/exec.go b/cli/cmd/exec.go index ef1ad8ee..cc089123 100644 --- a/cli/cmd/exec.go +++ b/cli/cmd/exec.go @@ -27,10 +27,12 @@ import ( "github.com/spf13/cobra" "github.com/docker/api/client" + "github.com/docker/api/containers" ) type execOpts struct { - Tty bool + tty bool + interactive bool } // ExecCommand runs a command in a running container @@ -45,8 +47,8 @@ func ExecCommand() *cobra.Command { }, } - cmd.Flags().BoolVarP(&opts.Tty, "tty", "t", false, "Allocate a pseudo-TTY") - cmd.Flags().BoolP("interactive", "i", false, "Keep STDIN open even if not attached") + cmd.Flags().BoolVarP(&opts.tty, "tty", "t", false, "Allocate a pseudo-TTY") + cmd.Flags().BoolVarP(&opts.interactive, "interactive", "i", false, "Keep STDIN open even if not attached") return cmd } @@ -57,7 +59,16 @@ func runExec(ctx context.Context, opts execOpts, name string, command string) er return errors.Wrap(err, "cannot connect to backend") } - if opts.Tty { + request := containers.ExecRequest{ + Command: command, + Tty: opts.tty, + Interactive: opts.interactive, + Stdin: os.Stdin, + Stdout: os.Stdout, + Stderr: os.Stderr, + } + + if opts.tty { con := console.Current() if err := con.SetRaw(); err != nil { return err @@ -67,7 +78,11 @@ func runExec(ctx context.Context, opts execOpts, name string, command string) er fmt.Println("Unable to close the console") } }() - return c.ContainerService().Exec(ctx, name, command, con, con) + + request.Stdin = con + request.Stdout = con + request.Stderr = con } - return c.ContainerService().Exec(ctx, name, command, os.Stdin, os.Stdout) + + return c.ContainerService().Exec(ctx, name, request) } diff --git a/containers/api.go b/containers/api.go index bacfc836..650dabfb 100644 --- a/containers/api.go +++ b/containers/api.go @@ -72,6 +72,16 @@ type ContainerConfig struct { Environment []string } +// ExecRequest contaiens configuration about an exec request +type ExecRequest struct { + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer + Command string + Interactive bool + Tty bool +} + // LogsRequest contains configuration about a log request type LogsRequest struct { Follow bool @@ -88,7 +98,7 @@ type Service interface { // Run creates and starts a container Run(ctx context.Context, config ContainerConfig) error // Exec executes a command inside a running container - Exec(ctx context.Context, containerName string, command string, reader io.Reader, writer io.Writer) error + Exec(ctx context.Context, containerName string, request ExecRequest) error // Logs returns all the logs of a container Logs(ctx context.Context, containerName string, request LogsRequest) error // Delete removes containers diff --git a/example/backend.go b/example/backend.go index 5232d73d..befce4f2 100644 --- a/example/backend.go +++ b/example/backend.go @@ -22,7 +22,6 @@ import ( "context" "errors" "fmt" - "io" "github.com/compose-spec/compose-go/cli" @@ -95,8 +94,8 @@ func (cs *containerService) Stop(ctx context.Context, containerName string, time return errors.New("not implemented") } -func (cs *containerService) Exec(ctx context.Context, name string, command string, reader io.Reader, writer io.Writer) error { - fmt.Printf("Executing command %q on container %q", command, name) +func (cs *containerService) Exec(ctx context.Context, name string, request containers.ExecRequest) error { + fmt.Printf("Executing command %q on container %q", request.Command, name) return nil } diff --git a/local/backend.go b/local/backend.go index 6b9090b7..dd41111f 100644 --- a/local/backend.go +++ b/local/backend.go @@ -179,9 +179,9 @@ func (ms *local) Stop(ctx context.Context, containerID string, timeout *uint32) return ms.apiClient.ContainerStop(ctx, containerID, t) } -func (ms *local) Exec(ctx context.Context, name string, command string, reader io.Reader, writer io.Writer) error { +func (ms *local) Exec(ctx context.Context, name string, request containers.ExecRequest) error { cec, err := ms.apiClient.ContainerExecCreate(ctx, name, types.ExecConfig{ - Cmd: []string{command}, + Cmd: []string{request.Command}, Tty: true, AttachStdin: true, AttachStdout: true, @@ -202,12 +202,12 @@ func (ms *local) Exec(ctx context.Context, name string, command string, reader i writeChannel := make(chan error, 10) go func() { - _, err := io.Copy(writer, resp.Reader) + _, err := io.Copy(request.Stdout, resp.Reader) readChannel <- err }() go func() { - _, err := io.Copy(resp.Conn, reader) + _, err := io.Copy(resp.Conn, request.Stdin) writeChannel <- err }() diff --git a/server/proxy/containers.go b/server/proxy/containers.go index 9bcd65b7..92e7cc90 100644 --- a/server/proxy/containers.go +++ b/server/proxy/containers.go @@ -92,7 +92,12 @@ func (p *proxy) Exec(ctx context.Context, request *containersv1.ExecRequest) (*c Stream: stream, } - return &containersv1.ExecResponse{}, Client(ctx).ContainerService().Exec(ctx, request.GetId(), request.GetCommand(), io, io) + return &containersv1.ExecResponse{}, Client(ctx).ContainerService().Exec(ctx, request.GetId(), containers.ExecRequest{ + Stdin: io, + Stdout: io, + Command: request.GetCommand(), + Tty: request.GetTty(), + }) } func (p *proxy) Logs(request *containersv1.LogsRequest, stream containersv1.Containers_LogsServer) error { diff --git a/tests/aci-e2e/e2e-aci_test.go b/tests/aci-e2e/e2e-aci_test.go index f9a85ac8..4e73a4f7 100644 --- a/tests/aci-e2e/e2e-aci_test.go +++ b/tests/aci-e2e/e2e-aci_test.go @@ -105,6 +105,9 @@ func (s *E2eACISuite) TestACIRunSingleContainer() { }) s.Step("exec command", func() { + output := s.NewDockerCommand("exec", testContainerName, "pwd").ExecOrDie() + Expect(output).To(ContainSubstring("/")) + _, err := s.NewDockerCommand("exec", testContainerName, "echo", "fail_with_argument").Exec() Expect(err.Error()).To(ContainSubstring("ACI exec command does not accept arguments to the command. " + "Only the binary should be specified"))