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 }