github.com/jdolitsky/cnab-go@v0.7.1-beta1/driver/kubernetes/kubernetes.go (about) 1 package kubernetes 2 3 import ( 4 "fmt" 5 "io" 6 "os" 7 "path/filepath" 8 "strings" 9 "time" 10 11 // load credential helpers 12 _ "k8s.io/client-go/plugin/pkg/client/auth" 13 // Convert transitive deps to direct deps so that we can use constraints in our Gopkg.toml 14 _ "github.com/Azure/go-autorest/autorest" 15 16 batchv1 "k8s.io/api/batch/v1" 17 v1 "k8s.io/api/core/v1" 18 "k8s.io/apimachinery/pkg/api/resource" 19 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 20 "k8s.io/apimachinery/pkg/labels" 21 batchclientv1 "k8s.io/client-go/kubernetes/typed/batch/v1" 22 coreclientv1 "k8s.io/client-go/kubernetes/typed/core/v1" 23 "k8s.io/client-go/rest" 24 "k8s.io/client-go/tools/clientcmd" 25 26 "github.com/deislabs/cnab-go/bundle" 27 "github.com/deislabs/cnab-go/driver" 28 ) 29 30 const ( 31 k8sContainerName = "invocation" 32 k8sFileSecretVolume = "files" 33 numBackoffLoops = 6 34 ) 35 36 // Driver runs an invocation image in a Kubernetes cluster. 37 type Driver struct { 38 Namespace string 39 ServiceAccountName string 40 Annotations map[string]string 41 LimitCPU resource.Quantity 42 LimitMemory resource.Quantity 43 Tolerations []v1.Toleration 44 ActiveDeadlineSeconds int64 45 BackoffLimit int32 46 SkipCleanup bool 47 skipJobStatusCheck bool 48 jobs batchclientv1.JobInterface 49 secrets coreclientv1.SecretInterface 50 pods coreclientv1.PodInterface 51 deletionPolicy metav1.DeletionPropagation 52 requiredCompletions int32 53 } 54 55 // New initializes a Kubernetes driver. 56 func New(namespace, serviceAccount string, conf *rest.Config) (*Driver, error) { 57 driver := &Driver{ 58 Namespace: namespace, 59 ServiceAccountName: serviceAccount, 60 } 61 driver.setDefaults() 62 err := driver.setClient(conf) 63 return driver, err 64 } 65 66 // Handles receives an ImageType* and answers whether this driver supports that type. 67 func (k *Driver) Handles(imagetype string) bool { 68 return imagetype == driver.ImageTypeDocker || imagetype == driver.ImageTypeOCI 69 } 70 71 // Config returns the Kubernetes driver configuration options. 72 func (k *Driver) Config() map[string]string { 73 return map[string]string{ 74 "KUBE_NAMESPACE": "Kubernetes namespace in which to run the invocation image", 75 "SERVICE_ACCOUNT": "Kubernetes service account to be mounted by the invocation image (if empty, no service account token will be mounted)", 76 "KUBECONFIG": "Absolute path to the kubeconfig file", 77 "MASTER_URL": "Kubernetes master endpoint", 78 } 79 } 80 81 // SetConfig sets Kubernetes driver configuration. 82 func (k *Driver) SetConfig(settings map[string]string) { 83 k.setDefaults() 84 k.Namespace = settings["KUBE_NAMESPACE"] 85 k.ServiceAccountName = settings["SERVICE_ACCOUNT"] 86 87 var kubeconfig string 88 if kpath := settings["KUBECONFIG"]; kpath != "" { 89 kubeconfig = kpath 90 } else if home := homeDir(); home != "" { 91 kubeconfig = filepath.Join(home, ".kube", "config") 92 } 93 94 conf, err := clientcmd.BuildConfigFromFlags(settings["MASTER_URL"], kubeconfig) 95 if err != nil { 96 panic(err) 97 } 98 err = k.setClient(conf) 99 if err != nil { 100 panic(err) 101 } 102 } 103 104 func (k *Driver) setDefaults() { 105 k.SkipCleanup = false 106 k.BackoffLimit = 0 107 k.ActiveDeadlineSeconds = 300 108 k.requiredCompletions = 1 109 k.deletionPolicy = metav1.DeletePropagationBackground 110 } 111 112 func (k *Driver) setClient(conf *rest.Config) error { 113 coreClient, err := coreclientv1.NewForConfig(conf) 114 if err != nil { 115 return err 116 } 117 batchClient, err := batchclientv1.NewForConfig(conf) 118 if err != nil { 119 return err 120 } 121 k.jobs = batchClient.Jobs(k.Namespace) 122 k.secrets = coreClient.Secrets(k.Namespace) 123 k.pods = coreClient.Pods(k.Namespace) 124 125 return nil 126 } 127 128 // Run executes the operation inside of the invocation image. 129 func (k *Driver) Run(op *driver.Operation) (driver.OperationResult, error) { 130 if k.Namespace == "" { 131 return driver.OperationResult{}, fmt.Errorf("KUBE_NAMESPACE is required") 132 } 133 labelMap := generateLabels(op) 134 meta := metav1.ObjectMeta{ 135 Namespace: k.Namespace, 136 GenerateName: generateNameTemplate(op), 137 Labels: labelMap, 138 } 139 // Mount SA token if a non-zero value for ServiceAccountName has been specified 140 mountServiceAccountToken := k.ServiceAccountName != "" 141 job := &batchv1.Job{ 142 ObjectMeta: meta, 143 Spec: batchv1.JobSpec{ 144 ActiveDeadlineSeconds: &k.ActiveDeadlineSeconds, 145 Completions: &k.requiredCompletions, 146 BackoffLimit: &k.BackoffLimit, 147 Template: v1.PodTemplateSpec{ 148 ObjectMeta: metav1.ObjectMeta{ 149 Labels: labelMap, 150 Annotations: k.Annotations, 151 }, 152 Spec: v1.PodSpec{ 153 ServiceAccountName: k.ServiceAccountName, 154 AutomountServiceAccountToken: &mountServiceAccountToken, 155 RestartPolicy: v1.RestartPolicyNever, 156 Tolerations: k.Tolerations, 157 }, 158 }, 159 }, 160 } 161 container := v1.Container{ 162 Name: k8sContainerName, 163 Image: imageWithDigest(op.Image), 164 Command: []string{"/cnab/app/run"}, 165 Resources: v1.ResourceRequirements{ 166 Limits: v1.ResourceList{ 167 v1.ResourceCPU: k.LimitCPU, 168 v1.ResourceMemory: k.LimitMemory, 169 }, 170 }, 171 ImagePullPolicy: v1.PullIfNotPresent, 172 } 173 174 if len(op.Environment) > 0 { 175 secret := &v1.Secret{ 176 ObjectMeta: meta, 177 StringData: op.Environment, 178 } 179 secret.ObjectMeta.GenerateName += "env-" 180 envsecret, err := k.secrets.Create(secret) 181 if err != nil { 182 return driver.OperationResult{}, err 183 } 184 if !k.SkipCleanup { 185 defer k.deleteSecret(envsecret.ObjectMeta.Name) 186 } 187 188 container.EnvFrom = []v1.EnvFromSource{ 189 { 190 SecretRef: &v1.SecretEnvSource{ 191 LocalObjectReference: v1.LocalObjectReference{ 192 Name: envsecret.ObjectMeta.Name, 193 }, 194 }, 195 }, 196 } 197 } 198 199 if len(op.Files) > 0 { 200 secret, mounts := generateFileSecret(op.Files) 201 secret.ObjectMeta = metav1.ObjectMeta{ 202 Namespace: k.Namespace, 203 GenerateName: generateNameTemplate(op) + "files-", 204 Labels: labelMap, 205 } 206 secret, err := k.secrets.Create(secret) 207 if err != nil { 208 return driver.OperationResult{}, err 209 } 210 if !k.SkipCleanup { 211 defer k.deleteSecret(secret.ObjectMeta.Name) 212 } 213 214 job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, v1.Volume{ 215 Name: k8sFileSecretVolume, 216 VolumeSource: v1.VolumeSource{ 217 Secret: &v1.SecretVolumeSource{ 218 SecretName: secret.ObjectMeta.Name, 219 }, 220 }, 221 }) 222 container.VolumeMounts = mounts 223 } 224 225 job.Spec.Template.Spec.Containers = []v1.Container{container} 226 job, err := k.jobs.Create(job) 227 if err != nil { 228 return driver.OperationResult{}, err 229 } 230 if !k.SkipCleanup { 231 defer k.deleteJob(job.ObjectMeta.Name) 232 } 233 234 // Return early for unit testing purposes (the fake k8s client implementation just 235 // hangs during watch because no events are ever created on the Job) 236 if k.skipJobStatusCheck { 237 return driver.OperationResult{}, nil 238 } 239 240 // Create a selector to detect the job just created 241 jobSelector := metav1.ListOptions{ 242 LabelSelector: labels.Set(job.ObjectMeta.Labels).String(), 243 FieldSelector: newSingleFieldSelector("metadata.name", job.ObjectMeta.Name), 244 } 245 246 // Prevent detecting pods from prior jobs by adding the job name to the labels 247 podSelector := metav1.ListOptions{ 248 LabelSelector: newSingleFieldSelector("job-name", job.ObjectMeta.Name), 249 } 250 251 return driver.OperationResult{}, k.watchJobStatusAndLogs(podSelector, jobSelector, op.Out) 252 } 253 254 func (k *Driver) watchJobStatusAndLogs(podSelector metav1.ListOptions, jobSelector metav1.ListOptions, out io.Writer) error { 255 // Stream Pod logs in the background 256 logsStreamingComplete := make(chan bool) 257 err := k.streamPodLogs(podSelector, out, logsStreamingComplete) 258 if err != nil { 259 return err 260 } 261 // Watch job events and exit on failure/success 262 watch, err := k.jobs.Watch(jobSelector) 263 if err != nil { 264 return err 265 } 266 for event := range watch.ResultChan() { 267 job, ok := event.Object.(*batchv1.Job) 268 if !ok { 269 return fmt.Errorf("unexpected type") 270 } 271 complete := false 272 for _, cond := range job.Status.Conditions { 273 if cond.Type == batchv1.JobFailed { 274 err = fmt.Errorf(cond.Message) 275 complete = true 276 break 277 } 278 if cond.Type == batchv1.JobComplete { 279 complete = true 280 break 281 } 282 } 283 if complete { 284 break 285 } 286 } 287 if err != nil { 288 return err 289 } 290 291 // Wait for pod logs to finish printing 292 for i := 0; i < int(k.requiredCompletions); i++ { 293 <-logsStreamingComplete 294 } 295 296 return nil 297 } 298 299 func (k *Driver) streamPodLogs(options metav1.ListOptions, out io.Writer, done chan bool) error { 300 watcher, err := k.pods.Watch(options) 301 if err != nil { 302 return err 303 } 304 305 go func() { 306 // Track pods whose logs have been streamed by pod name. We need to know when we've already 307 // processed logs for a given pod, since multiple lifecycle events are received per pod. 308 streamedLogs := map[string]bool{} 309 for event := range watcher.ResultChan() { 310 pod, ok := event.Object.(*v1.Pod) 311 if !ok { 312 continue 313 } 314 podName := pod.GetName() 315 if streamedLogs[podName] { 316 // The event was for a pod whose logs have already been streamed, so do nothing. 317 continue 318 } 319 320 for i := 0; i < numBackoffLoops; i++ { 321 time.Sleep(time.Duration(i*i/2) * time.Second) 322 req := k.pods.GetLogs(podName, &v1.PodLogOptions{ 323 Container: k8sContainerName, 324 Follow: true, 325 }) 326 reader, err := req.Stream() 327 if err != nil { 328 // There was an error connecting to the pod, so continue the loop and attempt streaming 329 // the logs again. 330 continue 331 } 332 333 // Block the loop until all logs from the pod have been processed. 334 bytesRead, err := io.Copy(out, reader) 335 reader.Close() 336 if err != nil { 337 continue 338 } 339 if bytesRead == 0 { 340 // There is a chance where we have connected to the pod, but it has yet to write something. 341 // In that case, we continue to to keep streaming until it does. 342 continue 343 } 344 // Set the pod to have successfully streamed data. 345 streamedLogs[podName] = true 346 break 347 } 348 349 done <- true 350 } 351 }() 352 353 return nil 354 } 355 356 func (k *Driver) deleteSecret(name string) error { 357 return k.secrets.Delete(name, &metav1.DeleteOptions{ 358 PropagationPolicy: &k.deletionPolicy, 359 }) 360 } 361 362 func (k *Driver) deleteJob(name string) error { 363 return k.jobs.Delete(name, &metav1.DeleteOptions{ 364 PropagationPolicy: &k.deletionPolicy, 365 }) 366 } 367 368 func generateNameTemplate(op *driver.Operation) string { 369 return fmt.Sprintf("%s-%s-", op.Installation, op.Action) 370 } 371 372 func generateLabels(op *driver.Operation) map[string]string { 373 return map[string]string{ 374 "cnab.io/installation": op.Installation, 375 "cnab.io/action": op.Action, 376 "cnab.io/revision": op.Revision, 377 } 378 } 379 380 func generateFileSecret(files map[string]string) (*v1.Secret, []v1.VolumeMount) { 381 size := len(files) 382 data := make(map[string]string, size) 383 mounts := make([]v1.VolumeMount, size) 384 385 i := 0 386 for path, contents := range files { 387 key := strings.Replace(filepath.ToSlash(path), "/", "_", -1) 388 data[key] = contents 389 mounts[i] = v1.VolumeMount{ 390 Name: k8sFileSecretVolume, 391 MountPath: path, 392 SubPath: key, 393 } 394 i++ 395 } 396 397 secret := &v1.Secret{ 398 StringData: data, 399 } 400 401 return secret, mounts 402 } 403 404 func newSingleFieldSelector(k, v string) string { 405 return labels.Set(map[string]string{ 406 k: v, 407 }).String() 408 } 409 410 func homeDir() string { 411 if h := os.Getenv("HOME"); h != "" { 412 return h 413 } 414 return os.Getenv("USERPROFILE") // windows 415 } 416 417 func imageWithDigest(img bundle.InvocationImage) string { 418 if img.Digest == "" { 419 return img.Image 420 } 421 return img.Image + "@" + img.Digest 422 }