github.com/secure-build/gitlab-runner@v12.5.0+incompatible/executors/docker/terminal.go (about)

     1  package docker
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"net/http"
     8  	"time"
     9  
    10  	"github.com/docker/docker/api/types"
    11  
    12  	"gitlab.com/gitlab-org/gitlab-runner/common"
    13  	"gitlab.com/gitlab-org/gitlab-runner/helpers/docker"
    14  	terminalsession "gitlab.com/gitlab-org/gitlab-runner/session/terminal"
    15  	"gitlab.com/gitlab-org/gitlab-terminal"
    16  )
    17  
    18  // buildContainerTerminalTimeout is the error used when the build container is
    19  // not running yet an we have a terminal request waiting for the container to
    20  // start and a certain amount of time is exceeded.
    21  type buildContainerTerminalTimeout struct {
    22  }
    23  
    24  func (buildContainerTerminalTimeout) Error() string {
    25  	return "timeout for waiting for build container"
    26  }
    27  
    28  func (s *commandExecutor) watchForRunningBuildContainer(deadline time.Time) (string, error) {
    29  	for time.Since(deadline) < 0 {
    30  		buildContainer := s.getBuildContainer()
    31  		if buildContainer == nil {
    32  			time.Sleep(time.Second)
    33  			continue
    34  		}
    35  
    36  		containerID := buildContainer.ID
    37  		container, err := s.client.ContainerInspect(s.Context, containerID)
    38  		if err != nil {
    39  			return "", err
    40  		}
    41  
    42  		if container.State.Running {
    43  			return containerID, nil
    44  		}
    45  	}
    46  
    47  	return "", buildContainerTerminalTimeout{}
    48  }
    49  
    50  func (s *commandExecutor) Connect() (terminalsession.Conn, error) {
    51  	// Waiting for the container to start,  is not ideal as it might be hiding a
    52  	// real issue and the user is not aware of it. Ideally, the runner should
    53  	// inform the user in an interactive way that the container has no started
    54  	// yet and should wait/try again. This isn't an easy task to do since we
    55  	// can't access the WebSocket here since that is the responsibility of
    56  	// `gitlab-terminal` package. There are plans to improve this please take a
    57  	// look at https://gitlab.com/gitlab-org/gitlab-ce/issues/50384#proposal and
    58  	// https://gitlab.com/gitlab-org/gitlab-terminal/issues/4
    59  	containerID, err := s.watchForRunningBuildContainer(time.Now().Add(waitForContainerTimeout))
    60  	if err != nil {
    61  		return nil, err
    62  	}
    63  
    64  	ctx, cancelFn := context.WithCancel(s.Context)
    65  
    66  	return terminalConn{
    67  		logger:      &s.BuildLogger,
    68  		ctx:         ctx,
    69  		cancelFn:    cancelFn,
    70  		executor:    s,
    71  		client:      s.client,
    72  		containerID: containerID,
    73  		shell:       s.BuildShell.DockerCommand,
    74  	}, nil
    75  }
    76  
    77  type terminalConn struct {
    78  	logger   *common.BuildLogger
    79  	ctx      context.Context
    80  	cancelFn func()
    81  
    82  	executor    *commandExecutor
    83  	client      docker_helpers.Client
    84  	containerID string
    85  	shell       []string
    86  }
    87  
    88  func (t terminalConn) Start(w http.ResponseWriter, r *http.Request, timeoutCh, disconnectCh chan error) {
    89  	execConfig := types.ExecConfig{
    90  		Tty:          true,
    91  		AttachStdin:  true,
    92  		AttachStderr: true,
    93  		AttachStdout: true,
    94  		Cmd:          t.shell,
    95  	}
    96  
    97  	exec, err := t.client.ContainerExecCreate(t.ctx, t.containerID, execConfig)
    98  	if err != nil {
    99  		t.logger.Errorln("Failed to create exec container for terminal:", err)
   100  		http.Error(w, "failed to create exec for build container", http.StatusInternalServerError)
   101  		return
   102  	}
   103  
   104  	execStartCfg := types.ExecStartCheck{Tty: true}
   105  
   106  	resp, err := t.client.ContainerExecAttach(t.ctx, exec.ID, execStartCfg)
   107  	if err != nil {
   108  		t.logger.Errorln("Failed to exec attach to container for terminal:", err)
   109  		http.Error(w, "failed to attach tty to build container", http.StatusInternalServerError)
   110  		return
   111  	}
   112  
   113  	dockerTTY := newDockerTTY(&resp)
   114  	proxy := terminal.NewStreamProxy(1) // one stopper: terminal exit handler
   115  
   116  	// wait for container to exit
   117  	go func() {
   118  		t.logger.Debugln("Waiting for the terminal container:", t.containerID)
   119  		err := t.executor.waitForContainer(t.ctx, t.containerID)
   120  		t.logger.Debugln("The terminal container:", t.containerID, "finished with:", err)
   121  
   122  		stopCh := proxy.GetStopCh()
   123  		if err != nil {
   124  			stopCh <- fmt.Errorf("build container exited with %q", err)
   125  		} else {
   126  			stopCh <- errors.New("build container exited")
   127  		}
   128  	}()
   129  
   130  	terminalsession.ProxyTerminal(
   131  		timeoutCh,
   132  		disconnectCh,
   133  		proxy.StopCh,
   134  		func() {
   135  			terminal.ProxyStream(w, r, dockerTTY, proxy)
   136  		},
   137  	)
   138  }
   139  
   140  func (t terminalConn) Close() error {
   141  	if t.cancelFn != nil {
   142  		t.cancelFn()
   143  	}
   144  	return nil
   145  }