gitlab.com/jfprevost/gitlab-runner-notlscheck@v11.11.4+incompatible/executors/kubernetes/executor_kubernetes.go (about) 1 package kubernetes 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io/ioutil" 7 "net/http" 8 "net/url" 9 "strings" 10 11 "github.com/sirupsen/logrus" 12 terminal "gitlab.com/gitlab-org/gitlab-terminal" 13 "golang.org/x/net/context" 14 api "k8s.io/api/core/v1" 15 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 "k8s.io/client-go/kubernetes" 17 "k8s.io/client-go/kubernetes/scheme" 18 _ "k8s.io/client-go/plugin/pkg/client/auth" // Register all available authentication methods 19 restclient "k8s.io/client-go/rest" 20 21 "gitlab.com/gitlab-org/gitlab-runner/common" 22 "gitlab.com/gitlab-org/gitlab-runner/executors" 23 "gitlab.com/gitlab-org/gitlab-runner/helpers/dns" 24 "gitlab.com/gitlab-org/gitlab-runner/helpers/docker/helperimage" 25 "gitlab.com/gitlab-org/gitlab-runner/helpers/featureflags" 26 terminalsession "gitlab.com/gitlab-org/gitlab-runner/session/terminal" 27 ) 28 29 var ( 30 executorOptions = executors.ExecutorOptions{ 31 DefaultCustomBuildsDirEnabled: true, 32 DefaultBuildsDir: "/builds", 33 DefaultCacheDir: "/cache", 34 SharedBuildsDir: false, 35 Shell: common.ShellScriptInfo{ 36 Shell: "bash", 37 Type: common.NormalShell, 38 RunnerCommand: "/usr/bin/gitlab-runner-helper", 39 }, 40 ShowHostname: true, 41 } 42 ) 43 44 type kubernetesOptions struct { 45 Image common.Image 46 Services common.Services 47 } 48 49 type executor struct { 50 executors.AbstractExecutor 51 52 kubeClient *kubernetes.Clientset 53 pod *api.Pod 54 credentials *api.Secret 55 options *kubernetesOptions 56 57 configurationOverwrites *overwrites 58 buildLimits api.ResourceList 59 serviceLimits api.ResourceList 60 helperLimits api.ResourceList 61 buildRequests api.ResourceList 62 serviceRequests api.ResourceList 63 helperRequests api.ResourceList 64 pullPolicy common.KubernetesPullPolicy 65 66 helperImageInfo helperimage.Info 67 } 68 69 func (s *executor) setupResources() error { 70 var err error 71 72 // Limit 73 if s.buildLimits, err = limits(s.Config.Kubernetes.CPULimit, s.Config.Kubernetes.MemoryLimit); err != nil { 74 return fmt.Errorf("invalid build limits specified: %s", err.Error()) 75 } 76 77 if s.serviceLimits, err = limits(s.Config.Kubernetes.ServiceCPULimit, s.Config.Kubernetes.ServiceMemoryLimit); err != nil { 78 return fmt.Errorf("invalid service limits specified: %s", err.Error()) 79 } 80 81 if s.helperLimits, err = limits(s.Config.Kubernetes.HelperCPULimit, s.Config.Kubernetes.HelperMemoryLimit); err != nil { 82 return fmt.Errorf("invalid helper limits specified: %s", err.Error()) 83 } 84 85 // Requests 86 if s.buildRequests, err = limits(s.Config.Kubernetes.CPURequest, s.Config.Kubernetes.MemoryRequest); err != nil { 87 return fmt.Errorf("invalid build requests specified: %s", err.Error()) 88 } 89 90 if s.serviceRequests, err = limits(s.Config.Kubernetes.ServiceCPURequest, s.Config.Kubernetes.ServiceMemoryRequest); err != nil { 91 return fmt.Errorf("invalid service requests specified: %s", err.Error()) 92 } 93 94 if s.helperRequests, err = limits(s.Config.Kubernetes.HelperCPURequest, s.Config.Kubernetes.HelperMemoryRequest); err != nil { 95 return fmt.Errorf("invalid helper requests specified: %s", err.Error()) 96 } 97 return nil 98 } 99 100 func (s *executor) Prepare(options common.ExecutorPrepareOptions) (err error) { 101 if err = s.AbstractExecutor.Prepare(options); err != nil { 102 return err 103 } 104 105 if s.BuildShell.PassFile { 106 return fmt.Errorf("kubernetes doesn't support shells that require script file") 107 } 108 109 if err = s.setupResources(); err != nil { 110 return err 111 } 112 113 if s.pullPolicy, err = s.Config.Kubernetes.PullPolicy.Get(); err != nil { 114 return err 115 } 116 117 if err = s.prepareOverwrites(options.Build.Variables); err != nil { 118 return err 119 } 120 121 s.prepareOptions(options.Build) 122 123 if err = s.checkDefaults(); err != nil { 124 return err 125 } 126 127 if s.kubeClient, err = getKubeClient(options.Config.Kubernetes, s.configurationOverwrites); err != nil { 128 return fmt.Errorf("error connecting to Kubernetes: %s", err.Error()) 129 } 130 131 s.Println("Using Kubernetes executor with image", s.options.Image.Name, "...") 132 133 return nil 134 } 135 136 func (s *executor) Run(cmd common.ExecutorCommand) error { 137 s.Debugln("Starting Kubernetes command...") 138 139 if s.pod == nil { 140 err := s.setupCredentials() 141 if err != nil { 142 return err 143 } 144 145 err = s.setupBuildPod() 146 if err != nil { 147 return err 148 } 149 } 150 151 containerName := "build" 152 containerCommand := s.BuildShell.DockerCommand 153 if cmd.Predefined { 154 containerName = "helper" 155 containerCommand = s.helperImageInfo.Cmd 156 } 157 158 ctx, cancel := context.WithCancel(context.Background()) 159 defer cancel() 160 161 s.Debugln(fmt.Sprintf( 162 "Starting in container %q the command %q with script: %s", 163 containerName, 164 containerCommand, 165 cmd.Script, 166 )) 167 168 select { 169 case err := <-s.runInContainer(ctx, containerName, containerCommand, cmd.Script): 170 s.Debugln(fmt.Sprintf("Container %q exited with error: %v", containerName, err)) 171 if err != nil && strings.Contains(err.Error(), "command terminated with exit code") { 172 return &common.BuildError{Inner: err} 173 } 174 return err 175 176 case <-cmd.Context.Done(): 177 return fmt.Errorf("build aborted") 178 } 179 } 180 181 func (s *executor) Cleanup() { 182 if s.pod != nil { 183 err := s.kubeClient.CoreV1().Pods(s.pod.Namespace).Delete(s.pod.Name, &metav1.DeleteOptions{}) 184 if err != nil { 185 s.Errorln(fmt.Sprintf("Error cleaning up pod: %s", err.Error())) 186 } 187 } 188 if s.credentials != nil { 189 err := s.kubeClient.CoreV1().Secrets(s.configurationOverwrites.namespace).Delete(s.credentials.Name, &metav1.DeleteOptions{}) 190 if err != nil { 191 s.Errorln(fmt.Sprintf("Error cleaning up secrets: %s", err.Error())) 192 } 193 } 194 closeKubeClient(s.kubeClient) 195 s.AbstractExecutor.Cleanup() 196 } 197 198 func (s *executor) buildContainer(name, image string, imageDefinition common.Image, requests, limits api.ResourceList, containerCommand ...string) api.Container { 199 privileged := false 200 if s.Config.Kubernetes != nil { 201 privileged = s.Config.Kubernetes.Privileged 202 } 203 204 command, args := s.getCommandAndArgs(imageDefinition, containerCommand...) 205 206 return api.Container{ 207 Name: name, 208 Image: image, 209 ImagePullPolicy: api.PullPolicy(s.pullPolicy), 210 Command: command, 211 Args: args, 212 Env: buildVariables(s.Build.GetAllVariables().PublicOrInternal()), 213 Resources: api.ResourceRequirements{ 214 Limits: limits, 215 Requests: requests, 216 }, 217 VolumeMounts: s.getVolumeMounts(), 218 SecurityContext: &api.SecurityContext{ 219 Privileged: &privileged, 220 }, 221 Stdin: true, 222 } 223 } 224 225 func (s *executor) getCommandAndArgs(imageDefinition common.Image, command ...string) ([]string, []string) { 226 if s.Build.IsFeatureFlagOn(featureflags.K8sEntrypointOverCommand) { 227 return s.getCommandsAndArgsV2(imageDefinition, command...) 228 } 229 230 return s.getCommandsAndArgsV1(imageDefinition, command...) 231 } 232 233 // TODO: Remove in 12.0 234 func (s *executor) getCommandsAndArgsV1(imageDefinition common.Image, command ...string) ([]string, []string) { 235 if len(command) == 0 && len(imageDefinition.Command) > 0 { 236 command = imageDefinition.Command 237 } 238 239 var args []string 240 if len(imageDefinition.Entrypoint) > 0 { 241 args = command 242 command = imageDefinition.Entrypoint 243 } 244 245 return command, args 246 } 247 248 // TODO: Make this the only proper way to setup command and args in 12.0 249 func (s *executor) getCommandsAndArgsV2(imageDefinition common.Image, command ...string) ([]string, []string) { 250 if len(command) == 0 && len(imageDefinition.Entrypoint) > 0 { 251 command = imageDefinition.Entrypoint 252 } 253 254 var args []string 255 if len(imageDefinition.Command) > 0 { 256 args = imageDefinition.Command 257 } 258 259 return command, args 260 } 261 262 func (s *executor) getVolumeMounts() (mounts []api.VolumeMount) { 263 mounts = append(mounts, api.VolumeMount{ 264 Name: "repo", 265 MountPath: s.Build.RootDir, 266 }) 267 268 for _, mount := range s.Config.Kubernetes.Volumes.HostPaths { 269 mounts = append(mounts, api.VolumeMount{ 270 Name: mount.Name, 271 MountPath: mount.MountPath, 272 ReadOnly: mount.ReadOnly, 273 }) 274 } 275 276 for _, mount := range s.Config.Kubernetes.Volumes.Secrets { 277 mounts = append(mounts, api.VolumeMount{ 278 Name: mount.Name, 279 MountPath: mount.MountPath, 280 ReadOnly: mount.ReadOnly, 281 }) 282 } 283 284 for _, mount := range s.Config.Kubernetes.Volumes.PVCs { 285 mounts = append(mounts, api.VolumeMount{ 286 Name: mount.Name, 287 MountPath: mount.MountPath, 288 ReadOnly: mount.ReadOnly, 289 }) 290 } 291 292 for _, mount := range s.Config.Kubernetes.Volumes.ConfigMaps { 293 mounts = append(mounts, api.VolumeMount{ 294 Name: mount.Name, 295 MountPath: mount.MountPath, 296 ReadOnly: mount.ReadOnly, 297 }) 298 } 299 300 for _, mount := range s.Config.Kubernetes.Volumes.EmptyDirs { 301 mounts = append(mounts, api.VolumeMount{ 302 Name: mount.Name, 303 MountPath: mount.MountPath, 304 }) 305 } 306 307 return 308 } 309 310 func (s *executor) getVolumes() (volumes []api.Volume) { 311 volumes = append(volumes, api.Volume{ 312 Name: "repo", 313 VolumeSource: api.VolumeSource{ 314 EmptyDir: &api.EmptyDirVolumeSource{}, 315 }, 316 }) 317 318 for _, volume := range s.Config.Kubernetes.Volumes.HostPaths { 319 path := volume.HostPath 320 // Make backward compatible with syntax introduced in version 9.3.0 321 if path == "" { 322 path = volume.MountPath 323 } 324 325 volumes = append(volumes, api.Volume{ 326 Name: volume.Name, 327 VolumeSource: api.VolumeSource{ 328 HostPath: &api.HostPathVolumeSource{ 329 Path: path, 330 }, 331 }, 332 }) 333 } 334 335 for _, volume := range s.Config.Kubernetes.Volumes.Secrets { 336 items := []api.KeyToPath{} 337 for key, path := range volume.Items { 338 items = append(items, api.KeyToPath{Key: key, Path: path}) 339 } 340 341 volumes = append(volumes, api.Volume{ 342 Name: volume.Name, 343 VolumeSource: api.VolumeSource{ 344 Secret: &api.SecretVolumeSource{ 345 SecretName: volume.Name, 346 Items: items, 347 }, 348 }, 349 }) 350 } 351 352 for _, volume := range s.Config.Kubernetes.Volumes.PVCs { 353 volumes = append(volumes, api.Volume{ 354 Name: volume.Name, 355 VolumeSource: api.VolumeSource{ 356 PersistentVolumeClaim: &api.PersistentVolumeClaimVolumeSource{ 357 ClaimName: volume.Name, 358 ReadOnly: volume.ReadOnly, 359 }, 360 }, 361 }) 362 } 363 364 for _, volume := range s.Config.Kubernetes.Volumes.ConfigMaps { 365 items := []api.KeyToPath{} 366 for key, path := range volume.Items { 367 items = append(items, api.KeyToPath{Key: key, Path: path}) 368 } 369 370 volumes = append(volumes, api.Volume{ 371 Name: volume.Name, 372 VolumeSource: api.VolumeSource{ 373 ConfigMap: &api.ConfigMapVolumeSource{ 374 LocalObjectReference: api.LocalObjectReference{ 375 Name: volume.Name, 376 }, 377 Items: items, 378 }, 379 }, 380 }) 381 } 382 383 for _, volume := range s.Config.Kubernetes.Volumes.EmptyDirs { 384 volumes = append(volumes, api.Volume{ 385 Name: volume.Name, 386 VolumeSource: api.VolumeSource{ 387 EmptyDir: &api.EmptyDirVolumeSource{ 388 Medium: api.StorageMedium(volume.Medium), 389 }, 390 }, 391 }) 392 } 393 394 return 395 } 396 397 type dockerConfigEntry struct { 398 Username, Password string 399 } 400 401 func (s *executor) projectUniqueName() string { 402 return dns.MakeRFC1123Compatible(s.Build.ProjectUniqueName()) 403 } 404 405 func (s *executor) setupCredentials() error { 406 authConfigs := make(map[string]dockerConfigEntry) 407 408 for _, credentials := range s.Build.Credentials { 409 if credentials.Type != "registry" { 410 continue 411 } 412 413 authConfigs[credentials.URL] = dockerConfigEntry{ 414 Username: credentials.Username, 415 Password: credentials.Password, 416 } 417 } 418 419 if len(authConfigs) == 0 { 420 return nil 421 } 422 423 dockerCfgContent, err := json.Marshal(authConfigs) 424 if err != nil { 425 return err 426 } 427 428 secret := api.Secret{} 429 secret.GenerateName = s.projectUniqueName() 430 secret.Namespace = s.configurationOverwrites.namespace 431 secret.Type = api.SecretTypeDockercfg 432 secret.Data = map[string][]byte{} 433 secret.Data[api.DockerConfigKey] = dockerCfgContent 434 435 s.credentials, err = s.kubeClient.CoreV1().Secrets(s.configurationOverwrites.namespace).Create(&secret) 436 if err != nil { 437 return err 438 } 439 return nil 440 } 441 442 func (s *executor) setupBuildPod() error { 443 services := make([]api.Container, len(s.options.Services)) 444 for i, service := range s.options.Services { 445 resolvedImage := s.Build.GetAllVariables().ExpandValue(service.Name) 446 services[i] = s.buildContainer(fmt.Sprintf("svc-%d", i), resolvedImage, service, s.serviceRequests, s.serviceLimits) 447 } 448 449 labels := make(map[string]string) 450 for k, v := range s.Build.Runner.Kubernetes.PodLabels { 451 labels[k] = s.Build.Variables.ExpandValue(v) 452 } 453 454 annotations := make(map[string]string) 455 for key, val := range s.configurationOverwrites.podAnnotations { 456 annotations[key] = s.Build.Variables.ExpandValue(val) 457 } 458 459 var imagePullSecrets []api.LocalObjectReference 460 for _, imagePullSecret := range s.Config.Kubernetes.ImagePullSecrets { 461 imagePullSecrets = append(imagePullSecrets, api.LocalObjectReference{Name: imagePullSecret}) 462 } 463 464 if s.credentials != nil { 465 imagePullSecrets = append(imagePullSecrets, api.LocalObjectReference{Name: s.credentials.Name}) 466 } 467 468 buildImage := s.Build.GetAllVariables().ExpandValue(s.options.Image.Name) 469 470 pod, err := s.kubeClient.CoreV1().Pods(s.configurationOverwrites.namespace).Create(&api.Pod{ 471 ObjectMeta: metav1.ObjectMeta{ 472 GenerateName: s.projectUniqueName(), 473 Namespace: s.configurationOverwrites.namespace, 474 Labels: labels, 475 Annotations: annotations, 476 }, 477 Spec: api.PodSpec{ 478 Volumes: s.getVolumes(), 479 ServiceAccountName: s.configurationOverwrites.serviceAccount, 480 RestartPolicy: api.RestartPolicyNever, 481 NodeSelector: s.Config.Kubernetes.NodeSelector, 482 Tolerations: s.Config.Kubernetes.GetNodeTolerations(), 483 Containers: append([]api.Container{ 484 // TODO use the build and helper template here 485 s.buildContainer("build", buildImage, s.options.Image, s.buildRequests, s.buildLimits, s.BuildShell.DockerCommand...), 486 s.buildContainer("helper", s.getHelperImage(), common.Image{}, s.helperRequests, s.helperLimits, s.BuildShell.DockerCommand...), 487 }, services...), 488 TerminationGracePeriodSeconds: &s.Config.Kubernetes.TerminationGracePeriodSeconds, 489 ImagePullSecrets: imagePullSecrets, 490 }, 491 }) 492 493 if err != nil { 494 return err 495 } 496 497 s.pod = pod 498 499 return nil 500 } 501 502 func (s *executor) getHelperImage() string { 503 if len(s.Config.Kubernetes.HelperImage) > 0 { 504 return common.AppVersion.Variables().ExpandValue(s.Config.Kubernetes.HelperImage) 505 } 506 507 return s.helperImageInfo.String() 508 } 509 510 func (s *executor) runInContainer(ctx context.Context, name string, command []string, script string) <-chan error { 511 errc := make(chan error, 1) 512 go func() { 513 defer close(errc) 514 515 status, err := waitForPodRunning(ctx, s.kubeClient, s.pod, s.Trace, s.Config.Kubernetes) 516 517 if err != nil { 518 errc <- err 519 return 520 } 521 522 if status != api.PodRunning { 523 errc <- fmt.Errorf("pod failed to enter running state: %s", status) 524 return 525 } 526 527 config, err := getKubeClientConfig(s.Config.Kubernetes, s.configurationOverwrites) 528 529 if err != nil { 530 errc <- err 531 return 532 } 533 534 exec := ExecOptions{ 535 PodName: s.pod.Name, 536 Namespace: s.pod.Namespace, 537 ContainerName: name, 538 Command: command, 539 In: strings.NewReader(script), 540 Out: s.Trace, 541 Err: s.Trace, 542 Stdin: true, 543 Config: config, 544 Client: s.kubeClient, 545 Executor: &DefaultRemoteExecutor{}, 546 } 547 548 errc <- exec.Run() 549 }() 550 551 return errc 552 } 553 554 func (s *executor) Connect() (terminalsession.Conn, error) { 555 settings, err := s.getTerminalSettings() 556 if err != nil { 557 return nil, err 558 } 559 560 return terminalConn{settings: settings}, nil 561 } 562 563 type terminalConn struct { 564 settings *terminal.TerminalSettings 565 } 566 567 func (t terminalConn) Start(w http.ResponseWriter, r *http.Request, timeoutCh, disconnectCh chan error) { 568 proxy := terminal.NewWebSocketProxy(1) // one stopper: terminal exit handler 569 570 terminalsession.ProxyTerminal( 571 timeoutCh, 572 disconnectCh, 573 proxy.StopCh, 574 func() { 575 terminal.ProxyWebSocket(w, r, t.settings, proxy) 576 }, 577 ) 578 } 579 580 func (t terminalConn) Close() error { 581 return nil 582 } 583 584 func (s *executor) getTerminalSettings() (*terminal.TerminalSettings, error) { 585 config, err := getKubeClientConfig(s.Config.Kubernetes, s.configurationOverwrites) 586 if err != nil { 587 return nil, err 588 } 589 590 wsURL, err := s.getTerminalWebSocketURL(config) 591 if err != nil { 592 return nil, err 593 } 594 595 caCert := "" 596 if len(config.CAFile) > 0 { 597 buf, err := ioutil.ReadFile(config.CAFile) 598 if err != nil { 599 return nil, err 600 } 601 caCert = string(buf) 602 } 603 604 term := &terminal.TerminalSettings{ 605 Subprotocols: []string{"channel.k8s.io"}, 606 Url: wsURL.String(), 607 Header: http.Header{"Authorization": []string{"Bearer " + config.BearerToken}}, 608 CAPem: caCert, 609 MaxSessionTime: 0, 610 } 611 612 return term, nil 613 } 614 615 func (s *executor) getTerminalWebSocketURL(config *restclient.Config) (*url.URL, error) { 616 wsURL := s.kubeClient.CoreV1().RESTClient().Post(). 617 Namespace(s.pod.Namespace). 618 Resource("pods"). 619 Name(s.pod.Name). 620 SubResource("exec"). 621 VersionedParams(&api.PodExecOptions{ 622 Stdin: true, 623 Stdout: true, 624 Stderr: true, 625 TTY: true, 626 Container: "build", 627 Command: []string{"sh", "-c", "bash || sh"}, 628 }, scheme.ParameterCodec).URL() 629 630 if wsURL.Scheme == "https" { 631 wsURL.Scheme = "wss" 632 } else if wsURL.Scheme == "http" { 633 wsURL.Scheme = "ws" 634 } 635 636 return wsURL, nil 637 } 638 639 func (s *executor) prepareOverwrites(variables common.JobVariables) error { 640 values, err := createOverwrites(s.Config.Kubernetes, variables, s.BuildLogger) 641 if err != nil { 642 return err 643 } 644 645 s.configurationOverwrites = values 646 return nil 647 } 648 649 func (s *executor) prepareOptions(job *common.Build) { 650 s.options = &kubernetesOptions{} 651 s.options.Image = job.Image 652 for _, service := range job.Services { 653 if service.Name == "" { 654 continue 655 } 656 s.options.Services = append(s.options.Services, service) 657 } 658 } 659 660 // checkDefaults Defines the configuration for the Pod on Kubernetes 661 func (s *executor) checkDefaults() error { 662 if s.options.Image.Name == "" { 663 if s.Config.Kubernetes.Image == "" { 664 return fmt.Errorf("no image specified and no default set in config") 665 } 666 667 s.options.Image = common.Image{ 668 Name: s.Config.Kubernetes.Image, 669 } 670 } 671 672 if s.configurationOverwrites.namespace == "" { 673 s.Warningln("Namespace is empty, therefore assuming 'default'.") 674 s.configurationOverwrites.namespace = "default" 675 } 676 677 s.Println("Using Kubernetes namespace:", s.configurationOverwrites.namespace) 678 679 return nil 680 } 681 682 func createFn() common.Executor { 683 helperImageInfo, err := helperimage.Get(common.REVISION, helperimage.Config{ 684 OSType: helperimage.OSTypeLinux, 685 Architecture: "amd64", 686 }) 687 if err != nil { 688 logrus.WithError(err).Fatal("Failed to set up helper image for kubernetes executor") 689 } 690 691 return &executor{ 692 AbstractExecutor: executors.AbstractExecutor{ 693 ExecutorOptions: executorOptions, 694 }, 695 helperImageInfo: helperImageInfo, 696 } 697 } 698 699 func featuresFn(features *common.FeaturesInfo) { 700 features.Variables = true 701 features.Image = true 702 features.Services = true 703 features.Artifacts = true 704 features.Cache = true 705 features.Session = true 706 features.Terminal = true 707 } 708 709 func init() { 710 common.RegisterExecutor("kubernetes", executors.DefaultExecutorProvider{ 711 Creator: createFn, 712 FeaturesUpdater: featuresFn, 713 DefaultShellName: executorOptions.Shell.Shell, 714 }) 715 }