mirror of
https://github.com/docker/compose.git
synced 2025-04-08 17:05:13 +02:00
use docker/cli RunExec and RunStart to handle all the interactive/tty/* terminal logic
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
parent
d999c230a5
commit
1d4b4e3c8e
@ -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,
|
||||
|
@ -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()
|
||||
|
@ -216,6 +216,7 @@ type RunOptions struct {
|
||||
Detach bool
|
||||
AutoRemove bool
|
||||
Tty bool
|
||||
Interactive bool
|
||||
WorkingDir string
|
||||
User string
|
||||
Environment []string
|
||||
|
@ -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)
|
||||
target, 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,
|
||||
})
|
||||
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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
@ -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
|
||||
start := cmd.NewStartOptions()
|
||||
start.OpenStdin = !opts.Detach && opts.Interactive
|
||||
start.Attach = !opts.Detach
|
||||
start.Containers = []string{containerID}
|
||||
|
||||
err = cmd.RunStart(s.dockerCli, &start)
|
||||
if sterr, ok := err.(cli.StatusError); ok {
|
||||
return sterr.StatusCode, 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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 {
|
||||
|
@ -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
pkg/e2e/toto.sh
Executable file
1
pkg/e2e/toto.sh
Executable file
@ -0,0 +1 @@
|
||||
../../bin/docker-compose -f ./fixtures/run-test/compose.yaml run --volumes $(pwd):/foo back ls /foo
|
Loading…
x
Reference in New Issue
Block a user