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