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 }