github.com/secure-build/gitlab-runner@v12.5.0+incompatible/executors/kubernetes/executor_kubernetes_test.go (about) 1 package kubernetes 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "net/http" 11 "net/http/httptest" 12 "net/url" 13 "os" 14 "strconv" 15 "strings" 16 "testing" 17 "time" 18 19 "github.com/gorilla/websocket" 20 "github.com/sirupsen/logrus" 21 "github.com/stretchr/testify/assert" 22 "github.com/stretchr/testify/require" 23 api "k8s.io/api/core/v1" 24 "k8s.io/apimachinery/pkg/api/resource" 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 "k8s.io/apimachinery/pkg/util/intstr" 27 "k8s.io/client-go/rest/fake" 28 29 "gitlab.com/gitlab-org/gitlab-runner/common" 30 "gitlab.com/gitlab-org/gitlab-runner/executors" 31 "gitlab.com/gitlab-org/gitlab-runner/helpers" 32 dns_test "gitlab.com/gitlab-org/gitlab-runner/helpers/dns/test" 33 "gitlab.com/gitlab-org/gitlab-runner/helpers/docker/helperimage" 34 "gitlab.com/gitlab-org/gitlab-runner/session" 35 "gitlab.com/gitlab-org/gitlab-runner/session/proxy" 36 ) 37 38 func TestLimits(t *testing.T) { 39 tests := []struct { 40 CPU, Memory string 41 Expected api.ResourceList 42 }{ 43 { 44 CPU: "100m", 45 Memory: "100Mi", 46 Expected: api.ResourceList{ 47 api.ResourceCPU: resource.MustParse("100m"), 48 api.ResourceMemory: resource.MustParse("100Mi"), 49 }, 50 }, 51 { 52 CPU: "100m", 53 Expected: api.ResourceList{ 54 api.ResourceCPU: resource.MustParse("100m"), 55 }, 56 }, 57 { 58 Memory: "100Mi", 59 Expected: api.ResourceList{ 60 api.ResourceMemory: resource.MustParse("100Mi"), 61 }, 62 }, 63 { 64 CPU: "100j", 65 Expected: api.ResourceList{}, 66 }, 67 { 68 Memory: "100j", 69 Expected: api.ResourceList{}, 70 }, 71 { 72 Expected: api.ResourceList{}, 73 }, 74 } 75 76 for _, test := range tests { 77 res, _ := limits(test.CPU, test.Memory) 78 assert.Equal(t, test.Expected, res) 79 } 80 } 81 82 func TestVolumeMounts(t *testing.T) { 83 tests := []struct { 84 GlobalConfig *common.Config 85 RunnerConfig common.RunnerConfig 86 Build *common.Build 87 88 Expected []api.VolumeMount 89 }{ 90 { 91 GlobalConfig: &common.Config{}, 92 RunnerConfig: common.RunnerConfig{ 93 RunnerSettings: common.RunnerSettings{ 94 Kubernetes: &common.KubernetesConfig{}, 95 }, 96 }, 97 Build: &common.Build{ 98 Runner: &common.RunnerConfig{}, 99 }, 100 Expected: []api.VolumeMount{ 101 {Name: "repo"}, 102 }, 103 }, 104 { 105 GlobalConfig: &common.Config{}, 106 RunnerConfig: common.RunnerConfig{ 107 RunnerSettings: common.RunnerSettings{ 108 Kubernetes: &common.KubernetesConfig{ 109 Volumes: common.KubernetesVolumes{ 110 HostPaths: []common.KubernetesHostPath{ 111 {Name: "docker", MountPath: "/var/run/docker.sock", HostPath: "/var/run/docker.sock"}, 112 }, 113 PVCs: []common.KubernetesPVC{ 114 {Name: "PVC", MountPath: "/path/to/whatever"}, 115 }, 116 EmptyDirs: []common.KubernetesEmptyDir{ 117 {Name: "emptyDir", MountPath: "/path/to/empty/dir"}, 118 }, 119 }, 120 }, 121 }, 122 }, 123 Build: &common.Build{ 124 Runner: &common.RunnerConfig{}, 125 }, 126 Expected: []api.VolumeMount{ 127 {Name: "repo"}, 128 {Name: "docker", MountPath: "/var/run/docker.sock"}, 129 {Name: "PVC", MountPath: "/path/to/whatever"}, 130 {Name: "emptyDir", MountPath: "/path/to/empty/dir"}, 131 }, 132 }, 133 { 134 GlobalConfig: &common.Config{}, 135 RunnerConfig: common.RunnerConfig{ 136 RunnerSettings: common.RunnerSettings{ 137 Kubernetes: &common.KubernetesConfig{ 138 Volumes: common.KubernetesVolumes{ 139 HostPaths: []common.KubernetesHostPath{ 140 {Name: "test", MountPath: "/opt/test/readonly", ReadOnly: true, HostPath: "/opt/test/rw"}, 141 {Name: "docker", MountPath: "/var/run/docker.sock"}, 142 }, 143 ConfigMaps: []common.KubernetesConfigMap{ 144 {Name: "configMap", MountPath: "/path/to/configmap", ReadOnly: true}, 145 }, 146 Secrets: []common.KubernetesSecret{ 147 {Name: "secret", MountPath: "/path/to/secret", ReadOnly: true}, 148 }, 149 }, 150 }, 151 }, 152 }, 153 Build: &common.Build{ 154 Runner: &common.RunnerConfig{}, 155 }, 156 Expected: []api.VolumeMount{ 157 {Name: "repo"}, 158 {Name: "test", MountPath: "/opt/test/readonly", ReadOnly: true}, 159 {Name: "docker", MountPath: "/var/run/docker.sock"}, 160 {Name: "secret", MountPath: "/path/to/secret", ReadOnly: true}, 161 {Name: "configMap", MountPath: "/path/to/configmap", ReadOnly: true}, 162 }, 163 }, 164 } 165 166 for _, test := range tests { 167 e := &executor{ 168 AbstractExecutor: executors.AbstractExecutor{ 169 ExecutorOptions: executorOptions, 170 Build: test.Build, 171 Config: test.RunnerConfig, 172 }, 173 } 174 175 mounts := e.getVolumeMounts() 176 for _, expected := range test.Expected { 177 assert.Contains(t, mounts, expected, "Expected volumeMount definition for %s was not found", expected.Name) 178 } 179 } 180 } 181 182 func TestVolumes(t *testing.T) { 183 tests := []struct { 184 GlobalConfig *common.Config 185 RunnerConfig common.RunnerConfig 186 Build *common.Build 187 188 Expected []api.Volume 189 }{ 190 { 191 GlobalConfig: &common.Config{}, 192 RunnerConfig: common.RunnerConfig{ 193 RunnerSettings: common.RunnerSettings{ 194 Kubernetes: &common.KubernetesConfig{}, 195 }, 196 }, 197 Build: &common.Build{ 198 Runner: &common.RunnerConfig{}, 199 }, 200 Expected: []api.Volume{ 201 {Name: "repo", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, 202 }, 203 }, 204 { 205 GlobalConfig: &common.Config{}, 206 RunnerConfig: common.RunnerConfig{ 207 RunnerSettings: common.RunnerSettings{ 208 Kubernetes: &common.KubernetesConfig{ 209 Volumes: common.KubernetesVolumes{ 210 HostPaths: []common.KubernetesHostPath{ 211 {Name: "docker", MountPath: "/var/run/docker.sock"}, 212 {Name: "host-path", MountPath: "/path/two", HostPath: "/path/one"}, 213 }, 214 PVCs: []common.KubernetesPVC{ 215 {Name: "PVC", MountPath: "/path/to/whatever"}, 216 }, 217 ConfigMaps: []common.KubernetesConfigMap{ 218 {Name: "ConfigMap", MountPath: "/path/to/config", Items: map[string]string{"key_1": "/path/to/key_1"}}, 219 }, 220 Secrets: []common.KubernetesSecret{ 221 {Name: "secret", MountPath: "/path/to/secret", ReadOnly: true, Items: map[string]string{"secret_1": "/path/to/secret_1"}}, 222 }, 223 EmptyDirs: []common.KubernetesEmptyDir{ 224 {Name: "emptyDir", MountPath: "/path/to/empty/dir", Medium: "Memory"}, 225 }, 226 }, 227 }, 228 }, 229 }, 230 Build: &common.Build{ 231 Runner: &common.RunnerConfig{}, 232 }, 233 Expected: []api.Volume{ 234 {Name: "repo", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, 235 {Name: "docker", VolumeSource: api.VolumeSource{HostPath: &api.HostPathVolumeSource{Path: "/var/run/docker.sock"}}}, 236 {Name: "host-path", VolumeSource: api.VolumeSource{HostPath: &api.HostPathVolumeSource{Path: "/path/one"}}}, 237 {Name: "PVC", VolumeSource: api.VolumeSource{PersistentVolumeClaim: &api.PersistentVolumeClaimVolumeSource{ClaimName: "PVC"}}}, 238 {Name: "emptyDir", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{Medium: "Memory"}}}, 239 { 240 Name: "ConfigMap", 241 VolumeSource: api.VolumeSource{ 242 ConfigMap: &api.ConfigMapVolumeSource{ 243 LocalObjectReference: api.LocalObjectReference{Name: "ConfigMap"}, 244 Items: []api.KeyToPath{{Key: "key_1", Path: "/path/to/key_1"}}, 245 }, 246 }, 247 }, 248 { 249 Name: "secret", 250 VolumeSource: api.VolumeSource{ 251 Secret: &api.SecretVolumeSource{ 252 SecretName: "secret", 253 Items: []api.KeyToPath{{Key: "secret_1", Path: "/path/to/secret_1"}}, 254 }, 255 }, 256 }, 257 }, 258 }, 259 } 260 261 for _, test := range tests { 262 e := &executor{ 263 AbstractExecutor: executors.AbstractExecutor{ 264 ExecutorOptions: executorOptions, 265 Build: test.Build, 266 Config: test.RunnerConfig, 267 }, 268 } 269 270 volumes := e.getVolumes() 271 for _, expected := range test.Expected { 272 assert.Contains(t, volumes, expected, "Expected volume definition for %s was not found", expected.Name) 273 } 274 } 275 } 276 277 func fakeKubeDeleteResponse(status int) *http.Response { 278 _, codec := testVersionAndCodec() 279 280 body := objBody(codec, &metav1.Status{Code: int32(status)}) 281 return &http.Response{StatusCode: status, Body: body, Header: map[string][]string{ 282 "Content-Type": {"application/json"}, 283 }} 284 } 285 286 func TestCleanup(t *testing.T) { 287 version, _ := testVersionAndCodec() 288 objectMeta := metav1.ObjectMeta{Name: "test-resource", Namespace: "test-ns"} 289 podsEndpointURI := "/api/" + version + "/namespaces/" + objectMeta.Namespace + "/pods/" + objectMeta.Name 290 servicesEndpointURI := "/api/" + version + "/namespaces/" + objectMeta.Namespace + "/services/" + objectMeta.Name 291 secretsEndpointURI := "/api/" + version + "/namespaces/" + objectMeta.Namespace + "/secrets/" + objectMeta.Name 292 293 tests := []struct { 294 Name string 295 Pod *api.Pod 296 Credentials *api.Secret 297 ClientFunc func(*http.Request) (*http.Response, error) 298 Services []api.Service 299 Error bool 300 }{ 301 { 302 Name: "Proper Cleanup", 303 Pod: &api.Pod{ObjectMeta: objectMeta}, 304 ClientFunc: func(req *http.Request) (*http.Response, error) { 305 switch p, m := req.URL.Path, req.Method; { 306 case m == http.MethodDelete && p == podsEndpointURI: 307 return fakeKubeDeleteResponse(http.StatusOK), nil 308 default: 309 return nil, fmt.Errorf("unexpected request. method: %s, path: %s", m, p) 310 } 311 }, 312 }, 313 { 314 Name: "Delete failure", 315 Pod: &api.Pod{ObjectMeta: objectMeta}, 316 ClientFunc: func(req *http.Request) (*http.Response, error) { 317 return nil, fmt.Errorf("delete failed") 318 }, 319 Error: true, 320 }, 321 { 322 Name: "POD already deleted", 323 Pod: &api.Pod{ObjectMeta: objectMeta}, 324 ClientFunc: func(req *http.Request) (*http.Response, error) { 325 switch p, m := req.URL.Path, req.Method; { 326 case m == http.MethodDelete && p == podsEndpointURI: 327 return fakeKubeDeleteResponse(http.StatusNotFound), nil 328 default: 329 return nil, fmt.Errorf("unexpected request. method: %s, path: %s", m, p) 330 } 331 }, 332 Error: true, 333 }, 334 { 335 Name: "POD creation failed, Secrets provided", 336 Pod: nil, // a failed POD create request will cause a nil Pod 337 Credentials: &api.Secret{ObjectMeta: objectMeta}, 338 ClientFunc: func(req *http.Request) (*http.Response, error) { 339 switch p, m := req.URL.Path, req.Method; { 340 case m == http.MethodDelete && p == secretsEndpointURI: 341 return fakeKubeDeleteResponse(http.StatusNotFound), nil 342 default: 343 return nil, fmt.Errorf("unexpected request. method: %s, path: %s", m, p) 344 } 345 }, 346 Error: true, 347 }, 348 { 349 Name: "POD created, Services created", 350 Pod: &api.Pod{ObjectMeta: objectMeta}, 351 Services: []api.Service{{ObjectMeta: objectMeta}}, 352 ClientFunc: func(req *http.Request) (*http.Response, error) { 353 switch p, m := req.URL.Path, req.Method; { 354 case m == http.MethodDelete && ((p == servicesEndpointURI) || (p == podsEndpointURI)): 355 return fakeKubeDeleteResponse(http.StatusOK), nil 356 default: 357 return nil, fmt.Errorf("unexpected request. method: %s, path: %s", m, p) 358 } 359 }, 360 }, 361 { 362 Name: "POD created, Services creation failed", 363 Pod: &api.Pod{ObjectMeta: objectMeta}, 364 Services: []api.Service{{ObjectMeta: objectMeta}}, 365 ClientFunc: func(req *http.Request) (*http.Response, error) { 366 switch p, m := req.URL.Path, req.Method; { 367 case m == http.MethodDelete && p == servicesEndpointURI: 368 return fakeKubeDeleteResponse(http.StatusNotFound), nil 369 case m == http.MethodDelete && p == podsEndpointURI: 370 return fakeKubeDeleteResponse(http.StatusOK), nil 371 default: 372 return nil, fmt.Errorf("unexpected request. method: %s, path: %s", m, p) 373 } 374 }, 375 Error: true, 376 }, 377 { 378 Name: "POD creation failed, Services created", 379 Pod: nil, // a failed POD create request will cause a nil Pod 380 Services: []api.Service{{ObjectMeta: objectMeta}}, 381 ClientFunc: func(req *http.Request) (*http.Response, error) { 382 switch p, m := req.URL.Path, req.Method; { 383 case m == http.MethodDelete && p == servicesEndpointURI: 384 return fakeKubeDeleteResponse(http.StatusOK), nil 385 default: 386 return nil, fmt.Errorf("unexpected request. method: %s, path: %s", m, p) 387 } 388 }, 389 }, 390 { 391 Name: "POD creation failed, Services cleanup failed", 392 Pod: nil, // a failed POD create request will cause a nil Pod 393 Services: []api.Service{{ObjectMeta: objectMeta}}, 394 ClientFunc: func(req *http.Request) (*http.Response, error) { 395 switch p, m := req.URL.Path, req.Method; { 396 case m == http.MethodDelete && p == servicesEndpointURI: 397 return fakeKubeDeleteResponse(http.StatusNotFound), nil 398 default: 399 return nil, fmt.Errorf("unexpected request. method: %s, path: %s", m, p) 400 } 401 }, 402 Error: true, 403 }, 404 } 405 406 for _, test := range tests { 407 t.Run(test.Name, func(t *testing.T) { 408 ex := executor{ 409 kubeClient: testKubernetesClient(version, fake.CreateHTTPClient(test.ClientFunc)), 410 pod: test.Pod, 411 credentials: test.Credentials, 412 services: test.Services, 413 } 414 ex.configurationOverwrites = &overwrites{namespace: "test-ns"} 415 errored := false 416 buildTrace := FakeBuildTrace{ 417 testWriter{ 418 call: func(b []byte) (int, error) { 419 if !errored { 420 if s := string(b); strings.Contains(s, "Error cleaning up") { 421 errored = true 422 } else if test.Error { 423 t.Errorf("expected failure. got: '%s'", string(b)) 424 } 425 } 426 return len(b), nil 427 }, 428 }, 429 } 430 ex.AbstractExecutor.Trace = buildTrace 431 ex.AbstractExecutor.BuildLogger = common.NewBuildLogger(buildTrace, logrus.WithFields(logrus.Fields{})) 432 433 ex.Cleanup() 434 435 if test.Error && !errored { 436 t.Errorf("expected cleanup to fail but it didn't") 437 } else if !test.Error && errored { 438 t.Errorf("expected cleanup not to fail but it did") 439 } 440 }) 441 } 442 } 443 444 func TestPrepare(t *testing.T) { 445 tests := []struct { 446 GlobalConfig *common.Config 447 RunnerConfig *common.RunnerConfig 448 Build *common.Build 449 450 Expected *executor 451 Error bool 452 }{ 453 { 454 GlobalConfig: &common.Config{}, 455 RunnerConfig: &common.RunnerConfig{ 456 RunnerSettings: common.RunnerSettings{ 457 Kubernetes: &common.KubernetesConfig{ 458 Host: "test-server", 459 ServiceCPULimit: "100m", 460 ServiceMemoryLimit: "200Mi", 461 CPULimit: "1.5", 462 MemoryLimit: "4Gi", 463 HelperCPULimit: "50m", 464 HelperMemoryLimit: "100Mi", 465 Privileged: true, 466 PullPolicy: "if-not-present", 467 }, 468 }, 469 }, 470 Build: &common.Build{ 471 JobResponse: common.JobResponse{ 472 GitInfo: common.GitInfo{ 473 Sha: "1234567890", 474 }, 475 Image: common.Image{ 476 Name: "test-image", 477 }, 478 Variables: []common.JobVariable{ 479 {Key: "privileged", Value: "true"}, 480 }, 481 }, 482 Runner: &common.RunnerConfig{}, 483 }, 484 Expected: &executor{ 485 options: &kubernetesOptions{ 486 Image: common.Image{ 487 Name: "test-image", 488 }, 489 }, 490 configurationOverwrites: &overwrites{namespace: "default"}, 491 serviceLimits: api.ResourceList{ 492 api.ResourceCPU: resource.MustParse("100m"), 493 api.ResourceMemory: resource.MustParse("200Mi"), 494 }, 495 buildLimits: api.ResourceList{ 496 api.ResourceCPU: resource.MustParse("1.5"), 497 api.ResourceMemory: resource.MustParse("4Gi"), 498 }, 499 helperLimits: api.ResourceList{ 500 api.ResourceCPU: resource.MustParse("50m"), 501 api.ResourceMemory: resource.MustParse("100Mi"), 502 }, 503 serviceRequests: api.ResourceList{}, 504 buildRequests: api.ResourceList{}, 505 helperRequests: api.ResourceList{}, 506 pullPolicy: "IfNotPresent", 507 }, 508 }, 509 { 510 GlobalConfig: &common.Config{}, 511 RunnerConfig: &common.RunnerConfig{ 512 RunnerSettings: common.RunnerSettings{ 513 Kubernetes: &common.KubernetesConfig{ 514 Host: "test-server", 515 ServiceAccount: "default", 516 ServiceAccountOverwriteAllowed: ".*", 517 BearerTokenOverwriteAllowed: true, 518 ServiceCPULimit: "100m", 519 ServiceMemoryLimit: "200Mi", 520 CPULimit: "1.5", 521 MemoryLimit: "4Gi", 522 HelperCPULimit: "50m", 523 HelperMemoryLimit: "100Mi", 524 ServiceCPURequest: "99m", 525 ServiceMemoryRequest: "5Mi", 526 CPURequest: "1", 527 MemoryRequest: "1.5Gi", 528 HelperCPURequest: "0.5m", 529 HelperMemoryRequest: "42Mi", 530 Privileged: false, 531 }, 532 }, 533 }, 534 Build: &common.Build{ 535 JobResponse: common.JobResponse{ 536 GitInfo: common.GitInfo{ 537 Sha: "1234567890", 538 }, 539 Image: common.Image{ 540 Name: "test-image", 541 }, 542 Variables: []common.JobVariable{ 543 {Key: ServiceAccountOverwriteVariableName, Value: "not-default"}, 544 }, 545 }, 546 Runner: &common.RunnerConfig{}, 547 }, 548 Expected: &executor{ 549 options: &kubernetesOptions{ 550 Image: common.Image{ 551 Name: "test-image", 552 }, 553 }, 554 configurationOverwrites: &overwrites{namespace: "default", serviceAccount: "not-default"}, 555 serviceLimits: api.ResourceList{ 556 api.ResourceCPU: resource.MustParse("100m"), 557 api.ResourceMemory: resource.MustParse("200Mi"), 558 }, 559 buildLimits: api.ResourceList{ 560 api.ResourceCPU: resource.MustParse("1.5"), 561 api.ResourceMemory: resource.MustParse("4Gi"), 562 }, 563 helperLimits: api.ResourceList{ 564 api.ResourceCPU: resource.MustParse("50m"), 565 api.ResourceMemory: resource.MustParse("100Mi"), 566 }, 567 serviceRequests: api.ResourceList{ 568 api.ResourceCPU: resource.MustParse("99m"), 569 api.ResourceMemory: resource.MustParse("5Mi"), 570 }, 571 buildRequests: api.ResourceList{ 572 api.ResourceCPU: resource.MustParse("1"), 573 api.ResourceMemory: resource.MustParse("1.5Gi"), 574 }, 575 helperRequests: api.ResourceList{ 576 api.ResourceCPU: resource.MustParse("0.5m"), 577 api.ResourceMemory: resource.MustParse("42Mi"), 578 }, 579 }, 580 Error: false, 581 }, 582 583 { 584 GlobalConfig: &common.Config{}, 585 RunnerConfig: &common.RunnerConfig{ 586 RunnerSettings: common.RunnerSettings{ 587 Kubernetes: &common.KubernetesConfig{ 588 Host: "test-server", 589 ServiceAccount: "default", 590 ServiceAccountOverwriteAllowed: "allowed-.*", 591 ServiceCPULimit: "100m", 592 ServiceMemoryLimit: "200Mi", 593 CPULimit: "1.5", 594 MemoryLimit: "4Gi", 595 HelperCPULimit: "50m", 596 HelperMemoryLimit: "100Mi", 597 ServiceCPURequest: "99m", 598 ServiceMemoryRequest: "5Mi", 599 CPURequest: "1", 600 MemoryRequest: "1.5Gi", 601 HelperCPURequest: "0.5m", 602 HelperMemoryRequest: "42Mi", 603 Privileged: false, 604 }, 605 }, 606 }, 607 Build: &common.Build{ 608 JobResponse: common.JobResponse{ 609 GitInfo: common.GitInfo{ 610 Sha: "1234567890", 611 }, 612 Image: common.Image{ 613 Name: "test-image", 614 }, 615 Variables: []common.JobVariable{ 616 {Key: ServiceAccountOverwriteVariableName, Value: "not-default"}, 617 }, 618 }, 619 Runner: &common.RunnerConfig{}, 620 }, 621 Expected: &executor{ 622 options: &kubernetesOptions{ 623 Image: common.Image{ 624 Name: "test-image", 625 }, 626 }, 627 configurationOverwrites: &overwrites{namespace: "namespacee"}, 628 serviceLimits: api.ResourceList{ 629 api.ResourceCPU: resource.MustParse("100m"), 630 api.ResourceMemory: resource.MustParse("200Mi"), 631 }, 632 buildLimits: api.ResourceList{ 633 api.ResourceCPU: resource.MustParse("1.5"), 634 api.ResourceMemory: resource.MustParse("4Gi"), 635 }, 636 helperLimits: api.ResourceList{ 637 api.ResourceCPU: resource.MustParse("50m"), 638 api.ResourceMemory: resource.MustParse("100Mi"), 639 }, 640 serviceRequests: api.ResourceList{ 641 api.ResourceCPU: resource.MustParse("99m"), 642 api.ResourceMemory: resource.MustParse("5Mi"), 643 }, 644 buildRequests: api.ResourceList{ 645 api.ResourceCPU: resource.MustParse("1"), 646 api.ResourceMemory: resource.MustParse("1.5Gi"), 647 }, 648 helperRequests: api.ResourceList{ 649 api.ResourceCPU: resource.MustParse("0.5m"), 650 api.ResourceMemory: resource.MustParse("42Mi"), 651 }, 652 }, 653 Error: true, 654 }, 655 { 656 GlobalConfig: &common.Config{}, 657 RunnerConfig: &common.RunnerConfig{ 658 RunnerSettings: common.RunnerSettings{ 659 Kubernetes: &common.KubernetesConfig{ 660 Host: "test-server", 661 Namespace: "namespace", 662 ServiceAccount: "a_service_account", 663 ServiceAccountOverwriteAllowed: ".*", 664 NamespaceOverwriteAllowed: "^n.*?e$", 665 ServiceCPULimit: "100m", 666 ServiceMemoryLimit: "200Mi", 667 CPULimit: "1.5", 668 MemoryLimit: "4Gi", 669 HelperCPULimit: "50m", 670 HelperMemoryLimit: "100Mi", 671 ServiceCPURequest: "99m", 672 ServiceMemoryRequest: "5Mi", 673 CPURequest: "1", 674 MemoryRequest: "1.5Gi", 675 HelperCPURequest: "0.5m", 676 HelperMemoryRequest: "42Mi", 677 Privileged: false, 678 }, 679 }, 680 }, 681 Build: &common.Build{ 682 JobResponse: common.JobResponse{ 683 GitInfo: common.GitInfo{ 684 Sha: "1234567890", 685 }, 686 Image: common.Image{ 687 Name: "test-image", 688 }, 689 Variables: []common.JobVariable{ 690 {Key: NamespaceOverwriteVariableName, Value: "namespacee"}, 691 }, 692 }, 693 Runner: &common.RunnerConfig{}, 694 }, 695 Expected: &executor{ 696 options: &kubernetesOptions{ 697 Image: common.Image{ 698 Name: "test-image", 699 }, 700 }, 701 configurationOverwrites: &overwrites{namespace: "namespacee", serviceAccount: "a_service_account"}, 702 serviceLimits: api.ResourceList{ 703 api.ResourceCPU: resource.MustParse("100m"), 704 api.ResourceMemory: resource.MustParse("200Mi"), 705 }, 706 buildLimits: api.ResourceList{ 707 api.ResourceCPU: resource.MustParse("1.5"), 708 api.ResourceMemory: resource.MustParse("4Gi"), 709 }, 710 helperLimits: api.ResourceList{ 711 api.ResourceCPU: resource.MustParse("50m"), 712 api.ResourceMemory: resource.MustParse("100Mi"), 713 }, 714 serviceRequests: api.ResourceList{ 715 api.ResourceCPU: resource.MustParse("99m"), 716 api.ResourceMemory: resource.MustParse("5Mi"), 717 }, 718 buildRequests: api.ResourceList{ 719 api.ResourceCPU: resource.MustParse("1"), 720 api.ResourceMemory: resource.MustParse("1.5Gi"), 721 }, 722 helperRequests: api.ResourceList{ 723 api.ResourceCPU: resource.MustParse("0.5m"), 724 api.ResourceMemory: resource.MustParse("42Mi"), 725 }, 726 }, 727 Error: true, 728 }, 729 { 730 GlobalConfig: &common.Config{}, 731 RunnerConfig: &common.RunnerConfig{ 732 RunnerSettings: common.RunnerSettings{ 733 Kubernetes: &common.KubernetesConfig{ 734 Namespace: "namespace", 735 Host: "test-server", 736 }, 737 }, 738 }, 739 Build: &common.Build{ 740 JobResponse: common.JobResponse{ 741 GitInfo: common.GitInfo{ 742 Sha: "1234567890", 743 }, 744 Image: common.Image{ 745 Name: "test-image", 746 }, 747 Variables: []common.JobVariable{ 748 {Key: NamespaceOverwriteVariableName, Value: "namespace"}, 749 }, 750 }, 751 Runner: &common.RunnerConfig{}, 752 }, 753 Expected: &executor{ 754 options: &kubernetesOptions{ 755 Image: common.Image{ 756 Name: "test-image", 757 }, 758 }, 759 configurationOverwrites: &overwrites{namespace: "namespace"}, 760 serviceLimits: api.ResourceList{}, 761 buildLimits: api.ResourceList{}, 762 helperLimits: api.ResourceList{}, 763 serviceRequests: api.ResourceList{}, 764 buildRequests: api.ResourceList{}, 765 helperRequests: api.ResourceList{}, 766 }, 767 }, 768 { 769 GlobalConfig: &common.Config{}, 770 RunnerConfig: &common.RunnerConfig{ 771 RunnerSettings: common.RunnerSettings{ 772 Kubernetes: &common.KubernetesConfig{ 773 Image: "test-image", 774 Host: "test-server", 775 }, 776 }, 777 }, 778 Build: &common.Build{ 779 JobResponse: common.JobResponse{ 780 GitInfo: common.GitInfo{ 781 Sha: "1234567890", 782 }, 783 }, 784 Runner: &common.RunnerConfig{}, 785 }, 786 Expected: &executor{ 787 options: &kubernetesOptions{ 788 Image: common.Image{ 789 Name: "test-image", 790 }, 791 }, 792 configurationOverwrites: &overwrites{namespace: "default"}, 793 serviceLimits: api.ResourceList{}, 794 buildLimits: api.ResourceList{}, 795 helperLimits: api.ResourceList{}, 796 serviceRequests: api.ResourceList{}, 797 buildRequests: api.ResourceList{}, 798 helperRequests: api.ResourceList{}, 799 }, 800 }, 801 { 802 GlobalConfig: &common.Config{}, 803 RunnerConfig: &common.RunnerConfig{ 804 RunnerSettings: common.RunnerSettings{ 805 Kubernetes: &common.KubernetesConfig{ 806 Host: "test-server", 807 }, 808 }, 809 }, 810 Build: &common.Build{ 811 JobResponse: common.JobResponse{ 812 GitInfo: common.GitInfo{ 813 Sha: "1234567890", 814 }, 815 Image: common.Image{ 816 Name: "test-image", 817 Entrypoint: []string{"/init", "run"}, 818 }, 819 Services: common.Services{ 820 { 821 Name: "test-service", 822 Entrypoint: []string{"/init", "run"}, 823 Command: []string{"application", "--debug"}, 824 }, 825 }, 826 }, 827 Runner: &common.RunnerConfig{}, 828 }, 829 Expected: &executor{ 830 options: &kubernetesOptions{ 831 Image: common.Image{ 832 Name: "test-image", 833 Entrypoint: []string{"/init", "run"}, 834 }, 835 Services: common.Services{ 836 { 837 Name: "test-service", 838 Entrypoint: []string{"/init", "run"}, 839 Command: []string{"application", "--debug"}, 840 }, 841 }, 842 }, 843 configurationOverwrites: &overwrites{namespace: "default"}, 844 serviceLimits: api.ResourceList{}, 845 buildLimits: api.ResourceList{}, 846 helperLimits: api.ResourceList{}, 847 serviceRequests: api.ResourceList{}, 848 buildRequests: api.ResourceList{}, 849 helperRequests: api.ResourceList{}, 850 }, 851 }, 852 { 853 GlobalConfig: &common.Config{}, 854 RunnerConfig: &common.RunnerConfig{ 855 RunnerSettings: common.RunnerSettings{ 856 Kubernetes: &common.KubernetesConfig{ 857 Host: "test-server", 858 Services: []common.Service{ 859 {Name: "test-service-k8s"}, 860 {Name: "test-service-k8s2"}, 861 {Name: ""}, 862 }, 863 }, 864 }, 865 }, 866 Build: &common.Build{ 867 JobResponse: common.JobResponse{ 868 GitInfo: common.GitInfo{ 869 Sha: "1234567890", 870 }, 871 Image: common.Image{ 872 Name: "test-image", 873 Entrypoint: []string{"/init", "run"}, 874 }, 875 Services: common.Services{ 876 { 877 Name: "test-service", 878 Entrypoint: []string{"/init", "run"}, 879 Command: []string{"application", "--debug"}, 880 }, 881 { 882 Name: "", 883 }, 884 }, 885 }, 886 Runner: &common.RunnerConfig{}, 887 }, 888 Expected: &executor{ 889 options: &kubernetesOptions{ 890 Image: common.Image{ 891 Name: "test-image", 892 Entrypoint: []string{"/init", "run"}, 893 }, 894 Services: common.Services{ 895 { 896 Name: "test-service-k8s", 897 }, 898 { 899 Name: "test-service-k8s2", 900 }, 901 { 902 Name: "test-service", 903 Entrypoint: []string{"/init", "run"}, 904 Command: []string{"application", "--debug"}, 905 }, 906 }, 907 }, 908 configurationOverwrites: &overwrites{namespace: "default"}, 909 serviceLimits: api.ResourceList{}, 910 buildLimits: api.ResourceList{}, 911 helperLimits: api.ResourceList{}, 912 serviceRequests: api.ResourceList{}, 913 buildRequests: api.ResourceList{}, 914 helperRequests: api.ResourceList{}, 915 }, 916 }, 917 } 918 919 for index, test := range tests { 920 t.Run(strconv.Itoa(index), func(t *testing.T) { 921 e := &executor{ 922 AbstractExecutor: executors.AbstractExecutor{ 923 ExecutorOptions: executorOptions, 924 }, 925 } 926 927 prepareOptions := common.ExecutorPrepareOptions{ 928 Config: test.RunnerConfig, 929 Build: test.Build, 930 Context: context.TODO(), 931 } 932 933 err := e.Prepare(prepareOptions) 934 935 if err != nil { 936 assert.False(t, test.Build.IsSharedEnv()) 937 if test.Error { 938 assert.Error(t, err) 939 } else { 940 assert.NoError(t, err) 941 } 942 if !test.Error { 943 t.Errorf("Got error. Expected: %v", test.Expected) 944 } 945 return 946 } 947 948 // Set this to nil so we aren't testing the functionality of the 949 // base AbstractExecutor's Prepare method 950 e.AbstractExecutor = executors.AbstractExecutor{} 951 952 // TODO: Improve this so we don't have to nil-ify the kubeClient. 953 // It currently contains some moving parts that are failing, meaning 954 // we'll need to mock _something_ 955 e.kubeClient = nil 956 assert.Equal(t, test.Expected, e) 957 }) 958 } 959 } 960 961 // This test reproduces the bug reported in https://gitlab.com/gitlab-org/gitlab-runner/issues/2583 962 func TestPrepareIssue2583(t *testing.T) { 963 if helpers.SkipIntegrationTests(t, "kubectl", "cluster-info") { 964 return 965 } 966 967 namespace := "my_namespace" 968 serviceAccount := "my_account" 969 970 runnerConfig := &common.RunnerConfig{ 971 RunnerSettings: common.RunnerSettings{ 972 Executor: "kubernetes", 973 Kubernetes: &common.KubernetesConfig{ 974 Image: "an/image:latest", 975 Namespace: namespace, 976 NamespaceOverwriteAllowed: ".*", 977 ServiceAccount: serviceAccount, 978 ServiceAccountOverwriteAllowed: ".*", 979 }, 980 }, 981 } 982 983 build := &common.Build{ 984 JobResponse: common.JobResponse{ 985 Variables: []common.JobVariable{ 986 {Key: NamespaceOverwriteVariableName, Value: "namespace"}, 987 {Key: ServiceAccountOverwriteVariableName, Value: "sa"}, 988 }, 989 }, 990 Runner: &common.RunnerConfig{}, 991 } 992 993 e := &executor{ 994 AbstractExecutor: executors.AbstractExecutor{ 995 ExecutorOptions: executorOptions, 996 }, 997 } 998 999 prepareOptions := common.ExecutorPrepareOptions{ 1000 Config: runnerConfig, 1001 Build: build, 1002 Context: context.TODO(), 1003 } 1004 1005 err := e.Prepare(prepareOptions) 1006 assert.NoError(t, err) 1007 assert.Equal(t, namespace, runnerConfig.Kubernetes.Namespace) 1008 assert.Equal(t, serviceAccount, runnerConfig.Kubernetes.ServiceAccount) 1009 } 1010 1011 func TestSetupCredentials(t *testing.T) { 1012 version, _ := testVersionAndCodec() 1013 1014 type testDef struct { 1015 RunnerCredentials *common.RunnerCredentials 1016 Credentials []common.Credentials 1017 VerifyFn func(*testing.T, testDef, *api.Secret) 1018 } 1019 tests := map[string]testDef{ 1020 "no credentials": { 1021 // don't execute VerifyFn 1022 VerifyFn: nil, 1023 }, 1024 "registry credentials": { 1025 Credentials: []common.Credentials{ 1026 { 1027 Type: "registry", 1028 URL: "http://example.com", 1029 Username: "user", 1030 Password: "password", 1031 }, 1032 }, 1033 VerifyFn: func(t *testing.T, test testDef, secret *api.Secret) { 1034 assert.Equal(t, api.SecretTypeDockercfg, secret.Type) 1035 assert.NotEmpty(t, secret.Data[api.DockerConfigKey]) 1036 }, 1037 }, 1038 "other credentials": { 1039 Credentials: []common.Credentials{ 1040 { 1041 Type: "other", 1042 URL: "http://example.com", 1043 Username: "user", 1044 Password: "password", 1045 }, 1046 }, 1047 // don't execute VerifyFn 1048 VerifyFn: nil, 1049 }, 1050 "non-DNS-1123-compatible-token": { 1051 RunnerCredentials: &common.RunnerCredentials{ 1052 Token: "ToK3_?OF", 1053 }, 1054 Credentials: []common.Credentials{ 1055 { 1056 Type: "registry", 1057 URL: "http://example.com", 1058 Username: "user", 1059 Password: "password", 1060 }, 1061 }, 1062 VerifyFn: func(t *testing.T, test testDef, secret *api.Secret) { 1063 dns_test.AssertRFC1123Compatibility(t, secret.GetGenerateName()) 1064 }, 1065 }, 1066 } 1067 1068 executed := false 1069 fakeClientRoundTripper := func(test testDef) func(req *http.Request) (*http.Response, error) { 1070 return func(req *http.Request) (resp *http.Response, err error) { 1071 podBytes, err := ioutil.ReadAll(req.Body) 1072 executed = true 1073 1074 if err != nil { 1075 t.Errorf("failed to read request body: %s", err.Error()) 1076 return 1077 } 1078 1079 p := new(api.Secret) 1080 1081 err = json.Unmarshal(podBytes, p) 1082 1083 if err != nil { 1084 t.Errorf("error decoding pod: %s", err.Error()) 1085 return 1086 } 1087 1088 if test.VerifyFn != nil { 1089 test.VerifyFn(t, test, p) 1090 } 1091 1092 resp = &http.Response{StatusCode: http.StatusOK, Body: FakeReadCloser{ 1093 Reader: bytes.NewBuffer(podBytes), 1094 }} 1095 resp.Header = make(http.Header) 1096 resp.Header.Add("Content-Type", "application/json") 1097 1098 return 1099 } 1100 } 1101 1102 for testName, test := range tests { 1103 t.Run(testName, func(t *testing.T) { 1104 ex := executor{ 1105 kubeClient: testKubernetesClient(version, fake.CreateHTTPClient(fakeClientRoundTripper(test))), 1106 options: &kubernetesOptions{}, 1107 AbstractExecutor: executors.AbstractExecutor{ 1108 Config: common.RunnerConfig{ 1109 RunnerSettings: common.RunnerSettings{ 1110 Kubernetes: &common.KubernetesConfig{ 1111 Namespace: "default", 1112 }, 1113 }, 1114 }, 1115 BuildShell: &common.ShellConfiguration{}, 1116 Build: &common.Build{ 1117 JobResponse: common.JobResponse{ 1118 Variables: []common.JobVariable{}, 1119 Credentials: test.Credentials, 1120 }, 1121 Runner: &common.RunnerConfig{}, 1122 }, 1123 }, 1124 } 1125 1126 if test.RunnerCredentials != nil { 1127 ex.Build.Runner = &common.RunnerConfig{ 1128 RunnerCredentials: *test.RunnerCredentials, 1129 } 1130 } 1131 1132 executed = false 1133 1134 err := ex.prepareOverwrites(make(common.JobVariables, 0)) 1135 assert.NoError(t, err) 1136 1137 err = ex.setupCredentials() 1138 assert.NoError(t, err) 1139 1140 if test.VerifyFn != nil { 1141 assert.True(t, executed) 1142 } else { 1143 assert.False(t, executed) 1144 } 1145 }) 1146 } 1147 } 1148 1149 type setupBuildPodTestDef struct { 1150 RunnerConfig common.RunnerConfig 1151 Variables []common.JobVariable 1152 Options *kubernetesOptions 1153 PrepareFn func(*testing.T, setupBuildPodTestDef, *executor) 1154 VerifyFn func(*testing.T, setupBuildPodTestDef, *api.Pod) 1155 VerifyExecutorFn func(*testing.T, setupBuildPodTestDef, *executor) 1156 } 1157 1158 type setupBuildPodFakeRoundTripper struct { 1159 t *testing.T 1160 test setupBuildPodTestDef 1161 executed bool 1162 } 1163 1164 func (rt *setupBuildPodFakeRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 1165 rt.executed = true 1166 podBytes, err := ioutil.ReadAll(req.Body) 1167 if !assert.NoError(rt.t, err, "failed to read request body") { 1168 return nil, err 1169 } 1170 1171 p := new(api.Pod) 1172 err = json.Unmarshal(podBytes, p) 1173 if !assert.NoError(rt.t, err, "failed to read request body") { 1174 return nil, err 1175 } 1176 1177 if rt.test.VerifyFn != nil { 1178 rt.test.VerifyFn(rt.t, rt.test, p) 1179 } 1180 1181 resp := &http.Response{ 1182 StatusCode: http.StatusOK, 1183 Body: FakeReadCloser{ 1184 Reader: bytes.NewBuffer(podBytes), 1185 }, 1186 } 1187 resp.Header = make(http.Header) 1188 resp.Header.Add("Content-Type", "application/json") 1189 1190 return resp, nil 1191 } 1192 1193 func TestSetupBuildPod(t *testing.T) { 1194 version, _ := testVersionAndCodec() 1195 1196 tests := map[string]setupBuildPodTestDef{ 1197 "passes node selector setting": { 1198 RunnerConfig: common.RunnerConfig{ 1199 RunnerSettings: common.RunnerSettings{ 1200 Kubernetes: &common.KubernetesConfig{ 1201 Namespace: "default", 1202 NodeSelector: map[string]string{ 1203 "a-selector": "first", 1204 "another-selector": "second", 1205 }, 1206 }, 1207 }, 1208 }, 1209 VerifyFn: func(t *testing.T, test setupBuildPodTestDef, pod *api.Pod) { 1210 assert.Equal(t, test.RunnerConfig.RunnerSettings.Kubernetes.NodeSelector, pod.Spec.NodeSelector) 1211 }, 1212 }, 1213 "uses configured credentials": { 1214 RunnerConfig: common.RunnerConfig{ 1215 RunnerSettings: common.RunnerSettings{ 1216 Kubernetes: &common.KubernetesConfig{ 1217 Namespace: "default", 1218 }, 1219 }, 1220 }, 1221 PrepareFn: func(t *testing.T, test setupBuildPodTestDef, e *executor) { 1222 e.credentials = &api.Secret{ 1223 ObjectMeta: metav1.ObjectMeta{ 1224 Name: "job-credentials", 1225 }, 1226 } 1227 }, 1228 VerifyFn: func(t *testing.T, test setupBuildPodTestDef, pod *api.Pod) { 1229 secrets := []api.LocalObjectReference{{Name: "job-credentials"}} 1230 assert.Equal(t, secrets, pod.Spec.ImagePullSecrets) 1231 }, 1232 }, 1233 "uses configured image pull secrets": { 1234 RunnerConfig: common.RunnerConfig{ 1235 RunnerSettings: common.RunnerSettings{ 1236 Kubernetes: &common.KubernetesConfig{ 1237 Namespace: "default", 1238 ImagePullSecrets: []string{ 1239 "docker-registry-credentials", 1240 }, 1241 }, 1242 }, 1243 }, 1244 VerifyFn: func(t *testing.T, test setupBuildPodTestDef, pod *api.Pod) { 1245 secrets := []api.LocalObjectReference{{Name: "docker-registry-credentials"}} 1246 assert.Equal(t, secrets, pod.Spec.ImagePullSecrets) 1247 }, 1248 }, 1249 "configures helper container": { 1250 RunnerConfig: common.RunnerConfig{ 1251 RunnerSettings: common.RunnerSettings{ 1252 Kubernetes: &common.KubernetesConfig{ 1253 Namespace: "default", 1254 }, 1255 }, 1256 }, 1257 VerifyFn: func(t *testing.T, test setupBuildPodTestDef, pod *api.Pod) { 1258 hasHelper := false 1259 for _, c := range pod.Spec.Containers { 1260 if c.Name == "helper" { 1261 hasHelper = true 1262 } 1263 } 1264 assert.True(t, hasHelper) 1265 }, 1266 }, 1267 "uses configured helper image": { 1268 RunnerConfig: common.RunnerConfig{ 1269 RunnerSettings: common.RunnerSettings{ 1270 Kubernetes: &common.KubernetesConfig{ 1271 Namespace: "default", 1272 HelperImage: "custom/helper-image", 1273 }, 1274 }, 1275 }, 1276 VerifyFn: func(t *testing.T, test setupBuildPodTestDef, pod *api.Pod) { 1277 for _, c := range pod.Spec.Containers { 1278 if c.Name == "helper" { 1279 assert.Equal(t, test.RunnerConfig.RunnerSettings.Kubernetes.HelperImage, c.Image) 1280 } 1281 } 1282 }, 1283 }, 1284 "expands variables for pod labels": { 1285 RunnerConfig: common.RunnerConfig{ 1286 RunnerSettings: common.RunnerSettings{ 1287 Kubernetes: &common.KubernetesConfig{ 1288 Namespace: "default", 1289 PodLabels: map[string]string{ 1290 "test": "label", 1291 "another": "label", 1292 "var": "$test", 1293 }, 1294 }, 1295 }, 1296 }, 1297 VerifyFn: func(t *testing.T, test setupBuildPodTestDef, pod *api.Pod) { 1298 assert.Equal(t, map[string]string{ 1299 "test": "label", 1300 "another": "label", 1301 "var": "sometestvar", 1302 "pod": pod.GenerateName, 1303 }, pod.ObjectMeta.Labels) 1304 }, 1305 Variables: []common.JobVariable{ 1306 {Key: "test", Value: "sometestvar"}, 1307 }, 1308 }, 1309 "expands variables for pod annotations": { 1310 RunnerConfig: common.RunnerConfig{ 1311 RunnerSettings: common.RunnerSettings{ 1312 Kubernetes: &common.KubernetesConfig{ 1313 Namespace: "default", 1314 PodAnnotations: map[string]string{ 1315 "test": "annotation", 1316 "another": "annotation", 1317 "var": "$test", 1318 }, 1319 }, 1320 }, 1321 }, 1322 VerifyFn: func(t *testing.T, test setupBuildPodTestDef, pod *api.Pod) { 1323 assert.Equal(t, map[string]string{ 1324 "test": "annotation", 1325 "another": "annotation", 1326 "var": "sometestvar", 1327 }, pod.ObjectMeta.Annotations) 1328 }, 1329 Variables: []common.JobVariable{ 1330 {Key: "test", Value: "sometestvar"}, 1331 }, 1332 }, 1333 "expands variables for helper image": { 1334 RunnerConfig: common.RunnerConfig{ 1335 RunnerSettings: common.RunnerSettings{ 1336 Kubernetes: &common.KubernetesConfig{ 1337 Namespace: "default", 1338 HelperImage: "custom/helper-image:${CI_RUNNER_REVISION}", 1339 }, 1340 }, 1341 }, 1342 VerifyFn: func(t *testing.T, test setupBuildPodTestDef, pod *api.Pod) { 1343 for _, c := range pod.Spec.Containers { 1344 if c.Name == "helper" { 1345 assert.Equal(t, "custom/helper-image:HEAD", c.Image) 1346 } 1347 } 1348 }, 1349 }, 1350 "support setting kubernetes pod taint tolerations": { 1351 RunnerConfig: common.RunnerConfig{ 1352 RunnerSettings: common.RunnerSettings{ 1353 Kubernetes: &common.KubernetesConfig{ 1354 Namespace: "default", 1355 NodeTolerations: map[string]string{ 1356 "node-role.kubernetes.io/master": "NoSchedule", 1357 "custom.toleration=value": "NoSchedule", 1358 "empty.value=": "PreferNoSchedule", 1359 "onlyKey": "", 1360 }, 1361 }, 1362 }, 1363 }, 1364 VerifyFn: func(t *testing.T, test setupBuildPodTestDef, pod *api.Pod) { 1365 expectedTolerations := []api.Toleration{ 1366 { 1367 Key: "node-role.kubernetes.io/master", 1368 Operator: api.TolerationOpExists, 1369 Effect: api.TaintEffectNoSchedule, 1370 }, 1371 { 1372 Key: "custom.toleration", 1373 Operator: api.TolerationOpEqual, 1374 Value: "value", 1375 Effect: api.TaintEffectNoSchedule, 1376 }, 1377 { 1378 1379 Key: "empty.value", 1380 Operator: api.TolerationOpEqual, 1381 Value: "", 1382 Effect: api.TaintEffectPreferNoSchedule, 1383 }, 1384 { 1385 Key: "onlyKey", 1386 Operator: api.TolerationOpExists, 1387 Effect: "", 1388 }, 1389 } 1390 assert.ElementsMatch(t, expectedTolerations, pod.Spec.Tolerations) 1391 }, 1392 }, 1393 "supports extended docker configuration for image and services": { 1394 RunnerConfig: common.RunnerConfig{ 1395 RunnerSettings: common.RunnerSettings{ 1396 Kubernetes: &common.KubernetesConfig{ 1397 Namespace: "default", 1398 HelperImage: "custom/helper-image", 1399 }, 1400 }, 1401 }, 1402 Options: &kubernetesOptions{ 1403 Image: common.Image{ 1404 Name: "test-image", 1405 Entrypoint: []string{"/init", "run"}, 1406 }, 1407 Services: common.Services{ 1408 { 1409 Name: "test-service", 1410 Entrypoint: []string{"/init", "run"}, 1411 Command: []string{"application", "--debug"}, 1412 }, 1413 { 1414 Name: "test-service-2", 1415 Command: []string{"application", "--debug"}, 1416 }, 1417 }, 1418 }, 1419 VerifyFn: func(t *testing.T, test setupBuildPodTestDef, pod *api.Pod) { 1420 require.Len(t, pod.Spec.Containers, 4) 1421 1422 assert.Equal(t, "build", pod.Spec.Containers[0].Name) 1423 assert.Equal(t, "test-image", pod.Spec.Containers[0].Image) 1424 assert.Equal(t, []string{"/init", "run"}, pod.Spec.Containers[0].Command) 1425 assert.Empty(t, pod.Spec.Containers[0].Args, "Build container args should be empty") 1426 1427 assert.Equal(t, "helper", pod.Spec.Containers[1].Name) 1428 assert.Equal(t, "custom/helper-image", pod.Spec.Containers[1].Image) 1429 assert.Empty(t, pod.Spec.Containers[1].Command, "Helper container command should be empty") 1430 assert.Empty(t, pod.Spec.Containers[1].Args, "Helper container args should be empty") 1431 1432 assert.Equal(t, "svc-0", pod.Spec.Containers[2].Name) 1433 assert.Equal(t, "test-service", pod.Spec.Containers[2].Image) 1434 assert.Equal(t, []string{"/init", "run"}, pod.Spec.Containers[2].Command) 1435 assert.Equal(t, []string{"application", "--debug"}, pod.Spec.Containers[2].Args) 1436 1437 assert.Equal(t, "svc-1", pod.Spec.Containers[3].Name) 1438 assert.Equal(t, "test-service-2", pod.Spec.Containers[3].Image) 1439 assert.Empty(t, pod.Spec.Containers[3].Command, "Service container command should be empty") 1440 assert.Equal(t, []string{"application", "--debug"}, pod.Spec.Containers[3].Args) 1441 }, 1442 }, 1443 "creates services in kubernetes if ports are set": { 1444 RunnerConfig: common.RunnerConfig{ 1445 RunnerSettings: common.RunnerSettings{ 1446 Kubernetes: &common.KubernetesConfig{ 1447 Namespace: "default", 1448 HelperImage: "custom/helper-image", 1449 }, 1450 }, 1451 }, 1452 Options: &kubernetesOptions{ 1453 Image: common.Image{ 1454 Name: "test-image", 1455 Ports: []common.Port{ 1456 { 1457 Number: 80, 1458 }, 1459 }, 1460 }, 1461 Services: common.Services{ 1462 { 1463 Name: "test-service", 1464 Ports: []common.Port{ 1465 { 1466 Number: 82, 1467 }, 1468 { 1469 Number: 84, 1470 }, 1471 }, 1472 }, 1473 { 1474 Name: "test-service2", 1475 Ports: []common.Port{ 1476 { 1477 Number: 85, 1478 }, 1479 }, 1480 }, 1481 { 1482 Name: "test-service3", 1483 }, 1484 }, 1485 }, 1486 VerifyExecutorFn: func(t *testing.T, test setupBuildPodTestDef, e *executor) { 1487 expectedServices := []api.Service{ 1488 { 1489 ObjectMeta: metav1.ObjectMeta{ 1490 GenerateName: "build", 1491 Namespace: "default", 1492 }, 1493 Spec: api.ServiceSpec{ 1494 Ports: []api.ServicePort{ 1495 { 1496 Port: 80, 1497 TargetPort: intstr.FromInt(80), 1498 Name: "build-80", 1499 }, 1500 }, 1501 Selector: map[string]string{"pod": e.pod.GenerateName}, 1502 Type: api.ServiceTypeClusterIP, 1503 }, 1504 }, 1505 { 1506 ObjectMeta: metav1.ObjectMeta{ 1507 GenerateName: "proxy-svc-0", 1508 Namespace: "default", 1509 }, 1510 Spec: api.ServiceSpec{ 1511 Ports: []api.ServicePort{ 1512 { 1513 Port: 82, 1514 TargetPort: intstr.FromInt(82), 1515 Name: "proxy-svc-0-82", 1516 }, 1517 { 1518 Port: 84, 1519 TargetPort: intstr.FromInt(84), 1520 Name: "proxy-svc-0-84", 1521 }, 1522 }, 1523 Selector: map[string]string{"pod": e.pod.GenerateName}, 1524 Type: api.ServiceTypeClusterIP, 1525 }, 1526 }, 1527 { 1528 ObjectMeta: metav1.ObjectMeta{ 1529 GenerateName: "proxy-svc-1", 1530 Namespace: "default", 1531 }, 1532 Spec: api.ServiceSpec{ 1533 Ports: []api.ServicePort{ 1534 { 1535 Port: 85, 1536 TargetPort: intstr.FromInt(85), 1537 Name: "proxy-svc-1-85", 1538 }, 1539 }, 1540 Selector: map[string]string{"pod": e.pod.GenerateName}, 1541 Type: api.ServiceTypeClusterIP, 1542 }, 1543 }, 1544 } 1545 1546 assert.ElementsMatch(t, expectedServices, e.services) 1547 }, 1548 }, 1549 "the default service name for the build container is build": { 1550 RunnerConfig: common.RunnerConfig{ 1551 RunnerSettings: common.RunnerSettings{ 1552 Kubernetes: &common.KubernetesConfig{ 1553 Namespace: "default", 1554 HelperImage: "custom/helper-image", 1555 }, 1556 }, 1557 }, 1558 Options: &kubernetesOptions{ 1559 Image: common.Image{ 1560 Name: "test-image", 1561 Ports: []common.Port{ 1562 { 1563 Number: 80, 1564 }, 1565 }, 1566 }, 1567 }, 1568 VerifyExecutorFn: func(t *testing.T, test setupBuildPodTestDef, e *executor) { 1569 assert.Equal(t, "build", e.services[0].GenerateName) 1570 }, 1571 }, 1572 "the services have a selector pointing to the 'pod' label in the pod": { 1573 RunnerConfig: common.RunnerConfig{ 1574 RunnerSettings: common.RunnerSettings{ 1575 Kubernetes: &common.KubernetesConfig{ 1576 Namespace: "default", 1577 HelperImage: "custom/helper-image", 1578 }, 1579 }, 1580 }, 1581 Options: &kubernetesOptions{ 1582 Image: common.Image{ 1583 Name: "test-image", 1584 Ports: []common.Port{ 1585 { 1586 Number: 80, 1587 }, 1588 }, 1589 }, 1590 Services: common.Services{ 1591 { 1592 Name: "test-service", 1593 Ports: []common.Port{ 1594 { 1595 Number: 82, 1596 }, 1597 }, 1598 }, 1599 }, 1600 }, 1601 VerifyExecutorFn: func(t *testing.T, test setupBuildPodTestDef, e *executor) { 1602 for _, service := range e.services { 1603 assert.Equal(t, map[string]string{"pod": e.pod.GenerateName}, service.Spec.Selector) 1604 } 1605 }, 1606 }, 1607 "the service is named as the alias if set": { 1608 RunnerConfig: common.RunnerConfig{ 1609 RunnerSettings: common.RunnerSettings{ 1610 Kubernetes: &common.KubernetesConfig{ 1611 Namespace: "default", 1612 HelperImage: "custom/helper-image", 1613 }, 1614 }, 1615 }, 1616 Options: &kubernetesOptions{ 1617 Image: common.Image{ 1618 Name: "test-image", 1619 }, 1620 Services: common.Services{ 1621 { 1622 Name: "test-service", 1623 Alias: "custom-name", 1624 Ports: []common.Port{ 1625 { 1626 Number: 82, 1627 }, 1628 }, 1629 }, 1630 }, 1631 }, 1632 VerifyExecutorFn: func(t *testing.T, test setupBuildPodTestDef, e *executor) { 1633 assert.Equal(t, "custom-name", e.services[0].GenerateName) 1634 }, 1635 }, 1636 "proxies are configured if services have been created": { 1637 RunnerConfig: common.RunnerConfig{ 1638 RunnerSettings: common.RunnerSettings{ 1639 Kubernetes: &common.KubernetesConfig{ 1640 Namespace: "default", 1641 HelperImage: "custom/helper-image", 1642 }, 1643 }, 1644 }, 1645 Options: &kubernetesOptions{ 1646 Image: common.Image{ 1647 Name: "test-image", 1648 Ports: []common.Port{ 1649 { 1650 Number: 80, 1651 }, 1652 }, 1653 }, 1654 Services: common.Services{ 1655 { 1656 Name: "test-service", 1657 Alias: "custom_name", 1658 Ports: []common.Port{ 1659 { 1660 Number: 81, 1661 Name: "custom_port_name", 1662 Protocol: "http", 1663 }, 1664 }, 1665 }, 1666 { 1667 Name: "test-service2", 1668 Ports: []common.Port{ 1669 { 1670 Number: 82, 1671 }, 1672 }, 1673 }, 1674 }, 1675 }, 1676 VerifyExecutorFn: func(t *testing.T, test setupBuildPodTestDef, e *executor) { 1677 require.Len(t, e.ProxyPool, 3) 1678 1679 assert.NotEmpty(t, "proxy-svc-1", e.ProxyPool) 1680 assert.NotEmpty(t, "custom_name", e.ProxyPool) 1681 assert.NotEmpty(t, "build", e.ProxyPool) 1682 1683 port := e.ProxyPool["proxy-svc-1"].Settings.Ports[0] 1684 assert.Equal(t, 82, port.Number) 1685 1686 port = e.ProxyPool["custom_name"].Settings.Ports[0] 1687 assert.Equal(t, 81, port.Number) 1688 assert.Equal(t, "custom_port_name", port.Name) 1689 assert.Equal(t, "http", port.Protocol) 1690 1691 port = e.ProxyPool["build"].Settings.Ports[0] 1692 assert.Equal(t, 80, port.Number) 1693 }, 1694 }, 1695 "makes service name compatible with RFC1123": { 1696 RunnerConfig: common.RunnerConfig{ 1697 RunnerSettings: common.RunnerSettings{ 1698 Kubernetes: &common.KubernetesConfig{ 1699 Namespace: "default", 1700 HelperImage: "custom/helper-image", 1701 }, 1702 }, 1703 }, 1704 Options: &kubernetesOptions{ 1705 Image: common.Image{ 1706 Name: "test-image", 1707 }, 1708 Services: common.Services{ 1709 { 1710 Name: "test-service", 1711 Alias: "service,name-.non-compat!ble", 1712 Ports: []common.Port{ 1713 { 1714 Number: 81, 1715 Name: "port,name-.non-compat!ble", 1716 Protocol: "http", 1717 }, 1718 }, 1719 }, 1720 }, 1721 }, 1722 VerifyExecutorFn: func(t *testing.T, test setupBuildPodTestDef, e *executor) { 1723 assert.Equal(t, "servicename-non-compatble", e.services[0].GenerateName) 1724 assert.NotEmpty(t, e.ProxyPool["service,name-.non-compat!ble"]) 1725 assert.Equal(t, "port,name-.non-compat!ble", e.ProxyPool["service,name-.non-compat!ble"].Settings.Ports[0].Name) 1726 }, 1727 }, 1728 "sets command (entrypoint) and args": { 1729 RunnerConfig: common.RunnerConfig{ 1730 RunnerSettings: common.RunnerSettings{ 1731 Kubernetes: &common.KubernetesConfig{ 1732 Namespace: "default", 1733 HelperImage: "custom/helper-image", 1734 }, 1735 }, 1736 }, 1737 Options: &kubernetesOptions{ 1738 Image: common.Image{ 1739 Name: "test-image", 1740 }, 1741 Services: common.Services{ 1742 { 1743 Name: "test-service-0", 1744 Command: []string{"application", "--debug"}, 1745 }, 1746 { 1747 Name: "test-service-1", 1748 Entrypoint: []string{"application", "--debug"}, 1749 }, 1750 { 1751 Name: "test-service-2", 1752 Entrypoint: []string{"application", "--debug"}, 1753 Command: []string{"argument1", "argument2"}, 1754 }, 1755 }, 1756 }, 1757 VerifyFn: func(t *testing.T, test setupBuildPodTestDef, pod *api.Pod) { 1758 require.Len(t, pod.Spec.Containers, 5) 1759 1760 assert.Equal(t, "build", pod.Spec.Containers[0].Name) 1761 assert.Equal(t, "test-image", pod.Spec.Containers[0].Image) 1762 assert.Empty(t, pod.Spec.Containers[0].Command, "Build container command should be empty") 1763 assert.Empty(t, pod.Spec.Containers[0].Args, "Build container args should be empty") 1764 1765 assert.Equal(t, "helper", pod.Spec.Containers[1].Name) 1766 assert.Equal(t, "custom/helper-image", pod.Spec.Containers[1].Image) 1767 assert.Empty(t, pod.Spec.Containers[1].Command, "Helper container command should be empty") 1768 assert.Empty(t, pod.Spec.Containers[1].Args, "Helper container args should be empty") 1769 1770 assert.Equal(t, "svc-0", pod.Spec.Containers[2].Name) 1771 assert.Equal(t, "test-service-0", pod.Spec.Containers[2].Image) 1772 assert.Empty(t, pod.Spec.Containers[2].Command, "Service container command should be empty") 1773 assert.Equal(t, []string{"application", "--debug"}, pod.Spec.Containers[2].Args) 1774 1775 assert.Equal(t, "svc-1", pod.Spec.Containers[3].Name) 1776 assert.Equal(t, "test-service-1", pod.Spec.Containers[3].Image) 1777 assert.Equal(t, []string{"application", "--debug"}, pod.Spec.Containers[3].Command) 1778 assert.Empty(t, pod.Spec.Containers[3].Args, "Service container args should be empty") 1779 1780 assert.Equal(t, "svc-2", pod.Spec.Containers[4].Name) 1781 assert.Equal(t, "test-service-2", pod.Spec.Containers[4].Image) 1782 assert.Equal(t, []string{"application", "--debug"}, pod.Spec.Containers[4].Command) 1783 assert.Equal(t, []string{"argument1", "argument2"}, pod.Spec.Containers[4].Args) 1784 }, 1785 }, 1786 "non-DNS-1123-compatible-token": { 1787 RunnerConfig: common.RunnerConfig{ 1788 RunnerCredentials: common.RunnerCredentials{ 1789 Token: "ToK3_?OF", 1790 }, 1791 RunnerSettings: common.RunnerSettings{ 1792 Kubernetes: &common.KubernetesConfig{ 1793 Namespace: "default", 1794 }, 1795 }, 1796 }, 1797 VerifyFn: func(t *testing.T, test setupBuildPodTestDef, pod *api.Pod) { 1798 dns_test.AssertRFC1123Compatibility(t, pod.GetGenerateName()) 1799 }, 1800 }, 1801 "supports pod security context": { 1802 RunnerConfig: common.RunnerConfig{ 1803 RunnerSettings: common.RunnerSettings{ 1804 Kubernetes: &common.KubernetesConfig{ 1805 Namespace: "default", 1806 PodSecurityContext: common.KubernetesPodSecurityContext{ 1807 FSGroup: func() *int64 { i := int64(200); return &i }(), 1808 RunAsGroup: func() *int64 { i := int64(200); return &i }(), 1809 RunAsNonRoot: func() *bool { i := bool(true); return &i }(), 1810 RunAsUser: func() *int64 { i := int64(200); return &i }(), 1811 SupplementalGroups: []int64{200}, 1812 }, 1813 }, 1814 }, 1815 }, 1816 VerifyFn: func(t *testing.T, test setupBuildPodTestDef, pod *api.Pod) { 1817 assert.Equal(t, int64(200), *pod.Spec.SecurityContext.FSGroup) 1818 assert.Equal(t, int64(200), *pod.Spec.SecurityContext.RunAsGroup) 1819 assert.Equal(t, int64(200), *pod.Spec.SecurityContext.RunAsUser) 1820 assert.Equal(t, true, *pod.Spec.SecurityContext.RunAsNonRoot) 1821 assert.Equal(t, []int64{200}, pod.Spec.SecurityContext.SupplementalGroups) 1822 }, 1823 }, 1824 "uses default security context when unspecified": { 1825 RunnerConfig: common.RunnerConfig{ 1826 RunnerSettings: common.RunnerSettings{ 1827 Kubernetes: &common.KubernetesConfig{ 1828 Namespace: "default", 1829 }, 1830 }, 1831 }, 1832 VerifyFn: func(t *testing.T, test setupBuildPodTestDef, pod *api.Pod) { 1833 assert.Empty(t, pod.Spec.SecurityContext, "Security context should be empty") 1834 }, 1835 }, 1836 } 1837 1838 for testName, test := range tests { 1839 t.Run(testName, func(t *testing.T) { 1840 helperImageInfo, err := helperimage.Get(common.REVISION, helperimage.Config{ 1841 OSType: helperimage.OSTypeLinux, 1842 Architecture: "amd64", 1843 }) 1844 require.NoError(t, err) 1845 1846 vars := test.Variables 1847 if vars == nil { 1848 vars = []common.JobVariable{} 1849 } 1850 1851 options := test.Options 1852 if options == nil { 1853 options = &kubernetesOptions{} 1854 } 1855 1856 rt := setupBuildPodFakeRoundTripper{ 1857 t: t, 1858 test: test, 1859 } 1860 1861 ex := executor{ 1862 kubeClient: testKubernetesClient(version, fake.CreateHTTPClient(rt.RoundTrip)), 1863 options: options, 1864 AbstractExecutor: executors.AbstractExecutor{ 1865 Config: test.RunnerConfig, 1866 BuildShell: &common.ShellConfiguration{}, 1867 Build: &common.Build{ 1868 JobResponse: common.JobResponse{ 1869 Variables: vars, 1870 }, 1871 Runner: &test.RunnerConfig, 1872 }, 1873 ProxyPool: proxy.NewPool(), 1874 }, 1875 helperImageInfo: helperImageInfo, 1876 } 1877 1878 if test.PrepareFn != nil { 1879 test.PrepareFn(t, test, &ex) 1880 } 1881 1882 err = ex.prepareOverwrites(make(common.JobVariables, 0)) 1883 assert.NoError(t, err, "error preparing overwrites") 1884 1885 err = ex.setupBuildPod() 1886 assert.NoError(t, err, "error setting up build pod") 1887 1888 assert.True(t, rt.executed, "RoundTrip for kubernetes client should be executed") 1889 1890 if test.VerifyExecutorFn != nil { 1891 test.VerifyExecutorFn(t, test, &ex) 1892 } 1893 }) 1894 } 1895 } 1896 1897 func TestSetupBuildPodServiceCreationError(t *testing.T) { 1898 version, _ := testVersionAndCodec() 1899 helperImageInfo, err := helperimage.Get(common.REVISION, helperimage.Config{ 1900 OSType: helperimage.OSTypeLinux, 1901 Architecture: "amd64", 1902 }) 1903 require.NoError(t, err) 1904 1905 runnerConfig := common.RunnerConfig{ 1906 RunnerSettings: common.RunnerSettings{ 1907 Kubernetes: &common.KubernetesConfig{ 1908 Namespace: "default", 1909 HelperImage: "custom/helper-image", 1910 }, 1911 }, 1912 } 1913 1914 fakeRoundTripper := func(req *http.Request) (*http.Response, error) { 1915 body, err := ioutil.ReadAll(req.Body) 1916 if !assert.NoError(t, err, "failed to read request body") { 1917 return nil, err 1918 } 1919 1920 p := new(api.Pod) 1921 err = json.Unmarshal(body, p) 1922 if !assert.NoError(t, err, "failed to read request body") { 1923 return nil, err 1924 } 1925 1926 if req.URL.Path == "/api/v1/namespaces/default/services" { 1927 return nil, fmt.Errorf("foobar") 1928 } 1929 1930 resp := &http.Response{ 1931 StatusCode: http.StatusOK, 1932 Body: FakeReadCloser{ 1933 Reader: bytes.NewBuffer(body), 1934 }, 1935 } 1936 resp.Header = make(http.Header) 1937 resp.Header.Add("Content-Type", "application/json") 1938 1939 return resp, nil 1940 } 1941 1942 ex := executor{ 1943 kubeClient: testKubernetesClient(version, fake.CreateHTTPClient(fakeRoundTripper)), 1944 options: &kubernetesOptions{ 1945 Image: common.Image{ 1946 Name: "test-image", 1947 Ports: []common.Port{{Number: 80}}, 1948 }, 1949 Services: common.Services{ 1950 { 1951 Name: "test-service", 1952 Alias: "custom_name", 1953 Ports: []common.Port{ 1954 { 1955 Number: 81, 1956 Name: "custom_port_name", 1957 Protocol: "http", 1958 }, 1959 }, 1960 }, 1961 }, 1962 }, 1963 AbstractExecutor: executors.AbstractExecutor{ 1964 Config: runnerConfig, 1965 BuildShell: &common.ShellConfiguration{}, 1966 Build: &common.Build{ 1967 JobResponse: common.JobResponse{ 1968 Variables: []common.JobVariable{}, 1969 }, 1970 Runner: &runnerConfig, 1971 }, 1972 ProxyPool: proxy.NewPool(), 1973 }, 1974 helperImageInfo: helperImageInfo, 1975 } 1976 1977 err = ex.prepareOverwrites(make(common.JobVariables, 0)) 1978 assert.NoError(t, err) 1979 1980 err = ex.setupBuildPod() 1981 assert.Error(t, err) 1982 assert.Contains(t, err.Error(), "error creating the proxy service") 1983 } 1984 1985 func TestKubernetesSuccessRun(t *testing.T) { 1986 if helpers.SkipIntegrationTests(t, "kubectl", "cluster-info") { 1987 return 1988 } 1989 1990 successfulBuild, err := common.GetRemoteSuccessfulBuild() 1991 assert.NoError(t, err) 1992 successfulBuild.Image.Name = common.TestDockerGitImage 1993 build := &common.Build{ 1994 JobResponse: successfulBuild, 1995 Runner: &common.RunnerConfig{ 1996 RunnerSettings: common.RunnerSettings{ 1997 Executor: "kubernetes", 1998 Kubernetes: &common.KubernetesConfig{ 1999 PullPolicy: common.PullPolicyIfNotPresent, 2000 }, 2001 }, 2002 }, 2003 } 2004 2005 err = build.Run(&common.Config{}, &common.Trace{Writer: os.Stdout}) 2006 assert.NoError(t, err) 2007 } 2008 2009 func TestKubernetesNoRootImage(t *testing.T) { 2010 if helpers.SkipIntegrationTests(t, "kubectl", "cluster-info") { 2011 return 2012 } 2013 2014 successfulBuild, err := common.GetRemoteSuccessfulBuildWithDumpedVariables() 2015 2016 assert.NoError(t, err) 2017 successfulBuild.Image.Name = common.TestAlpineNoRootImage 2018 build := &common.Build{ 2019 JobResponse: successfulBuild, 2020 Runner: &common.RunnerConfig{ 2021 RunnerSettings: common.RunnerSettings{ 2022 Executor: "kubernetes", 2023 Kubernetes: &common.KubernetesConfig{ 2024 Image: common.TestAlpineImage, 2025 PullPolicy: common.PullPolicyIfNotPresent, 2026 }, 2027 }, 2028 }, 2029 } 2030 2031 err = build.Run(&common.Config{}, &common.Trace{Writer: os.Stdout}) 2032 assert.NoError(t, err) 2033 } 2034 2035 func TestKubernetesCustomClonePath(t *testing.T) { 2036 if helpers.SkipIntegrationTests(t, "kubectl", "cluster-info") { 2037 return 2038 } 2039 2040 jobResponse, err := common.GetRemoteBuildResponse( 2041 "ls -al $CI_BUILDS_DIR/go/src/gitlab.com/gitlab-org/repo") 2042 require.NoError(t, err) 2043 2044 tests := map[string]struct { 2045 clonePath string 2046 expectedErrorType interface{} 2047 }{ 2048 "uses custom clone path": { 2049 clonePath: "$CI_BUILDS_DIR/go/src/gitlab.com/gitlab-org/repo", 2050 expectedErrorType: nil, 2051 }, 2052 "path has to be within CI_BUILDS_DIR": { 2053 clonePath: "/unknown/go/src/gitlab.com/gitlab-org/repo", 2054 expectedErrorType: &common.BuildError{}, 2055 }, 2056 } 2057 2058 for name, test := range tests { 2059 t.Run(name, func(t *testing.T) { 2060 build := &common.Build{ 2061 JobResponse: jobResponse, 2062 Runner: &common.RunnerConfig{ 2063 RunnerSettings: common.RunnerSettings{ 2064 Executor: "kubernetes", 2065 Kubernetes: &common.KubernetesConfig{ 2066 Image: common.TestAlpineImage, 2067 PullPolicy: common.PullPolicyIfNotPresent, 2068 }, 2069 Environment: []string{ 2070 "GIT_CLONE_PATH=" + test.clonePath, 2071 }, 2072 }, 2073 }, 2074 } 2075 2076 err = build.Run(&common.Config{}, &common.Trace{Writer: os.Stdout}) 2077 assert.IsType(t, test.expectedErrorType, err) 2078 }) 2079 } 2080 } 2081 func TestKubernetesBuildFail(t *testing.T) { 2082 if helpers.SkipIntegrationTests(t, "kubectl", "cluster-info") { 2083 return 2084 } 2085 2086 failedBuild, err := common.GetRemoteFailedBuild() 2087 assert.NoError(t, err) 2088 build := &common.Build{ 2089 JobResponse: failedBuild, 2090 Runner: &common.RunnerConfig{ 2091 RunnerSettings: common.RunnerSettings{ 2092 Executor: "kubernetes", 2093 Kubernetes: &common.KubernetesConfig{ 2094 PullPolicy: common.PullPolicyIfNotPresent, 2095 }, 2096 }, 2097 }, 2098 } 2099 build.Image.Name = common.TestDockerGitImage 2100 2101 err = build.Run(&common.Config{}, &common.Trace{Writer: os.Stdout}) 2102 require.Error(t, err, "error") 2103 assert.IsType(t, err, &common.BuildError{}) 2104 assert.Contains(t, err.Error(), "command terminated with exit code") 2105 } 2106 2107 func TestKubernetesMissingImage(t *testing.T) { 2108 if helpers.SkipIntegrationTests(t, "kubectl", "cluster-info") { 2109 return 2110 } 2111 2112 failedBuild, err := common.GetRemoteFailedBuild() 2113 assert.NoError(t, err) 2114 build := &common.Build{ 2115 JobResponse: failedBuild, 2116 Runner: &common.RunnerConfig{ 2117 RunnerSettings: common.RunnerSettings{ 2118 Executor: "kubernetes", 2119 Kubernetes: &common.KubernetesConfig{}, 2120 }, 2121 }, 2122 } 2123 build.Image.Name = "some/non-existing/image" 2124 2125 err = build.Run(&common.Config{}, &common.Trace{Writer: os.Stdout}) 2126 require.Error(t, err) 2127 assert.IsType(t, err, &common.BuildError{}) 2128 assert.Contains(t, err.Error(), "image pull failed") 2129 } 2130 2131 func TestKubernetesMissingTag(t *testing.T) { 2132 if helpers.SkipIntegrationTests(t, "kubectl", "cluster-info") { 2133 return 2134 } 2135 2136 failedBuild, err := common.GetRemoteFailedBuild() 2137 assert.NoError(t, err) 2138 build := &common.Build{ 2139 JobResponse: failedBuild, 2140 Runner: &common.RunnerConfig{ 2141 RunnerSettings: common.RunnerSettings{ 2142 Executor: "kubernetes", 2143 Kubernetes: &common.KubernetesConfig{}, 2144 }, 2145 }, 2146 } 2147 build.Image.Name = "docker:missing-tag" 2148 2149 err = build.Run(&common.Config{}, &common.Trace{Writer: os.Stdout}) 2150 require.Error(t, err) 2151 assert.IsType(t, err, &common.BuildError{}) 2152 assert.Contains(t, err.Error(), "image pull failed") 2153 } 2154 2155 func TestKubernetesBuildAbort(t *testing.T) { 2156 if helpers.SkipIntegrationTests(t, "kubectl", "cluster-info") { 2157 return 2158 } 2159 2160 failedBuild, err := common.GetRemoteFailedBuild() 2161 assert.NoError(t, err) 2162 build := &common.Build{ 2163 JobResponse: failedBuild, 2164 Runner: &common.RunnerConfig{ 2165 RunnerSettings: common.RunnerSettings{ 2166 Executor: "kubernetes", 2167 Kubernetes: &common.KubernetesConfig{ 2168 PullPolicy: common.PullPolicyIfNotPresent, 2169 }, 2170 }, 2171 }, 2172 SystemInterrupt: make(chan os.Signal, 1), 2173 } 2174 build.Image.Name = common.TestDockerGitImage 2175 2176 abortTimer := time.AfterFunc(time.Second, func() { 2177 t.Log("Interrupt") 2178 build.SystemInterrupt <- os.Interrupt 2179 }) 2180 defer abortTimer.Stop() 2181 2182 timeoutTimer := time.AfterFunc(time.Minute, func() { 2183 t.Log("Timedout") 2184 t.FailNow() 2185 }) 2186 defer timeoutTimer.Stop() 2187 2188 err = build.Run(&common.Config{}, &common.Trace{Writer: os.Stdout}) 2189 assert.EqualError(t, err, "aborted: interrupt") 2190 } 2191 2192 func TestKubernetesBuildCancel(t *testing.T) { 2193 if helpers.SkipIntegrationTests(t, "kubectl", "cluster-info") { 2194 return 2195 } 2196 2197 failedBuild, err := common.GetRemoteFailedBuild() 2198 assert.NoError(t, err) 2199 build := &common.Build{ 2200 JobResponse: failedBuild, 2201 Runner: &common.RunnerConfig{ 2202 RunnerSettings: common.RunnerSettings{ 2203 Executor: "kubernetes", 2204 Kubernetes: &common.KubernetesConfig{ 2205 PullPolicy: common.PullPolicyIfNotPresent, 2206 }, 2207 }, 2208 }, 2209 SystemInterrupt: make(chan os.Signal, 1), 2210 } 2211 build.Image.Name = common.TestDockerGitImage 2212 2213 trace := &common.Trace{Writer: os.Stdout} 2214 2215 abortTimer := time.AfterFunc(time.Second, func() { 2216 t.Log("Interrupt") 2217 trace.CancelFunc() 2218 }) 2219 defer abortTimer.Stop() 2220 2221 timeoutTimer := time.AfterFunc(time.Minute, func() { 2222 t.Log("Timedout") 2223 t.FailNow() 2224 }) 2225 defer timeoutTimer.Stop() 2226 2227 err = build.Run(&common.Config{}, trace) 2228 assert.IsType(t, err, &common.BuildError{}) 2229 assert.EqualError(t, err, "canceled") 2230 } 2231 2232 func TestOverwriteNamespaceNotMatch(t *testing.T) { 2233 if helpers.SkipIntegrationTests(t, "kubectl", "cluster-info") { 2234 return 2235 } 2236 2237 build := &common.Build{ 2238 JobResponse: common.JobResponse{ 2239 GitInfo: common.GitInfo{ 2240 Sha: "1234567890", 2241 }, 2242 Image: common.Image{ 2243 Name: "test-image", 2244 }, 2245 Variables: []common.JobVariable{ 2246 {Key: NamespaceOverwriteVariableName, Value: "namespace"}, 2247 }, 2248 }, 2249 Runner: &common.RunnerConfig{ 2250 RunnerSettings: common.RunnerSettings{ 2251 Executor: "kubernetes", 2252 Kubernetes: &common.KubernetesConfig{ 2253 NamespaceOverwriteAllowed: "^not_a_match$", 2254 PullPolicy: common.PullPolicyIfNotPresent, 2255 }, 2256 }, 2257 }, 2258 SystemInterrupt: make(chan os.Signal, 1), 2259 } 2260 build.Image.Name = common.TestDockerGitImage 2261 2262 err := build.Run(&common.Config{}, &common.Trace{Writer: os.Stdout}) 2263 require.Error(t, err) 2264 assert.Contains(t, err.Error(), "does not match") 2265 } 2266 2267 func TestOverwriteServiceAccountNotMatch(t *testing.T) { 2268 if helpers.SkipIntegrationTests(t, "kubectl", "cluster-info") { 2269 return 2270 } 2271 2272 build := &common.Build{ 2273 JobResponse: common.JobResponse{ 2274 GitInfo: common.GitInfo{ 2275 Sha: "1234567890", 2276 }, 2277 Image: common.Image{ 2278 Name: "test-image", 2279 }, 2280 Variables: []common.JobVariable{ 2281 {Key: ServiceAccountOverwriteVariableName, Value: "service-account"}, 2282 }, 2283 }, 2284 Runner: &common.RunnerConfig{ 2285 RunnerSettings: common.RunnerSettings{ 2286 Executor: "kubernetes", 2287 Kubernetes: &common.KubernetesConfig{ 2288 ServiceAccountOverwriteAllowed: "^not_a_match$", 2289 PullPolicy: common.PullPolicyIfNotPresent, 2290 }, 2291 }, 2292 }, 2293 SystemInterrupt: make(chan os.Signal, 1), 2294 } 2295 build.Image.Name = common.TestDockerGitImage 2296 2297 err := build.Run(&common.Config{}, &common.Trace{Writer: os.Stdout}) 2298 require.Error(t, err) 2299 assert.Contains(t, err.Error(), "does not match") 2300 } 2301 2302 func TestInteractiveTerminal(t *testing.T) { 2303 if helpers.SkipIntegrationTests(t, "kubectl", "cluster-info") { 2304 return 2305 } 2306 2307 client, err := getKubeClient(&common.KubernetesConfig{}, &overwrites{}) 2308 require.NoError(t, err) 2309 secrets, err := client.CoreV1().Secrets("default").List(metav1.ListOptions{}) 2310 require.NoError(t, err) 2311 2312 successfulBuild, err := common.GetRemoteBuildResponse("sleep 5") 2313 require.NoError(t, err) 2314 successfulBuild.Image.Name = "docker:git" 2315 build := &common.Build{ 2316 JobResponse: successfulBuild, 2317 Runner: &common.RunnerConfig{ 2318 RunnerSettings: common.RunnerSettings{ 2319 Executor: "kubernetes", 2320 Kubernetes: &common.KubernetesConfig{ 2321 BearerToken: string(secrets.Items[0].Data["token"]), 2322 }, 2323 }, 2324 }, 2325 } 2326 2327 sess, err := session.NewSession(nil) 2328 build.Session = sess 2329 2330 outBuffer := bytes.NewBuffer(nil) 2331 outCh := make(chan string) 2332 2333 go func() { 2334 err = build.Run( 2335 &common.Config{ 2336 SessionServer: common.SessionServer{ 2337 SessionTimeout: 2, 2338 }, 2339 }, 2340 &common.Trace{Writer: outBuffer}, 2341 ) 2342 require.NoError(t, err) 2343 2344 outCh <- outBuffer.String() 2345 }() 2346 2347 for build.Session.Mux() == nil { 2348 time.Sleep(10 * time.Millisecond) 2349 } 2350 2351 time.Sleep(5 * time.Second) 2352 2353 srv := httptest.NewServer(build.Session.Mux()) 2354 defer srv.Close() 2355 2356 u := url.URL{ 2357 Scheme: "ws", 2358 Host: srv.Listener.Addr().String(), 2359 Path: build.Session.Endpoint + "/exec", 2360 } 2361 headers := http.Header{ 2362 "Authorization": []string{build.Session.Token}, 2363 } 2364 conn, resp, err := websocket.DefaultDialer.Dial(u.String(), headers) 2365 defer func() { 2366 if conn != nil { 2367 _ = conn.Close() 2368 } 2369 }() 2370 require.NoError(t, err) 2371 assert.Equal(t, resp.StatusCode, http.StatusSwitchingProtocols) 2372 2373 out := <-outCh 2374 t.Log(out) 2375 2376 assert.Contains(t, out, "Terminal is connected, will time out in 2s...") 2377 } 2378 2379 type FakeReadCloser struct { 2380 io.Reader 2381 } 2382 2383 func (f FakeReadCloser) Close() error { 2384 return nil 2385 } 2386 2387 type FakeBuildTrace struct { 2388 testWriter 2389 } 2390 2391 func (f FakeBuildTrace) Success() {} 2392 func (f FakeBuildTrace) Fail(err error, failureReason common.JobFailureReason) {} 2393 func (f FakeBuildTrace) Notify(func()) {} 2394 func (f FakeBuildTrace) SetCancelFunc(cancelFunc context.CancelFunc) {} 2395 func (f FakeBuildTrace) SetFailuresCollector(fc common.FailuresCollector) {} 2396 func (f FakeBuildTrace) SetMasked(masked []string) {} 2397 func (f FakeBuildTrace) IsStdout() bool { 2398 return false 2399 }