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
61 | 61 | if err != nil { |
62 | 62 | return err |
63 | 63 | } |
64 | ||
65 | target.Tty = !opts.noTty | |
66 | target.StdinOpen = opts.interactive | |
64 | 67 | if !opts.servicePorts { |
65 | 68 | target.Ports = []types.ServicePortConfig{} |
66 | 69 | } |
206 | 209 | Detach: opts.Detach, |
207 | 210 | AutoRemove: opts.Remove, |
208 | 211 | Tty: !opts.noTty, |
212 | Interactive: opts.interactive, | |
209 | 213 | WorkingDir: opts.workdir, |
210 | 214 | User: opts.user, |
211 | 215 | Environment: opts.environment, |
67 | 67 | } |
68 | 68 | |
69 | 69 | func main() { |
70 | if commands.RunningAsStandalone() { | |
70 | if plugin.RunningStandalone() { | |
71 | 71 | os.Args = append([]string{"docker"}, compatibility.Convert(os.Args[1:])...) |
72 | 72 | } |
73 | 73 | pluginMain() |
215 | 215 | Detach bool |
216 | 216 | AutoRemove bool |
217 | 217 | Tty bool |
218 | Interactive bool | |
218 | 219 | WorkingDir string |
219 | 220 | User string |
220 | 221 | Environment []string |
18 | 18 | import ( |
19 | 19 | "context" |
20 | 20 | "fmt" |
21 | "io" | |
22 | 21 | |
22 | "github.com/docker/cli/cli" | |
23 | "github.com/docker/cli/cli/command/container" | |
24 | "github.com/docker/compose/v2/pkg/api" | |
23 | 25 | moby "github.com/docker/docker/api/types" |
24 | 26 | "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" | |
29 | 27 | ) |
30 | 28 | |
31 | 29 | 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) | |
33 | 31 | if err != nil { |
34 | 32 | return 0, err |
35 | 33 | } |
36 | 34 | |
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) | |
71 | 46 | if err != nil { |
72 | 47 | return 0, err |
73 | 48 | } |
74 | 49 | } |
75 | 50 | |
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 | |
79 | 54 | } |
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 | |
138 | 56 | } |
139 | 57 | |
140 | 58 | func (s *composeService) getExecTarget(ctx context.Context, projectName string, opts api.RunOptions) (moby.Container, error) { |
154 | 72 | container := containers[0] |
155 | 73 | return container, nil |
156 | 74 | } |
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 | /* | |
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 | } |
18 | 18 | import ( |
19 | 19 | "context" |
20 | 20 | "fmt" |
21 | "io" | |
22 | ||
23 | 21 | "github.com/compose-spec/compose-go/types" |
22 | "github.com/docker/cli/cli" | |
23 | cmd "github.com/docker/cli/cli/command/container" | |
24 | 24 | "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" | |
29 | 25 | "github.com/docker/docker/pkg/stringid" |
30 | "github.com/moby/term" | |
31 | 26 | ) |
32 | 27 | |
33 | 28 | func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.Project, opts api.RunOptions) (int, error) { |
36 | 31 | return 0, err |
37 | 32 | } |
38 | 33 | |
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 | |
46 | 42 | } |
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 | |
131 | 44 | } |
132 | 45 | |
133 | 46 | func (s *composeService) prepareRun(ctx context.Context, project *types.Project, opts api.RunOptions) (string, error) { |
146 | 59 | service.ContainerName = fmt.Sprintf("%s_%s_run_%s", project.Name, service.Name, stringid.TruncateID(slug)) |
147 | 60 | } |
148 | 61 | service.Scale = 1 |
149 | service.StdinOpen = true | |
150 | 62 | service.Restart = "" |
151 | 63 | if service.Deploy != nil { |
152 | 64 | service.Deploy.RestartPolicy = nil |
170 | 82 | } |
171 | 83 | updateServices(&service, observedState) |
172 | 84 | |
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) | |
174 | 87 | if err != nil { |
175 | 88 | return "", err |
176 | 89 | } |
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 | |
194 | 91 | } |
195 | 92 | |
196 | 93 | func applyRunOptions(project *types.Project, service *types.ServiceConfig, opts api.RunOptions) { |
197 | 94 | service.Tty = opts.Tty |
198 | service.StdinOpen = true | |
95 | service.StdinOpen = opts.Interactive | |
199 | 96 | service.ContainerName = opts.Name |
200 | 97 | |
201 | 98 | if len(opts.Command) > 0 { |
28 | 28 | c := NewParallelE2eCLI(t, binDir) |
29 | 29 | |
30 | 30 | 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") | |
32 | 32 | lines := Lines(res.Stdout()) |
33 | 33 | assert.Equal(t, lines[len(lines)-1], "Hello there!!", res.Stdout()) |
34 | 34 | 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") | |
36 | 36 | lines = Lines(res.Stdout()) |
37 | 37 | assert.Equal(t, lines[len(lines)-1], "Hello one more time", res.Stdout()) |
38 | 38 | assert.Assert(t, !strings.Contains(res.Combined(), "orphan")) |
67 | 67 | }) |
68 | 68 | |
69 | 69 | 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") | |
71 | 71 | lines := Lines(res.Stdout()) |
72 | 72 | assert.Equal(t, lines[len(lines)-1], "Hello again", res.Stdout()) |
73 | 73 | |
84 | 84 | t.Run("compose run --volumes", func(t *testing.T) { |
85 | 85 | wd, err := os.Getwd() |
86 | 86 | 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") | |
88 | 88 | res.Assert(t, icmd.Expected{Out: "compose_run_test.go"}) |
89 | 89 | |
90 | 90 | res = c.RunDockerCmd("ps", "--all") |
92 | 92 | }) |
93 | 93 | |
94 | 94 | 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") | |
96 | 96 | res := c.RunDockerCmd("ps") |
97 | 97 | assert.Assert(t, strings.Contains(res.Stdout(), "8081->80/tcp"), res.Stdout()) |
98 | 98 | }) |
99 | 99 | |
100 | 100 | t.Run("compose run orphan", func(t *testing.T) { |
101 | 101 | // 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") | |
104 | 104 | assert.Assert(t, strings.Contains(res.Combined(), "orphan")) |
105 | 105 | |
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") | |
107 | 107 | res = icmd.RunCmd(cmd, func(cmd *icmd.Cmd) { |
108 | 108 | cmd.Env = append(cmd.Env, "COMPOSE_IGNORE_ORPHANS=True") |
109 | 109 | }) |
0 | ../../bin/docker-compose -f ./fixtures/run-test/compose.yaml run --volumes $(pwd):/foo back ls /foo |