github.com/verrazzano/verrazzano@v1.7.1/tests/e2e/backup/helpers/common_helper.go (about) 1 // Copyright (c) 2022, 2023, Oracle and/or its affiliates. 2 // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. 3 4 package helpers 5 6 import ( 7 "bytes" 8 "context" 9 "encoding/json" 10 "fmt" 11 "io" 12 "net/http" 13 "os" 14 "os/exec" 15 "strings" 16 "text/template" 17 "time" 18 19 "github.com/Jeffail/gabs/v2" 20 "github.com/hashicorp/go-retryablehttp" 21 "github.com/verrazzano/verrazzano/pkg/httputil" 22 "github.com/verrazzano/verrazzano/pkg/k8sutil" 23 "github.com/verrazzano/verrazzano/platform-operator/constants" 24 "github.com/verrazzano/verrazzano/tests/e2e/pkg" 25 "go.uber.org/zap" 26 corev1 "k8s.io/api/core/v1" 27 k8serror "k8s.io/apimachinery/pkg/api/errors" 28 "k8s.io/apimachinery/pkg/api/meta" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 31 k8sYaml "k8s.io/apimachinery/pkg/runtime/serializer/yaml" 32 "k8s.io/apimachinery/pkg/types" 33 "k8s.io/client-go/discovery" 34 "k8s.io/client-go/discovery/cached/memory" 35 "k8s.io/client-go/dynamic" 36 "k8s.io/client-go/restmapper" 37 ) 38 39 var decUnstructured = k8sYaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) 40 41 // GatherInfo invoked at the beginning to set up all the values taken as input 42 // The gingko runs will fail if any of these values are not set or set incorrectly 43 // The values are originally set from the jenkins pipeline 44 func GatherInfo() { 45 VeleroNameSpace = os.Getenv("VELERO_NAMESPACE") 46 VeleroOpenSearchSecretName = os.Getenv("VELERO_SECRET_NAME") 47 VeleroMySQLSecretName = os.Getenv("VELERO_MYSQL_SECRET_NAME") 48 RancherSecretName = os.Getenv("RANCHER_SECRET_NAME") 49 OciBucketID = os.Getenv("OCI_OS_BUCKET_ID") 50 OciBucketName = os.Getenv("OCI_OS_BUCKET_NAME") 51 OciOsAccessKey = os.Getenv("OCI_OS_ACCESS_KEY") 52 OciOsAccessSecretKey = os.Getenv("OCI_OS_ACCESS_SECRET_KEY") 53 OciCompartmentID = os.Getenv("OCI_OS_COMPARTMENT_ID") 54 OciNamespaceName = os.Getenv("OCI_OS_NAMESPACE") 55 BackupResourceName = os.Getenv("BACKUP_RESOURCE") 56 BackupOpensearchName = os.Getenv("BACKUP_OPENSEARCH") 57 BackupRancherName = os.Getenv("BACKUP_RANCHER") 58 BackupMySQLName = os.Getenv("BACKUP_MYSQL") 59 RestoreOpensearchName = os.Getenv("RESTORE_OPENSEARCH") 60 RestoreRancherName = os.Getenv("RESTORE_RANCHER") 61 RestoreMySQLName = os.Getenv("RESTORE_MYSQL") 62 BackupOpensearchStorageName = os.Getenv("BACKUP_OPENSEARCH_STORAGE") 63 BackupMySQLStorageName = os.Getenv("BACKUP_MYSQL_STORAGE") 64 BackupRegion = os.Getenv("BACKUP_REGION") 65 OciCliTenancy = os.Getenv("OCI_CLI_TENANCY") 66 OciCliUser = os.Getenv("OCI_CLI_USER") 67 OciCliFingerprint = os.Getenv("OCI_CLI_FINGERPRINT") 68 OciCliKeyFile = os.Getenv("OCI_CLI_KEY_FILE") 69 MySQLBackupMode = os.Getenv("MYSQL_BACKUP_MODE") 70 } 71 72 // GetRancherURL fetches the elastic search URL from the cluster 73 func GetRancherURL(log *zap.SugaredLogger) (string, error) { 74 kubeconfigPath, err := k8sutil.GetKubeConfigLocation() 75 if err != nil { 76 log.Errorf("Failed to get kubeconfigPath with error: %v", err) 77 return "", err 78 } 79 api, err := pkg.GetAPIEndpoint(kubeconfigPath) 80 if err != nil { 81 log.Errorf("Unable to fetch api endpoint due to %v", zap.Error(err)) 82 return "", err 83 } 84 ingress, err := api.GetIngress("cattle-system", "rancher") 85 if err != nil { 86 return "", err 87 } 88 return fmt.Sprintf("https://%s", ingress.Spec.Rules[0].Host), nil 89 } 90 91 // GetRancherLoginToken fetches the login token for rancher console 92 func GetRancherLoginToken(log *zap.SugaredLogger) string { 93 94 kubeconfigPath, err := k8sutil.GetKubeConfigLocation() 95 if err != nil { 96 log.Errorf("Unable to fetch kubeconfig url due to %v", zap.Error(err)) 97 return "" 98 } 99 100 httpClient, err := pkg.GetVerrazzanoHTTPClient(kubeconfigPath) 101 if err != nil { 102 log.Errorf("Unable to fetch httpClient due to %v", zap.Error(err)) 103 return "" 104 } 105 106 rancherURL, err := GetRancherURL(log) 107 if err != nil { 108 return "" 109 } 110 111 return pkg.GetRancherAdminToken(log, httpClient, rancherURL) 112 } 113 114 // GetEsURL fetches the elastic search URL from the cluster 115 func GetEsURL(log *zap.SugaredLogger) (string, error) { 116 kubeconfigPath, err := k8sutil.GetKubeConfigLocation() 117 if err != nil { 118 log.Errorf("Failed to get kubeconfigPath with error: %v", err) 119 return "", err 120 } 121 api := pkg.EventuallyGetAPIEndpoint(kubeconfigPath) 122 ingress, err := api.GetIngress(constants.VerrazzanoSystemNamespace, "opensearch") 123 if err != nil { 124 return "", err 125 } 126 return fmt.Sprintf("https://%s", ingress.Spec.Rules[0].Host), nil 127 } 128 129 // GetVZPasswd fetches the verrazzano password from the cluster 130 func GetVZPasswd(log *zap.SugaredLogger) (string, error) { 131 clientset, err := k8sutil.GetKubernetesClientset() 132 if err != nil { 133 log.Errorf("Failed to get clientset with error: %v", err) 134 return "", err 135 } 136 137 secret, err := clientset.CoreV1().Secrets(constants.VerrazzanoSystemNamespace).Get(context.TODO(), "verrazzano", metav1.GetOptions{}) 138 if err != nil { 139 log.Infof("Error creating secret ", zap.Error(err)) 140 return "", err 141 } 142 return string(secret.Data["password"]), nil 143 } 144 145 // DynamicSSA uses dynamic client to apply data without registered golang structs 146 // This is used to apply configurations related to velero and rancher as they are crds 147 func DynamicSSA(ctx context.Context, deploymentYAML string, log *zap.SugaredLogger) error { 148 149 kubeconfig, err := k8sutil.GetKubeConfig() 150 if err != nil { 151 log.Errorf("Error getting kubeconfig, error: %v", err) 152 return err 153 } 154 155 // Prepare a RESTMapper to find GVR followed by creating the dynamic client 156 dc, err := discovery.NewDiscoveryClientForConfig(kubeconfig) 157 if err != nil { 158 return err 159 } 160 mapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(dc)) 161 162 dynamicClient, err := dynamic.NewForConfig(kubeconfig) 163 if err != nil { 164 return err 165 } 166 167 // Convert to unstructured since this will be used for CRDS 168 obj := &unstructured.Unstructured{} 169 _, gvk, err := decUnstructured.Decode([]byte(deploymentYAML), nil, obj) 170 if err != nil { 171 return err 172 } 173 mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version) 174 if err != nil { 175 return err 176 } 177 178 // Create a dynamic REST interface 179 var dynamicRest dynamic.ResourceInterface 180 if mapping.Scope.Name() == meta.RESTScopeNameNamespace { 181 // namespaced resources should specify the namespace 182 dynamicRest = dynamicClient.Resource(mapping.Resource).Namespace(obj.GetNamespace()) 183 } else { 184 // for cluster-wide resources 185 dynamicRest = dynamicClient.Resource(mapping.Resource) 186 } 187 188 data, err := json.Marshal(obj) 189 if err != nil { 190 return err 191 } 192 193 //Apply the Yaml 194 _, err = dynamicRest.Patch(ctx, obj.GetName(), types.ApplyPatchType, data, metav1.PatchOptions{ 195 FieldManager: "backup-controller", 196 }) 197 return err 198 } 199 200 // CheckPvcsTerminated utility to wait for all pvcs to be terminated 201 func CheckPvcsTerminated(labelSelector, namespace string, log *zap.SugaredLogger) error { 202 clientset, err := k8sutil.GetKubernetesClientset() 203 if err != nil { 204 log.Errorf("Failed to get clientset with error: %v", err) 205 return err 206 } 207 208 log.Infof("Wait for 60 seconds to allow pod termination as it has already been triggered") 209 time.Sleep(60 * time.Second) 210 211 listOptions := metav1.ListOptions{LabelSelector: labelSelector} 212 pvcs, err := clientset.CoreV1().PersistentVolumeClaims(namespace).List(context.TODO(), listOptions) 213 if err != nil { 214 return err 215 } 216 if len(pvcs.Items) > 0 { 217 log.Infof("Pvcs with label selector '%s' in namespace '%s' are still present", labelSelector, namespace) 218 return fmt.Errorf("Pvcs with label selector '%s' in namespace '%s' are still present", labelSelector, namespace) 219 } 220 log.Infof("All pvcs with label selector '%s' in namespace '%s' have been removed", labelSelector, namespace) 221 return nil 222 223 } 224 225 // DeleteSecret cleans up secrets as part of AfterSuite 226 func DeleteSecret(namespace string, name string, log *zap.SugaredLogger) error { 227 clientset, err := k8sutil.GetKubernetesClientset() 228 if err != nil { 229 log.Errorf("Failed to get clientset with error: %v", err) 230 return err 231 } 232 233 err = clientset.CoreV1().Secrets(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{}) 234 if err != nil { 235 if !k8serror.IsNotFound(err) { 236 log.Errorf("Failed to delete namespace '%s' due to: %v", name, zap.Error(err)) 237 return err 238 } 239 } 240 return nil 241 } 242 243 // CreateCredentialsSecretFromFile creates opaque secret from a file 244 func CreateCredentialsSecretFromFile(namespace string, name string, log *zap.SugaredLogger) error { 245 clientset, err := k8sutil.GetKubernetesClientset() 246 if err != nil { 247 log.Errorf("Failed to get clientset with error: %v", err) 248 return err 249 } 250 251 var b bytes.Buffer 252 template, _ := template.New("testsecrets").Parse(SecretsData) 253 data := AccessData{ 254 AccessName: ObjectStoreCredsAccessKeyName, 255 ScrtName: ObjectStoreCredsSecretAccessKeyName, 256 ObjectStoreAccessValue: OciOsAccessKey, 257 ObjectStoreScrt: OciOsAccessSecretKey, 258 } 259 260 template.Execute(&b, data) 261 secretData := make(map[string]string) 262 secretData["cloud"] = b.String() 263 264 secret := &corev1.Secret{ 265 ObjectMeta: metav1.ObjectMeta{ 266 Name: name, 267 Namespace: namespace, 268 }, 269 Type: corev1.SecretTypeOpaque, 270 StringData: secretData, 271 } 272 273 _, err = clientset.CoreV1().Secrets(namespace).Create(context.TODO(), secret, metav1.CreateOptions{}) 274 if err != nil { 275 log.Errorf("Error creating secret ", zap.Error(err)) 276 return err 277 } 278 return nil 279 } 280 281 // CreateMySQLCredentialsSecretFromFile creates opaque secret from a file 282 func CreateMySQLCredentialsSecretFromFile(namespace string, name string, log *zap.SugaredLogger) error { 283 log.Infof("Creating MySQL secret for S3 backup") 284 clientset, err := k8sutil.GetKubernetesClientset() 285 if err != nil { 286 log.Errorf("Failed to get clientset with error: %v", err) 287 return err 288 } 289 290 var b bytes.Buffer 291 template, err := template.New("testsecrets").Parse(SecretsData) 292 if err != nil { 293 return err 294 } 295 data := AccessData{ 296 AccessName: ObjectStoreCredsAccessKeyName, 297 ScrtName: ObjectStoreCredsSecretAccessKeyName, 298 ObjectStoreAccessValue: OciOsAccessKey, 299 ObjectStoreScrt: OciOsAccessSecretKey, 300 } 301 302 template.Execute(&b, data) 303 304 profileTemplate, err := template.New("testprofile").Parse(ProfileData) 305 if err != nil { 306 return err 307 } 308 var profileByte bytes.Buffer 309 pdata := InnoDBSecret{ 310 Region: BackupRegion, 311 } 312 profileTemplate.Execute(&profileByte, pdata) 313 314 secretData := make(map[string]string) 315 secretData["config"] = profileByte.String() 316 secretData["credentials"] = b.String() 317 318 secret := &corev1.Secret{ 319 ObjectMeta: metav1.ObjectMeta{ 320 Name: name, 321 Namespace: namespace, 322 }, 323 Type: corev1.SecretTypeOpaque, 324 StringData: secretData, 325 } 326 327 _, err = clientset.CoreV1().Secrets(namespace).Create(context.TODO(), secret, metav1.CreateOptions{}) 328 if err != nil { 329 log.Errorf("Error creating secret ", zap.Error(err)) 330 return err 331 } 332 return nil 333 } 334 335 // CreateMySQLCredentialsSecretFromFile creates opaque secret from a file 336 func CreateMySQLCredentialsSecretFromUserPrincipal(namespace string, name string, log *zap.SugaredLogger) error { 337 clientset, err := k8sutil.GetKubernetesClientset() 338 if err != nil { 339 log.Errorf("Failed to get clientset with error: %v", err) 340 return err 341 } 342 343 keydata, err := os.ReadFile(OciCliKeyFile) 344 if err != nil { 345 log.Errorf("Unable to read file '%s' due to %v", OciCliKeyFile, zap.Error(err)) 346 } 347 348 secretData := make(map[string]string) 349 secretData["region"] = BackupRegion 350 secretData["passphrase"] = "" 351 secretData["user"] = OciCliUser 352 secretData["tenancy"] = OciCliTenancy 353 secretData["fingerprint"] = OciCliFingerprint 354 secretData["privatekey"] = string(keydata) 355 356 secret := &corev1.Secret{ 357 ObjectMeta: metav1.ObjectMeta{ 358 Name: name, 359 Namespace: namespace, 360 }, 361 Type: corev1.SecretTypeOpaque, 362 StringData: secretData, 363 } 364 365 _, err = clientset.CoreV1().Secrets(namespace).Create(context.TODO(), secret, metav1.CreateOptions{}) 366 if err != nil { 367 log.Errorf("Error creating secret ", zap.Error(err)) 368 return err 369 } 370 return nil 371 } 372 373 // DeleteNamespace method to delete a namespace 374 func DeleteNamespace(namespace string, log *zap.SugaredLogger) error { 375 clientset, err := k8sutil.GetKubernetesClientset() 376 if err != nil { 377 log.Errorf("Failed to get clientset with error: %v", err) 378 return err 379 } 380 381 err = clientset.CoreV1().Namespaces().Delete(context.TODO(), namespace, metav1.DeleteOptions{}) 382 if err != nil { 383 if !k8serror.IsNotFound(err) { 384 log.Errorf("Failed to delete namespace '%s' due to: %v", namespace, zap.Error(err)) 385 return err 386 } 387 } 388 return nil 389 } 390 391 // HTTPHelper utility for http method use cases 392 func HTTPHelper(httpClient *retryablehttp.Client, method, httpURL, token, tokenType string, expectedResponseCode int, payload interface{}, log *zap.SugaredLogger) (*gabs.Container, error) { 393 394 var retryableRequest *retryablehttp.Request 395 var err error 396 397 switch method { 398 case "GET": 399 retryableRequest, err = retryablehttp.NewRequest(http.MethodGet, httpURL, payload) 400 retryableRequest.Header.Set("Content-Type", "application/json") 401 case "POST": 402 retryableRequest, err = retryablehttp.NewRequest(http.MethodPost, httpURL, payload) 403 case "DELETE": 404 retryableRequest, err = retryablehttp.NewRequest(http.MethodDelete, httpURL, payload) 405 case "PUT": 406 retryableRequest, err = retryablehttp.NewRequest(http.MethodPut, httpURL, payload) 407 } 408 if err != nil { 409 log.Error(fmt.Sprintf("error creating retryable api request for %s: %v", httpURL, err)) 410 return nil, err 411 } 412 413 switch tokenType { 414 case "Bearer": 415 retryableRequest.Header.Set("Authorization", fmt.Sprintf("Bearer %v", token)) 416 case "Basic": 417 retryableRequest.SetBasicAuth(strings.Split(token, ":")[0], strings.Split(token, ":")[1]) 418 } 419 retryableRequest.Header.Set("Accept", "application/json") 420 response, err := httpClient.Do(retryableRequest) 421 if err != nil { 422 log.Error(fmt.Sprintf("error invoking api request %s: %v", httpURL, err)) 423 return nil, err 424 } 425 defer response.Body.Close() 426 427 // To handle 204 returns for delete apis 428 if method == "DELETE" && expectedResponseCode == 200 { 429 err = httputil.ValidateResponseCode(response, expectedResponseCode, http.StatusNoContent) 430 } else { 431 err = httputil.ValidateResponseCode(response, expectedResponseCode) 432 } 433 if err != nil { 434 log.Errorf("expected response code = %v, actual response code = %v, Error = %v", expectedResponseCode, response.StatusCode, zap.Error(err)) 435 return nil, err 436 } 437 438 // extract the response body 439 body, err := io.ReadAll(response.Body) 440 if err != nil { 441 log.Errorf("Failed to read response body: %v", zap.Error(err)) 442 return nil, err 443 } 444 445 jsonParsed, err := gabs.ParseJSON(body) 446 if err != nil { 447 log.Errorf("Failed to parse json: %v", zap.Error(err)) 448 return nil, err 449 } 450 451 return jsonParsed, nil 452 } 453 454 // DisplayHookLogs is used to display the logs from the pod where the backup hook was run 455 // It execs into the pod and fetches the log file contents 456 func DisplayHookLogs(log *zap.SugaredLogger) error { 457 458 log.Infof("Retrieving verrazzano hook logs ...") 459 clientset, err := k8sutil.GetKubernetesClientset() 460 if err != nil { 461 log.Errorf("Failed to get clientset with error: %v", err) 462 return err 463 } 464 465 config, err := k8sutil.GetKubeConfig() 466 if err != nil { 467 log.Errorf("Failed to get config with error: %v", err) 468 return err 469 } 470 471 podSpec, err := clientset.CoreV1().Pods(constants.VerrazzanoLoggingNamespace).Get(context.TODO(), "opensearch-es-master-0", metav1.GetOptions{}) 472 if err != nil { 473 return err 474 } 475 476 cmdLogFileName := []string{"/bin/sh", "-c", "ls -alt --time=ctime /tmp/ | grep verrazzano | head -1"} 477 stdout, _, err := k8sutil.ExecPod(clientset, config, podSpec, "opensearch", cmdLogFileName) 478 if err != nil { 479 log.Errorf("Error = %v", zap.Error(err)) 480 return err 481 } 482 483 logFileData := strings.TrimSpace(strings.Trim(stdout, "\n")) 484 logFileName := strings.Split(logFileData, " ")[len(strings.Split(logFileData, " "))-1] 485 486 if len(logFileName) <= 0 { 487 log.Infof("Failed to find log file. The pod might have restarted") 488 return nil 489 } 490 491 var execCmd []string 492 execCmd = append(execCmd, "cat") 493 execCmd = append(execCmd, fmt.Sprintf("/tmp/%s", logFileName)) 494 stdout, _, err = k8sutil.ExecPod(clientset, config, podSpec, "opensearch", execCmd) 495 if err != nil { 496 log.Errorf("Error = %v", zap.Error(err)) 497 return err 498 } 499 log.Infof(stdout) 500 return nil 501 } 502 503 func Runner(bcmd *BashCommand, log *zap.SugaredLogger) *RunnerResponse { 504 var stdoutBuf, stderrBuf bytes.Buffer 505 var bashCommandResponse RunnerResponse 506 bashCommand := exec.Command(bcmd.CommandArgs[0], bcmd.CommandArgs[1:]...) //nolint:gosec 507 bashCommand.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) 508 bashCommand.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) 509 510 //log.Infof("Executing command '%v'", bashCommand.String()) 511 err := bashCommand.Run() 512 if err != nil { 513 log.Errorf("Cmd '%v' execution failed due to '%v'", bashCommand.String(), zap.Error(err)) 514 bashCommandResponse.CommandError = err 515 return &bashCommandResponse 516 } 517 bashCommandResponse.StandardOut = stdoutBuf 518 bashCommandResponse.CommandError = err 519 return &bashCommandResponse 520 }