github.com/vmware/go-vcloud-director/v2@v2.24.0/govcd/cse_util.go (about)

     1  package govcd
     2  
     3  import (
     4  	_ "embed"
     5  	"encoding/base64"
     6  	"encoding/json"
     7  	"fmt"
     8  	semver "github.com/hashicorp/go-version"
     9  	"github.com/vmware/go-vcloud-director/v2/types/v56"
    10  	"github.com/vmware/go-vcloud-director/v2/util"
    11  	"net"
    12  	"net/url"
    13  	"regexp"
    14  	"sort"
    15  	"strconv"
    16  	"strings"
    17  	"time"
    18  )
    19  
    20  // getCseComponentsVersions gets the versions of the subcomponents that are part of Container Service Extension.
    21  // NOTE: This function should be updated on every CSE release to update the supported versions.
    22  func getCseComponentsVersions(cseVersion semver.Version) (*cseComponentsVersions, error) {
    23  	v43, _ := semver.NewVersion("4.3.0")
    24  	v42, _ := semver.NewVersion("4.2.0")
    25  	v41, _ := semver.NewVersion("4.1.0")
    26  	err := fmt.Errorf("the Container Service Extension version '%s' is not supported", cseVersion.String())
    27  
    28  	if cseVersion.GreaterThanOrEqual(v43) {
    29  		return nil, err
    30  	}
    31  	if cseVersion.GreaterThanOrEqual(v42) {
    32  		return &cseComponentsVersions{
    33  			VcdKeConfigRdeTypeVersion: "1.1.0",
    34  			CapvcdRdeTypeVersion:      "1.3.0",
    35  			CseInterfaceVersion:       "1.0.0",
    36  		}, nil
    37  	}
    38  	if cseVersion.GreaterThanOrEqual(v41) {
    39  		return &cseComponentsVersions{
    40  			VcdKeConfigRdeTypeVersion: "1.1.0",
    41  			CapvcdRdeTypeVersion:      "1.2.0",
    42  			CseInterfaceVersion:       "1.0.0",
    43  		}, nil
    44  	}
    45  	return nil, err
    46  }
    47  
    48  // cseConvertToCseKubernetesClusterType takes a generic RDE that must represent an existing CSE Kubernetes cluster,
    49  // and transforms it to an equivalent CseKubernetesCluster object that represents the same cluster, but
    50  // it is easy to explore and consume. If the input RDE is not a CSE Kubernetes cluster, this method
    51  // will obviously return an error.
    52  //
    53  // The transformation from a generic RDE to a CseKubernetesCluster is done by querying VCD for every needed item,
    54  // such as Network IDs, Compute Policies IDs, vApp Template IDs, etc. It deeply explores the RDE contents
    55  // (even the CAPI YAML) to retrieve information and getting the missing pieces from VCD.
    56  //
    57  // WARNING: Don't use this method inside loops or avoid calling it multiple times in a row, as it performs many queries
    58  // to VCD.
    59  func cseConvertToCseKubernetesClusterType(rde *DefinedEntity) (*CseKubernetesCluster, error) {
    60  	requiredType := fmt.Sprintf("%s:%s", cseKubernetesClusterVendor, cseKubernetesClusterNamespace)
    61  
    62  	if !strings.Contains(rde.DefinedEntity.ID, requiredType) || !strings.Contains(rde.DefinedEntity.EntityType, requiredType) {
    63  		return nil, fmt.Errorf("the receiver RDE is not a '%s' entity, it is '%s'", requiredType, rde.DefinedEntity.EntityType)
    64  	}
    65  
    66  	entityBytes, err := json.Marshal(rde.DefinedEntity.Entity)
    67  	if err != nil {
    68  		return nil, fmt.Errorf("could not marshal the RDE contents to create a capvcdType instance: %s", err)
    69  	}
    70  
    71  	capvcd := &types.Capvcd{}
    72  	err = json.Unmarshal(entityBytes, &capvcd)
    73  	if err != nil {
    74  		return nil, fmt.Errorf("could not unmarshal the RDE contents to create a Capvcd instance: %s", err)
    75  	}
    76  
    77  	result := &CseKubernetesCluster{
    78  		CseClusterSettings: CseClusterSettings{
    79  			Name:               rde.DefinedEntity.Name,
    80  			ApiToken:           "******", // We must not return this one, we return the "standard" 6-asterisk value
    81  			AutoRepairOnErrors: capvcd.Spec.VcdKe.AutoRepairOnErrors,
    82  			ControlPlane:       CseControlPlaneSettings{},
    83  		},
    84  		ID:                         rde.DefinedEntity.ID,
    85  		Etag:                       rde.Etag,
    86  		ClusterResourceSetBindings: make([]string, len(capvcd.Status.Capvcd.ClusterResourceSetBindings)),
    87  		State:                      capvcd.Status.VcdKe.State,
    88  		Events:                     make([]CseClusterEvent, 0),
    89  		client:                     rde.client,
    90  		capvcdType:                 capvcd,
    91  		supportedUpgrades:          make([]*types.VAppTemplate, 0),
    92  	}
    93  
    94  	// Add all events to the resulting cluster
    95  	for _, s := range capvcd.Status.VcdKe.EventSet {
    96  		result.Events = append(result.Events, CseClusterEvent{
    97  			Name:         s.Name,
    98  			Type:         "event",
    99  			ResourceId:   s.VcdResourceId,
   100  			ResourceName: s.VcdResourceName,
   101  			OccurredAt:   s.OccurredAt,
   102  			Details:      s.AdditionalDetails.DetailedEvent,
   103  		})
   104  	}
   105  	for _, s := range capvcd.Status.VcdKe.ErrorSet {
   106  		result.Events = append(result.Events, CseClusterEvent{
   107  			Name:         s.Name,
   108  			Type:         "error",
   109  			ResourceId:   s.VcdResourceId,
   110  			ResourceName: s.VcdResourceName,
   111  			OccurredAt:   s.OccurredAt,
   112  			Details:      s.AdditionalDetails.DetailedError,
   113  		})
   114  	}
   115  	for _, s := range capvcd.Status.Capvcd.EventSet {
   116  		result.Events = append(result.Events, CseClusterEvent{
   117  			Name:         s.Name,
   118  			Type:         "event",
   119  			ResourceId:   s.VcdResourceId,
   120  			ResourceName: s.VcdResourceName,
   121  			OccurredAt:   s.OccurredAt,
   122  			Details:      s.Name,
   123  		})
   124  	}
   125  	for _, s := range capvcd.Status.Capvcd.ErrorSet {
   126  		result.Events = append(result.Events, CseClusterEvent{
   127  			Name:         s.Name,
   128  			Type:         "error",
   129  			ResourceId:   s.VcdResourceId,
   130  			ResourceName: s.VcdResourceName,
   131  			OccurredAt:   s.OccurredAt,
   132  			Details:      s.AdditionalDetails.DetailedError,
   133  		})
   134  	}
   135  	for _, s := range capvcd.Status.Cpi.EventSet {
   136  		result.Events = append(result.Events, CseClusterEvent{
   137  			Name:         s.Name,
   138  			Type:         "event",
   139  			ResourceId:   s.VcdResourceId,
   140  			ResourceName: s.VcdResourceName,
   141  			OccurredAt:   s.OccurredAt,
   142  			Details:      s.Name,
   143  		})
   144  	}
   145  	for _, s := range capvcd.Status.Cpi.ErrorSet {
   146  		result.Events = append(result.Events, CseClusterEvent{
   147  			Name:         s.Name,
   148  			Type:         "error",
   149  			ResourceId:   s.VcdResourceId,
   150  			ResourceName: s.VcdResourceName,
   151  			OccurredAt:   s.OccurredAt,
   152  			Details:      s.AdditionalDetails.DetailedError,
   153  		})
   154  	}
   155  	for _, s := range capvcd.Status.Csi.EventSet {
   156  		result.Events = append(result.Events, CseClusterEvent{
   157  			Name:         s.Name,
   158  			Type:         "event",
   159  			ResourceId:   s.VcdResourceId,
   160  			ResourceName: s.VcdResourceName,
   161  			OccurredAt:   s.OccurredAt,
   162  			Details:      s.Name,
   163  		})
   164  	}
   165  	for _, s := range capvcd.Status.Csi.ErrorSet {
   166  		result.Events = append(result.Events, CseClusterEvent{
   167  			Name:         s.Name,
   168  			Type:         "error",
   169  			ResourceId:   s.VcdResourceId,
   170  			ResourceName: s.VcdResourceName,
   171  			OccurredAt:   s.OccurredAt,
   172  			Details:      s.AdditionalDetails.DetailedError,
   173  		})
   174  	}
   175  	for _, s := range capvcd.Status.Projector.EventSet {
   176  		result.Events = append(result.Events, CseClusterEvent{
   177  			Name:         s.Name,
   178  			Type:         "event",
   179  			ResourceId:   s.VcdResourceId,
   180  			ResourceName: s.VcdResourceName,
   181  			OccurredAt:   s.OccurredAt,
   182  			Details:      s.Name,
   183  		})
   184  	}
   185  	for _, s := range capvcd.Status.Projector.ErrorSet {
   186  		result.Events = append(result.Events, CseClusterEvent{
   187  			Name:         s.Name,
   188  			Type:         "error",
   189  			ResourceId:   s.VcdResourceId,
   190  			ResourceName: s.VcdResourceName,
   191  			OccurredAt:   s.OccurredAt,
   192  			Details:      s.AdditionalDetails.DetailedError,
   193  		})
   194  	}
   195  	sort.SliceStable(result.Events, func(i, j int) bool {
   196  		return result.Events[i].OccurredAt.After(result.Events[j].OccurredAt)
   197  	})
   198  
   199  	if capvcd.Status.Capvcd.CapvcdVersion != "" {
   200  		version, err := semver.NewVersion(capvcd.Status.Capvcd.CapvcdVersion)
   201  		if err != nil {
   202  			return nil, fmt.Errorf("could not read Capvcd version: %s", err)
   203  		}
   204  		result.CapvcdVersion = *version
   205  	}
   206  
   207  	if capvcd.Status.Cpi.Version != "" {
   208  		version, err := semver.NewVersion(strings.TrimSpace(capvcd.Status.Cpi.Version)) // Note: We use trim as the version comes with spacing characters
   209  		if err != nil {
   210  			return nil, fmt.Errorf("could not read CPI version: %s", err)
   211  		}
   212  		result.CpiVersion = *version
   213  	}
   214  
   215  	if capvcd.Status.Csi.Version != "" {
   216  		version, err := semver.NewVersion(capvcd.Status.Csi.Version)
   217  		if err != nil {
   218  			return nil, fmt.Errorf("could not read CSI version: %s", err)
   219  		}
   220  		result.CsiVersion = *version
   221  	}
   222  
   223  	if capvcd.Status.VcdKe.VcdKeVersion != "" {
   224  		cseVersion, err := semver.NewVersion(capvcd.Status.VcdKe.VcdKeVersion)
   225  		if err != nil {
   226  			return nil, fmt.Errorf("could not read the CSE Version that the cluster uses: %s", err)
   227  		}
   228  		// Remove the possible version suffixes as we just want MAJOR.MINOR.PATCH
   229  		// TODO: This can be replaced with (*cseVersion).Core() in newer versions of the library
   230  		cseVersionSegs := (*cseVersion).Segments()
   231  		cseVersion, err = semver.NewVersion(fmt.Sprintf("%d.%d.%d", cseVersionSegs[0], cseVersionSegs[1], cseVersionSegs[2]))
   232  		if err != nil {
   233  			return nil, fmt.Errorf("could not read the CSE Version that the cluster uses: %s", err)
   234  		}
   235  		result.CseVersion = *cseVersion
   236  	}
   237  
   238  	// Retrieve the Owner
   239  	if rde.DefinedEntity.Owner != nil {
   240  		result.Owner = rde.DefinedEntity.Owner.Name
   241  	}
   242  
   243  	// Retrieve the Organization ID
   244  	for i, binding := range capvcd.Status.Capvcd.ClusterResourceSetBindings {
   245  		result.ClusterResourceSetBindings[i] = binding.Name
   246  	}
   247  
   248  	if len(capvcd.Status.Capvcd.ClusterApiStatus.ApiEndpoints) > 0 {
   249  		result.ControlPlane.Ip = capvcd.Status.Capvcd.ClusterApiStatus.ApiEndpoints[0].Host
   250  	}
   251  
   252  	if len(result.capvcdType.Status.Capvcd.VcdProperties.Organizations) > 0 {
   253  		result.OrganizationId = result.capvcdType.Status.Capvcd.VcdProperties.Organizations[0].Id
   254  	}
   255  
   256  	// If the Org/VDC information is not set, we can't continue retrieving information for the cluster.
   257  	// This scenario is when the cluster is not correctly provisioned (Error state)
   258  	if len(result.capvcdType.Status.Capvcd.VcdProperties.OrgVdcs) == 0 {
   259  		return result, nil
   260  	}
   261  
   262  	// NOTE: The code below, until the end of this function, requires the Org/VDC information
   263  
   264  	// Retrieve the VDC ID
   265  	result.VdcId = result.capvcdType.Status.Capvcd.VcdProperties.OrgVdcs[0].Id
   266  	// FIXME: This is a workaround, because for some reason the OrgVdcs[*].Id property contains the VDC name instead of the VDC ID.
   267  	//        Once this is fixed, this conditional should not be needed anymore.
   268  	if result.VdcId == result.capvcdType.Status.Capvcd.VcdProperties.OrgVdcs[0].Name {
   269  		vdcs, err := queryOrgVdcList(rde.client, map[string]string{})
   270  		if err != nil {
   271  			return nil, fmt.Errorf("could not get VDC IDs as no VDC was found: %s", err)
   272  		}
   273  		found := false
   274  		for _, vdc := range vdcs {
   275  			if vdc.Name == result.capvcdType.Status.Capvcd.VcdProperties.OrgVdcs[0].Name {
   276  				result.VdcId = fmt.Sprintf("urn:vcloud:vdc:%s", extractUuid(vdc.HREF))
   277  				found = true
   278  				break
   279  			}
   280  		}
   281  		if !found {
   282  			return nil, fmt.Errorf("could not get VDC IDs as no VDC with name '%s' was found", result.capvcdType.Status.Capvcd.VcdProperties.OrgVdcs[0].Name)
   283  		}
   284  	}
   285  
   286  	// Retrieve the Network ID
   287  	params := url.Values{}
   288  	params.Add("filter", fmt.Sprintf("name==%s", result.capvcdType.Status.Capvcd.VcdProperties.OrgVdcs[0].OvdcNetworkName))
   289  	params = queryParameterFilterAnd("ownerRef.id=="+result.VdcId, params)
   290  	networks, err := getAllOpenApiOrgVdcNetworks(rde.client, params)
   291  	if err != nil {
   292  		return nil, fmt.Errorf("could not read Org VDC Network from Capvcd type: %s", err)
   293  	}
   294  	if len(networks) != 1 {
   295  		return nil, fmt.Errorf("expected one Org VDC Network from Capvcd type, but got %d", len(networks))
   296  	}
   297  	result.NetworkId = networks[0].OpenApiOrgVdcNetwork.ID
   298  
   299  	// Here we retrieve several items that we need from now onwards, like Storage Profiles and Compute Policies
   300  	storageProfiles := map[string]string{}
   301  	if rde.client.IsSysAdmin {
   302  		allSp, err := queryAdminOrgVdcStorageProfilesByVdcId(rde.client, result.VdcId)
   303  		if err != nil {
   304  			return nil, fmt.Errorf("could not get all the Storage Profiles: %s", err)
   305  		}
   306  		for _, recordType := range allSp {
   307  			storageProfiles[recordType.Name] = fmt.Sprintf("urn:vcloud:vdcstorageProfile:%s", extractUuid(recordType.HREF))
   308  		}
   309  	} else {
   310  		allSp, err := queryOrgVdcStorageProfilesByVdcId(rde.client, result.VdcId)
   311  		if err != nil {
   312  			return nil, fmt.Errorf("could not get all the Storage Profiles: %s", err)
   313  		}
   314  		for _, recordType := range allSp {
   315  			storageProfiles[recordType.Name] = fmt.Sprintf("urn:vcloud:vdcstorageProfile:%s", extractUuid(recordType.HREF))
   316  		}
   317  	}
   318  
   319  	computePolicies, err := getAllVdcComputePoliciesV2(rde.client, nil)
   320  	if err != nil {
   321  		return nil, fmt.Errorf("could not get all the Compute Policies: %s", err)
   322  	}
   323  
   324  	if result.capvcdType.Spec.VcdKe.DefaultStorageClassOptions.K8SStorageClassName != "" { // This would mean there is a Default Storage Class defined
   325  		result.DefaultStorageClass = &CseDefaultStorageClassSettings{
   326  			Name:          result.capvcdType.Spec.VcdKe.DefaultStorageClassOptions.K8SStorageClassName,
   327  			ReclaimPolicy: "retain",
   328  			Filesystem:    result.capvcdType.Spec.VcdKe.DefaultStorageClassOptions.Filesystem,
   329  		}
   330  		for spName, spId := range storageProfiles {
   331  			if spName == result.capvcdType.Spec.VcdKe.DefaultStorageClassOptions.VcdStorageProfileName {
   332  				result.DefaultStorageClass.StorageProfileId = spId
   333  			}
   334  		}
   335  		if result.capvcdType.Spec.VcdKe.DefaultStorageClassOptions.UseDeleteReclaimPolicy {
   336  			result.DefaultStorageClass.ReclaimPolicy = "delete"
   337  		}
   338  	}
   339  
   340  	// NOTE: We get the remaining elements from the CAPI YAML, despite they are also inside capvcdType.Status.
   341  	// The reason is that any change on the cluster is immediately reflected in the CAPI YAML, but not in the capvcdType.Status
   342  	// elements, which may take more than 10 minutes to be refreshed.
   343  	yamlDocuments, err := unmarshalMultipleYamlDocuments(result.capvcdType.Spec.CapiYaml)
   344  	if err != nil {
   345  		return nil, err
   346  	}
   347  
   348  	// We need a map of worker pools and not a slice, because there are two types of YAML documents
   349  	// that contain data about a specific worker pool (VCDMachineTemplate and MachineDeployment), and we can get them in no
   350  	// particular order, so we store the worker pools with their name as key. This way we can easily (O(1)) fetch and update them.
   351  	workerPools := map[string]CseWorkerPoolSettings{}
   352  	for _, yamlDocument := range yamlDocuments {
   353  		switch yamlDocument["kind"] {
   354  		case "KubeadmControlPlane":
   355  			result.ControlPlane.MachineCount = int(traverseMapAndGet[float64](yamlDocument, "spec.replicas"))
   356  			users := traverseMapAndGet[[]interface{}](yamlDocument, "spec.kubeadmConfigSpec.users")
   357  			if len(users) == 0 {
   358  				return nil, fmt.Errorf("expected 'spec.kubeadmConfigSpec.users' slice to not to be empty")
   359  			}
   360  			keys := traverseMapAndGet[[]interface{}](users[0], "sshAuthorizedKeys")
   361  			if len(keys) > 0 {
   362  				result.SshPublicKey = keys[0].(string) // Optional field
   363  			}
   364  
   365  			version, err := semver.NewVersion(traverseMapAndGet[string](yamlDocument, "spec.version"))
   366  			if err != nil {
   367  				return nil, fmt.Errorf("could not read Kubernetes version: %s", err)
   368  			}
   369  			result.KubernetesVersion = *version
   370  
   371  		case "VCDMachineTemplate":
   372  			name := traverseMapAndGet[string](yamlDocument, "metadata.name")
   373  			sizingPolicyName := traverseMapAndGet[string](yamlDocument, "spec.template.spec.sizingPolicy")
   374  			placementPolicyName := traverseMapAndGet[string](yamlDocument, "spec.template.spec.placementPolicy")
   375  			storageProfileName := traverseMapAndGet[string](yamlDocument, "spec.template.spec.storageProfile")
   376  			diskSizeGi, err := strconv.Atoi(strings.ReplaceAll(traverseMapAndGet[string](yamlDocument, "spec.template.spec.diskSize"), "Gi", ""))
   377  			if err != nil {
   378  				return nil, err
   379  			}
   380  
   381  			if strings.Contains(name, "control-plane-node-pool") {
   382  				// This is the single Control Plane
   383  				for _, policy := range computePolicies {
   384  					if sizingPolicyName == policy.VdcComputePolicyV2.Name && policy.VdcComputePolicyV2.IsSizingOnly {
   385  						result.ControlPlane.SizingPolicyId = policy.VdcComputePolicyV2.ID
   386  					} else if placementPolicyName == policy.VdcComputePolicyV2.Name && !policy.VdcComputePolicyV2.IsSizingOnly {
   387  						result.ControlPlane.PlacementPolicyId = policy.VdcComputePolicyV2.ID
   388  					}
   389  				}
   390  				for spName, spId := range storageProfiles {
   391  					if storageProfileName == spName {
   392  						result.ControlPlane.StorageProfileId = spId
   393  					}
   394  				}
   395  
   396  				result.ControlPlane.DiskSizeGi = diskSizeGi
   397  
   398  				// We retrieve the Kubernetes Template OVA just once for the Control Plane because all YAML blocks share the same
   399  				vAppTemplateName := traverseMapAndGet[string](yamlDocument, "spec.template.spec.template")
   400  				catalogName := traverseMapAndGet[string](yamlDocument, "spec.template.spec.catalog")
   401  				vAppTemplates, err := queryVappTemplateListWithFilter(rde.client, map[string]string{
   402  					"catalogName": catalogName,
   403  					"name":        vAppTemplateName,
   404  				})
   405  				if err != nil {
   406  					return nil, fmt.Errorf("could not find any vApp Template with name '%s' in Catalog '%s': %s", vAppTemplateName, catalogName, err)
   407  				}
   408  				if len(vAppTemplates) == 0 {
   409  					return nil, fmt.Errorf("could not find any vApp Template with name '%s' in Catalog '%s'", vAppTemplateName, catalogName)
   410  				}
   411  				// The records don't have ID set, so we calculate it
   412  				result.KubernetesTemplateOvaId = fmt.Sprintf("urn:vcloud:vapptemplate:%s", extractUuid(vAppTemplates[0].HREF))
   413  			} else {
   414  				// This is one Worker Pool. We need to check the map of worker pools, just in case we already saved the
   415  				// machine count from MachineDeployment.
   416  				if _, ok := workerPools[name]; !ok {
   417  					workerPools[name] = CseWorkerPoolSettings{}
   418  				}
   419  				workerPool := workerPools[name]
   420  				workerPool.Name = name
   421  				for _, policy := range computePolicies {
   422  					if sizingPolicyName == policy.VdcComputePolicyV2.Name && policy.VdcComputePolicyV2.IsSizingOnly {
   423  						workerPool.SizingPolicyId = policy.VdcComputePolicyV2.ID
   424  					} else if placementPolicyName == policy.VdcComputePolicyV2.Name && !policy.VdcComputePolicyV2.IsSizingOnly && !policy.VdcComputePolicyV2.IsVgpuPolicy {
   425  						workerPool.PlacementPolicyId = policy.VdcComputePolicyV2.ID
   426  					} else if placementPolicyName == policy.VdcComputePolicyV2.Name && !policy.VdcComputePolicyV2.IsSizingOnly && policy.VdcComputePolicyV2.IsVgpuPolicy {
   427  						workerPool.VGpuPolicyId = policy.VdcComputePolicyV2.ID
   428  					}
   429  				}
   430  				for spName, spId := range storageProfiles {
   431  					if storageProfileName == spName {
   432  						workerPool.StorageProfileId = spId
   433  					}
   434  				}
   435  				workerPool.DiskSizeGi = diskSizeGi
   436  				workerPools[name] = workerPool // Override the worker pool with the updated data
   437  			}
   438  		case "MachineDeployment":
   439  			name := traverseMapAndGet[string](yamlDocument, "metadata.name")
   440  			// This is one Worker Pool. We need to check the map of worker pools, just in case we already saved the
   441  			// other information from VCDMachineTemplate.
   442  			if _, ok := workerPools[name]; !ok {
   443  				workerPools[name] = CseWorkerPoolSettings{}
   444  			}
   445  			workerPool := workerPools[name]
   446  			workerPool.MachineCount = int(traverseMapAndGet[float64](yamlDocument, "spec.replicas"))
   447  			workerPools[name] = workerPool // Override the worker pool with the updated data
   448  		case "VCDCluster":
   449  			result.VirtualIpSubnet = traverseMapAndGet[string](yamlDocument, "spec.loadBalancerConfigSpec.vipSubnet")
   450  		case "Cluster":
   451  			version, err := semver.NewVersion(traverseMapAndGet[string](yamlDocument, "metadata.annotations.TKGVERSION"))
   452  			if err != nil {
   453  				return nil, fmt.Errorf("could not read TKG version: %s", err)
   454  			}
   455  			result.TkgVersion = *version
   456  
   457  			cidrBlocks := traverseMapAndGet[[]interface{}](yamlDocument, "spec.clusterNetwork.pods.cidrBlocks")
   458  			if len(cidrBlocks) == 0 {
   459  				return nil, fmt.Errorf("expected at least one 'spec.clusterNetwork.pods.cidrBlocks' item")
   460  			}
   461  			result.PodCidr = cidrBlocks[0].(string)
   462  
   463  			cidrBlocks = traverseMapAndGet[[]interface{}](yamlDocument, "spec.clusterNetwork.services.cidrBlocks")
   464  			if len(cidrBlocks) == 0 {
   465  				return nil, fmt.Errorf("expected at least one 'spec.clusterNetwork.services.cidrBlocks' item")
   466  			}
   467  			result.ServiceCidr = cidrBlocks[0].(string)
   468  		case "MachineHealthCheck":
   469  			// This is quite simple, if we find this document, means that Machine Health Check is enabled
   470  			result.NodeHealthCheck = true
   471  		}
   472  	}
   473  	result.WorkerPools = make([]CseWorkerPoolSettings, len(workerPools))
   474  	i := 0
   475  	for _, workerPool := range workerPools {
   476  		result.WorkerPools[i] = workerPool
   477  		i++
   478  	}
   479  
   480  	return result, nil
   481  }
   482  
   483  // waitUntilClusterIsProvisioned waits for the Kubernetes cluster to be in "provisioned" state, either indefinitely (if timeout = 0)
   484  // or until the timeout is reached.
   485  // If one of the states of the cluster at a given point is "error", this function also checks whether the cluster has the "AutoRepairOnErrors" flag enabled,
   486  // so it keeps waiting if it's true.
   487  // If timeout is reached before the cluster is in "provisioned" state, it returns an error.
   488  func waitUntilClusterIsProvisioned(client *Client, clusterId string, timeout time.Duration) error {
   489  	var elapsed time.Duration
   490  	sleepTime := 10
   491  
   492  	start := time.Now()
   493  	capvcd := &types.Capvcd{}
   494  	for elapsed <= timeout || timeout == 0 { // If the user specifies timeout=0, we wait forever
   495  		rde, err := getRdeById(client, clusterId)
   496  		if err != nil {
   497  			return err
   498  		}
   499  
   500  		// Here we don't use cseConvertToCseKubernetesClusterType to avoid calling VCD. We only need the state.
   501  		entityBytes, err := json.Marshal(rde.DefinedEntity.Entity)
   502  		if err != nil {
   503  			return fmt.Errorf("could not check the Kubernetes cluster state: %s", err)
   504  		}
   505  		err = json.Unmarshal(entityBytes, &capvcd)
   506  		if err != nil {
   507  			return fmt.Errorf("could not check the Kubernetes cluster state: %s", err)
   508  		}
   509  
   510  		switch capvcd.Status.VcdKe.State {
   511  		case "provisioned":
   512  			return nil
   513  		case "error":
   514  			// We just finish if auto-recovery is disabled, otherwise we just let CSE fixing things in background
   515  			if !capvcd.Spec.VcdKe.AutoRepairOnErrors {
   516  				// Give feedback about what went wrong
   517  				errors := ""
   518  				for _, event := range capvcd.Status.Capvcd.ErrorSet {
   519  					errors += fmt.Sprintf("%s,\n", event.AdditionalDetails.DetailedError)
   520  				}
   521  				return fmt.Errorf("got an error and 'AutoRepairOnErrors' is disabled, aborting. Error events:\n%s", errors)
   522  			}
   523  		}
   524  
   525  		util.Logger.Printf("[DEBUG] Cluster '%s' is in '%s' state, will check again in %d seconds", rde.DefinedEntity.ID, capvcd.Status.VcdKe.State, sleepTime)
   526  		elapsed = time.Since(start)
   527  		time.Sleep(time.Duration(sleepTime) * time.Second)
   528  	}
   529  	return fmt.Errorf("timeout of %s reached, latest cluster state obtained was '%s'", timeout, capvcd.Status.VcdKe.State)
   530  }
   531  
   532  // validate validates the receiver CseClusterSettings. Returns an error if any of the fields is empty or wrong.
   533  func (input *CseClusterSettings) validate() error {
   534  	if input == nil {
   535  		return fmt.Errorf("the receiver CseClusterSettings cannot be nil")
   536  	}
   537  	// This regular expression is used to validate the constraints placed by Container Service Extension on the names
   538  	// of the components of the Kubernetes clusters:
   539  	// Names must contain only lowercase alphanumeric characters or '-', start with an alphabetic character, end with an alphanumeric, and contain at most 31 characters.
   540  	cseNamesRegex, err := regexp.Compile(`^[a-z](?:[a-z0-9-]{0,29}[a-z0-9])?$`)
   541  	if err != nil {
   542  		return fmt.Errorf("could not compile regular expression '%s'", err)
   543  	}
   544  
   545  	_, err = getCseComponentsVersions(input.CseVersion)
   546  	if err != nil {
   547  		return err
   548  	}
   549  	if !cseNamesRegex.MatchString(input.Name) {
   550  		return fmt.Errorf("the name '%s' must contain only lowercase alphanumeric characters or '-', start with an alphabetic character, end with an alphanumeric, and contain at most 31 characters", input.Name)
   551  	}
   552  	if input.OrganizationId == "" {
   553  		return fmt.Errorf("the Organization ID is required")
   554  	}
   555  	if input.VdcId == "" {
   556  		return fmt.Errorf("the VDC ID is required")
   557  	}
   558  	if input.NetworkId == "" {
   559  		return fmt.Errorf("the Network ID is required")
   560  	}
   561  	if input.KubernetesTemplateOvaId == "" {
   562  		return fmt.Errorf("the Kubernetes Template OVA ID is required")
   563  	}
   564  	if input.ControlPlane.MachineCount < 1 || input.ControlPlane.MachineCount%2 == 0 {
   565  		return fmt.Errorf("number of Control Plane nodes must be odd and higher than 0, but it was '%d'", input.ControlPlane.MachineCount)
   566  	}
   567  	if input.ControlPlane.DiskSizeGi < 20 {
   568  		return fmt.Errorf("disk size for the Control Plane in Gibibytes (Gi) must be at least 20, but it was '%d'", input.ControlPlane.DiskSizeGi)
   569  	}
   570  	if len(input.WorkerPools) == 0 {
   571  		return fmt.Errorf("there must be at least one Worker Pool")
   572  	}
   573  	existingWorkerPools := map[string]bool{}
   574  	for _, workerPool := range input.WorkerPools {
   575  		if _, alreadyExists := existingWorkerPools[workerPool.Name]; alreadyExists {
   576  			return fmt.Errorf("the names of the Worker Pools must be unique, but '%s' is repeated", workerPool.Name)
   577  		}
   578  		if workerPool.MachineCount < 1 {
   579  			return fmt.Errorf("number of Worker Pool '%s' nodes must higher than 0, but it was '%d'", workerPool.Name, workerPool.MachineCount)
   580  		}
   581  		if workerPool.DiskSizeGi < 20 {
   582  			return fmt.Errorf("disk size for the Worker Pool '%s' in Gibibytes (Gi) must be at least 20, but it was '%d'", workerPool.Name, workerPool.DiskSizeGi)
   583  		}
   584  		if !cseNamesRegex.MatchString(workerPool.Name) {
   585  			return fmt.Errorf("the Worker Pool name '%s' must contain only lowercase alphanumeric characters or '-', start with an alphabetic character, end with an alphanumeric, and contain at most 31 characters", workerPool.Name)
   586  		}
   587  		existingWorkerPools[workerPool.Name] = true
   588  	}
   589  	if input.DefaultStorageClass != nil { // This field is optional
   590  		if !cseNamesRegex.MatchString(input.DefaultStorageClass.Name) {
   591  			return fmt.Errorf("the Default Storage Class name '%s' must contain only lowercase alphanumeric characters or '-', start with an alphabetic character, end with an alphanumeric, and contain at most 31 characters", input.DefaultStorageClass.Name)
   592  		}
   593  		if input.DefaultStorageClass.StorageProfileId == "" {
   594  			return fmt.Errorf("the Storage Profile ID for the Default Storage Class is required")
   595  		}
   596  		if input.DefaultStorageClass.ReclaimPolicy != "delete" && input.DefaultStorageClass.ReclaimPolicy != "retain" {
   597  			return fmt.Errorf("the Reclaim Policy for the Default Storage Class must be either 'delete' or 'retain', but it was '%s'", input.DefaultStorageClass.ReclaimPolicy)
   598  		}
   599  		if input.DefaultStorageClass.Filesystem != "ext4" && input.DefaultStorageClass.ReclaimPolicy != "xfs" {
   600  			return fmt.Errorf("the filesystem for the Default Storage Class must be either 'ext4' or 'xfs', but it was '%s'", input.DefaultStorageClass.Filesystem)
   601  		}
   602  	}
   603  	if input.ApiToken == "" {
   604  		return fmt.Errorf("the API token is required")
   605  	}
   606  	if input.PodCidr == "" {
   607  		return fmt.Errorf("the Pod CIDR is required")
   608  	}
   609  	if _, _, err := net.ParseCIDR(input.PodCidr); err != nil {
   610  		return fmt.Errorf("the Pod CIDR is malformed: %s", err)
   611  	}
   612  	if input.ServiceCidr == "" {
   613  		return fmt.Errorf("the Service CIDR is required")
   614  	}
   615  	if _, _, err := net.ParseCIDR(input.ServiceCidr); err != nil {
   616  		return fmt.Errorf("the Service CIDR is malformed: %s", err)
   617  	}
   618  	if input.VirtualIpSubnet != "" {
   619  		if _, _, err := net.ParseCIDR(input.VirtualIpSubnet); err != nil {
   620  			return fmt.Errorf("the Virtual IP Subnet is malformed: %s", err)
   621  		}
   622  	}
   623  	if input.ControlPlane.Ip != "" {
   624  		if r := net.ParseIP(input.ControlPlane.Ip); r == nil {
   625  			return fmt.Errorf("the Control Plane IP is malformed: %s", input.ControlPlane.Ip)
   626  		}
   627  	}
   628  	return nil
   629  }
   630  
   631  // toCseClusterSettingsInternal transforms user input data (CseClusterSettings) into the final payload that
   632  // will be used to define a Container Service Extension Kubernetes cluster (cseClusterSettingsInternal).
   633  //
   634  // For example, the most relevant transformation is the change of the item IDs that are present in CseClusterSettings
   635  // (such as CseClusterSettings.KubernetesTemplateOvaId) to their corresponding Names (e.g. cseClusterSettingsInternal.KubernetesTemplateOvaName),
   636  // which are the identifiers that Container Service Extension uses internally.
   637  func (input *CseClusterSettings) toCseClusterSettingsInternal(org Org) (*cseClusterSettingsInternal, error) {
   638  	err := input.validate()
   639  	if err != nil {
   640  		return nil, err
   641  	}
   642  
   643  	output := &cseClusterSettingsInternal{}
   644  	if org.Org == nil {
   645  		return nil, fmt.Errorf("could not retrieve the Organization, it is nil")
   646  	}
   647  	output.OrganizationName = org.Org.Name
   648  
   649  	vdc, err := org.GetVDCById(input.VdcId, true)
   650  	if err != nil {
   651  		return nil, fmt.Errorf("could not retrieve the VDC with ID '%s': %s", input.VdcId, err)
   652  	}
   653  	output.VdcName = vdc.Vdc.Name
   654  
   655  	vAppTemplate, err := getVAppTemplateById(org.client, input.KubernetesTemplateOvaId)
   656  	if err != nil {
   657  		return nil, fmt.Errorf("could not retrieve the Kubernetes Template OVA with ID '%s': %s", input.KubernetesTemplateOvaId, err)
   658  	}
   659  	output.KubernetesTemplateOvaName = vAppTemplate.VAppTemplate.Name
   660  
   661  	tkgVersions, err := getTkgVersionBundleFromVAppTemplate(vAppTemplate.VAppTemplate)
   662  	if err != nil {
   663  		return nil, fmt.Errorf("could not retrieve the required information from the Kubernetes Template OVA: %s", err)
   664  	}
   665  	output.TkgVersionBundle = tkgVersions
   666  
   667  	catalogName, err := vAppTemplate.GetCatalogName()
   668  	if err != nil {
   669  		return nil, fmt.Errorf("could not retrieve the Catalog name where the the Kubernetes Template OVA '%s' (%s) is hosted: %s", input.KubernetesTemplateOvaId, vAppTemplate.VAppTemplate.Name, err)
   670  	}
   671  	output.CatalogName = catalogName
   672  
   673  	network, err := vdc.GetOrgVdcNetworkById(input.NetworkId, true)
   674  	if err != nil {
   675  		return nil, fmt.Errorf("could not retrieve the Org VDC Network with ID '%s': %s", input.NetworkId, err)
   676  	}
   677  	output.NetworkName = network.OrgVDCNetwork.Name
   678  
   679  	cseComponentsVersions, err := getCseComponentsVersions(input.CseVersion)
   680  	if err != nil {
   681  		return nil, err
   682  	}
   683  	rdeType, err := getRdeType(org.client, cseKubernetesClusterVendor, cseKubernetesClusterNamespace, cseComponentsVersions.CapvcdRdeTypeVersion)
   684  	if err != nil {
   685  		return nil, err
   686  	}
   687  	output.RdeType = rdeType.DefinedEntityType
   688  
   689  	// Gather all the IDs of the Compute Policies and Storage Profiles, so we can transform them to Names in bulk.
   690  	var computePolicyIds []string
   691  	var storageProfileIds []string
   692  	for _, w := range input.WorkerPools {
   693  		computePolicyIds = append(computePolicyIds, w.SizingPolicyId, w.PlacementPolicyId, w.VGpuPolicyId)
   694  		storageProfileIds = append(storageProfileIds, w.StorageProfileId)
   695  	}
   696  	computePolicyIds = append(computePolicyIds, input.ControlPlane.SizingPolicyId, input.ControlPlane.PlacementPolicyId)
   697  	storageProfileIds = append(storageProfileIds, input.ControlPlane.StorageProfileId)
   698  	if input.DefaultStorageClass != nil {
   699  		storageProfileIds = append(storageProfileIds, input.DefaultStorageClass.StorageProfileId)
   700  	}
   701  
   702  	idToNameCache, err := idToNames(org.client, computePolicyIds, storageProfileIds)
   703  	if err != nil {
   704  		return nil, err
   705  	}
   706  
   707  	// Now that everything is cached in memory, we can build the Node pools and Storage Class payloads in a trivial way.
   708  	output.WorkerPools = make([]cseWorkerPoolSettingsInternal, len(input.WorkerPools))
   709  	for i, w := range input.WorkerPools {
   710  		output.WorkerPools[i] = cseWorkerPoolSettingsInternal{
   711  			Name:         w.Name,
   712  			MachineCount: w.MachineCount,
   713  			DiskSizeGi:   w.DiskSizeGi,
   714  		}
   715  		output.WorkerPools[i].SizingPolicyName = idToNameCache[w.SizingPolicyId]
   716  		output.WorkerPools[i].PlacementPolicyName = idToNameCache[w.PlacementPolicyId]
   717  		output.WorkerPools[i].VGpuPolicyName = idToNameCache[w.VGpuPolicyId]
   718  		output.WorkerPools[i].StorageProfileName = idToNameCache[w.StorageProfileId]
   719  	}
   720  	output.ControlPlane = cseControlPlaneSettingsInternal{
   721  		MachineCount:        input.ControlPlane.MachineCount,
   722  		DiskSizeGi:          input.ControlPlane.DiskSizeGi,
   723  		SizingPolicyName:    idToNameCache[input.ControlPlane.SizingPolicyId],
   724  		PlacementPolicyName: idToNameCache[input.ControlPlane.PlacementPolicyId],
   725  		StorageProfileName:  idToNameCache[input.ControlPlane.StorageProfileId],
   726  		Ip:                  input.ControlPlane.Ip,
   727  	}
   728  
   729  	if input.DefaultStorageClass != nil {
   730  		output.DefaultStorageClass = cseDefaultStorageClassInternal{
   731  			StorageProfileName: idToNameCache[input.DefaultStorageClass.StorageProfileId],
   732  			Name:               input.DefaultStorageClass.Name,
   733  			Filesystem:         input.DefaultStorageClass.Filesystem,
   734  		}
   735  		output.DefaultStorageClass.UseDeleteReclaimPolicy = false
   736  		if input.DefaultStorageClass.ReclaimPolicy == "delete" {
   737  			output.DefaultStorageClass.UseDeleteReclaimPolicy = true
   738  		}
   739  	}
   740  
   741  	vcdKeConfig, err := getVcdKeConfig(org.client, cseComponentsVersions.VcdKeConfigRdeTypeVersion, input.NodeHealthCheck)
   742  	if err != nil {
   743  		return nil, err
   744  	}
   745  	output.VcdKeConfig = vcdKeConfig
   746  
   747  	output.Owner = input.Owner
   748  	if input.Owner == "" {
   749  		sessionInfo, err := org.client.GetSessionInfo()
   750  		if err != nil {
   751  			return nil, fmt.Errorf("error getting the Owner: %s", err)
   752  		}
   753  		output.Owner = sessionInfo.User.Name
   754  	}
   755  
   756  	output.VcdUrl = strings.Replace(org.client.VCDHREF.String(), "/api", "", 1)
   757  
   758  	// These don't change, don't need mapping
   759  	output.ApiToken = input.ApiToken
   760  	output.AutoRepairOnErrors = input.AutoRepairOnErrors
   761  	output.CseVersion = input.CseVersion
   762  	output.Name = input.Name
   763  	output.PodCidr = input.PodCidr
   764  	output.ServiceCidr = input.ServiceCidr
   765  	output.SshPublicKey = input.SshPublicKey
   766  	output.VirtualIpSubnet = input.VirtualIpSubnet
   767  
   768  	return output, nil
   769  }
   770  
   771  // getTkgVersionBundleFromVAppTemplate returns a tkgVersionBundle with the details of
   772  // all the Kubernetes cluster components versions given a valid Kubernetes Template OVA.
   773  // If it is not a valid Kubernetes Template OVA, returns an error.
   774  func getTkgVersionBundleFromVAppTemplate(template *types.VAppTemplate) (tkgVersionBundle, error) {
   775  	result := tkgVersionBundle{}
   776  	if template == nil {
   777  		return result, fmt.Errorf("the Kubernetes Template OVA is nil")
   778  	}
   779  	if template.Children == nil || len(template.Children.VM) == 0 {
   780  		return result, fmt.Errorf("the Kubernetes Template OVA '%s' doesn't have any child VM", template.Name)
   781  	}
   782  	if template.Children.VM[0].ProductSection == nil {
   783  		return result, fmt.Errorf("the Product section of the Kubernetes Template OVA '%s' is empty, can't proceed", template.Name)
   784  	}
   785  	id := ""
   786  	for _, prop := range template.Children.VM[0].ProductSection.Property {
   787  		if prop != nil && prop.Key == "VERSION" {
   788  			id = prop.DefaultValue // Use DefaultValue and not Value as the value we want is in the "value" attr
   789  		}
   790  	}
   791  	if id == "" {
   792  		return result, fmt.Errorf("could not find any VERSION property inside the Kubernetes Template OVA '%s' Product section", template.Name)
   793  	}
   794  
   795  	tkgVersionsMap := "cse/tkg_versions.json"
   796  	cseTkgVersionsJson, err := cseFiles.ReadFile(tkgVersionsMap)
   797  	if err != nil {
   798  		return result, fmt.Errorf("failed reading %s: %s", tkgVersionsMap, err)
   799  	}
   800  
   801  	versionsMap := map[string]interface{}{}
   802  	err = json.Unmarshal(cseTkgVersionsJson, &versionsMap)
   803  	if err != nil {
   804  		return result, fmt.Errorf("failed unmarshalling %s: %s", tkgVersionsMap, err)
   805  	}
   806  	versionMap, ok := versionsMap[id]
   807  	if !ok {
   808  		return result, fmt.Errorf("the Kubernetes Template OVA '%s' is not supported", template.Name)
   809  	}
   810  
   811  	// We don't need to check the Split result because the map checking above guarantees that the ID is well-formed.
   812  	idParts := strings.Split(id, "-")
   813  	result.KubernetesVersion = idParts[0]
   814  	result.TkrVersion = versionMap.(map[string]interface{})["tkr"].(string)
   815  	result.TkgVersion = versionMap.(map[string]interface{})["tkg"].(string)
   816  	result.EtcdVersion = versionMap.(map[string]interface{})["etcd"].(string)
   817  	result.CoreDnsVersion = versionMap.(map[string]interface{})["coreDns"].(string)
   818  	return result, nil
   819  }
   820  
   821  // compareTkgVersion returns -1, 0 or 1 if the receiver TKG version is less than, equal or higher to the input TKG version.
   822  // If they cannot be compared it returns -2.
   823  func (tkgVersions tkgVersionBundle) compareTkgVersion(tkgVersion string) int {
   824  	receiverVersion, err := semver.NewVersion(tkgVersions.TkgVersion)
   825  	if err != nil {
   826  		return -2
   827  	}
   828  	inputVersion, err := semver.NewVersion(tkgVersion)
   829  	if err != nil {
   830  		return -2
   831  	}
   832  	return receiverVersion.Compare(inputVersion)
   833  }
   834  
   835  // kubernetesVersionIsUpgradeableFrom returns true either if the receiver Kubernetes version is exactly one minor version higher
   836  // than the given input version (being the minor digit the 'Y' in 'X.Y.Z') or if the minor is the same, but the patch is higher
   837  // (being the minor digit the 'Z' in 'X.Y.Z').
   838  // Any malformed version returns false.
   839  // Examples:
   840  // * "1.19.2".kubernetesVersionIsUpgradeableFrom("1.18.7") = true
   841  // * "1.19.2".kubernetesVersionIsUpgradeableFrom("1.19.2") = false
   842  // * "1.19.2".kubernetesVersionIsUpgradeableFrom("1.19.0") = true
   843  // * "1.19.10".kubernetesVersionIsUpgradeableFrom("1.18.0") = true
   844  // * "1.20.2".kubernetesVersionIsUpgradeableFrom("1.18.7") = false
   845  // * "1.21.2".kubernetesVersionIsUpgradeableFrom("1.18.7") = false
   846  // * "1.18.0".kubernetesVersionIsUpgradeableFrom("1.18.7") = false
   847  func (tkgVersions tkgVersionBundle) kubernetesVersionIsUpgradeableFrom(kubernetesVersion string) bool {
   848  	upgradeToVersion, err := semver.NewVersion(tkgVersions.KubernetesVersion)
   849  	if err != nil {
   850  		return false
   851  	}
   852  	fromVersion, err := semver.NewVersion(kubernetesVersion)
   853  	if err != nil {
   854  		return false
   855  	}
   856  
   857  	if upgradeToVersion.Equal(fromVersion) {
   858  		return false
   859  	}
   860  
   861  	upgradeToVersionSegments := upgradeToVersion.Segments()
   862  	if len(upgradeToVersionSegments) < 2 {
   863  		return false
   864  	}
   865  	fromVersionSegments := fromVersion.Segments()
   866  	if len(fromVersionSegments) < 2 {
   867  		return false
   868  	}
   869  
   870  	majorIsEqual := upgradeToVersionSegments[0] == fromVersionSegments[0]
   871  	minorIsJustOneHigher := upgradeToVersionSegments[1]-1 == fromVersionSegments[1]
   872  	minorIsEqual := upgradeToVersionSegments[1] == fromVersionSegments[1]
   873  	patchIsHigher := upgradeToVersionSegments[2] > fromVersionSegments[2]
   874  
   875  	return majorIsEqual && (minorIsJustOneHigher || (minorIsEqual && patchIsHigher))
   876  }
   877  
   878  // getVcdKeConfig gets the required information from the CSE Server configuration RDE (VCDKEConfig), such as the
   879  // Machine Health Check settings and the Container Registry URL.
   880  func getVcdKeConfig(client *Client, vcdKeConfigVersion string, retrieveMachineHealtchCheckInfo bool) (vcdKeConfig, error) {
   881  	result := vcdKeConfig{}
   882  	rdes, err := getRdesByName(client, "vmware", "VCDKEConfig", vcdKeConfigVersion, "vcdKeConfig")
   883  	if err != nil {
   884  		return result, err
   885  	}
   886  	if len(rdes) != 1 {
   887  		return result, fmt.Errorf("expected exactly one VCDKEConfig RDE with version '%s', but got %d", vcdKeConfigVersion, len(rdes))
   888  	}
   889  
   890  	profiles, ok := rdes[0].DefinedEntity.Entity["profiles"].([]interface{})
   891  	if !ok {
   892  		return result, fmt.Errorf("wrong format of VCDKEConfig RDE contents, expected a 'profiles' array")
   893  	}
   894  	if len(profiles) == 0 {
   895  		return result, fmt.Errorf("wrong format of VCDKEConfig RDE contents, expected a non-empty 'profiles' element")
   896  	}
   897  
   898  	// We append /tkg as required, even in air-gapped environments:
   899  	// https://docs.vmware.com/en/VMware-Cloud-Director-Container-Service-Extension/4.2/VMware-Cloud-Director-Container-Service-Extension-Install-provider-4.2/GUID-B5C19221-2ECA-4DCD-8EA1-8E391F6217C1.html
   900  	result.ContainerRegistryUrl = fmt.Sprintf("%s/tkg", profiles[0].(map[string]interface{})["containerRegistryUrl"])
   901  
   902  	k8sConfig, ok := profiles[0].(map[string]interface{})["K8Config"].(map[string]interface{})
   903  	if !ok {
   904  		return result, fmt.Errorf("wrong format of VCDKEConfig RDE contents, expected a 'K8Config' object")
   905  	}
   906  	certificates, ok := k8sConfig["certificateAuthorities"]
   907  	if ok {
   908  		result.Base64Certificates = make([]string, len(certificates.([]interface{})))
   909  		for i, certificate := range certificates.([]interface{}) {
   910  			result.Base64Certificates[i] = base64.StdEncoding.EncodeToString([]byte(certificate.(string)))
   911  		}
   912  	}
   913  
   914  	if retrieveMachineHealtchCheckInfo {
   915  		mhc, ok := profiles[0].(map[string]interface{})["K8Config"].(map[string]interface{})["mhc"]
   916  		if !ok {
   917  			// If there is no "mhc" entry in the VCDKEConfig JSON, we skip setting this part of the Kubernetes cluster configuration
   918  			return result, nil
   919  		}
   920  		result.MaxUnhealthyNodesPercentage = mhc.(map[string]interface{})["maxUnhealthyNodes"].(float64)
   921  		result.NodeStartupTimeout = mhc.(map[string]interface{})["nodeStartupTimeout"].(string)
   922  		result.NodeNotReadyTimeout = mhc.(map[string]interface{})["nodeUnknownTimeout"].(string)
   923  		result.NodeUnknownTimeout = mhc.(map[string]interface{})["nodeNotReadyTimeout"].(string)
   924  	}
   925  
   926  	return result, nil
   927  }
   928  
   929  // idToNames returns a map that associates Compute Policies/Storage Profiles IDs with their respective names.
   930  // This is useful as the input to create/update a cluster uses different entities IDs, but CSE cluster creation/update process uses Names.
   931  // For that reason, we need to transform IDs to Names by querying VCD
   932  func idToNames(client *Client, computePolicyIds, storageProfileIds []string) (map[string]string, error) {
   933  	result := map[string]string{
   934  		"": "", // Default empty value to map optional values that were not set, to avoid extra checks. For example, an empty vGPU Policy.
   935  	}
   936  	// Retrieve the Compute Policies and Storage Profiles names and put them in the resulting map. This map also can
   937  	// be used to reduce the calls to VCD. The URN format used by VCD guarantees that IDs are unique, so there is no possibility of clashes here.
   938  	for _, id := range storageProfileIds {
   939  		if _, alreadyPresent := result[id]; !alreadyPresent {
   940  			storageProfile, err := getStorageProfileById(client, id)
   941  			if err != nil {
   942  				return nil, fmt.Errorf("could not retrieve Storage Profile with ID '%s': %s", id, err)
   943  			}
   944  			result[id] = storageProfile.Name
   945  		}
   946  	}
   947  	for _, id := range computePolicyIds {
   948  		if _, alreadyPresent := result[id]; !alreadyPresent {
   949  			computePolicy, err := getVdcComputePolicyV2ById(client, id)
   950  			if err != nil {
   951  				return nil, fmt.Errorf("could not retrieve Compute Policy with ID '%s': %s", id, err)
   952  			}
   953  			result[id] = computePolicy.VdcComputePolicyV2.Name
   954  		}
   955  	}
   956  	return result, nil
   957  }
   958  
   959  // getCseTemplate reads the Go template present in the embedded cseFiles filesystem.
   960  func getCseTemplate(cseVersion semver.Version, templateName string) (string, error) {
   961  	minimumVersion, err := semver.NewVersion("4.1")
   962  	if err != nil {
   963  		return "", err
   964  	}
   965  	if cseVersion.LessThan(minimumVersion) {
   966  		return "", fmt.Errorf("the Container Service minimum version is '%s'", minimumVersion.String())
   967  	}
   968  	versionSegments := cseVersion.Segments()
   969  	// We try with major.minor.patch
   970  	fullTemplatePath := fmt.Sprintf("cse/%d.%d.%d/%s.tmpl", versionSegments[0], versionSegments[1], versionSegments[2], templateName)
   971  	result, err := cseFiles.ReadFile(fullTemplatePath)
   972  	if err != nil {
   973  		// We try now just with major.minor
   974  		fullTemplatePath = fmt.Sprintf("cse/%d.%d/%s.tmpl", versionSegments[0], versionSegments[1], templateName)
   975  		result, err = cseFiles.ReadFile(fullTemplatePath)
   976  		if err != nil {
   977  			return "", fmt.Errorf("could not read Go template '%s.tmpl' for CSE version %s", templateName, cseVersion.String())
   978  		}
   979  	}
   980  	return string(result), nil
   981  }