github.com/secure-build/gitlab-runner@v12.5.0+incompatible/executors/kubernetes/service_proxy_test.go (about) 1 package kubernetes 2 3 import ( 4 "bytes" 5 "errors" 6 "io/ioutil" 7 "net/http" 8 "net/http/httptest" 9 "net/url" 10 "strconv" 11 "testing" 12 13 "github.com/gorilla/websocket" 14 "github.com/stretchr/testify/assert" 15 "github.com/stretchr/testify/require" 16 api "k8s.io/api/core/v1" 17 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 "k8s.io/apimachinery/pkg/runtime" 19 "k8s.io/apimachinery/pkg/runtime/schema" 20 "k8s.io/client-go/kubernetes" 21 restclient "k8s.io/client-go/rest" 22 "k8s.io/client-go/rest/fake" 23 24 "gitlab.com/gitlab-org/gitlab-runner/common" 25 "gitlab.com/gitlab-org/gitlab-runner/executors" 26 "gitlab.com/gitlab-org/gitlab-runner/session/proxy" 27 ) 28 29 func TestPoolGetter(t *testing.T) { 30 pool := proxy.Pool{"test": &proxy.Proxy{Settings: fakeProxySettings()}} 31 ex := executor{ 32 AbstractExecutor: executors.AbstractExecutor{ 33 ProxyPool: pool, 34 }, 35 } 36 37 assert.Equal(t, pool, ex.Pool()) 38 } 39 40 func TestProxyRequestError(t *testing.T) { 41 version, codec := testVersionAndCodec() 42 objectInfo := metav1.ObjectMeta{Name: "test-pod", Namespace: "test-ns"} 43 44 tests := map[string]struct { 45 port string 46 podStatus api.PodPhase 47 containerReady bool 48 expectedErrCode int 49 }{ 50 "Invalid port number": { 51 port: "81", 52 podStatus: api.PodRunning, 53 expectedErrCode: http.StatusNotFound, 54 }, 55 "Invalid port name": { 56 port: "foobar", 57 podStatus: api.PodRunning, 58 expectedErrCode: http.StatusNotFound, 59 }, 60 "Pod is not ready yet": { 61 port: "80", 62 podStatus: api.PodPending, 63 expectedErrCode: http.StatusServiceUnavailable, 64 }, 65 "Service containers are not ready yet": { 66 port: "80", 67 podStatus: api.PodRunning, 68 containerReady: false, 69 expectedErrCode: http.StatusServiceUnavailable, 70 }, 71 } 72 73 for name, test := range tests { 74 t.Run(name, func(t *testing.T) { 75 ex := executor{ 76 pod: &api.Pod{ObjectMeta: objectInfo}, 77 kubeClient: testKubernetesClient(version, fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { 78 return mockPodRunningStatus(req, version, codec, objectInfo, test.podStatus, test.containerReady) 79 })), 80 } 81 82 h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 83 ex.ProxyRequest(w, r, "", test.port, fakeProxySettings()) 84 }) 85 86 rw := httptest.NewRecorder() 87 req, err := http.NewRequest(http.MethodGet, "/", nil) 88 require.NoError(t, err) 89 90 h.ServeHTTP(rw, req) 91 92 resp := rw.Result() 93 assert.Equal(t, test.expectedErrCode, resp.StatusCode) 94 }) 95 } 96 } 97 98 func fakeProxySettings() *proxy.Settings { 99 return &proxy.Settings{ 100 ServiceName: "name", 101 Ports: []proxy.Port{ 102 { 103 Number: 80, 104 Protocol: "http", 105 Name: "port-name", 106 }, 107 }, 108 } 109 } 110 111 func TestProxyRequestHTTP(t *testing.T) { 112 version, codec := testVersionAndCodec() 113 objectInfo := metav1.ObjectMeta{Name: "test-pod", Namespace: "test-ns"} 114 defaultBody := "ACK" 115 defaultPort := "80" 116 defaultPortNumber, err := strconv.Atoi(defaultPort) 117 require.NoError(t, err) 118 119 serviceName := "service-name" 120 proxyEndpointURI := "/api/" + version + "/namespaces/" + objectInfo.Namespace + "/services/http:" + serviceName + ":" + defaultPort + "/proxy" 121 defaultProxySettings := proxy.Settings{ 122 ServiceName: serviceName, 123 Ports: []proxy.Port{ 124 { 125 Number: defaultPortNumber, 126 Protocol: "http", 127 }, 128 }, 129 } 130 131 ex := executor{ 132 pod: &api.Pod{ObjectMeta: objectInfo}, 133 } 134 135 tests := map[string]struct { 136 podStatus api.PodPhase 137 requestedURI string 138 proxySettings proxy.Settings 139 endpointURI string 140 expectedBody string 141 expectedStatusCode int 142 }{ 143 "Returns error if the pod is not ready": { 144 podStatus: api.PodPending, 145 proxySettings: defaultProxySettings, 146 expectedBody: "Service Unavailable\n", 147 expectedStatusCode: http.StatusServiceUnavailable, 148 }, 149 "Returns error if invalid port protocol": { 150 podStatus: api.PodRunning, 151 proxySettings: proxy.Settings{ 152 ServiceName: serviceName, 153 Ports: []proxy.Port{ 154 { 155 Number: defaultPortNumber, 156 Protocol: "whatever", 157 }, 158 }, 159 }, 160 expectedBody: "Service Unavailable\n", 161 expectedStatusCode: http.StatusServiceUnavailable, 162 }, 163 "Handles HTTP requests": { 164 podStatus: api.PodRunning, 165 proxySettings: defaultProxySettings, 166 endpointURI: proxyEndpointURI, 167 expectedBody: defaultBody, 168 expectedStatusCode: http.StatusOK, 169 }, 170 "Adds the requested URI to the proxy path": { 171 podStatus: api.PodRunning, 172 requestedURI: "foobar", 173 proxySettings: defaultProxySettings, 174 endpointURI: proxyEndpointURI + "/foobar", 175 expectedBody: defaultBody, 176 expectedStatusCode: http.StatusOK, 177 }, 178 "Uses the right protocol based on the proxy configuration": { 179 podStatus: api.PodRunning, 180 proxySettings: proxy.Settings{ 181 ServiceName: serviceName, 182 Ports: []proxy.Port{ 183 { 184 Number: defaultPortNumber, 185 Protocol: "https", 186 }, 187 }, 188 }, 189 endpointURI: "/api/" + version + "/namespaces/" + objectInfo.Namespace + "/services/https:" + serviceName + ":" + defaultPort + "/proxy", 190 expectedBody: defaultBody, 191 expectedStatusCode: http.StatusOK, 192 }, 193 } 194 195 for name, test := range tests { 196 t.Run(name, func(t *testing.T) { 197 h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 198 ex.ProxyRequest(w, r, test.requestedURI, defaultPort, &test.proxySettings) 199 }) 200 201 ex.kubeClient = testKubernetesClient(version, fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { 202 switch p, m := req.URL.Path, req.Method; { 203 case p == test.endpointURI && m == http.MethodGet: 204 return &http.Response{StatusCode: http.StatusOK, Body: ioutil.NopCloser(bytes.NewReader([]byte(defaultBody)))}, nil 205 default: 206 return mockPodRunningStatus(req, version, codec, objectInfo, test.podStatus, true) 207 } 208 })) 209 210 rw := httptest.NewRecorder() 211 req, err := http.NewRequest(http.MethodGet, "/", nil) 212 require.NoError(t, err) 213 214 h.ServeHTTP(rw, req) 215 216 resp := rw.Result() 217 defer resp.Body.Close() 218 219 b, err := ioutil.ReadAll(resp.Body) 220 require.NoError(t, err) 221 assert.Equal(t, test.expectedStatusCode, resp.StatusCode) 222 assert.Equal(t, test.expectedBody, string(b)) 223 }) 224 } 225 } 226 227 func TestProxyRequestHTTPError(t *testing.T) { 228 version, codec := testVersionAndCodec() 229 objectInfo := metav1.ObjectMeta{Name: "test-pod", Namespace: "test-ns"} 230 231 ex := executor{ 232 pod: &api.Pod{ObjectMeta: objectInfo}, 233 } 234 235 proxySettings := proxy.Settings{ 236 ServiceName: "service-name", 237 Ports: []proxy.Port{ 238 { 239 Number: 80, 240 Protocol: "http", 241 }, 242 }, 243 } 244 245 endpointURI := "/api/" + version + "/namespaces/" + objectInfo.Namespace + "/services/http:service-name:80/proxy" 246 errorMessage := "Error Message" 247 248 tests := map[string]struct { 249 expectedErrorCode int 250 expectedErrorMsg string 251 }{ 252 "Error is StatusServiceUnavailable": { 253 expectedErrorCode: http.StatusServiceUnavailable, 254 expectedErrorMsg: "", 255 }, 256 "Any other error": { 257 expectedErrorCode: http.StatusNotFound, 258 expectedErrorMsg: errorMessage, 259 }, 260 } 261 262 for name, test := range tests { 263 t.Run(name, func(t *testing.T) { 264 h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 265 ex.ProxyRequest(w, r, "", "80", &proxySettings) 266 }) 267 268 ex.kubeClient = testKubernetesClient(version, fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { 269 switch p, m := req.URL.Path, req.Method; { 270 case p == endpointURI && m == http.MethodGet: 271 return &http.Response{StatusCode: test.expectedErrorCode, Body: ioutil.NopCloser(bytes.NewReader([]byte(errorMessage)))}, nil 272 default: 273 return mockPodRunningStatus(req, version, codec, objectInfo, api.PodRunning, true) 274 } 275 })) 276 277 rw := httptest.NewRecorder() 278 req, err := http.NewRequest(http.MethodGet, "/", nil) 279 require.NoError(t, err) 280 281 h.ServeHTTP(rw, req) 282 283 resp := rw.Result() 284 defer resp.Body.Close() 285 286 b, err := ioutil.ReadAll(resp.Body) 287 require.NoError(t, err) 288 assert.Equal(t, test.expectedErrorCode, resp.StatusCode) 289 assert.Equal(t, test.expectedErrorMsg, string(b)) 290 }) 291 } 292 } 293 294 func mockPodRunningStatus(req *http.Request, version string, codec runtime.Codec, objectInfo metav1.ObjectMeta, status api.PodPhase, servicesReady bool) (*http.Response, error) { 295 switch p, m := req.URL.Path, req.Method; { 296 case p == "/api/"+version+"/namespaces/"+objectInfo.Namespace+"/pods/"+objectInfo.Name && m == http.MethodGet: 297 pod := &api.Pod{ 298 ObjectMeta: objectInfo, 299 Status: api.PodStatus{ 300 Phase: status, 301 ContainerStatuses: []api.ContainerStatus{{Ready: servicesReady}}, 302 }, 303 } 304 return &http.Response{StatusCode: http.StatusOK, Body: objBody(codec, pod), Header: map[string][]string{ 305 "Content-Type": {"application/json"}, 306 }}, nil 307 default: 308 return nil, errors.New("unexpected request") 309 } 310 } 311 312 func TestProxyRequestWebsockets(t *testing.T) { 313 version, codec := testVersionAndCodec() 314 objectInfo := metav1.ObjectMeta{Name: "test-pod", Namespace: "test-ns"} 315 defaultPort := "80" 316 defaultPortNumber, err := strconv.Atoi(defaultPort) 317 require.NoError(t, err) 318 319 serviceName := "service-name" 320 proxyEndpointURI := "/api/" + version + "/namespaces/" + objectInfo.Namespace + "/services/http:" + serviceName + ":" + defaultPort + "/proxy" 321 defaultProxySettings := proxy.Settings{ 322 ServiceName: serviceName, 323 Ports: []proxy.Port{ 324 { 325 Number: defaultPortNumber, 326 Protocol: "http", 327 }, 328 }, 329 } 330 331 ex := executor{ 332 AbstractExecutor: executors.AbstractExecutor{ 333 Config: common.RunnerConfig{ 334 RunnerSettings: common.RunnerSettings{ 335 Kubernetes: &common.KubernetesConfig{ 336 Host: "localhost", 337 }, 338 }, 339 }, 340 }, 341 configurationOverwrites: &overwrites{}, 342 pod: &api.Pod{ObjectMeta: objectInfo}, 343 } 344 345 tests := map[string]struct { 346 podStatus api.PodPhase 347 requestedURI string 348 proxySettings proxy.Settings 349 endpointURI string 350 expectedStatusCode int 351 }{ 352 "Returns error if the service is not ready": { 353 podStatus: api.PodPending, 354 proxySettings: defaultProxySettings, 355 expectedStatusCode: http.StatusServiceUnavailable, 356 }, 357 "Returns error if invalid port protocol": { 358 podStatus: api.PodRunning, 359 proxySettings: proxy.Settings{ 360 ServiceName: serviceName, 361 Ports: []proxy.Port{ 362 { 363 Number: 80, 364 Protocol: "whatever", 365 }, 366 }, 367 }, 368 expectedStatusCode: http.StatusServiceUnavailable, 369 }, 370 "Handles Websockets requests": { 371 podStatus: api.PodRunning, 372 proxySettings: defaultProxySettings, 373 endpointURI: proxyEndpointURI, 374 expectedStatusCode: http.StatusSwitchingProtocols, 375 }, 376 "Adds the requested URI to the proxy path": { 377 podStatus: api.PodRunning, 378 requestedURI: "foobar", 379 proxySettings: defaultProxySettings, 380 endpointURI: proxyEndpointURI + "/foobar", 381 expectedStatusCode: http.StatusSwitchingProtocols, 382 }, 383 "Uses the right protocol based on the proxy configuration": { 384 podStatus: api.PodRunning, 385 proxySettings: proxy.Settings{ 386 ServiceName: "service-name", 387 Ports: []proxy.Port{ 388 { 389 Number: 80, 390 Protocol: "https", 391 }, 392 }, 393 }, 394 endpointURI: "/api/" + version + "/namespaces/" + objectInfo.Namespace + "/services/https:service-name:80/proxy", 395 expectedStatusCode: http.StatusSwitchingProtocols, 396 }, 397 } 398 399 for name, test := range tests { 400 t.Run(name, func(t *testing.T) { 401 h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 402 ex.ProxyRequest(w, r, r.URL.Path, defaultPort, &test.proxySettings) 403 }) 404 405 // Mocked Kubernetes API server making the proxy request 406 kubeAPISrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 407 assert.Equal(t, test.endpointURI, r.URL.Path) 408 409 upgrader := websocket.Upgrader{} 410 c, err := upgrader.Upgrade(w, r, nil) 411 require.NoError(t, err) 412 413 for { 414 mt, message, err := c.ReadMessage() 415 if err != nil { 416 break 417 } 418 err = c.WriteMessage(mt, message) 419 if err != nil { 420 break 421 } 422 } 423 defer c.Close() 424 })) 425 defer kubeAPISrv.Close() 426 427 ex.kubeClient = mockKubernetesClientWithHost(version, kubeAPISrv.Listener.Addr().String(), fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { 428 return mockPodRunningStatus(req, version, codec, objectInfo, test.podStatus, true) 429 })) 430 431 // HTTP server 432 srv := httptest.NewServer(h) 433 defer srv.Close() 434 435 u := url.URL{ 436 Scheme: "ws", 437 Host: srv.Listener.Addr().String(), 438 Path: test.requestedURI, 439 } 440 441 conn, resp, _ := websocket.DefaultDialer.Dial(u.String(), http.Header{}) 442 defer func() { 443 if conn != nil { 444 _ = conn.Close() 445 } 446 }() 447 448 assert.Equal(t, test.expectedStatusCode, resp.StatusCode) 449 450 if resp.StatusCode == http.StatusSwitchingProtocols { 451 testMessage := "testmessage" 452 err := conn.WriteMessage(websocket.TextMessage, []byte(testMessage)) 453 require.NoError(t, err) 454 455 _, p, err := conn.ReadMessage() 456 require.NoError(t, err) 457 assert.Equal(t, testMessage, string(p)) 458 } 459 }) 460 } 461 } 462 463 func mockKubernetesClientWithHost(version string, host string, httpClient *http.Client) *kubernetes.Clientset { 464 conf := restclient.Config{ 465 Host: host, 466 ContentConfig: restclient.ContentConfig{ 467 GroupVersion: &schema.GroupVersion{Version: version}, 468 }, 469 } 470 kube := kubernetes.NewForConfigOrDie(&conf) 471 fakeClient := fake.RESTClient{Client: httpClient} 472 kube.CoreV1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client 473 kube.ExtensionsV1beta1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client 474 475 return kube 476 }