github.com/verrazzano/verrazzano@v1.7.1/tests/e2e/clusterapi/ocne-driver/helpers.go (about)

     1  // Copyright (c) 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 ocnedriver
     5  
     6  import (
     7  	"context"
     8  	"encoding/json"
     9  	"fmt"
    10  	"net/http"
    11  	"os"
    12  
    13  	"github.com/Jeffail/gabs/v2"
    14  	"github.com/verrazzano/verrazzano/pkg/k8sutil"
    15  	"github.com/verrazzano/verrazzano/tests/e2e/backup/helpers"
    16  	"github.com/verrazzano/verrazzano/tests/e2e/pkg"
    17  	"go.uber.org/zap"
    18  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    19  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    20  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    21  	"k8s.io/apimachinery/pkg/runtime/schema"
    22  	"k8s.io/client-go/dynamic"
    23  )
    24  
    25  // Acts as a cache, mapping the cluster names to cluster IDs
    26  var clusterIDMapping = map[string]string{}
    27  
    28  type clusterState string
    29  type transitioningFlag string
    30  
    31  const (
    32  	provisioningClusterState clusterState      = "provisioning"
    33  	activeClusterState       clusterState      = "active"
    34  	transitioningFlagNo      transitioningFlag = "no"
    35  	transitioningFlagError   transitioningFlag = "error"
    36  )
    37  
    38  // Creates the cloud credential through the Rancher REST API
    39  func createCloudCredential(credentialName string, log *zap.SugaredLogger) (string, error) {
    40  	requestURL, adminToken := setupRequest(rancherURL, "v3/cloudcredentials", log)
    41  	privateKeyContents, err := getFileContents(privateKeyPath, log)
    42  	if err != nil {
    43  		log.Errorf("error reading private key file: %v", err)
    44  		return "", err
    45  	}
    46  
    47  	var cloudCreds RancherCloudCred
    48  	cloudCreds.Name = credentialName
    49  	cloudCreds.InternalName = credentialName
    50  	cloudCreds.Type = "provisioning.cattle.io/cloud-credential"
    51  	cloudCreds.InternalType = "provisioning.cattle.io/cloud-credential"
    52  
    53  	var cloudCredConfig RancherOcicredentialConfig
    54  	cloudCredConfig.Fingerprint = fingerprint
    55  	cloudCredConfig.PrivateKeyContents = privateKeyContents
    56  	cloudCredConfig.TenancyID = tenancyID
    57  	cloudCredConfig.UserID = userID
    58  	cloudCredConfig.Region = region
    59  	cloudCreds.RancherOcicredentialConfig = cloudCredConfig
    60  
    61  	cloudCredsBdata, err := json.Marshal(cloudCreds)
    62  	if err != nil {
    63  		log.Errorf("json marshalling error: %v", zap.Error(err))
    64  		return "", err
    65  	}
    66  
    67  	jsonBody, err := helpers.HTTPHelper(httpClient, "POST", requestURL, adminToken, "Bearer", http.StatusCreated, cloudCredsBdata, log)
    68  	if err != nil {
    69  		log.Errorf("error while retrieving http data: %v", zap.Error(err))
    70  		return "", err
    71  	}
    72  	credID := fmt.Sprint(jsonBody.Path("id").Data())
    73  	return credID, nil
    74  }
    75  
    76  // Sends a test request to check that the cloud credential is configured properly
    77  func validateCloudCredential(credID string, log *zap.SugaredLogger) error {
    78  	urlPath := fmt.Sprintf("meta/oci/nodeImages?cloudCredentialId=%s&compartment=%s&region=%s", credID, compartmentID, region)
    79  	requestURL, adminToken := setupRequest(rancherURL, urlPath, log)
    80  	log.Infof("validateCloudCredential URL = %s", requestURL)
    81  	res, err := helpers.HTTPHelper(httpClient, "POST", requestURL, adminToken, "Bearer", http.StatusOK, nil, log)
    82  	if err != nil {
    83  		log.Errorf("error while retrieving http data: %v", zap.Error(err))
    84  		return err
    85  	}
    86  	log.Infof("validate cloud credential response: %s", fmt.Sprint(res))
    87  	return nil
    88  }
    89  
    90  // Deletes the cloud credential through the Rancher REST API
    91  func deleteCredential(credID string, log *zap.SugaredLogger) {
    92  	requestURL, adminToken := setupRequest(rancherURL, fmt.Sprintf("%s%s", "v3/cloudCredentials/", credID), log)
    93  	helpers.HTTPHelper(httpClient, "DELETE", requestURL, adminToken, "Bearer", http.StatusNoContent, nil, log)
    94  }
    95  
    96  // Returns true if the cloud credential is deleted/does not exist
    97  func isCredentialDeleted(credID string, log *zap.SugaredLogger) (bool, error) {
    98  	jsonBody, err := getCredential(credID, log)
    99  	if err != nil {
   100  		return false, err
   101  	}
   102  	// A deleted credential should have an empty "data" array
   103  	data := jsonBody.Path("data").Children()
   104  	if len(data) > 0 {
   105  		err = fmt.Errorf("credential %s still has a non-empty data array from GET call to the API", credID)
   106  		log.Error(err)
   107  		return false, err
   108  	}
   109  	return true, nil
   110  }
   111  
   112  // Makes a GET request for the specified cloud credential
   113  func getCredential(credID string, log *zap.SugaredLogger) (*gabs.Container, error) {
   114  	requestURL, adminToken := setupRequest(rancherURL, fmt.Sprintf("%s%s", "v3/cloudcredentials?id=", credID), log)
   115  	return helpers.HTTPHelper(httpClient, "GET", requestURL, adminToken, "Bearer", http.StatusOK, nil, log)
   116  }
   117  
   118  type mutateRancherOCNEClusterFunc func(config *RancherOCNECluster)
   119  
   120  // This returns a mutateRancherOCNEClusterFunc, which edits a cluster config to have a node pool with the specified name and number of replicas.
   121  // Both the control plane and node pool nodes use the specified volume size, number of ocpus, and memory.
   122  func getMutateFnNodePoolsAndResourceUsage(nodePoolName string, poolReplicas, volumeSize, ocpus, memory int) mutateRancherOCNEClusterFunc {
   123  	return func(config *RancherOCNECluster) {
   124  		config.OciocneEngineConfig.ControlPlaneVolumeGbs = volumeSize
   125  		config.OciocneEngineConfig.ControlPlaneOcpus = ocpus
   126  		config.OciocneEngineConfig.ControlPlaneMemoryGbs = memory
   127  
   128  		config.OciocneEngineConfig.NodePools = []string{
   129  			getNodePoolSpec(nodePoolName, nodeShape, poolReplicas, memory, ocpus, volumeSize),
   130  		}
   131  	}
   132  }
   133  
   134  // Creates an OCNE Cluster, and returns an error if not successful. Creates a single node cluster by default.
   135  // `config` is expected to point to an empty RancherOCNECluster struct, which is populated with values by this function.
   136  // `mutateFn`, if not nil, can be used to make additional changes to the cluster config before the cluster creation request is made.
   137  func createClusterAndFillConfig(clusterName string, config *RancherOCNECluster, log *zap.SugaredLogger, mutateFn mutateRancherOCNEClusterFunc) error {
   138  	nodePublicKeyContents, err := getFileContents(nodePublicKeyPath, log)
   139  	if err != nil {
   140  		log.Errorf("error reading node public key file: %v", err)
   141  		return err
   142  	}
   143  
   144  	// Fill in the values for the create cluster API request body
   145  	config.fillCommonValues()
   146  	config.OciocneEngineConfig.CloudCredentialID = cloudCredentialID
   147  	config.OciocneEngineConfig.DisplayName = clusterName
   148  	config.OciocneEngineConfig.NodePublicKeyContents = nodePublicKeyContents
   149  	config.OciocneEngineConfig.NodePools = []string{}
   150  	config.CloudCredentialID = cloudCredentialID
   151  	config.Name = clusterName
   152  
   153  	// Make additional changes to the cluster config
   154  	if mutateFn != nil {
   155  		mutateFn(config)
   156  	}
   157  
   158  	return createCluster(clusterName, *config, log)
   159  }
   160  
   161  // Creates an OCNE cluster through ClusterAPI by making a request to the Rancher API
   162  func createCluster(clusterName string, requestPayload RancherOCNECluster, log *zap.SugaredLogger) error {
   163  	requestURL, adminToken := setupRequest(rancherURL, "v3/cluster?_replace=true", log)
   164  	clusterBData, err := json.Marshal(requestPayload)
   165  	if err != nil {
   166  		log.Errorf("json marshalling error: %v", zap.Error(err))
   167  		return err
   168  	}
   169  	log.Infof("create cluster body: %s", string(clusterBData))
   170  	res, err := helpers.HTTPHelper(httpClient, "POST", requestURL, adminToken, "Bearer", http.StatusCreated, clusterBData, log)
   171  	if res != nil {
   172  		log.Infof("create cluster response body: %s", res.String())
   173  	}
   174  	if err != nil {
   175  		log.Errorf("error while retrieving http data: %v", zap.Error(err))
   176  		return err
   177  	}
   178  	return nil
   179  }
   180  
   181  // Deletes the OCNE cluster by sending a DELETE request to the Rancher API
   182  func deleteCluster(clusterName string, log *zap.SugaredLogger) error {
   183  	clusterID, err := getClusterIDFromName(clusterName, log)
   184  	if err != nil {
   185  		log.Errorf("could not fetch cluster ID from cluster name %s: %s", clusterName, err)
   186  		return err
   187  	}
   188  	log.Infof("clusterID for deletion: %s", clusterID)
   189  	requestURL, adminToken := setupRequest(rancherURL, fmt.Sprintf("%s/%s", "v1/provisioning.cattle.io.clusters/fleet-default", clusterID), log)
   190  
   191  	_, err = helpers.HTTPHelper(httpClient, "DELETE", requestURL, adminToken, "Bearer", http.StatusOK, nil, log)
   192  	if err != nil {
   193  		log.Errorf("error while deleting cluster: %v", err)
   194  		return err
   195  	}
   196  	return nil
   197  }
   198  
   199  // This function takes in the cluster config of an existing cluster, and changes the fields required to make the update.
   200  // Then, this triggers an update for the OCNE cluster.
   201  func updateConfigAndCluster(config *RancherOCNECluster, mutateFn mutateRancherOCNEClusterFunc, log *zap.SugaredLogger) error {
   202  	if mutateFn == nil {
   203  		err := fmt.Errorf("cannot provide a nil mutate function to update the cluster")
   204  		log.Error(err)
   205  		return err
   206  	}
   207  
   208  	clusterName := config.Name
   209  	mutateFn(config)
   210  	return updateCluster(clusterName, *config, log)
   211  }
   212  
   213  // Requests an update to the node pool configuration of the OCNE cluster
   214  // via a PUT request to the Rancher API
   215  func updateCluster(clusterName string, requestPayload RancherOCNECluster, log *zap.SugaredLogger) error {
   216  	clusterID, err := getClusterIDFromName(clusterName, log)
   217  	if err != nil {
   218  		log.Errorf("Could not fetch cluster ID from cluster name %s: %s", clusterName, err)
   219  	}
   220  	requestURL, adminToken := setupRequest(rancherURL, fmt.Sprintf("v3/clusters/%s?_replace=true", clusterID), log)
   221  	clusterBData, err := json.Marshal(requestPayload)
   222  	if err != nil {
   223  		log.Errorf("json marshalling error: %v", zap.Error(err))
   224  		return err
   225  	}
   226  	_, err = helpers.HTTPHelper(httpClient, "PUT", requestURL, adminToken, "Bearer", http.StatusOK, clusterBData, log)
   227  	if err != nil {
   228  		log.Errorf("error while retrieving http data: %v", zap.Error(err))
   229  		return err
   230  	}
   231  	return nil
   232  }
   233  
   234  // Returns true if the cluster currently exists and is Active
   235  func isClusterActive(clusterName string, log *zap.SugaredLogger) (bool, error) {
   236  	clusterID, err := getClusterIDFromName(clusterName, log)
   237  	if err != nil {
   238  		log.Errorf("Could not fetch cluster ID from cluster name %s: %s", clusterName, err)
   239  	}
   240  
   241  	// Debug logging
   242  	var cmd helpers.BashCommand
   243  	var cmdArgs []string
   244  	cmdArgs = append(cmdArgs, "kubectl", "get", "clusters.cluster.x-k8s.io", "-A")
   245  	cmd.CommandArgs = cmdArgs
   246  	response := helpers.Runner(&cmd, log)
   247  	log.Infof("All CAPI clusters =  %s", (&response.StandardOut).String())
   248  
   249  	cmdArgs = []string{}
   250  	cmdArgs = append(cmdArgs, "kubectl", "get", "clusters.management.cattle.io")
   251  	cmd.CommandArgs = cmdArgs
   252  	response = helpers.Runner(&cmd, log)
   253  	log.Infof("All management clusters =  %s", (&response.StandardOut).String())
   254  
   255  	cmdArgs = []string{}
   256  	cmdArgs = append(cmdArgs, "kubectl", "get", "clusters.provisioning.cattle.io", "-A")
   257  	cmd.CommandArgs = cmdArgs
   258  	response = helpers.Runner(&cmd, log)
   259  	log.Infof("All provisioning clusters =  %s", (&response.StandardOut).String())
   260  
   261  	cmdArgs = []string{}
   262  	cmdArgs = append(cmdArgs, "kubectl", "get", "ma", "-A")
   263  	cmd.CommandArgs = cmdArgs
   264  	response = helpers.Runner(&cmd, log)
   265  	log.Infof("All CAPI machines =  %s", (&response.StandardOut).String())
   266  
   267  	kubeconfigPath, err := getWorkloadKubeconfig(clusterID, log)
   268  	if err != nil {
   269  		log.Error("could not download kubeconfig from rancher")
   270  	}
   271  
   272  	cmdArgs = []string{}
   273  	cmdArgs = append(cmdArgs, "kubectl", "--kubeconfig", kubeconfigPath, "get", "nodes", "-o", "wide")
   274  	cmd.CommandArgs = cmdArgs
   275  	response = helpers.Runner(&cmd, log)
   276  	log.Infof("All nodes in workload cluster =  %s", (&response.StandardOut).String())
   277  
   278  	cmdArgs = []string{}
   279  	cmdArgs = append(cmdArgs, "kubectl", "--kubeconfig", kubeconfigPath, "get", "pod", "-A", "-o", "wide")
   280  	cmd.CommandArgs = cmdArgs
   281  	response = helpers.Runner(&cmd, log)
   282  	log.Infof("All pods in workload cluster =  %s", (&response.StandardOut).String())
   283  
   284  	// Check if the cluster is active
   285  	return checkProvisioningClusterReady("fleet-default", clusterID, log)
   286  }
   287  
   288  // Returns true if the OCNE cluster is deleted/does not exist
   289  func isClusterDeleted(clusterName string, log *zap.SugaredLogger) (bool, error) {
   290  	// Check that the CAPI cluster object was deleted
   291  	clusterID, err := getClusterIDFromName(clusterName, log)
   292  	if err != nil {
   293  		return false, err
   294  	}
   295  	clusterObjectFound, err := checkClusterExistsFromK8s(clusterID, clusterID, log)
   296  	return !clusterObjectFound, err
   297  }
   298  
   299  // Returns true if the requested provisioning cluster object has a ready status set to true
   300  func checkProvisioningClusterReady(namespace, clusterID string, log *zap.SugaredLogger) (bool, error) {
   301  	provClusterFetched, err := fetchProvisioningClusterFromK8s(namespace, clusterID, log)
   302  	if err != nil {
   303  		return false, err
   304  	}
   305  	if provClusterFetched == nil {
   306  		err = fmt.Errorf("no provisioning cluster %s found", clusterID)
   307  		log.Error(err)
   308  		return false, err
   309  	}
   310  
   311  	// convert the fetched unstructured object to a provisioning cluster struct
   312  	var provCluster ProvisioningCluster
   313  	bdata, err := json.Marshal(provClusterFetched)
   314  	if err != nil {
   315  		log.Errorf("json marshalling error %v", zap.Error(err))
   316  		return false, err
   317  	}
   318  	err = json.Unmarshal(bdata, &provCluster)
   319  	if err != nil {
   320  		log.Errorf("json unmarshall error %v", zap.Error(err))
   321  		return false, err
   322  	}
   323  
   324  	if provCluster.Status.Ready {
   325  		log.Infof("provisioning cluster %s is ready", clusterID)
   326  	}
   327  	return provCluster.Status.Ready, err
   328  }
   329  
   330  // Fetches the provisioning cluster object
   331  func fetchProvisioningClusterFromK8s(namespace, clusterID string, log *zap.SugaredLogger) (*unstructured.Unstructured, error) {
   332  	clusterFetched, err := getUnstructuredData("provisioning.cattle.io", "v1", "clusters", clusterID, namespace, log)
   333  	if err != nil {
   334  		log.Errorf("unable to fetch provisioning cluster '%s' due to '%v'", clusterID, zap.Error(err))
   335  		return nil, err
   336  	}
   337  	return clusterFetched, nil
   338  }
   339  
   340  // This retrieves the clusters.cluster.x-k8s.io object and returns true if it exists.
   341  func checkClusterExistsFromK8s(namespace, clusterID string, log *zap.SugaredLogger) (bool, error) {
   342  	clusterFetched, err := fetchClusterFromK8s(namespace, clusterID, log)
   343  	if err != nil {
   344  		return false, err
   345  	}
   346  	if clusterFetched == nil {
   347  		log.Infof("No CAPI clusters with id '%s' in namespace '%s' was detected", clusterID, namespace)
   348  		return false, nil
   349  	}
   350  	return true, nil
   351  }
   352  
   353  // This fetches the clusters.cluster.x-k8s.io object
   354  func fetchClusterFromK8s(namespace, clusterID string, log *zap.SugaredLogger) (*unstructured.Unstructured, error) {
   355  	clusterFetched, err := getUnstructuredData("cluster.x-k8s.io", "v1beta1", "clusters", clusterID, namespace, log)
   356  	if err != nil {
   357  		log.Errorf("unable to fetch CAPI cluster '%s' due to '%v'", clusterID, zap.Error(err))
   358  		return nil, err
   359  	}
   360  	return clusterFetched, nil
   361  }
   362  
   363  // Checks whether the cluster was created as expected. Returns nil if all is good.
   364  func verifyCluster(clusterName string, expectedNodes int, expectedClusterState clusterState, expectedTransitioning transitioningFlag, log *zap.SugaredLogger) error {
   365  	// Check if the cluster looks good from the Rancher API
   366  	var err error
   367  	if err = verifyGetRequest(clusterName, expectedNodes, expectedClusterState, expectedTransitioning, log); err != nil {
   368  		log.Errorf("error validating GET request for cluster %s: %s", clusterName, err)
   369  		return err
   370  	}
   371  
   372  	// Get the kubeconfig of the cluster to look inside
   373  	clusterID, err := getClusterIDFromName(clusterName, log)
   374  	if err != nil {
   375  		log.Errorf("could not get cluster ID for cluster %s", clusterName, log)
   376  		return err
   377  	}
   378  	workloadKubeconfigPath, err := getWorkloadKubeconfig(clusterID, log)
   379  	if err != nil {
   380  		log.Errorf("could not get kubeconfig for cluster %s", clusterName)
   381  		return err
   382  	}
   383  
   384  	if expectedNodes > 0 {
   385  		// Check if the cluster has the expected nodes and pods running
   386  		if err = verifyClusterNodes(clusterName, workloadKubeconfigPath, expectedNodes, log); err != nil {
   387  			log.Errorf("error validating number of nodes in %s: %s", clusterName, err)
   388  			return err
   389  		}
   390  		return verifyClusterPods(clusterName, workloadKubeconfigPath, log)
   391  	}
   392  	return nil
   393  }
   394  
   395  // Verifies that a GET request to the Rancher API for this cluster returns expected values.
   396  // Intended to be called on clusters with expected number of nodes and cluster state.
   397  func verifyGetRequest(clusterName string, expectedNodes int, expectedClusterState clusterState, expectedTransitioning transitioningFlag, log *zap.SugaredLogger) error {
   398  	jsonBody, err := getCluster(clusterName, log)
   399  	if err != nil {
   400  		return err
   401  	}
   402  	jsonData := jsonBody.Path("data.0")
   403  
   404  	// Assert that these attributes are as expected
   405  	resourceType := jsonBody.Path("resourceType").Data()
   406  	name := jsonData.Path("name").Data()
   407  	nodeCount := jsonData.Path("nodeCount").Data()
   408  	state := jsonData.Path("state").Data()
   409  	transitioning := jsonData.Path("transitioning").Data()
   410  	fleetNamespace := jsonData.Path("fleetWorkspaceName").Data()
   411  	driver := jsonData.Path("driver").Data()
   412  
   413  	attributes := []struct {
   414  		actual   interface{}
   415  		expected interface{}
   416  		name     string
   417  	}{
   418  		{resourceType, "cluster", "resource type"},
   419  		{name, clusterName, "cluster name"},
   420  		{nodeCount, float64(expectedNodes), "node count"},
   421  		{state, string(expectedClusterState), "state"},
   422  		{transitioning, string(expectedTransitioning), "transitioning flag"},
   423  		{fleetNamespace, "fleet-default", "fleet workspace"},
   424  		{driver, "ociocne", "driver"},
   425  	}
   426  	for _, a := range attributes {
   427  		if a.actual != a.expected {
   428  			return fmt.Errorf("cluster %s has a %s value of %v but should be %v", clusterName, a.name, a.actual, a.expected)
   429  		}
   430  	}
   431  
   432  	// Assert that these attributes are not nil
   433  	caCert := jsonData.Path("caCert").Data()
   434  	requestedResources := jsonData.Path("requested").Data()
   435  
   436  	if expectedClusterState == activeClusterState {
   437  		nonNilAttributes := []struct {
   438  			value interface{}
   439  			name  string
   440  		}{
   441  			{caCert, "CA certificate"},
   442  			{requestedResources, "requested resources"},
   443  		}
   444  		for _, n := range nonNilAttributes {
   445  			if n.value == nil {
   446  				return fmt.Errorf("cluster %s should have a non-nil value for %s", clusterName, n.name)
   447  			}
   448  		}
   449  	}
   450  
   451  	log.Infof("cluster %s looks as expected from a GET call to the Rancher API", clusterName)
   452  	return nil
   453  }
   454  
   455  // Verifies that the workload cluster has the expected number of nodes.
   456  func verifyClusterNodes(clusterName, kubeconfigPath string, expectedNumberNodes int, log *zap.SugaredLogger) error {
   457  	numNodes, err := pkg.GetNodeCountInCluster(kubeconfigPath)
   458  	if err != nil {
   459  		log.Errorf("could not verify number of nodes in cluster %s: %s", clusterName, err)
   460  		return err
   461  	}
   462  	if numNodes != expectedNumberNodes {
   463  		err = fmt.Errorf("expected %v nodes in cluster %s but got %v", expectedNumberNodes, clusterName, numNodes)
   464  		log.Error(err)
   465  		return err
   466  	}
   467  	log.Infof("cluster %s had the expected number of nodes", clusterName)
   468  	return nil
   469  }
   470  
   471  // Verifies that all expected pods in the workload cluster are active,
   472  // given the cluster's kubeconfig path and the cluster name
   473  func verifyClusterPods(clusterName, kubeconfigPath string, log *zap.SugaredLogger) error {
   474  	// keys are the namespaces, and values are the pod name prefixes
   475  	expectedPods := map[string][]string{
   476  		"verrazzano-module-operator": {"verrazzano-module-operator"},
   477  		"calico-apiserver":           {"calico-apiserver"},
   478  		"calico-system":              {"calico-kube-controllers", "calico-node", "calico-typha", "csi-node-driver"},
   479  		"cattle-fleet-system":        {"fleet-agent"},
   480  		"cattle-system":              {"cattle-cluster-agent"},
   481  		"default":                    {"tigera-operator"},
   482  		"kube-system": {"coredns", "csi-oci-controller", "csi-oci-node", "etcd", "kube-apiserver",
   483  			"kube-controller-manager", "kube-proxy", "kube-scheduler", "oci-cloud-controller-manager"},
   484  	}
   485  
   486  	// check the expected pods inside the workload cluster
   487  	for namespace, namePrefixes := range expectedPods {
   488  		podsRunning, err := pkg.PodsRunningInCluster(namespace, namePrefixes, kubeconfigPath)
   489  		if err != nil {
   490  			log.Errorf("error while verifying running pods: %s", err)
   491  			return err
   492  		}
   493  		if !podsRunning {
   494  			err = fmt.Errorf("there are missing pods in the %s namespace", namespace)
   495  			log.Error(err)
   496  			return err
   497  		}
   498  	}
   499  
   500  	log.Infof("all expected pods in cluster %s are running", clusterName)
   501  	return nil
   502  }
   503  
   504  // Gets a specified cluster by using the Rancher REST API.
   505  func getCluster(clusterName string, log *zap.SugaredLogger) (*gabs.Container, error) {
   506  	requestURL, adminToken := setupRequest(rancherURL, fmt.Sprintf("v3/cluster?name=%s", clusterName), log)
   507  	return helpers.HTTPHelper(httpClient, "GET", requestURL, adminToken, "Bearer", http.StatusOK, nil, log)
   508  }
   509  
   510  // Returns the cluster ID corresponding the given name
   511  func getClusterIDFromName(clusterName string, log *zap.SugaredLogger) (string, error) {
   512  	// Check if we already have this value cached
   513  	if id, ok := clusterIDMapping[clusterName]; ok {
   514  		return id, nil
   515  	}
   516  
   517  	// Get the cluster ID from the Rancher REST API
   518  	jsonBody, err := getCluster(clusterName, log)
   519  	if err != nil {
   520  		log.Errorf("failed getting cluster ID from GET call to Rancher API: %s", err)
   521  		return "", err
   522  	}
   523  	id := fmt.Sprint(jsonBody.Path("data.0.id").Data())
   524  
   525  	// Cache this value and return
   526  	clusterIDMapping[clusterName] = id
   527  	return id, nil
   528  }
   529  
   530  // Returns a string representing a node pool
   531  func getNodePoolSpec(name, shape string, replicas, memory, ocpus, volumeSize int) string {
   532  	return fmt.Sprintf("{\"name\":\"%s\",\"replicas\":%d,\"memory\":%d,\"ocpus\":%d,\"volumeSize\":%d,\"shape\":\"%s\"}",
   533  		name, replicas, memory, ocpus, volumeSize, shape)
   534  }
   535  
   536  // Generates the kubeconfig of the workload cluster with an ID of `clusterID`
   537  // Writes the kubeconfig to a file inside /tmp. Returns the path of the kubeconfig file.
   538  func getWorkloadKubeconfig(clusterID string, log *zap.SugaredLogger) (string, error) {
   539  	outputPath := fmt.Sprintf("/tmp/%s-kubeconfig", clusterID)
   540  
   541  	// First check if the kubeconfig is already present on our filesystem
   542  	if _, err := os.Stat(outputPath); err == nil {
   543  		return outputPath, nil
   544  	}
   545  
   546  	// Otherwise, download the kubeconfig through an API call
   547  	requestURL, adminToken := setupRequest(rancherURL, fmt.Sprintf("v3/clusters/%s?action=generateKubeconfig", clusterID), log)
   548  	jsonBody, err := helpers.HTTPHelper(httpClient, "POST", requestURL, adminToken, "Bearer", http.StatusOK, nil, log)
   549  	if err != nil {
   550  		log.Errorf("error while retrieving http data: %v", zap.Error(err))
   551  		return "", err
   552  	}
   553  
   554  	workloadKubeconfig := fmt.Sprint(jsonBody.Path("config").Data())
   555  	err = os.WriteFile(outputPath, []byte(workloadKubeconfig), 0600)
   556  	if err != nil {
   557  		log.Errorf("error writing workload cluster kubeconfig to a file: %v", zap.Error(err))
   558  		return "", err
   559  	}
   560  	return outputPath, nil
   561  }
   562  
   563  // Given a file path, returns the file's contents as a string
   564  func getFileContents(file string, log *zap.SugaredLogger) (string, error) {
   565  	data, err := os.ReadFile(file)
   566  	if err != nil {
   567  		log.Errorf("failed reading file contents: %v", err)
   568  		return "", err
   569  	}
   570  	return string(data), nil
   571  }
   572  
   573  // Given the base Rancher URL and the additional URL path,
   574  // returns the full URL and a refreshed Bearer token
   575  func setupRequest(rancherBaseURL, urlPath string, log *zap.SugaredLogger) (string, string) {
   576  	adminToken := helpers.GetRancherLoginToken(log)
   577  	log.Infof("adminToken: %s", adminToken)
   578  	requestURL := fmt.Sprintf("%s/%s", rancherBaseURL, urlPath)
   579  	log.Infof("requestURL: %s", requestURL)
   580  	return requestURL, adminToken
   581  }
   582  
   583  // getUnstructuredData common utility to fetch unstructured data
   584  func getUnstructuredData(group, version, resource, resourceName, nameSpaceName string, log *zap.SugaredLogger) (*unstructured.Unstructured, error) {
   585  	var dataFetched *unstructured.Unstructured
   586  	var err error
   587  	config, err := k8sutil.GetKubeConfig()
   588  	if err != nil {
   589  		log.Errorf("Unable to fetch kubeconfig %v", zap.Error(err))
   590  		return nil, err
   591  	}
   592  	dclient, err := dynamic.NewForConfig(config)
   593  	if err != nil {
   594  		log.Errorf("Unable to create dynamic client %v", zap.Error(err))
   595  		return nil, err
   596  	}
   597  
   598  	gvr := schema.GroupVersionResource{
   599  		Group:    group,
   600  		Version:  version,
   601  		Resource: resource,
   602  	}
   603  
   604  	if nameSpaceName != "" {
   605  		log.Infof("fetching '%s' '%s' in namespace '%s'", resource, resourceName, nameSpaceName)
   606  		dataFetched, err = dclient.Resource(gvr).Namespace(nameSpaceName).Get(context.TODO(), resourceName, metav1.GetOptions{})
   607  	} else {
   608  		log.Infof("fetching '%s' '%s'", resource, resourceName)
   609  		dataFetched, err = dclient.Resource(gvr).Get(context.TODO(), resourceName, metav1.GetOptions{})
   610  	}
   611  	if err != nil {
   612  		if apierrors.IsNotFound(err) {
   613  			log.Errorf("resource %s %s not found", resource, resourceName)
   614  			return nil, nil
   615  		}
   616  		log.Errorf("Unable to fetch %s %s due to '%v'", resource, resourceName, zap.Error(err))
   617  		return nil, err
   618  	}
   619  	return dataFetched, nil
   620  }