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 }