Browse Source

Allow non-interactive exec on ACI

If the request is for a non-interactive exec we don't attach the stdin
when executing.
Djordje Lukic 5 years ago
parent
commit
ef2d304762
8 changed files with 75 additions and 42 deletions
  1. 24 21
      azure/aci.go
  2. 4 6
      azure/backend.go
  3. 21 6
      cli/cmd/exec.go
  4. 11 1
      containers/api.go
  5. 2 3
      example/backend.go
  6. 4 4
      local/backend.go
  7. 6 1
      server/proxy/containers.go
  8. 3 0
      tests/aci-e2e/e2e-aci_test.go

+ 24 - 21
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 {

+ 4 - 6
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,
 	)
 }
 

+ 21 - 6
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)
 }

+ 11 - 1
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

+ 2 - 3
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
 }
 

+ 4 - 4
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
 	}()
 

+ 6 - 1
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 {

+ 3 - 0
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"))