github.com/iter8-tools/iter8@v1.1.2/driver/kubedriver.go (about)

     1  package driver
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"time"
     9  
    10  	// Import to initialize client auth plugins.
    11  
    12  	// auth import enables automated authentication to various hosted clouds
    13  	_ "k8s.io/client-go/plugin/pkg/client/auth"
    14  	"k8s.io/client-go/util/retry"
    15  
    16  	helmerrors "github.com/pkg/errors"
    17  	helmdriver "helm.sh/helm/v3/pkg/storage/driver"
    18  
    19  	"github.com/iter8-tools/iter8/base"
    20  	"github.com/iter8-tools/iter8/base/log"
    21  	"helm.sh/helm/v3/pkg/action"
    22  	"helm.sh/helm/v3/pkg/cli"
    23  	"helm.sh/helm/v3/pkg/release"
    24  	"k8s.io/client-go/kubernetes"
    25  
    26  	corev1 "k8s.io/api/core/v1"
    27  	kerrors "k8s.io/apimachinery/pkg/api/errors"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/util/wait"
    30  )
    31  
    32  const (
    33  	// secretTimeout is max time to wait for secret ops
    34  	secretTimeout = 60 * time.Second
    35  	// retryInterval is the duration between retries
    36  	retryInterval = 1 * time.Second
    37  	// ManifestFile is the name of the Kubernetes manifest file
    38  	ManifestFile = "manifest.yaml"
    39  )
    40  
    41  // KubeDriver embeds Helm and Kube configuration, and
    42  // enables interaction with a Kubernetes cluster through Kube APIs and Helm APIs
    43  type KubeDriver struct {
    44  	// EnvSettings provides generic Kubernetes and Helm options
    45  	*cli.EnvSettings
    46  	// Clientset enables interaction with a Kubernetes cluster
    47  	Clientset kubernetes.Interface
    48  	// Configuration enables Helm-based interaction with a Kubernetes cluster
    49  	*action.Configuration
    50  	// Test is the test name
    51  	Test string
    52  	// revision is the revision of the test
    53  	revision int
    54  }
    55  
    56  // NewKubeDriver creates and returns a new KubeDriver
    57  func NewKubeDriver(s *cli.EnvSettings) *KubeDriver {
    58  	kd := &KubeDriver{
    59  		EnvSettings:   s,
    60  		Test:          DefaultTestName,
    61  		Configuration: nil,
    62  		Clientset:     nil,
    63  	}
    64  	return kd
    65  }
    66  
    67  // InitKube initializes the Kubernetes clientset
    68  func (kd *KubeDriver) InitKube() error {
    69  	if kd.Clientset == nil {
    70  		// get REST config
    71  		restConfig, err := kd.EnvSettings.RESTClientGetter().ToRESTConfig()
    72  		if err != nil {
    73  			e := errors.New("unable to get Kubernetes REST config")
    74  			log.Logger.WithStackTrace(err.Error()).Error(e)
    75  			return e
    76  		}
    77  		// get clientset
    78  		kd.Clientset, err = kubernetes.NewForConfig(restConfig)
    79  		if err != nil {
    80  			e := errors.New("unable to get Kubernetes clientset")
    81  			log.Logger.WithStackTrace(err.Error()).Error(e)
    82  			return e
    83  		}
    84  	}
    85  	return nil
    86  }
    87  
    88  // initHelm initializes the Helm configuration
    89  func (kd *KubeDriver) initHelm() error {
    90  	if kd.Configuration == nil {
    91  		// getting kube config
    92  		kd.Configuration = new(action.Configuration)
    93  		helmDriver := os.Getenv("HELM_DRIVER")
    94  		if err := kd.Configuration.Init(kd.EnvSettings.RESTClientGetter(), kd.EnvSettings.Namespace(), helmDriver, log.Logger.Debugf); err != nil {
    95  			e := errors.New("unable to get Helm client config")
    96  			log.Logger.WithStackTrace(err.Error()).Error(e)
    97  			return e
    98  		}
    99  		log.Logger.Info("inited Helm config")
   100  	}
   101  	return nil
   102  }
   103  
   104  // initRevision initializes the latest revision
   105  func (kd *KubeDriver) initRevision() error {
   106  	// update revision to latest, if none is specified
   107  	if kd.revision <= 0 {
   108  		if rel, err := kd.getLastRelease(); err == nil && rel != nil {
   109  			kd.revision = rel.Version
   110  		} else {
   111  			return err
   112  		}
   113  	}
   114  	return nil
   115  }
   116  
   117  // Init initializes the KubeDriver
   118  func (kd *KubeDriver) Init() error {
   119  	if err := kd.InitKube(); err != nil {
   120  		return err
   121  	}
   122  	if err := kd.initHelm(); err != nil {
   123  		return err
   124  	}
   125  	return kd.initRevision()
   126  }
   127  
   128  // getLastRelease fetches the last release of an Iter8 experiment
   129  func (kd *KubeDriver) getLastRelease() (*release.Release, error) {
   130  	log.Logger.Debugf("fetching latest revision for experiment group %v", kd.Test)
   131  	// getting last revision
   132  	rel, err := kd.Configuration.Releases.Last(kd.Test)
   133  	if err != nil {
   134  		if helmerrors.Is(err, helmdriver.ErrReleaseNotFound) {
   135  			log.Logger.Debugf("experiment release not found")
   136  			return nil, nil
   137  		}
   138  		e := fmt.Errorf("unable to get latest revision for experiment group %v", kd.Test)
   139  		log.Logger.WithStackTrace(err.Error()).Error(e)
   140  		return nil, e
   141  	}
   142  	return rel, nil
   143  }
   144  
   145  // getExperimentSecretName yields the name of the experiment secret
   146  func (kd *KubeDriver) getExperimentSecretName() string {
   147  	return fmt.Sprintf("%v", kd.Test)
   148  }
   149  
   150  // getSecretWithRetry attempts to get a Kubernetes secret with retries
   151  func (kd *KubeDriver) getSecretWithRetry(name string) (sec *corev1.Secret, err error) {
   152  	err1 := retry.OnError(
   153  		wait.Backoff{
   154  			Steps:    int(secretTimeout / retryInterval),
   155  			Cap:      secretTimeout,
   156  			Duration: retryInterval,
   157  			Factor:   1.0,
   158  			Jitter:   0.1,
   159  		},
   160  		func(err2 error) bool { // retry on specific failures
   161  			return kerrors.ReasonForError(err2) == metav1.StatusReasonForbidden
   162  		},
   163  		func() (err3 error) {
   164  			secretsClient := kd.Clientset.CoreV1().Secrets(kd.Namespace())
   165  			sec, err3 = secretsClient.Get(context.Background(), name, metav1.GetOptions{})
   166  			return err3
   167  		},
   168  	)
   169  	if err1 != nil {
   170  		err = fmt.Errorf("unable to get secret %v", name)
   171  		log.Logger.WithStackTrace(err1.Error()).Error(err)
   172  		return nil, err
   173  	}
   174  	return sec, nil
   175  }
   176  
   177  // getExperimentSecret gets the Kubernetes experiment secret
   178  func (kd *KubeDriver) getExperimentSecret() (s *corev1.Secret, err error) {
   179  	return kd.getSecretWithRetry(kd.getExperimentSecretName())
   180  }
   181  
   182  // Read experiment from secret
   183  func (kd *KubeDriver) Read() (*base.Experiment, error) {
   184  	s, err := kd.getExperimentSecret()
   185  	if err != nil {
   186  		log.Logger.WithStackTrace(err.Error()).Error("unable to read experiment")
   187  		return nil, errors.New("unable to read experiment")
   188  	}
   189  
   190  	b, ok := s.Data[base.ExperimentFile]
   191  	if !ok {
   192  		err = fmt.Errorf("unable to extract experiment; spec secret has no %v field", base.ExperimentFile)
   193  		log.Logger.Error(err)
   194  		return nil, err
   195  	}
   196  
   197  	return ExperimentFromBytes(b)
   198  }
   199  
   200  // Write writes a Kubernetes experiment
   201  func (kd *KubeDriver) Write(exp *base.Experiment) error {
   202  	// write to metrics server
   203  	// get URL of metrics server from environment variable
   204  	metricsServerURL, ok := os.LookupEnv(base.MetricsServerURL)
   205  	if !ok {
   206  		errorMessage := "could not look up METRICS_SERVER_URL environment variable"
   207  		log.Logger.Error(errorMessage)
   208  		return fmt.Errorf(errorMessage)
   209  	}
   210  
   211  	err := base.PutExperimentResultToMetricsService(metricsServerURL, exp.Metadata.Namespace, exp.Metadata.Name, exp.Result)
   212  	if err != nil {
   213  		errorMessage := "could not write experiment result to metrics service"
   214  		log.Logger.Error(errorMessage)
   215  		return fmt.Errorf(errorMessage)
   216  	}
   217  
   218  	return nil
   219  }
   220  
   221  // GetRevision gets the experiment revision
   222  func (kd *KubeDriver) GetRevision() int {
   223  	return kd.revision
   224  }