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  }