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 }