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  }