1/*
2   Copyright 2020 Docker Compose CLI authors
3
4   Licensed under the Apache License, Version 2.0 (the "License");
5   you may not use this file except in compliance with the License.
6   You may obtain a copy of the License at
7
8       http://www.apache.org/licenses/LICENSE-2.0
9
10   Unless required by applicable law or agreed to in writing, software
11   distributed under the License is distributed on an "AS IS" BASIS,
12   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   See the License for the specific language governing permissions and
14   limitations under the License.
15*/
16
17package compose
18
19import (
20	"context"
21	"fmt"
22	"io"
23
24	"github.com/docker/cli/cli/streams"
25	moby "github.com/docker/docker/api/types"
26	"github.com/docker/docker/api/types/filters"
27	"github.com/docker/docker/pkg/stdcopy"
28	"github.com/moby/term"
29
30	"github.com/docker/compose/v2/pkg/api"
31)
32
33func (s *composeService) Exec(ctx context.Context, project string, opts api.RunOptions) (int, error) {
34	container, err := s.getExecTarget(ctx, project, opts)
35	if err != nil {
36		return 0, err
37	}
38
39	exec, err := s.apiClient.ContainerExecCreate(ctx, container.ID, moby.ExecConfig{
40		Cmd:        opts.Command,
41		Env:        opts.Environment,
42		User:       opts.User,
43		Privileged: opts.Privileged,
44		Tty:        opts.Tty,
45		Detach:     opts.Detach,
46		WorkingDir: opts.WorkingDir,
47
48		AttachStdin:  true,
49		AttachStdout: true,
50		AttachStderr: true,
51	})
52	if err != nil {
53		return 0, err
54	}
55
56	if opts.Detach {
57		return 0, s.apiClient.ContainerExecStart(ctx, exec.ID, moby.ExecStartCheck{
58			Detach: true,
59			Tty:    opts.Tty,
60		})
61	}
62
63	resp, err := s.apiClient.ContainerExecAttach(ctx, exec.ID, moby.ExecStartCheck{
64		Tty: opts.Tty,
65	})
66	if err != nil {
67		return 0, err
68	}
69	defer resp.Close() //nolint:errcheck
70
71	if opts.Tty {
72		s.monitorTTySize(ctx, exec.ID, s.apiClient.ContainerExecResize)
73		if err != nil {
74			return 0, err
75		}
76	}
77
78	err = s.interactiveExec(ctx, opts, resp)
79	if err != nil {
80		return 0, err
81	}
82
83	return s.getExecExitStatus(ctx, exec.ID)
84}
85
86// inspired by https://github.com/docker/cli/blob/master/cli/command/container/exec.go#L116
87func (s *composeService) interactiveExec(ctx context.Context, opts api.RunOptions, resp moby.HijackedResponse) error {
88	outputDone := make(chan error)
89	inputDone := make(chan error)
90
91	stdout := ContainerStdout{HijackedResponse: resp}
92	stdin := ContainerStdin{HijackedResponse: resp}
93	r, err := s.getEscapeKeyProxy(opts.Stdin)
94	if err != nil {
95		return err
96	}
97
98	in := streams.NewIn(opts.Stdin)
99	if in.IsTerminal() {
100		state, err := term.SetRawTerminal(in.FD())
101		if err != nil {
102			return err
103		}
104		defer term.RestoreTerminal(in.FD(), state) //nolint:errcheck
105	}
106
107	go func() {
108		if opts.Tty {
109			_, err := io.Copy(opts.Stdout, stdout)
110			outputDone <- err
111		} else {
112			_, err := stdcopy.StdCopy(opts.Stdout, opts.Stderr, stdout)
113			outputDone <- err
114		}
115		stdout.Close() //nolint:errcheck
116	}()
117
118	go func() {
119		_, err := io.Copy(stdin, r)
120		inputDone <- err
121		stdin.Close() //nolint:errcheck
122	}()
123
124	for {
125		select {
126		case err := <-outputDone:
127			return err
128		case err := <-inputDone:
129			if _, ok := err.(term.EscapeError); ok {
130				return nil
131			}
132			if err != nil {
133				return err
134			}
135			// Wait for output to complete streaming
136		case <-ctx.Done():
137			return ctx.Err()
138		}
139	}
140}
141
142func (s *composeService) getExecTarget(ctx context.Context, projectName string, opts api.RunOptions) (moby.Container, error) {
143	containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
144		Filters: filters.NewArgs(
145			projectFilter(projectName),
146			serviceFilter(opts.Service),
147			containerNumberFilter(opts.Index),
148		),
149	})
150	if err != nil {
151		return moby.Container{}, err
152	}
153	if len(containers) < 1 {
154		return moby.Container{}, fmt.Errorf("service %q is not running container #%d", opts.Service, opts.Index)
155	}
156	container := containers[0]
157	return container, nil
158}
159
160func (s *composeService) getExecExitStatus(ctx context.Context, execID string) (int, error) {
161	resp, err := s.apiClient.ContainerExecInspect(ctx, execID)
162	if err != nil {
163		return 0, err
164	}
165	return resp.ExitCode, nil
166}
167