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