github.com/alloyci/alloy-runner@v1.0.1-0.20180222164613-925503ccafd6/executors/kubernetes/executor_kubernetes.go (about) 1 package kubernetes 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "strings" 7 8 "golang.org/x/net/context" 9 "k8s.io/kubernetes/pkg/api" 10 client "k8s.io/kubernetes/pkg/client/unversioned" 11 "k8s.io/kubernetes/pkg/credentialprovider" 12 13 "gitlab.com/gitlab-org/gitlab-runner/common" 14 "gitlab.com/gitlab-org/gitlab-runner/executors" 15 ) 16 17 var ( 18 executorOptions = executors.ExecutorOptions{ 19 SharedBuildsDir: false, 20 Shell: common.ShellScriptInfo{ 21 Shell: "bash", 22 Type: common.NormalShell, 23 RunnerCommand: "/usr/bin/gitlab-runner-helper", 24 }, 25 ShowHostname: true, 26 } 27 ) 28 29 type kubernetesOptions struct { 30 Image common.Image 31 Services common.Services 32 } 33 34 type executor struct { 35 executors.AbstractExecutor 36 37 kubeClient *client.Client 38 pod *api.Pod 39 credentials *api.Secret 40 options *kubernetesOptions 41 42 configurationOverwrites *overwrites 43 buildLimits api.ResourceList 44 serviceLimits api.ResourceList 45 helperLimits api.ResourceList 46 buildRequests api.ResourceList 47 serviceRequests api.ResourceList 48 helperRequests api.ResourceList 49 pullPolicy common.KubernetesPullPolicy 50 } 51 52 func (s *executor) setupResources() error { 53 var err error 54 55 // Limit 56 if s.buildLimits, err = limits(s.Config.Kubernetes.CPULimit, s.Config.Kubernetes.MemoryLimit); err != nil { 57 return fmt.Errorf("invalid build limits specified: %s", err.Error()) 58 } 59 60 if s.serviceLimits, err = limits(s.Config.Kubernetes.ServiceCPULimit, s.Config.Kubernetes.ServiceMemoryLimit); err != nil { 61 return fmt.Errorf("invalid service limits specified: %s", err.Error()) 62 } 63 64 if s.helperLimits, err = limits(s.Config.Kubernetes.HelperCPULimit, s.Config.Kubernetes.HelperMemoryLimit); err != nil { 65 return fmt.Errorf("invalid helper limits specified: %s", err.Error()) 66 } 67 68 // Requests 69 if s.buildRequests, err = limits(s.Config.Kubernetes.CPURequest, s.Config.Kubernetes.MemoryRequest); err != nil { 70 return fmt.Errorf("invalid build requests specified: %s", err.Error()) 71 } 72 73 if s.serviceRequests, err = limits(s.Config.Kubernetes.ServiceCPURequest, s.Config.Kubernetes.ServiceMemoryRequest); err != nil { 74 return fmt.Errorf("invalid service requests specified: %s", err.Error()) 75 } 76 77 if s.helperRequests, err = limits(s.Config.Kubernetes.HelperCPURequest, s.Config.Kubernetes.HelperMemoryRequest); err != nil { 78 return fmt.Errorf("invalid helper requests specified: %s", err.Error()) 79 } 80 return nil 81 } 82 83 func (s *executor) Prepare(options common.ExecutorPrepareOptions) (err error) { 84 if err = s.AbstractExecutor.Prepare(options); err != nil { 85 return err 86 } 87 88 if s.BuildShell.PassFile { 89 return fmt.Errorf("kubernetes doesn't support shells that require script file") 90 } 91 92 if err = s.setupResources(); err != nil { 93 return err 94 } 95 96 if s.pullPolicy, err = s.Config.Kubernetes.PullPolicy.Get(); err != nil { 97 return err 98 } 99 100 if err = s.prepareOverwrites(options.Build.Variables); err != nil { 101 return err 102 } 103 104 s.prepareOptions(options.Build) 105 106 if err = s.checkDefaults(); err != nil { 107 return err 108 } 109 110 if s.kubeClient, err = getKubeClient(options.Config.Kubernetes, s.configurationOverwrites); err != nil { 111 return fmt.Errorf("error connecting to Kubernetes: %s", err.Error()) 112 } 113 114 s.Println("Using Kubernetes executor with image", s.options.Image.Name, "...") 115 116 return nil 117 } 118 119 func (s *executor) Run(cmd common.ExecutorCommand) error { 120 s.Debugln("Starting Kubernetes command...") 121 122 if s.pod == nil { 123 err := s.setupCredentials() 124 if err != nil { 125 return err 126 } 127 128 err = s.setupBuildPod() 129 if err != nil { 130 return err 131 } 132 } 133 134 containerName := "build" 135 containerCommand := s.BuildShell.DockerCommand 136 if cmd.Predefined { 137 containerName = "helper" 138 containerCommand = common.ContainerCommandBuild 139 } 140 141 ctx, cancel := context.WithCancel(context.Background()) 142 defer cancel() 143 144 select { 145 case err := <-s.runInContainer(ctx, containerName, containerCommand, cmd.Script): 146 if err != nil && strings.Contains(err.Error(), "executing in Docker Container") { 147 return &common.BuildError{Inner: err} 148 } 149 return err 150 151 case <-cmd.Context.Done(): 152 return fmt.Errorf("build aborted") 153 } 154 } 155 156 func (s *executor) Cleanup() { 157 if s.pod != nil { 158 err := s.kubeClient.Pods(s.pod.Namespace).Delete(s.pod.Name, nil) 159 if err != nil { 160 s.Errorln(fmt.Sprintf("Error cleaning up pod: %s", err.Error())) 161 } 162 } 163 if s.credentials != nil { 164 err := s.kubeClient.Secrets(s.configurationOverwrites.namespace).Delete(s.credentials.Name) 165 if err != nil { 166 s.Errorln(fmt.Sprintf("Error cleaning up secrets: %s", err.Error())) 167 } 168 } 169 closeKubeClient(s.kubeClient) 170 s.AbstractExecutor.Cleanup() 171 } 172 173 func (s *executor) buildContainer(name, image string, imageDefinition common.Image, requests, limits api.ResourceList, command ...string) api.Container { 174 privileged := false 175 if s.Config.Kubernetes != nil { 176 privileged = s.Config.Kubernetes.Privileged 177 } 178 179 if len(command) == 0 && len(imageDefinition.Command) > 0 { 180 command = imageDefinition.Command 181 } 182 183 var args []string 184 if len(imageDefinition.Entrypoint) > 0 { 185 args = command 186 command = imageDefinition.Entrypoint 187 } 188 189 return api.Container{ 190 Name: name, 191 Image: image, 192 ImagePullPolicy: api.PullPolicy(s.pullPolicy), 193 Command: command, 194 Args: args, 195 Env: buildVariables(s.Build.GetAllVariables().PublicOrInternal()), 196 Resources: api.ResourceRequirements{ 197 Limits: limits, 198 Requests: requests, 199 }, 200 VolumeMounts: s.getVolumeMounts(), 201 SecurityContext: &api.SecurityContext{ 202 Privileged: &privileged, 203 }, 204 Stdin: true, 205 } 206 } 207 208 func (s *executor) getVolumeMounts() (mounts []api.VolumeMount) { 209 path := strings.Split(s.Build.BuildDir, "/") 210 path = path[:len(path)-1] 211 212 mounts = append(mounts, api.VolumeMount{ 213 Name: "repo", 214 MountPath: strings.Join(path, "/"), 215 }) 216 217 for _, mount := range s.Config.Kubernetes.Volumes.HostPaths { 218 mounts = append(mounts, api.VolumeMount{ 219 Name: mount.Name, 220 MountPath: mount.MountPath, 221 ReadOnly: mount.ReadOnly, 222 }) 223 } 224 225 for _, mount := range s.Config.Kubernetes.Volumes.Secrets { 226 mounts = append(mounts, api.VolumeMount{ 227 Name: mount.Name, 228 MountPath: mount.MountPath, 229 ReadOnly: mount.ReadOnly, 230 }) 231 } 232 233 for _, mount := range s.Config.Kubernetes.Volumes.PVCs { 234 mounts = append(mounts, api.VolumeMount{ 235 Name: mount.Name, 236 MountPath: mount.MountPath, 237 ReadOnly: mount.ReadOnly, 238 }) 239 } 240 241 for _, mount := range s.Config.Kubernetes.Volumes.ConfigMaps { 242 mounts = append(mounts, api.VolumeMount{ 243 Name: mount.Name, 244 MountPath: mount.MountPath, 245 ReadOnly: mount.ReadOnly, 246 }) 247 } 248 249 for _, mount := range s.Config.Kubernetes.Volumes.EmptyDirs { 250 mounts = append(mounts, api.VolumeMount{ 251 Name: mount.Name, 252 MountPath: mount.MountPath, 253 }) 254 } 255 256 return 257 } 258 259 func (s *executor) getVolumes() (volumes []api.Volume) { 260 volumes = append(volumes, api.Volume{ 261 Name: "repo", 262 VolumeSource: api.VolumeSource{ 263 EmptyDir: &api.EmptyDirVolumeSource{}, 264 }, 265 }) 266 267 for _, volume := range s.Config.Kubernetes.Volumes.HostPaths { 268 path := volume.HostPath 269 // Make backward compatible with syntax introduced in version 9.3.0 270 if path == "" { 271 path = volume.MountPath 272 } 273 274 volumes = append(volumes, api.Volume{ 275 Name: volume.Name, 276 VolumeSource: api.VolumeSource{ 277 HostPath: &api.HostPathVolumeSource{ 278 Path: path, 279 }, 280 }, 281 }) 282 } 283 284 for _, volume := range s.Config.Kubernetes.Volumes.Secrets { 285 items := []api.KeyToPath{} 286 for key, path := range volume.Items { 287 items = append(items, api.KeyToPath{Key: key, Path: path}) 288 } 289 290 volumes = append(volumes, api.Volume{ 291 Name: volume.Name, 292 VolumeSource: api.VolumeSource{ 293 Secret: &api.SecretVolumeSource{ 294 SecretName: volume.Name, 295 Items: items, 296 }, 297 }, 298 }) 299 } 300 301 for _, volume := range s.Config.Kubernetes.Volumes.PVCs { 302 volumes = append(volumes, api.Volume{ 303 Name: volume.Name, 304 VolumeSource: api.VolumeSource{ 305 PersistentVolumeClaim: &api.PersistentVolumeClaimVolumeSource{ 306 ClaimName: volume.Name, 307 ReadOnly: volume.ReadOnly, 308 }, 309 }, 310 }) 311 } 312 313 for _, volume := range s.Config.Kubernetes.Volumes.ConfigMaps { 314 items := []api.KeyToPath{} 315 for key, path := range volume.Items { 316 items = append(items, api.KeyToPath{Key: key, Path: path}) 317 } 318 319 volumes = append(volumes, api.Volume{ 320 Name: volume.Name, 321 VolumeSource: api.VolumeSource{ 322 ConfigMap: &api.ConfigMapVolumeSource{ 323 LocalObjectReference: api.LocalObjectReference{ 324 Name: volume.Name, 325 }, 326 Items: items, 327 }, 328 }, 329 }) 330 } 331 332 for _, volume := range s.Config.Kubernetes.Volumes.EmptyDirs { 333 volumes = append(volumes, api.Volume{ 334 Name: volume.Name, 335 VolumeSource: api.VolumeSource{ 336 EmptyDir: &api.EmptyDirVolumeSource{ 337 Medium: api.StorageMedium(volume.Medium), 338 }, 339 }, 340 }) 341 } 342 343 return 344 } 345 346 func (s *executor) setupCredentials() error { 347 authConfigs := make(map[string]credentialprovider.DockerConfigEntry) 348 349 for _, credentials := range s.Build.Credentials { 350 if credentials.Type != "registry" { 351 continue 352 } 353 354 authConfigs[credentials.URL] = credentialprovider.DockerConfigEntry{ 355 Username: credentials.Username, 356 Password: credentials.Password, 357 } 358 } 359 360 if len(authConfigs) == 0 { 361 return nil 362 } 363 364 dockerCfgContent, err := json.Marshal(authConfigs) 365 if err != nil { 366 return err 367 } 368 369 secret := api.Secret{} 370 secret.GenerateName = s.Build.ProjectUniqueName() 371 secret.Namespace = s.configurationOverwrites.namespace 372 secret.Type = api.SecretTypeDockercfg 373 secret.Data = map[string][]byte{} 374 secret.Data[api.DockerConfigKey] = dockerCfgContent 375 376 s.credentials, err = s.kubeClient.Secrets(s.configurationOverwrites.namespace).Create(&secret) 377 if err != nil { 378 return err 379 } 380 return nil 381 } 382 383 func (s *executor) setupBuildPod() error { 384 services := make([]api.Container, len(s.options.Services)) 385 for i, service := range s.options.Services { 386 resolvedImage := s.Build.GetAllVariables().ExpandValue(service.Name) 387 services[i] = s.buildContainer(fmt.Sprintf("svc-%d", i), resolvedImage, service, s.serviceRequests, s.serviceLimits) 388 } 389 390 labels := make(map[string]string) 391 for k, v := range s.Build.Runner.Kubernetes.PodLabels { 392 labels[k] = s.Build.Variables.ExpandValue(v) 393 } 394 395 annotations := make(map[string]string) 396 for key, val := range s.configurationOverwrites.podAnnotations { 397 annotations[key] = s.Build.Variables.ExpandValue(val) 398 } 399 400 var imagePullSecrets []api.LocalObjectReference 401 for _, imagePullSecret := range s.Config.Kubernetes.ImagePullSecrets { 402 imagePullSecrets = append(imagePullSecrets, api.LocalObjectReference{Name: imagePullSecret}) 403 } 404 405 if s.credentials != nil { 406 imagePullSecrets = append(imagePullSecrets, api.LocalObjectReference{Name: s.credentials.Name}) 407 } 408 409 buildImage := s.Build.GetAllVariables().ExpandValue(s.options.Image.Name) 410 pod, err := s.kubeClient.Pods(s.configurationOverwrites.namespace).Create(&api.Pod{ 411 ObjectMeta: api.ObjectMeta{ 412 GenerateName: s.Build.ProjectUniqueName(), 413 Namespace: s.configurationOverwrites.namespace, 414 Labels: labels, 415 Annotations: annotations, 416 }, 417 Spec: api.PodSpec{ 418 Volumes: s.getVolumes(), 419 ServiceAccountName: s.configurationOverwrites.serviceAccount, 420 RestartPolicy: api.RestartPolicyNever, 421 NodeSelector: s.Config.Kubernetes.NodeSelector, 422 Containers: append([]api.Container{ 423 // TODO use the build and helper template here 424 s.buildContainer("build", buildImage, s.options.Image, s.buildRequests, s.buildLimits, s.BuildShell.DockerCommand...), 425 s.buildContainer("helper", s.Config.Kubernetes.GetHelperImage(), common.Image{}, s.helperRequests, s.helperLimits, s.BuildShell.DockerCommand...), 426 }, services...), 427 TerminationGracePeriodSeconds: &s.Config.Kubernetes.TerminationGracePeriodSeconds, 428 ImagePullSecrets: imagePullSecrets, 429 }, 430 }) 431 432 if err != nil { 433 return err 434 } 435 436 s.pod = pod 437 438 return nil 439 } 440 441 func (s *executor) runInContainer(ctx context.Context, name string, command []string, script string) <-chan error { 442 errc := make(chan error, 1) 443 go func() { 444 defer close(errc) 445 446 status, err := waitForPodRunning(ctx, s.kubeClient, s.pod, s.Trace, s.Config.Kubernetes) 447 448 if err != nil { 449 errc <- err 450 return 451 } 452 453 if status != api.PodRunning { 454 errc <- fmt.Errorf("pod failed to enter running state: %s", status) 455 return 456 } 457 458 config, err := getKubeClientConfig(s.Config.Kubernetes, s.configurationOverwrites) 459 460 if err != nil { 461 errc <- err 462 return 463 } 464 465 exec := ExecOptions{ 466 PodName: s.pod.Name, 467 Namespace: s.pod.Namespace, 468 ContainerName: name, 469 Command: command, 470 In: strings.NewReader(script), 471 Out: s.Trace, 472 Err: s.Trace, 473 Stdin: true, 474 Config: config, 475 Client: s.kubeClient, 476 Executor: &DefaultRemoteExecutor{}, 477 } 478 479 errc <- exec.Run() 480 }() 481 482 return errc 483 } 484 485 func (s *executor) prepareOverwrites(variables common.JobVariables) error { 486 values, err := createOverwrites(s.Config.Kubernetes, variables, s.BuildLogger) 487 if err != nil { 488 return err 489 } 490 491 s.configurationOverwrites = values 492 return nil 493 } 494 495 func (s *executor) prepareOptions(job *common.Build) { 496 s.options = &kubernetesOptions{} 497 s.options.Image = job.Image 498 for _, service := range job.Services { 499 if service.Name == "" { 500 continue 501 } 502 s.options.Services = append(s.options.Services, service) 503 } 504 } 505 506 // checkDefaults Defines the configuration for the Pod on Kubernetes 507 func (s *executor) checkDefaults() error { 508 if s.options.Image.Name == "" { 509 if s.Config.Kubernetes.Image == "" { 510 return fmt.Errorf("no image specified and no default set in config") 511 } 512 513 s.options.Image = common.Image{ 514 Name: s.Config.Kubernetes.Image, 515 } 516 } 517 518 if s.configurationOverwrites.namespace == "" { 519 s.Warningln("Namespace is empty, therefore assuming 'default'.") 520 s.configurationOverwrites.namespace = "default" 521 } 522 523 s.Println("Using Kubernetes namespace:", s.configurationOverwrites.namespace) 524 525 return nil 526 } 527 528 func createFn() common.Executor { 529 return &executor{ 530 AbstractExecutor: executors.AbstractExecutor{ 531 ExecutorOptions: executorOptions, 532 }, 533 } 534 } 535 536 func featuresFn(features *common.FeaturesInfo) { 537 features.Variables = true 538 features.Image = true 539 features.Services = true 540 features.Artifacts = true 541 features.Cache = true 542 } 543 544 func init() { 545 common.RegisterExecutor("kubernetes", executors.DefaultExecutorProvider{ 546 Creator: createFn, 547 FeaturesUpdater: featuresFn, 548 }) 549 }