github.com/secure-build/gitlab-runner@v12.5.0+incompatible/executors/kubernetes/service_proxy.go (about) 1 package kubernetes 2 3 import ( 4 "fmt" 5 "io" 6 "net/http" 7 "strconv" 8 9 "github.com/gorilla/websocket" 10 "github.com/sirupsen/logrus" 11 terminal "gitlab.com/gitlab-org/gitlab-terminal" 12 "k8s.io/apimachinery/pkg/api/errors" 13 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 k8net "k8s.io/apimachinery/pkg/util/net" 15 "k8s.io/client-go/rest" 16 17 "gitlab.com/gitlab-org/gitlab-runner/session/proxy" 18 ) 19 20 const runningState = "Running" 21 22 func (s *executor) Pool() proxy.Pool { 23 return s.ProxyPool 24 } 25 26 func (s *executor) newProxy(serviceName string, ports []proxy.Port) *proxy.Proxy { 27 return &proxy.Proxy{ 28 Settings: proxy.NewProxySettings(serviceName, ports), 29 ConnectionHandler: s, 30 } 31 } 32 33 func (s *executor) ProxyRequest(w http.ResponseWriter, r *http.Request, requestedURI string, port string, settings *proxy.Settings) { 34 logger := logrus.WithFields(logrus.Fields{ 35 "uri": r.RequestURI, 36 "method": r.Method, 37 "port": port, 38 "settings": settings, 39 }) 40 41 portSettings, err := settings.PortByNameOrNumber(port) 42 if err != nil { 43 logger.WithError(err).Errorf("port proxy %q not found", port) 44 http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) 45 return 46 } 47 48 if !s.servicesRunning() { 49 logger.Errorf("services are not ready yet") 50 http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable) 51 return 52 } 53 54 if websocket.IsWebSocketUpgrade(r) { 55 proxyWSRequest(s, w, r, requestedURI, portSettings, settings, logger) 56 return 57 } 58 59 proxyHTTPRequest(s, w, r, requestedURI, portSettings, settings, logger) 60 } 61 62 func (s *executor) servicesRunning() bool { 63 pod, err := s.kubeClient.CoreV1().Pods(s.pod.Namespace).Get(s.pod.Name, metav1.GetOptions{}) 64 if err != nil || pod.Status.Phase != runningState { 65 return false 66 } 67 68 for _, container := range pod.Status.ContainerStatuses { 69 if !container.Ready { 70 return false 71 } 72 } 73 74 return true 75 } 76 77 func (s *executor) serviceEndpointRequest(verb, serviceName, requestedURI string, port proxy.Port) (*rest.Request, error) { 78 scheme, err := port.Scheme() 79 if err != nil { 80 return nil, err 81 } 82 83 result := s.kubeClient.CoreV1().RESTClient().Verb(verb). 84 Namespace(s.pod.Namespace). 85 Resource("services"). 86 SubResource("proxy"). 87 Name(k8net.JoinSchemeNamePort(scheme, serviceName, strconv.Itoa(port.Number))). 88 Suffix(requestedURI) 89 90 return result, nil 91 } 92 93 func proxyWSRequest(s *executor, w http.ResponseWriter, r *http.Request, requestedURI string, port proxy.Port, proxySettings *proxy.Settings, logger *logrus.Entry) { 94 // In order to avoid calling this method, and use one of its own, 95 // we should refactor the library "gitlab.com/gitlab-org/gitlab-terminal" 96 // and make it more generic, not so terminal focused, with a broader 97 // terminology. (https://gitlab.com/gitlab-org/gitlab-runner/issues/4059) 98 settings, err := s.getTerminalSettings() 99 if err != nil { 100 logger.WithError(err).Errorf("service proxy: error getting WS settings") 101 http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable) 102 return 103 } 104 105 req, err := s.serviceEndpointRequest(r.Method, proxySettings.ServiceName, requestedURI, port) 106 if err != nil { 107 logger.WithError(err).Errorf("service proxy: error proxying WS request") 108 http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable) 109 return 110 } 111 112 u := req.URL() 113 u.Scheme = proxy.WebsocketProtocolFor(u.Scheme) 114 115 settings.Url = u.String() 116 serviceProxy := terminal.NewWebSocketProxy(1) 117 118 terminal.ProxyWebSocket(w, r, settings, serviceProxy) 119 } 120 121 func proxyHTTPRequest(s *executor, w http.ResponseWriter, r *http.Request, requestedURI string, port proxy.Port, proxy *proxy.Settings, logger *logrus.Entry) { 122 req, err := s.serviceEndpointRequest(r.Method, proxy.ServiceName, requestedURI, port) 123 if err != nil { 124 logger.WithError(err).Errorf("service proxy: error proxying HTTP request") 125 http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable) 126 return 127 } 128 129 body, err := req.Stream() 130 if err != nil { 131 message, code := handleProxyHTTPErr(err, logger) 132 w.WriteHeader(code) 133 134 if message != "" { 135 _, _ = fmt.Fprint(w, message) 136 } 137 return 138 } 139 140 w.WriteHeader(http.StatusOK) 141 _, _ = io.Copy(w, body) 142 } 143 144 func handleProxyHTTPErr(err error, logger *logrus.Entry) (string, int) { 145 statusError, ok := err.(*errors.StatusError) 146 if !ok { 147 return "", http.StatusInternalServerError 148 } 149 150 code := int(statusError.Status().Code) 151 // When the error is a 503 we don't want to give any information 152 // coming from Kubernetes 153 if code == http.StatusServiceUnavailable { 154 logger.Error(statusError.Status().Message) 155 return "", code 156 } 157 158 details := statusError.Status().Details 159 if details == nil { 160 return "", code 161 } 162 163 causes := details.Causes 164 if len(causes) > 0 { 165 return causes[0].Message, code 166 } 167 168 return "", code 169 }