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