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  }