Codebase list docker-compose / 1d4b4e3
use docker/cli RunExec and RunStart to handle all the interactive/tty/* terminal logic Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> Nicolas De Loof authored 2 years ago Nicolas De loof committed 2 years ago
8 changed file(s) with 49 addition(s) and 310 deletion(s). Raw diff Collapse all Expand all
6161 if err != nil {
6262 return err
6363 }
64
65 target.Tty = !opts.noTty
66 target.StdinOpen = opts.interactive
6467 if !opts.servicePorts {
6568 target.Ports = []types.ServicePortConfig{}
6669 }
206209 Detach: opts.Detach,
207210 AutoRemove: opts.Remove,
208211 Tty: !opts.noTty,
212 Interactive: opts.interactive,
209213 WorkingDir: opts.workdir,
210214 User: opts.user,
211215 Environment: opts.environment,
6767 }
6868
6969 func main() {
70 if commands.RunningAsStandalone() {
70 if plugin.RunningStandalone() {
7171 os.Args = append([]string{"docker"}, compatibility.Convert(os.Args[1:])...)
7272 }
7373 pluginMain()
215215 Detach bool
216216 AutoRemove bool
217217 Tty bool
218 Interactive bool
218219 WorkingDir string
219220 User string
220221 Environment []string
1818 import (
1919 "context"
2020 "fmt"
21 "io"
2221
22 "github.com/docker/cli/cli"
23 "github.com/docker/cli/cli/command/container"
24 "github.com/docker/compose/v2/pkg/api"
2325 moby "github.com/docker/docker/api/types"
2426 "github.com/docker/docker/api/types/filters"
25 "github.com/docker/docker/pkg/stdcopy"
26 "github.com/moby/term"
27
28 "github.com/docker/compose/v2/pkg/api"
2927 )
3028
3129 func (s *composeService) Exec(ctx context.Context, project string, opts api.RunOptions) (int, error) {
32 container, err := s.getExecTarget(ctx, project, opts)
30 target, err := s.getExecTarget(ctx, project, opts)
3331 if err != nil {
3432 return 0, err
3533 }
3634
37 exec, err := s.apiClient().ContainerExecCreate(ctx, container.ID, moby.ExecConfig{
38 Cmd: opts.Command,
39 Env: opts.Environment,
40 User: opts.User,
41 Privileged: opts.Privileged,
42 Tty: opts.Tty,
43 Detach: opts.Detach,
44 WorkingDir: opts.WorkingDir,
45
46 AttachStdin: true,
47 AttachStdout: true,
48 AttachStderr: true,
49 })
50 if err != nil {
51 return 0, err
52 }
53
54 if opts.Detach {
55 return 0, s.apiClient().ContainerExecStart(ctx, exec.ID, moby.ExecStartCheck{
56 Detach: true,
57 Tty: opts.Tty,
58 })
59 }
60
61 resp, err := s.apiClient().ContainerExecAttach(ctx, exec.ID, moby.ExecStartCheck{
62 Tty: opts.Tty,
63 })
64 if err != nil {
65 return 0, err
66 }
67 defer resp.Close() //nolint:errcheck
68
69 if opts.Tty {
70 s.monitorTTySize(ctx, exec.ID, s.apiClient().ContainerExecResize)
35 exec := container.NewExecOptions()
36 exec.Interactive = opts.Interactive
37 exec.TTY = opts.Tty
38 exec.Detach = opts.Detach
39 exec.User = opts.User
40 exec.Privileged = opts.Privileged
41 exec.Workdir = opts.WorkingDir
42 exec.Container = target.ID
43 exec.Command = opts.Command
44 for _, v := range opts.Environment {
45 err := exec.Env.Set(v)
7146 if err != nil {
7247 return 0, err
7348 }
7449 }
7550
76 err = s.interactiveExec(ctx, opts, resp)
77 if err != nil {
78 return 0, err
51 err = container.RunExec(s.dockerCli, exec)
52 if sterr, ok := err.(cli.StatusError); ok {
53 return sterr.StatusCode, nil
7954 }
80
81 return s.getExecExitStatus(ctx, exec.ID)
82 }
83
84 // inspired by https://github.com/docker/cli/blob/master/cli/command/container/exec.go#L116
85 func (s *composeService) interactiveExec(ctx context.Context, opts api.RunOptions, resp moby.HijackedResponse) error {
86 outputDone := make(chan error)
87 inputDone := make(chan error)
88
89 stdout := ContainerStdout{HijackedResponse: resp}
90 stdin := ContainerStdin{HijackedResponse: resp}
91 r, err := s.getEscapeKeyProxy(s.stdin(), opts.Tty)
92 if err != nil {
93 return err
94 }
95
96 in := s.stdin()
97 if in.IsTerminal() && opts.Tty {
98 state, err := term.SetRawTerminal(in.FD())
99 if err != nil {
100 return err
101 }
102 defer term.RestoreTerminal(in.FD(), state) //nolint:errcheck
103 }
104
105 go func() {
106 if opts.Tty {
107 _, err := io.Copy(s.stdout(), stdout)
108 outputDone <- err
109 } else {
110 _, err := stdcopy.StdCopy(s.stdout(), s.stderr(), stdout)
111 outputDone <- err
112 }
113 stdout.Close() //nolint:errcheck
114 }()
115
116 go func() {
117 _, err := io.Copy(stdin, r)
118 inputDone <- err
119 stdin.Close() //nolint:errcheck
120 }()
121
122 for {
123 select {
124 case err := <-outputDone:
125 return err
126 case err := <-inputDone:
127 if _, ok := err.(term.EscapeError); ok {
128 return nil
129 }
130 if err != nil {
131 return err
132 }
133 // Wait for output to complete streaming
134 case <-ctx.Done():
135 return ctx.Err()
136 }
137 }
55 return 0, err
13856 }
13957
14058 func (s *composeService) getExecTarget(ctx context.Context, projectName string, opts api.RunOptions) (moby.Container, error) {
15472 container := containers[0]
15573 return container, nil
15674 }
157
158 func (s *composeService) getExecExitStatus(ctx context.Context, execID string) (int, error) {
159 resp, err := s.apiClient().ContainerExecInspect(ctx, execID)
160 if err != nil {
161 return 0, err
162 }
163 return resp.ExitCode, nil
164 }
+0
-74
pkg/compose/resize.go less more
0 /*
1 Copyright 2020 Docker Compose CLI authors
2
3 Licensed under the Apache License, Version 2.0 (the "License");
4 you may not use this file except in compliance with the License.
5 You may obtain a copy of the License at
6
7 http://www.apache.org/licenses/LICENSE-2.0
8
9 Unless required by applicable law or agreed to in writing, software
10 distributed under the License is distributed on an "AS IS" BASIS,
11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 See the License for the specific language governing permissions and
13 limitations under the License.
14 */
15
16 package compose
17
18 import (
19 "context"
20 "os"
21 gosignal "os/signal"
22 "runtime"
23 "time"
24
25 "github.com/buger/goterm"
26 moby "github.com/docker/docker/api/types"
27 "github.com/docker/docker/pkg/signal"
28 )
29
30 func (s *composeService) monitorTTySize(ctx context.Context, container string, resize func(context.Context, string, moby.ResizeOptions) error) {
31 err := resize(ctx, container, moby.ResizeOptions{ // nolint:errcheck
32 Height: uint(goterm.Height()),
33 Width: uint(goterm.Width()),
34 })
35 if err != nil {
36 return
37 }
38
39 sigchan := make(chan os.Signal, 1)
40 gosignal.Notify(sigchan, signal.SIGWINCH)
41
42 if runtime.GOOS == "windows" {
43 // Windows has no SIGWINCH support, so we have to poll tty size ¯\_(ツ)_/¯
44 go func() {
45 prevH := goterm.Height()
46 prevW := goterm.Width()
47 for {
48 time.Sleep(time.Millisecond * 250)
49 h := goterm.Height()
50 w := goterm.Width()
51 if prevW != w || prevH != h {
52 sigchan <- signal.SIGWINCH
53 }
54 prevH = h
55 prevW = w
56 }
57 }()
58 }
59
60 go func() {
61 for {
62 select {
63 case <-sigchan:
64 resize(ctx, container, moby.ResizeOptions{ // nolint:errcheck
65 Height: uint(goterm.Height()),
66 Width: uint(goterm.Width()),
67 })
68 case <-ctx.Done():
69 return
70 }
71 }
72 }()
73 }
1818 import (
1919 "context"
2020 "fmt"
21 "io"
22
2321 "github.com/compose-spec/compose-go/types"
22 "github.com/docker/cli/cli"
23 cmd "github.com/docker/cli/cli/command/container"
2424 "github.com/docker/compose/v2/pkg/api"
25 moby "github.com/docker/docker/api/types"
26 "github.com/docker/docker/api/types/container"
27 "github.com/docker/docker/pkg/ioutils"
28 "github.com/docker/docker/pkg/stdcopy"
2925 "github.com/docker/docker/pkg/stringid"
30 "github.com/moby/term"
3126 )
3227
3328 func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.Project, opts api.RunOptions) (int, error) {
3631 return 0, err
3732 }
3833
39 if opts.Detach {
40 err := s.apiClient().ContainerStart(ctx, containerID, moby.ContainerStartOptions{})
41 if err != nil {
42 return 0, err
43 }
44 fmt.Fprintln(s.stdout(), containerID)
45 return 0, nil
34 start := cmd.NewStartOptions()
35 start.OpenStdin = !opts.Detach && opts.Interactive
36 start.Attach = !opts.Detach
37 start.Containers = []string{containerID}
38
39 err = cmd.RunStart(s.dockerCli, &start)
40 if sterr, ok := err.(cli.StatusError); ok {
41 return sterr.StatusCode, nil
4642 }
47
48 return s.runInteractive(ctx, containerID, opts)
49 }
50
51 func (s *composeService) runInteractive(ctx context.Context, containerID string, opts api.RunOptions) (int, error) {
52 in := s.stdin()
53 r, err := s.getEscapeKeyProxy(in, opts.Tty)
54 if err != nil {
55 return 0, err
56 }
57
58 stdin, stdout, err := s.getContainerStreams(ctx, containerID)
59 if err != nil {
60 return 0, err
61 }
62
63 if in.IsTerminal() && opts.Tty {
64 state, err := term.SetRawTerminal(in.FD())
65 if err != nil {
66 return 0, err
67 }
68 defer term.RestoreTerminal(in.FD(), state) //nolint:errcheck
69 }
70
71 outputDone := make(chan error)
72 inputDone := make(chan error)
73
74 go func() {
75 if opts.Tty {
76 _, err := io.Copy(s.stdout(), stdout) //nolint:errcheck
77 outputDone <- err
78 } else {
79 _, err := stdcopy.StdCopy(s.stdout(), s.stderr(), stdout) //nolint:errcheck
80 outputDone <- err
81 }
82 stdout.Close() //nolint:errcheck
83 }()
84
85 go func() {
86 _, err := io.Copy(stdin, r)
87 inputDone <- err
88 stdin.Close() //nolint:errcheck
89 }()
90
91 err = s.apiClient().ContainerStart(ctx, containerID, moby.ContainerStartOptions{})
92 if err != nil {
93 return 0, err
94 }
95
96 s.monitorTTySize(ctx, containerID, s.apiClient().ContainerResize)
97
98 for {
99 select {
100 case err := <-outputDone:
101 if err != nil {
102 return 0, err
103 }
104 return s.terminateRun(ctx, containerID, opts)
105 case err := <-inputDone:
106 if _, ok := err.(term.EscapeError); ok {
107 return 0, nil
108 }
109 if err != nil {
110 return 0, err
111 }
112 // Wait for output to complete streaming
113 case <-ctx.Done():
114 return 0, ctx.Err()
115 }
116 }
117 }
118
119 func (s *composeService) terminateRun(ctx context.Context, containerID string, opts api.RunOptions) (exitCode int, err error) {
120 exitCh, errCh := s.apiClient().ContainerWait(ctx, containerID, container.WaitConditionNotRunning)
121 select {
122 case exit := <-exitCh:
123 exitCode = int(exit.StatusCode)
124 case err = <-errCh:
125 return
126 }
127 if opts.AutoRemove {
128 err = s.apiClient().ContainerRemove(ctx, containerID, moby.ContainerRemoveOptions{})
129 }
130 return
43 return 0, err
13144 }
13245
13346 func (s *composeService) prepareRun(ctx context.Context, project *types.Project, opts api.RunOptions) (string, error) {
14659 service.ContainerName = fmt.Sprintf("%s_%s_run_%s", project.Name, service.Name, stringid.TruncateID(slug))
14760 }
14861 service.Scale = 1
149 service.StdinOpen = true
15062 service.Restart = ""
15163 if service.Deploy != nil {
15264 service.Deploy.RestartPolicy = nil
17082 }
17183 updateServices(&service, observedState)
17284
173 created, err := s.createContainer(ctx, project, service, service.ContainerName, 1, opts.Detach && opts.AutoRemove, opts.UseNetworkAliases, true)
85 created, err := s.createContainer(ctx, project, service, service.ContainerName, 1,
86 opts.Detach && opts.AutoRemove, opts.UseNetworkAliases, opts.Interactive)
17487 if err != nil {
17588 return "", err
17689 }
177 containerID := created.ID
178 return containerID, nil
179 }
180
181 func (s *composeService) getEscapeKeyProxy(r io.ReadCloser, isTty bool) (io.ReadCloser, error) {
182 if !isTty {
183 return r, nil
184 }
185 var escapeKeys = []byte{16, 17}
186 if s.configFile().DetachKeys != "" {
187 customEscapeKeys, err := term.ToBytes(s.configFile().DetachKeys)
188 if err != nil {
189 return nil, err
190 }
191 escapeKeys = customEscapeKeys
192 }
193 return ioutils.NewReadCloserWrapper(term.NewEscapeProxy(r, escapeKeys), r.Close), nil
90 return created.ID, nil
19491 }
19592
19693 func applyRunOptions(project *types.Project, service *types.ServiceConfig, opts api.RunOptions) {
19794 service.Tty = opts.Tty
198 service.StdinOpen = true
95 service.StdinOpen = opts.Interactive
19996 service.ContainerName = opts.Name
20097
20198 if len(opts.Command) > 0 {
2828 c := NewParallelE2eCLI(t, binDir)
2929
3030 t.Run("compose run", func(t *testing.T) {
31 res := c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "back")
31 res := c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "-T", "back")
3232 lines := Lines(res.Stdout())
3333 assert.Equal(t, lines[len(lines)-1], "Hello there!!", res.Stdout())
3434 assert.Assert(t, !strings.Contains(res.Combined(), "orphan"))
35 res = c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "back", "echo", "Hello one more time")
35 res = c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "-T", "back", "echo", "Hello one more time")
3636 lines = Lines(res.Stdout())
3737 assert.Equal(t, lines[len(lines)-1], "Hello one more time", res.Stdout())
3838 assert.Assert(t, !strings.Contains(res.Combined(), "orphan"))
6767 })
6868
6969 t.Run("compose run --rm", func(t *testing.T) {
70 res := c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "--rm", "back", "echo", "Hello again")
70 res := c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "-T", "--rm", "back", "echo", "Hello again")
7171 lines := Lines(res.Stdout())
7272 assert.Equal(t, lines[len(lines)-1], "Hello again", res.Stdout())
7373
8484 t.Run("compose run --volumes", func(t *testing.T) {
8585 wd, err := os.Getwd()
8686 assert.NilError(t, err)
87 res := c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "--volumes", wd+":/foo", "back", "/bin/sh", "-c", "ls /foo")
87 res := c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "-T", "--volumes", wd+":/foo", "back", "/bin/sh", "-c", "ls /foo")
8888 res.Assert(t, icmd.Expected{Out: "compose_run_test.go"})
8989
9090 res = c.RunDockerCmd("ps", "--all")
9292 })
9393
9494 t.Run("compose run --publish", func(t *testing.T) {
95 c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "--publish", "8081:80", "-d", "back", "/bin/sh", "-c", "sleep 1")
95 c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "-T", "--publish", "8081:80", "-d", "back", "/bin/sh", "-c", "sleep 1")
9696 res := c.RunDockerCmd("ps")
9797 assert.Assert(t, strings.Contains(res.Stdout(), "8081->80/tcp"), res.Stdout())
9898 })
9999
100100 t.Run("compose run orphan", func(t *testing.T) {
101101 // Use different compose files to get an orphan container
102 c.RunDockerComposeCmd("-f", "./fixtures/run-test/orphan.yaml", "run", "simple")
103 res := c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "back", "echo", "Hello")
102 c.RunDockerComposeCmd("-f", "./fixtures/run-test/orphan.yaml", "run", "-T", "simple")
103 res := c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "-T", "back", "echo", "Hello")
104104 assert.Assert(t, strings.Contains(res.Combined(), "orphan"))
105105
106 cmd := c.NewDockerCmd("compose", "-f", "./fixtures/run-test/compose.yaml", "run", "back", "echo", "Hello")
106 cmd := c.NewDockerCmd("compose", "-f", "./fixtures/run-test/compose.yaml", "run", "-T", "back", "echo", "Hello")
107107 res = icmd.RunCmd(cmd, func(cmd *icmd.Cmd) {
108108 cmd.Env = append(cmd.Env, "COMPOSE_IGNORE_ORPHANS=True")
109109 })
0 ../../bin/docker-compose -f ./fixtures/run-test/compose.yaml run --volumes $(pwd):/foo back ls /foo