Explorar o código

use docker/cli RunExec and RunStart to handle all the interactive/tty/* terminal logic

Signed-off-by: Nicolas De Loof <[email protected]>
Nicolas De Loof %!s(int64=3) %!d(string=hai) anos
pai
achega
1d4b4e3c8e
Modificáronse 8 ficheiros con 48 adicións e 309 borrados
  1. 4 0
      cmd/compose/run.go
  2. 1 1
      cmd/main.go
  3. 1 0
      pkg/api/api.go
  4. 19 109
      pkg/compose/exec.go
  5. 0 74
      pkg/compose/resize.go
  6. 14 117
      pkg/compose/run.go
  7. 8 8
      pkg/e2e/compose_run_test.go
  8. 1 0
      pkg/e2e/toto.sh

+ 4 - 0
cmd/compose/run.go

@@ -62,6 +62,9 @@ func (opts runOptions) apply(project *types.Project) error {
 	if err != nil {
 		return err
 	}
+
+	target.Tty = !opts.noTty
+	target.StdinOpen = opts.interactive
 	if !opts.servicePorts {
 		target.Ports = []types.ServicePortConfig{}
 	}
@@ -207,6 +210,7 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
 		Detach:            opts.Detach,
 		AutoRemove:        opts.Remove,
 		Tty:               !opts.noTty,
+		Interactive:       opts.interactive,
 		WorkingDir:        opts.workdir,
 		User:              opts.user,
 		Environment:       opts.environment,

+ 1 - 1
cmd/main.go

@@ -68,7 +68,7 @@ func pluginMain() {
 }
 
 func main() {
-	if commands.RunningAsStandalone() {
+	if plugin.RunningStandalone() {
 		os.Args = append([]string{"docker"}, compatibility.Convert(os.Args[1:])...)
 	}
 	pluginMain()

+ 1 - 0
pkg/api/api.go

@@ -216,6 +216,7 @@ type RunOptions struct {
 	Detach            bool
 	AutoRemove        bool
 	Tty               bool
+	Interactive       bool
 	WorkingDir        string
 	User              string
 	Environment       []string

+ 19 - 109
pkg/compose/exec.go

@@ -19,123 +19,41 @@ package compose
 import (
 	"context"
 	"fmt"
-	"io"
 
+	"github.com/docker/cli/cli"
+	"github.com/docker/cli/cli/command/container"
+	"github.com/docker/compose/v2/pkg/api"
 	moby "github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/filters"
-	"github.com/docker/docker/pkg/stdcopy"
-	"github.com/moby/term"
-
-	"github.com/docker/compose/v2/pkg/api"
 )
 
 func (s *composeService) Exec(ctx context.Context, project string, opts api.RunOptions) (int, error) {
-	container, err := s.getExecTarget(ctx, project, opts)
-	if err != nil {
-		return 0, err
-	}
-
-	exec, err := s.apiClient().ContainerExecCreate(ctx, container.ID, moby.ExecConfig{
-		Cmd:        opts.Command,
-		Env:        opts.Environment,
-		User:       opts.User,
-		Privileged: opts.Privileged,
-		Tty:        opts.Tty,
-		Detach:     opts.Detach,
-		WorkingDir: opts.WorkingDir,
-
-		AttachStdin:  true,
-		AttachStdout: true,
-		AttachStderr: true,
-	})
-	if err != nil {
-		return 0, err
-	}
-
-	if opts.Detach {
-		return 0, s.apiClient().ContainerExecStart(ctx, exec.ID, moby.ExecStartCheck{
-			Detach: true,
-			Tty:    opts.Tty,
-		})
-	}
-
-	resp, err := s.apiClient().ContainerExecAttach(ctx, exec.ID, moby.ExecStartCheck{
-		Tty: opts.Tty,
-	})
+	target, err := s.getExecTarget(ctx, project, opts)
 	if err != nil {
 		return 0, err
 	}
-	defer resp.Close() //nolint:errcheck
 
-	if opts.Tty {
-		s.monitorTTySize(ctx, exec.ID, s.apiClient().ContainerExecResize)
+	exec := container.NewExecOptions()
+	exec.Interactive = opts.Interactive
+	exec.TTY = opts.Tty
+	exec.Detach = opts.Detach
+	exec.User = opts.User
+	exec.Privileged = opts.Privileged
+	exec.Workdir = opts.WorkingDir
+	exec.Container = target.ID
+	exec.Command = opts.Command
+	for _, v := range opts.Environment {
+		err := exec.Env.Set(v)
 		if err != nil {
 			return 0, err
 		}
 	}
 
-	err = s.interactiveExec(ctx, opts, resp)
-	if err != nil {
-		return 0, err
-	}
-
-	return s.getExecExitStatus(ctx, exec.ID)
-}
-
-// inspired by https://github.com/docker/cli/blob/master/cli/command/container/exec.go#L116
-func (s *composeService) interactiveExec(ctx context.Context, opts api.RunOptions, resp moby.HijackedResponse) error {
-	outputDone := make(chan error)
-	inputDone := make(chan error)
-
-	stdout := ContainerStdout{HijackedResponse: resp}
-	stdin := ContainerStdin{HijackedResponse: resp}
-	r, err := s.getEscapeKeyProxy(s.stdin(), opts.Tty)
-	if err != nil {
-		return err
-	}
-
-	in := s.stdin()
-	if in.IsTerminal() && opts.Tty {
-		state, err := term.SetRawTerminal(in.FD())
-		if err != nil {
-			return err
-		}
-		defer term.RestoreTerminal(in.FD(), state) //nolint:errcheck
-	}
-
-	go func() {
-		if opts.Tty {
-			_, err := io.Copy(s.stdout(), stdout)
-			outputDone <- err
-		} else {
-			_, err := stdcopy.StdCopy(s.stdout(), s.stderr(), stdout)
-			outputDone <- err
-		}
-		stdout.Close() //nolint:errcheck
-	}()
-
-	go func() {
-		_, err := io.Copy(stdin, r)
-		inputDone <- err
-		stdin.Close() //nolint:errcheck
-	}()
-
-	for {
-		select {
-		case err := <-outputDone:
-			return err
-		case err := <-inputDone:
-			if _, ok := err.(term.EscapeError); ok {
-				return nil
-			}
-			if err != nil {
-				return err
-			}
-			// Wait for output to complete streaming
-		case <-ctx.Done():
-			return ctx.Err()
-		}
+	err = container.RunExec(s.dockerCli, exec)
+	if sterr, ok := err.(cli.StatusError); ok {
+		return sterr.StatusCode, nil
 	}
+	return 0, err
 }
 
 func (s *composeService) getExecTarget(ctx context.Context, projectName string, opts api.RunOptions) (moby.Container, error) {
@@ -155,11 +73,3 @@ func (s *composeService) getExecTarget(ctx context.Context, projectName string,
 	container := containers[0]
 	return container, nil
 }
-
-func (s *composeService) getExecExitStatus(ctx context.Context, execID string) (int, error) {
-	resp, err := s.apiClient().ContainerExecInspect(ctx, execID)
-	if err != nil {
-		return 0, err
-	}
-	return resp.ExitCode, nil
-}

+ 0 - 74
pkg/compose/resize.go

@@ -1,74 +0,0 @@
-/*
-   Copyright 2020 Docker Compose CLI authors
-
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.
-*/
-
-package compose
-
-import (
-	"context"
-	"os"
-	gosignal "os/signal"
-	"runtime"
-	"time"
-
-	"github.com/buger/goterm"
-	moby "github.com/docker/docker/api/types"
-	"github.com/docker/docker/pkg/signal"
-)
-
-func (s *composeService) monitorTTySize(ctx context.Context, container string, resize func(context.Context, string, moby.ResizeOptions) error) {
-	err := resize(ctx, container, moby.ResizeOptions{ // nolint:errcheck
-		Height: uint(goterm.Height()),
-		Width:  uint(goterm.Width()),
-	})
-	if err != nil {
-		return
-	}
-
-	sigchan := make(chan os.Signal, 1)
-	gosignal.Notify(sigchan, signal.SIGWINCH)
-
-	if runtime.GOOS == "windows" {
-		// Windows has no SIGWINCH support, so we have to poll tty size ¯\_(ツ)_/¯
-		go func() {
-			prevH := goterm.Height()
-			prevW := goterm.Width()
-			for {
-				time.Sleep(time.Millisecond * 250)
-				h := goterm.Height()
-				w := goterm.Width()
-				if prevW != w || prevH != h {
-					sigchan <- signal.SIGWINCH
-				}
-				prevH = h
-				prevW = w
-			}
-		}()
-	}
-
-	go func() {
-		for {
-			select {
-			case <-sigchan:
-				resize(ctx, container, moby.ResizeOptions{ // nolint:errcheck
-					Height: uint(goterm.Height()),
-					Width:  uint(goterm.Width()),
-				})
-			case <-ctx.Done():
-				return
-			}
-		}
-	}()
-}

+ 14 - 117
pkg/compose/run.go

@@ -19,16 +19,11 @@ package compose
 import (
 	"context"
 	"fmt"
-	"io"
-
 	"github.com/compose-spec/compose-go/types"
+	"github.com/docker/cli/cli"
+	cmd "github.com/docker/cli/cli/command/container"
 	"github.com/docker/compose/v2/pkg/api"
-	moby "github.com/docker/docker/api/types"
-	"github.com/docker/docker/api/types/container"
-	"github.com/docker/docker/pkg/ioutils"
-	"github.com/docker/docker/pkg/stdcopy"
 	"github.com/docker/docker/pkg/stringid"
-	"github.com/moby/term"
 )
 
 func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.Project, opts api.RunOptions) (int, error) {
@@ -37,98 +32,16 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.
 		return 0, err
 	}
 
-	if opts.Detach {
-		err := s.apiClient().ContainerStart(ctx, containerID, moby.ContainerStartOptions{})
-		if err != nil {
-			return 0, err
-		}
-		fmt.Fprintln(s.stdout(), containerID)
-		return 0, nil
-	}
-
-	return s.runInteractive(ctx, containerID, opts)
-}
-
-func (s *composeService) runInteractive(ctx context.Context, containerID string, opts api.RunOptions) (int, error) {
-	in := s.stdin()
-	r, err := s.getEscapeKeyProxy(in, opts.Tty)
-	if err != nil {
-		return 0, err
-	}
-
-	stdin, stdout, err := s.getContainerStreams(ctx, containerID)
-	if err != nil {
-		return 0, err
-	}
-
-	if in.IsTerminal() && opts.Tty {
-		state, err := term.SetRawTerminal(in.FD())
-		if err != nil {
-			return 0, err
-		}
-		defer term.RestoreTerminal(in.FD(), state) //nolint:errcheck
-	}
-
-	outputDone := make(chan error)
-	inputDone := make(chan error)
-
-	go func() {
-		if opts.Tty {
-			_, err := io.Copy(s.stdout(), stdout) //nolint:errcheck
-			outputDone <- err
-		} else {
-			_, err := stdcopy.StdCopy(s.stdout(), s.stderr(), stdout) //nolint:errcheck
-			outputDone <- err
-		}
-		stdout.Close() //nolint:errcheck
-	}()
-
-	go func() {
-		_, err := io.Copy(stdin, r)
-		inputDone <- err
-		stdin.Close() //nolint:errcheck
-	}()
-
-	err = s.apiClient().ContainerStart(ctx, containerID, moby.ContainerStartOptions{})
-	if err != nil {
-		return 0, err
-	}
-
-	s.monitorTTySize(ctx, containerID, s.apiClient().ContainerResize)
-
-	for {
-		select {
-		case err := <-outputDone:
-			if err != nil {
-				return 0, err
-			}
-			return s.terminateRun(ctx, containerID, opts)
-		case err := <-inputDone:
-			if _, ok := err.(term.EscapeError); ok {
-				return 0, nil
-			}
-			if err != nil {
-				return 0, err
-			}
-			// Wait for output to complete streaming
-		case <-ctx.Done():
-			return 0, ctx.Err()
-		}
-	}
-}
+	start := cmd.NewStartOptions()
+	start.OpenStdin = !opts.Detach && opts.Interactive
+	start.Attach = !opts.Detach
+	start.Containers = []string{containerID}
 
-func (s *composeService) terminateRun(ctx context.Context, containerID string, opts api.RunOptions) (exitCode int, err error) {
-	exitCh, errCh := s.apiClient().ContainerWait(ctx, containerID, container.WaitConditionNotRunning)
-	select {
-	case exit := <-exitCh:
-		exitCode = int(exit.StatusCode)
-	case err = <-errCh:
-		return
+	err = cmd.RunStart(s.dockerCli, &start)
+	if sterr, ok := err.(cli.StatusError); ok {
+		return sterr.StatusCode, nil
 	}
-	if opts.AutoRemove {
-		err = s.apiClient().ContainerRemove(ctx, containerID, moby.ContainerRemoveOptions{})
-	}
-	return
+	return 0, err
 }
 
 func (s *composeService) prepareRun(ctx context.Context, project *types.Project, opts api.RunOptions) (string, error) {
@@ -147,7 +60,6 @@ func (s *composeService) prepareRun(ctx context.Context, project *types.Project,
 		service.ContainerName = fmt.Sprintf("%s_%s_run_%s", project.Name, service.Name, stringid.TruncateID(slug))
 	}
 	service.Scale = 1
-	service.StdinOpen = true
 	service.Restart = ""
 	if service.Deploy != nil {
 		service.Deploy.RestartPolicy = nil
@@ -171,32 +83,17 @@ func (s *composeService) prepareRun(ctx context.Context, project *types.Project,
 	}
 	updateServices(&service, observedState)
 
-	created, err := s.createContainer(ctx, project, service, service.ContainerName, 1, opts.Detach && opts.AutoRemove, opts.UseNetworkAliases, true)
+	created, err := s.createContainer(ctx, project, service, service.ContainerName, 1,
+		opts.Detach && opts.AutoRemove, opts.UseNetworkAliases, opts.Interactive)
 	if err != nil {
 		return "", err
 	}
-	containerID := created.ID
-	return containerID, nil
-}
-
-func (s *composeService) getEscapeKeyProxy(r io.ReadCloser, isTty bool) (io.ReadCloser, error) {
-	if !isTty {
-		return r, nil
-	}
-	var escapeKeys = []byte{16, 17}
-	if s.configFile().DetachKeys != "" {
-		customEscapeKeys, err := term.ToBytes(s.configFile().DetachKeys)
-		if err != nil {
-			return nil, err
-		}
-		escapeKeys = customEscapeKeys
-	}
-	return ioutils.NewReadCloserWrapper(term.NewEscapeProxy(r, escapeKeys), r.Close), nil
+	return created.ID, nil
 }
 
 func applyRunOptions(project *types.Project, service *types.ServiceConfig, opts api.RunOptions) {
 	service.Tty = opts.Tty
-	service.StdinOpen = true
+	service.StdinOpen = opts.Interactive
 	service.ContainerName = opts.Name
 
 	if len(opts.Command) > 0 {

+ 8 - 8
pkg/e2e/compose_run_test.go

@@ -29,11 +29,11 @@ func TestLocalComposeRun(t *testing.T) {
 	c := NewParallelE2eCLI(t, binDir)
 
 	t.Run("compose run", func(t *testing.T) {
-		res := c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "back")
+		res := c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "-T", "back")
 		lines := Lines(res.Stdout())
 		assert.Equal(t, lines[len(lines)-1], "Hello there!!", res.Stdout())
 		assert.Assert(t, !strings.Contains(res.Combined(), "orphan"))
-		res = c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "back", "echo", "Hello one more time")
+		res = c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "-T", "back", "echo", "Hello one more time")
 		lines = Lines(res.Stdout())
 		assert.Equal(t, lines[len(lines)-1], "Hello one more time", res.Stdout())
 		assert.Assert(t, !strings.Contains(res.Combined(), "orphan"))
@@ -68,7 +68,7 @@ func TestLocalComposeRun(t *testing.T) {
 	})
 
 	t.Run("compose run --rm", func(t *testing.T) {
-		res := c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "--rm", "back", "echo", "Hello again")
+		res := c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "-T", "--rm", "back", "echo", "Hello again")
 		lines := Lines(res.Stdout())
 		assert.Equal(t, lines[len(lines)-1], "Hello again", res.Stdout())
 
@@ -85,7 +85,7 @@ func TestLocalComposeRun(t *testing.T) {
 	t.Run("compose run --volumes", func(t *testing.T) {
 		wd, err := os.Getwd()
 		assert.NilError(t, err)
-		res := c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "--volumes", wd+":/foo", "back", "/bin/sh", "-c", "ls /foo")
+		res := c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "-T", "--volumes", wd+":/foo", "back", "/bin/sh", "-c", "ls /foo")
 		res.Assert(t, icmd.Expected{Out: "compose_run_test.go"})
 
 		res = c.RunDockerCmd("ps", "--all")
@@ -93,18 +93,18 @@ func TestLocalComposeRun(t *testing.T) {
 	})
 
 	t.Run("compose run --publish", func(t *testing.T) {
-		c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "--publish", "8081:80", "-d", "back", "/bin/sh", "-c", "sleep 1")
+		c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "-T", "--publish", "8081:80", "-d", "back", "/bin/sh", "-c", "sleep 1")
 		res := c.RunDockerCmd("ps")
 		assert.Assert(t, strings.Contains(res.Stdout(), "8081->80/tcp"), res.Stdout())
 	})
 
 	t.Run("compose run orphan", func(t *testing.T) {
 		// Use different compose files to get an orphan container
-		c.RunDockerComposeCmd("-f", "./fixtures/run-test/orphan.yaml", "run", "simple")
-		res := c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "back", "echo", "Hello")
+		c.RunDockerComposeCmd("-f", "./fixtures/run-test/orphan.yaml", "run", "-T", "simple")
+		res := c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "-T", "back", "echo", "Hello")
 		assert.Assert(t, strings.Contains(res.Combined(), "orphan"))
 
-		cmd := c.NewDockerCmd("compose", "-f", "./fixtures/run-test/compose.yaml", "run", "back", "echo", "Hello")
+		cmd := c.NewDockerCmd("compose", "-f", "./fixtures/run-test/compose.yaml", "run", "-T", "back", "echo", "Hello")
 		res = icmd.RunCmd(cmd, func(cmd *icmd.Cmd) {
 			cmd.Env = append(cmd.Env, "COMPOSE_IGNORE_ORPHANS=True")
 		})

+ 1 - 0
pkg/e2e/toto.sh

@@ -0,0 +1 @@
+../../bin/docker-compose -f ./fixtures/run-test/compose.yaml run --volumes $(pwd):/foo back ls /foo