1package exec
2
3import (
4	"context"
5	"errors"
6	"io"
7	"net"
8
9	"github.com/docker/docker/api/types"
10	"github.com/docker/docker/pkg/stdcopy"
11	"github.com/sirupsen/logrus"
12
13	"gitlab.com/gitlab-org/gitlab-runner/executors/docker/internal/wait"
14	"gitlab.com/gitlab-org/gitlab-runner/helpers/docker"
15)
16
17// conn is an interface wrapper used to generate mocks that are next used for tests
18// nolint:deadcode
19type conn interface {
20	net.Conn
21}
22
23// reader is an interface wrapper used to generate mocks that are next used for tests
24// nolint:deadcode
25type reader interface {
26	io.Reader
27}
28
29type Docker interface {
30	Exec(ctx context.Context, containerID string, input io.Reader, output io.Writer) error
31}
32
33func NewDocker(c docker.Client, waiter wait.KillWaiter, logger logrus.FieldLogger) Docker {
34	return &defaultDocker{
35		c:      c,
36		waiter: waiter,
37		logger: logger,
38	}
39}
40
41type defaultDocker struct {
42	c      docker.Client
43	waiter wait.KillWaiter
44	logger logrus.FieldLogger
45}
46
47func (d *defaultDocker) Exec(ctx context.Context, containerID string, input io.Reader, output io.Writer) error {
48	d.logger.Debugln("Attaching to container", containerID, "...")
49
50	hijacked, err := d.c.ContainerAttach(ctx, containerID, attachOptions())
51	if err != nil {
52		return err
53	}
54	defer hijacked.Close()
55
56	d.logger.Debugln("Starting container", containerID, "...")
57	err = d.c.ContainerStart(ctx, containerID, types.ContainerStartOptions{})
58	if err != nil {
59		return err
60	}
61
62	// Copy any output to the build trace
63	stdoutErrCh := make(chan error)
64	go func() {
65		_, errCopy := stdcopy.StdCopy(output, output, hijacked.Reader)
66		stdoutErrCh <- errCopy
67	}()
68
69	// Write the input to the container and close its STDIN to get it to finish
70	stdinErrCh := make(chan error)
71	go func() {
72		_, errCopy := io.Copy(hijacked.Conn, input)
73		_ = hijacked.CloseWrite()
74		if errCopy != nil {
75			stdinErrCh <- errCopy
76		}
77	}()
78
79	// Wait until either:
80	// - the job is aborted/cancelled/deadline exceeded
81	// - stdin has an error
82	// - stdout returns an error or nil, indicating the stream has ended and
83	//   the container has exited
84	select {
85	case <-ctx.Done():
86		err = errors.New("aborted")
87	case err = <-stdinErrCh:
88	case err = <-stdoutErrCh:
89	}
90
91	if err != nil {
92		d.logger.Debugln("Container", containerID, "finished with", err)
93	}
94
95	// Kill and wait for exit.
96	// Containers are stopped so that they can be reused by the job.
97	return d.waiter.KillWait(ctx, containerID)
98}
99
100func attachOptions() types.ContainerAttachOptions {
101	return types.ContainerAttachOptions{
102		Stream: true,
103		Stdin:  true,
104		Stdout: true,
105		Stderr: true,
106	}
107}
108