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®ion=%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 }