github.com/secure-build/gitlab-runner@v12.5.0+incompatible/executors/kubernetes/util.go (about)

     1  package kubernetes
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	"time"
     9  
    10  	"golang.org/x/net/context"
    11  	api "k8s.io/api/core/v1"
    12  	"k8s.io/apimachinery/pkg/api/resource"
    13  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    14  	"k8s.io/client-go/kubernetes"
    15  	restclient "k8s.io/client-go/rest"
    16  	"k8s.io/client-go/tools/clientcmd"
    17  
    18  	"gitlab.com/gitlab-org/gitlab-runner/common"
    19  )
    20  
    21  type kubeConfigProvider func() (*restclient.Config, error)
    22  
    23  var (
    24  	// inClusterConfig parses kubernets configuration reading in cluster values
    25  	inClusterConfig kubeConfigProvider = restclient.InClusterConfig
    26  	// defaultKubectlConfig parses kubectl configuration ad loads the default cluster
    27  	defaultKubectlConfig kubeConfigProvider = loadDefaultKubectlConfig
    28  )
    29  
    30  func getKubeClientConfig(config *common.KubernetesConfig, overwrites *overwrites) (kubeConfig *restclient.Config, err error) {
    31  	if len(config.Host) > 0 {
    32  		kubeConfig, err = getOutClusterClientConfig(config)
    33  	} else {
    34  		kubeConfig, err = guessClientConfig()
    35  	}
    36  	if err != nil {
    37  		return nil, err
    38  	}
    39  
    40  	//apply overwrites
    41  	if len(overwrites.bearerToken) > 0 {
    42  		kubeConfig.BearerToken = string(overwrites.bearerToken)
    43  	}
    44  
    45  	kubeConfig.UserAgent = common.AppVersion.UserAgent()
    46  
    47  	return kubeConfig, nil
    48  }
    49  
    50  func getOutClusterClientConfig(config *common.KubernetesConfig) (*restclient.Config, error) {
    51  	kubeConfig := &restclient.Config{
    52  		Host:        config.Host,
    53  		BearerToken: config.BearerToken,
    54  		TLSClientConfig: restclient.TLSClientConfig{
    55  			CAFile: config.CAFile,
    56  		},
    57  	}
    58  
    59  	// certificate based auth
    60  	if len(config.CertFile) > 0 {
    61  		if len(config.KeyFile) == 0 || len(config.CAFile) == 0 {
    62  			return nil, fmt.Errorf("ca file, cert file and key file must be specified when using file based auth")
    63  		}
    64  
    65  		kubeConfig.TLSClientConfig.CertFile = config.CertFile
    66  		kubeConfig.TLSClientConfig.KeyFile = config.KeyFile
    67  	}
    68  
    69  	return kubeConfig, nil
    70  }
    71  
    72  func guessClientConfig() (*restclient.Config, error) {
    73  	// Try in cluster config first
    74  	if inClusterCfg, err := inClusterConfig(); err == nil {
    75  		return inClusterCfg, nil
    76  	}
    77  
    78  	// in cluster config failed. Reading default kubectl config
    79  	return defaultKubectlConfig()
    80  }
    81  
    82  func loadDefaultKubectlConfig() (*restclient.Config, error) {
    83  	config, err := clientcmd.NewDefaultClientConfigLoadingRules().Load()
    84  	if err != nil {
    85  		return nil, err
    86  	}
    87  
    88  	return clientcmd.NewDefaultClientConfig(*config, &clientcmd.ConfigOverrides{}).ClientConfig()
    89  }
    90  
    91  func getKubeClient(config *common.KubernetesConfig, overwrites *overwrites) (*kubernetes.Clientset, error) {
    92  	restConfig, err := getKubeClientConfig(config, overwrites)
    93  	if err != nil {
    94  		return nil, err
    95  	}
    96  
    97  	return kubernetes.NewForConfig(restConfig)
    98  }
    99  
   100  func closeKubeClient(client *kubernetes.Clientset) bool {
   101  	if client == nil {
   102  		return false
   103  	}
   104  	rest, ok := client.CoreV1().RESTClient().(*restclient.RESTClient)
   105  	if !ok || rest.Client == nil || rest.Client.Transport == nil {
   106  		return false
   107  	}
   108  	if transport, ok := rest.Client.Transport.(*http.Transport); ok {
   109  		transport.CloseIdleConnections()
   110  		return true
   111  	}
   112  	return false
   113  }
   114  
   115  func isRunning(pod *api.Pod) (bool, error) {
   116  	switch pod.Status.Phase {
   117  	case api.PodRunning:
   118  		return true, nil
   119  	case api.PodSucceeded:
   120  		return false, fmt.Errorf("pod already succeeded before it begins running")
   121  	case api.PodFailed:
   122  		return false, fmt.Errorf("pod status is failed")
   123  	default:
   124  		return false, nil
   125  	}
   126  }
   127  
   128  type podPhaseResponse struct {
   129  	done  bool
   130  	phase api.PodPhase
   131  	err   error
   132  }
   133  
   134  func getPodPhase(c *kubernetes.Clientset, pod *api.Pod, out io.Writer) podPhaseResponse {
   135  	pod, err := c.CoreV1().Pods(pod.Namespace).Get(pod.Name, metav1.GetOptions{})
   136  	if err != nil {
   137  		return podPhaseResponse{true, api.PodUnknown, err}
   138  	}
   139  
   140  	ready, err := isRunning(pod)
   141  
   142  	if err != nil {
   143  		return podPhaseResponse{true, pod.Status.Phase, err}
   144  	}
   145  
   146  	if ready {
   147  		return podPhaseResponse{true, pod.Status.Phase, nil}
   148  	}
   149  
   150  	// check status of containers
   151  	for _, container := range pod.Status.ContainerStatuses {
   152  		if container.Ready {
   153  			continue
   154  		}
   155  		if container.State.Waiting == nil {
   156  			continue
   157  		}
   158  
   159  		switch container.State.Waiting.Reason {
   160  		case "ErrImagePull", "ImagePullBackOff":
   161  			err = fmt.Errorf("image pull failed: %s", container.State.Waiting.Message)
   162  			err = &common.BuildError{Inner: err}
   163  			return podPhaseResponse{true, api.PodUnknown, err}
   164  		}
   165  	}
   166  
   167  	fmt.Fprintf(out, "Waiting for pod %s/%s to be running, status is %s\n", pod.Namespace, pod.Name, pod.Status.Phase)
   168  	return podPhaseResponse{false, pod.Status.Phase, nil}
   169  
   170  }
   171  
   172  func triggerPodPhaseCheck(c *kubernetes.Clientset, pod *api.Pod, out io.Writer) <-chan podPhaseResponse {
   173  	errc := make(chan podPhaseResponse)
   174  	go func() {
   175  		defer close(errc)
   176  		errc <- getPodPhase(c, pod, out)
   177  	}()
   178  	return errc
   179  }
   180  
   181  // waitForPodRunning will use client c to detect when pod reaches the PodRunning
   182  // state. It returns the final PodPhase once either PodRunning, PodSucceeded or
   183  // PodFailed has been reached. In the case of PodRunning, it will also wait until
   184  // all containers within the pod are also Ready.
   185  // It returns error if the call to retrieve pod details fails or the timeout is
   186  // reached.
   187  // The timeout and polling values are configurable through KubernetesConfig
   188  // parameters.
   189  func waitForPodRunning(ctx context.Context, c *kubernetes.Clientset, pod *api.Pod, out io.Writer, config *common.KubernetesConfig) (api.PodPhase, error) {
   190  	pollInterval := config.GetPollInterval()
   191  	pollAttempts := config.GetPollAttempts()
   192  	for i := 0; i <= pollAttempts; i++ {
   193  		select {
   194  		case r := <-triggerPodPhaseCheck(c, pod, out):
   195  			if !r.done {
   196  				time.Sleep(time.Duration(pollInterval) * time.Second)
   197  				continue
   198  			}
   199  			return r.phase, r.err
   200  		case <-ctx.Done():
   201  			return api.PodUnknown, ctx.Err()
   202  		}
   203  	}
   204  	return api.PodUnknown, errors.New("timed out waiting for pod to start")
   205  }
   206  
   207  // limits takes a string representing CPU & memory limits,
   208  // and returns a ResourceList with appropriately scaled Quantity
   209  // values for Kubernetes. This allows users to write "500m" for CPU,
   210  // and "50Mi" for memory (etc.)
   211  func limits(cpu, memory string) (api.ResourceList, error) {
   212  	var rCPU, rMem resource.Quantity
   213  	var err error
   214  
   215  	parse := func(s string) (resource.Quantity, error) {
   216  		var q resource.Quantity
   217  		if len(s) == 0 {
   218  			return q, nil
   219  		}
   220  		if q, err = resource.ParseQuantity(s); err != nil {
   221  			return q, fmt.Errorf("error parsing resource limit: %s", err.Error())
   222  		}
   223  		return q, nil
   224  	}
   225  
   226  	if rCPU, err = parse(cpu); err != nil {
   227  		return api.ResourceList{}, nil
   228  	}
   229  
   230  	if rMem, err = parse(memory); err != nil {
   231  		return api.ResourceList{}, nil
   232  	}
   233  
   234  	l := make(api.ResourceList)
   235  
   236  	q := resource.Quantity{}
   237  	if rCPU != q {
   238  		l[api.ResourceCPU] = rCPU
   239  	}
   240  	if rMem != q {
   241  		l[api.ResourceMemory] = rMem
   242  	}
   243  
   244  	return l, nil
   245  }
   246  
   247  // buildVariables converts a common.BuildVariables into a list of
   248  // kubernetes EnvVar objects
   249  func buildVariables(bv common.JobVariables) []api.EnvVar {
   250  	e := make([]api.EnvVar, len(bv))
   251  	for i, b := range bv {
   252  		e[i] = api.EnvVar{
   253  			Name:  b.Key,
   254  			Value: b.Value,
   255  		}
   256  	}
   257  	return e
   258  }