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  }