github.com/openshift/installer@v1.4.17/pkg/asset/installconfig/ibmcloud/validation.go (about)

     1  package ibmcloud
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"net/http"
     8  	"net/url"
     9  
    10  	"github.com/IBM/vpc-go-sdk/vpcv1"
    11  	"k8s.io/apimachinery/pkg/util/sets"
    12  	"k8s.io/apimachinery/pkg/util/validation/field"
    13  
    14  	configv1 "github.com/openshift/api/config/v1"
    15  	"github.com/openshift/installer/pkg/types"
    16  	"github.com/openshift/installer/pkg/types/ibmcloud"
    17  )
    18  
    19  // Validate executes platform-specific validation.
    20  func Validate(client API, ic *types.InstallConfig) error {
    21  	allErrs := field.ErrorList{}
    22  	platformPath := field.NewPath("platform").Child("ibmcloud")
    23  	allErrs = append(allErrs, validatePlatform(client, ic, platformPath)...)
    24  
    25  	if ic.ControlPlane != nil && ic.ControlPlane.Platform.IBMCloud != nil {
    26  		machinePool := ic.ControlPlane.Platform.IBMCloud
    27  		fldPath := field.NewPath("controlPlane").Child("platform").Child("ibmcloud")
    28  		allErrs = append(allErrs, validateMachinePool(client, ic.Platform.IBMCloud, machinePool, fldPath)...)
    29  	}
    30  	for idx, compute := range ic.Compute {
    31  		machinePool := compute.Platform.IBMCloud
    32  		fldPath := field.NewPath("compute").Index(idx).Child("platform").Child("ibmcloud")
    33  		if machinePool != nil {
    34  			allErrs = append(allErrs, validateMachinePool(client, ic.Platform.IBMCloud, machinePool, fldPath)...)
    35  		}
    36  	}
    37  
    38  	return allErrs.ToAggregate()
    39  }
    40  
    41  func validatePlatform(client API, ic *types.InstallConfig, path *field.Path) field.ErrorList {
    42  	allErrs := field.ErrorList{}
    43  
    44  	if ic.Platform.IBMCloud.ResourceGroupName != "" {
    45  		allErrs = append(allErrs, validateResourceGroup(client, ic.IBMCloud.ResourceGroupName, "resourceGroupName", path)...)
    46  	}
    47  
    48  	if ic.Platform.IBMCloud.NetworkResourceGroupName != "" || ic.Platform.IBMCloud.VPCName != "" {
    49  		allErrs = append(allErrs, validateExistingVPC(client, ic, path)...)
    50  	}
    51  
    52  	if ic.Platform.IBMCloud.DefaultMachinePlatform != nil {
    53  		allErrs = append(allErrs, validateMachinePool(client, ic.IBMCloud, ic.Platform.IBMCloud.DefaultMachinePlatform, path)...)
    54  	}
    55  	return allErrs
    56  }
    57  
    58  func validateMachinePool(client API, platform *ibmcloud.Platform, machinePool *ibmcloud.MachinePool, path *field.Path) field.ErrorList {
    59  	allErrs := field.ErrorList{}
    60  
    61  	if machinePool.InstanceType != "" {
    62  		allErrs = append(allErrs, validateMachinePoolType(client, machinePool.InstanceType, path.Child("type"))...)
    63  	}
    64  
    65  	if len(machinePool.Zones) > 0 {
    66  		allErrs = append(allErrs, validateMachinePoolZones(client, platform.Region, machinePool.Zones, path.Child("zones"))...)
    67  	}
    68  
    69  	if machinePool.BootVolume != nil {
    70  		allErrs = append(allErrs, validateMachinePoolBootVolume(client, *machinePool.BootVolume, path.Child("bootVolume"))...)
    71  	}
    72  
    73  	if len(machinePool.DedicatedHosts) > 0 {
    74  		allErrs = append(allErrs, validateMachinePoolDedicatedHosts(client, machinePool.DedicatedHosts, machinePool.InstanceType, machinePool.Zones, platform.Region, path.Child("dedicatedHosts"))...)
    75  	}
    76  
    77  	return allErrs
    78  }
    79  
    80  func validateMachinePoolDedicatedHosts(client API, dhosts []ibmcloud.DedicatedHost, machineType string, zones []string, region string, path *field.Path) field.ErrorList {
    81  	allErrs := field.ErrorList{}
    82  
    83  	// Get list of supported profiles in region
    84  	dhostProfiles, err := client.GetDedicatedHostProfiles(context.TODO(), region)
    85  	if err != nil {
    86  		allErrs = append(allErrs, field.InternalError(path, err))
    87  	}
    88  
    89  	for i, dhost := range dhosts {
    90  		if dhost.Name != "" {
    91  			// Check if host with name exists
    92  			dh, err := client.GetDedicatedHostByName(context.TODO(), dhost.Name, region)
    93  			if err != nil {
    94  				allErrs = append(allErrs, field.InternalError(path.Index(i).Child("name"), err))
    95  			}
    96  
    97  			if dh != nil {
    98  				// Check if instance is provisionable on host
    99  				if !*dh.InstancePlacementEnabled || !*dh.Provisionable {
   100  					allErrs = append(allErrs, field.Invalid(path.Index(i).Child("name"), dhost.Name, "dedicated host is unable to provision instances"))
   101  				}
   102  
   103  				// Check if host is in zone
   104  				if *dh.Zone.Name != zones[i] {
   105  					allErrs = append(allErrs, field.Invalid(path.Index(i).Child("name"), dhost.Name, fmt.Sprintf("dedicated host not in zone %s", zones[i])))
   106  				}
   107  
   108  				// Check if host profile supports machine type
   109  				if !isInstanceProfileInList(machineType, dh.SupportedInstanceProfiles) {
   110  					allErrs = append(allErrs, field.Invalid(path.Index(i).Child("name"), dhost.Name, fmt.Sprintf("dedicated host does not support machine type %s", machineType)))
   111  				}
   112  			}
   113  		} else {
   114  			// Check if host profile is supported in region
   115  			if !isDedicatedHostProfileInList(dhost.Profile, dhostProfiles) {
   116  				allErrs = append(allErrs, field.Invalid(path.Index(i).Child("profile"), dhost.Profile, fmt.Sprintf("dedicated host profile not supported in region %s", region)))
   117  			}
   118  
   119  			// Check if host profile supports machine type
   120  			for _, profile := range dhostProfiles {
   121  				if *profile.Name == dhost.Profile {
   122  					if !isInstanceProfileInList(machineType, profile.SupportedInstanceProfiles) {
   123  						allErrs = append(allErrs, field.Invalid(path.Index(i).Child("profile"), dhost.Profile, fmt.Sprintf("dedicated host profile does not support machine type %s", machineType)))
   124  						break
   125  					}
   126  				}
   127  			}
   128  		}
   129  	}
   130  
   131  	return allErrs
   132  }
   133  
   134  func isInstanceProfileInList(profile string, list []vpcv1.InstanceProfileReference) bool {
   135  	for _, each := range list {
   136  		if *each.Name == profile {
   137  			return true
   138  		}
   139  	}
   140  	return false
   141  }
   142  
   143  func isDedicatedHostProfileInList(profile string, list []vpcv1.DedicatedHostProfile) bool {
   144  	for _, each := range list {
   145  		if *each.Name == profile {
   146  			return true
   147  		}
   148  	}
   149  	return false
   150  }
   151  
   152  func validateMachinePoolType(client API, machineType string, path *field.Path) field.ErrorList {
   153  	vsiProfiles, err := client.GetVSIProfiles(context.TODO())
   154  	if err != nil {
   155  		return field.ErrorList{field.InternalError(path, err)}
   156  	}
   157  
   158  	for _, profile := range vsiProfiles {
   159  		if *profile.Name == machineType {
   160  			return nil
   161  		}
   162  	}
   163  
   164  	return field.ErrorList{field.NotFound(path, machineType)}
   165  }
   166  
   167  func validateMachinePoolZones(client API, region string, zones []string, path *field.Path) field.ErrorList {
   168  	regionalZones, err := client.GetVPCZonesForRegion(context.TODO(), region)
   169  	if err != nil {
   170  		return field.ErrorList{field.InternalError(path, err)}
   171  	}
   172  
   173  	for idx, zone := range zones {
   174  		validZones := sets.NewString(regionalZones...)
   175  		if !validZones.Has(zone) {
   176  			return field.ErrorList{field.Invalid(path.Index(idx), zone, fmt.Sprintf("zone must be in region %q", region))}
   177  		}
   178  	}
   179  	return nil
   180  }
   181  
   182  func validateMachinePoolBootVolume(client API, bootVolume ibmcloud.BootVolume, path *field.Path) field.ErrorList {
   183  	allErrs := field.ErrorList{}
   184  
   185  	if bootVolume.EncryptionKey == "" {
   186  		return allErrs
   187  	}
   188  
   189  	// Make sure the encryptionKey exists and meets requirements for use
   190  	key, err := client.GetEncryptionKey(context.TODO(), bootVolume.EncryptionKey)
   191  	if err != nil {
   192  		return field.ErrorList{field.InternalError(path.Child("encryptionKey"), err)}
   193  	}
   194  
   195  	if key == nil {
   196  		return field.ErrorList{field.NotFound(path.Child("encryptionKey"), bootVolume.EncryptionKey)}
   197  	}
   198  
   199  	if key.CRN != bootVolume.EncryptionKey {
   200  		allErrs = append(allErrs, field.Invalid(path.Child("encryptionKey"), bootVolume.EncryptionKey, fmt.Sprintf("key CRN does not match: %s", key.CRN)))
   201  	}
   202  
   203  	if key.State != 1 {
   204  		allErrs = append(allErrs, field.Invalid(path.Child("encryptionKey"), bootVolume.EncryptionKey, "key is disabled"))
   205  	}
   206  
   207  	if key.Deleted != nil && *key.Deleted {
   208  		allErrs = append(allErrs, field.Invalid(path.Child("encryptionKey"), bootVolume.EncryptionKey, "key has been deleted"))
   209  	}
   210  
   211  	return allErrs
   212  }
   213  
   214  func validateResourceGroup(client API, resourceGroupName string, platformField string, path *field.Path) field.ErrorList {
   215  	allErrs := field.ErrorList{}
   216  
   217  	if resourceGroupName == "" {
   218  		return allErrs
   219  	}
   220  
   221  	resourceGroups, err := client.GetResourceGroups(context.TODO())
   222  	if err != nil {
   223  		return append(allErrs, field.InternalError(path.Child(platformField), err))
   224  	}
   225  
   226  	found := false
   227  	for _, rg := range resourceGroups {
   228  		if *rg.ID == resourceGroupName || *rg.Name == resourceGroupName {
   229  			found = true
   230  		}
   231  	}
   232  
   233  	if !found {
   234  		return append(allErrs, field.NotFound(path.Child(platformField), resourceGroupName))
   235  	}
   236  
   237  	return allErrs
   238  }
   239  
   240  func validateExistingVPC(client API, ic *types.InstallConfig, path *field.Path) field.ErrorList {
   241  	allErrs := field.ErrorList{}
   242  
   243  	if ic.IBMCloud.VPCName == "" {
   244  		return append(allErrs, field.Invalid(path.Child("vpcName"), ic.IBMCloud.VPCName, fmt.Sprintf("vpcName cannot be empty when providing a networkResourceGroupName: %s", ic.IBMCloud.NetworkResourceGroupName)))
   245  	}
   246  
   247  	if ic.IBMCloud.NetworkResourceGroupName == "" {
   248  		return append(allErrs, field.Invalid(path.Child("networkResourceGroupName"), ic.IBMCloud.NetworkResourceGroupName, fmt.Sprintf("networkResourceGroupName cannot be empty when providing a vpcName: %s", ic.IBMCloud.VPCName)))
   249  	}
   250  	allErrs = append(allErrs, validateResourceGroup(client, ic.IBMCloud.NetworkResourceGroupName, "networkResourceGroupName", path)...)
   251  
   252  	vpcs, err := client.GetVPCs(context.TODO(), ic.IBMCloud.Region)
   253  	if err != nil {
   254  		return append(allErrs, field.InternalError(path.Child("vpcName"), err))
   255  	}
   256  
   257  	found := false
   258  	for _, vpc := range vpcs {
   259  		if *vpc.Name == ic.IBMCloud.VPCName {
   260  			if *vpc.ResourceGroup.ID != ic.IBMCloud.NetworkResourceGroupName && *vpc.ResourceGroup.Name != ic.IBMCloud.NetworkResourceGroupName {
   261  				return append(allErrs, field.Invalid(path.Child("vpcName"), ic.IBMCloud.VPCName, fmt.Sprintf("vpc is not in provided Network ResourceGroup: %s", ic.IBMCloud.NetworkResourceGroupName)))
   262  			}
   263  			found = true
   264  			allErrs = append(allErrs, validateExistingSubnets(client, ic, path, *vpc.ID)...)
   265  			break
   266  		}
   267  	}
   268  
   269  	if !found {
   270  		allErrs = append(allErrs, field.NotFound(path.Child("vpcName"), ic.IBMCloud.VPCName))
   271  	}
   272  	return allErrs
   273  }
   274  
   275  func validateExistingSubnets(client API, ic *types.InstallConfig, path *field.Path, vpcID string) field.ErrorList {
   276  	allErrs := field.ErrorList{}
   277  	var regionalZones []string
   278  
   279  	if len(ic.IBMCloud.ControlPlaneSubnets) == 0 {
   280  		allErrs = append(allErrs, field.Invalid(path.Child("controlPlaneSubnets"), ic.IBMCloud.ControlPlaneSubnets, fmt.Sprintf("controlPlaneSubnets cannot be empty when providing a vpcName: %s", ic.IBMCloud.VPCName)))
   281  	} else {
   282  		controlPlaneSubnetZones := make(map[string]int)
   283  		for _, controlPlaneSubnet := range ic.IBMCloud.ControlPlaneSubnets {
   284  			subnet, err := client.GetSubnetByName(context.TODO(), controlPlaneSubnet, ic.IBMCloud.Region)
   285  			if err != nil {
   286  				if errors.Is(err, &VPCResourceNotFoundError{}) {
   287  					allErrs = append(allErrs, field.NotFound(path.Child("controlPlaneSubnets"), controlPlaneSubnet))
   288  				} else {
   289  					allErrs = append(allErrs, field.InternalError(path.Child("controlPlaneSubnets"), err))
   290  				}
   291  			} else {
   292  				if *subnet.VPC.ID != vpcID {
   293  					allErrs = append(allErrs, field.Invalid(path.Child("controlPlaneSubnets"), controlPlaneSubnet, fmt.Sprintf("controlPlaneSubnets contains subnet: %s, not found in expected vpcID: %s", controlPlaneSubnet, vpcID)))
   294  				}
   295  				if *subnet.ResourceGroup.ID != ic.IBMCloud.NetworkResourceGroupName && *subnet.ResourceGroup.Name != ic.IBMCloud.NetworkResourceGroupName {
   296  					allErrs = append(allErrs, field.Invalid(path.Child("controlPlaneSubnets"), controlPlaneSubnet, fmt.Sprintf("controlPlaneSubnets contains subnet: %s, not found in expected networkResourceGroupName: %s", controlPlaneSubnet, ic.IBMCloud.NetworkResourceGroupName)))
   297  				}
   298  				controlPlaneSubnetZones[*subnet.Zone.Name]++
   299  			}
   300  		}
   301  
   302  		var controlPlaneActualZones []string
   303  		// Verify the supplied ControlPlane Subnets cover the provided ControlPlane Zones, or default Regional Zones if not provided
   304  		if zones := getMachinePoolZones(*ic.ControlPlane); zones != nil {
   305  			controlPlaneActualZones = zones
   306  		} else {
   307  			regionalZones, err := client.GetVPCZonesForRegion(context.TODO(), ic.IBMCloud.Region)
   308  			if err != nil {
   309  				allErrs = append(allErrs, field.InternalError(path.Child("controlPlaneSubnets"), err))
   310  			}
   311  			controlPlaneActualZones = regionalZones
   312  		}
   313  
   314  		// If lenght of found zones doesn't match actual or if an actual zone was not found from provided subnets, that is an invalid configuration
   315  		if len(controlPlaneSubnetZones) != len(controlPlaneActualZones) {
   316  			allErrs = append(allErrs, field.Invalid(path.Child("controlPlaneSubnets"), ic.IBMCloud.ControlPlaneSubnets, fmt.Sprintf("number of zones (%d) covered by controlPlaneSubnets does not match number of provided or default zones (%d) for control plane in %s", len(controlPlaneSubnetZones), len(controlPlaneActualZones), ic.IBMCloud.Region)))
   317  		} else {
   318  			for _, actualZone := range controlPlaneActualZones {
   319  				if _, okay := controlPlaneSubnetZones[actualZone]; !okay {
   320  					allErrs = append(allErrs, field.Invalid(path.Child("controlPlaneSubnets"), ic.IBMCloud.ControlPlaneSubnets, fmt.Sprintf("%s zone does not have a provided control plane subnet", actualZone)))
   321  				}
   322  			}
   323  		}
   324  	}
   325  
   326  	if len(ic.IBMCloud.ComputeSubnets) == 0 {
   327  		allErrs = append(allErrs, field.Invalid(path.Child("computeSubnets"), ic.IBMCloud.ComputeSubnets, fmt.Sprintf("computeSubnets cannot be empty when providing a vpcName: %s", ic.IBMCloud.VPCName)))
   328  	} else {
   329  		computeSubnetZones := make(map[string]int)
   330  		for _, computeSubnet := range ic.IBMCloud.ComputeSubnets {
   331  			subnet, err := client.GetSubnetByName(context.TODO(), computeSubnet, ic.IBMCloud.Region)
   332  			if err != nil {
   333  				if errors.Is(err, &VPCResourceNotFoundError{}) {
   334  					allErrs = append(allErrs, field.NotFound(path.Child("computeSubnets"), computeSubnet))
   335  				} else {
   336  					allErrs = append(allErrs, field.InternalError(path.Child("computeSubnets"), err))
   337  				}
   338  			} else {
   339  				if *subnet.VPC.ID != vpcID {
   340  					allErrs = append(allErrs, field.Invalid(path.Child("computeSubnets"), computeSubnet, fmt.Sprintf("computeSubnets contains subnet: %s, not found in expected vpcID: %s", computeSubnet, vpcID)))
   341  				}
   342  				if *subnet.ResourceGroup.ID != ic.IBMCloud.NetworkResourceGroupName && *subnet.ResourceGroup.Name != ic.IBMCloud.NetworkResourceGroupName {
   343  					allErrs = append(allErrs, field.Invalid(path.Child("computeSubnets"), computeSubnet, fmt.Sprintf("computeSubnets contains subnet: %s, not found in expected networkResourceGroupName: %s", computeSubnet, ic.IBMCloud.NetworkResourceGroupName)))
   344  				}
   345  				computeSubnetZones[*subnet.Zone.Name]++
   346  			}
   347  		}
   348  		// Verify the supplied Compute(s) Subnets cover the provided Compute Zones, or default Region Zones if not specified, for each Compute block
   349  		for index, compute := range ic.Compute {
   350  			var computeActualZones []string
   351  			if zones := getMachinePoolZones(compute); zones != nil {
   352  				computeActualZones = zones
   353  			} else {
   354  				if regionalZones == nil {
   355  					var err error
   356  					regionalZones, err = client.GetVPCZonesForRegion(context.TODO(), ic.IBMCloud.Region)
   357  					if err != nil {
   358  						allErrs = append(allErrs, field.InternalError(path.Child("computeSubnets"), err))
   359  					}
   360  				}
   361  				computeActualZones = regionalZones
   362  			}
   363  
   364  			// If length of found zones doesn't match actual or if an actual zone was not found from provided subnets, that is an invalid configuration
   365  			if len(computeSubnetZones) != len(computeActualZones) {
   366  				allErrs = append(allErrs, field.Invalid(path.Child("computeSubnets"), ic.IBMCloud.ComputeSubnets, fmt.Sprintf("number of zones (%d) covered by computeSubnets does not match number of provided or default zones (%d) for compute[%d] in %s", len(computeSubnetZones), len(computeActualZones), index, ic.IBMCloud.Region)))
   367  			} else {
   368  				for _, actualZone := range computeActualZones {
   369  					if _, okay := computeSubnetZones[actualZone]; !okay {
   370  						allErrs = append(allErrs, field.Invalid(path.Child("computeSubnets"), ic.IBMCloud.ComputeSubnets, fmt.Sprintf("%s zone does not have a provided compute subnet", actualZone)))
   371  					}
   372  				}
   373  			}
   374  		}
   375  	}
   376  
   377  	return allErrs
   378  }
   379  
   380  func validateSubnetZone(client API, subnetID string, validZones sets.String, subnetPath *field.Path) field.ErrorList {
   381  	allErrs := field.ErrorList{}
   382  	if subnet, err := client.GetSubnet(context.TODO(), subnetID); err == nil {
   383  		zoneName := *subnet.Zone.Name
   384  		if !validZones.Has(zoneName) {
   385  			allErrs = append(allErrs, field.Invalid(subnetPath, subnetID, fmt.Sprintf("subnet is not in expected zones: %s", validZones.List())))
   386  		}
   387  	} else {
   388  		if errors.Is(err, &VPCResourceNotFoundError{}) {
   389  			allErrs = append(allErrs, field.NotFound(subnetPath, subnetID))
   390  		} else {
   391  			allErrs = append(allErrs, field.InternalError(subnetPath, err))
   392  		}
   393  	}
   394  	return allErrs
   395  }
   396  
   397  // ValidatePreExistingPublicDNS ensure no pre-existing DNS record exists in the CIS
   398  // DNS zone for cluster's Kubernetes API.
   399  func ValidatePreExistingPublicDNS(client API, ic *types.InstallConfig, metadata *Metadata) error {
   400  	// If this is an internal cluster, this check is not necessary
   401  	if ic.Publish == types.InternalPublishingStrategy {
   402  		return nil
   403  	}
   404  
   405  	// Get CIS CRN
   406  	crn, err := metadata.CISInstanceCRN(context.TODO())
   407  	if err != nil {
   408  		return err
   409  	}
   410  
   411  	// Get CIS zone ID by name
   412  	zoneID, err := client.GetDNSZoneIDByName(context.TODO(), ic.BaseDomain, ic.Publish)
   413  	if err != nil {
   414  		return field.InternalError(field.NewPath("baseDomain"), err)
   415  	}
   416  
   417  	// Get CIS DNS record by name
   418  	recordName := fmt.Sprintf("api.%s", ic.ClusterDomain())
   419  	records, err := client.GetDNSRecordsByName(context.TODO(), crn, zoneID, recordName)
   420  	if err != nil {
   421  		return field.InternalError(field.NewPath("baseDomain"), err)
   422  	}
   423  
   424  	// DNS record exists
   425  	if len(records) != 0 {
   426  		return fmt.Errorf("record %s already exists in CIS zone (%s) and might be in use by another cluster, please remove it to continue", recordName, zoneID)
   427  	}
   428  
   429  	return nil
   430  }
   431  
   432  // ValidateServiceEndpoints will validate a series of service endpoint overrides.
   433  func ValidateServiceEndpoints(ic *types.InstallConfig) error {
   434  	allErrs := field.ErrorList{}
   435  	serviceEndpointsPath := field.NewPath("platform").Child("ibmcloud").Child("serviceEndpoints")
   436  	// Verify services are valid for override and are not duplicated and that are in valid URI format and accessible.
   437  	overriddenServices := map[configv1.IBMCloudServiceName]bool{}
   438  	for id, service := range ic.Platform.IBMCloud.ServiceEndpoints {
   439  		// Check if we have a duplicate service (case is ignored)
   440  		if _, ok := overriddenServices[service.Name]; ok {
   441  			allErrs = append(allErrs, field.Duplicate(serviceEndpointsPath.Index(id).Child("name"), service.Name))
   442  			continue
   443  		}
   444  		// Add service to map to track for duplicates
   445  		overriddenServices[service.Name] = true
   446  
   447  		// Check that the provided service name is an expected override service
   448  		if _, ok := ibmcloud.IBMCloudServiceOverrides[service.Name]; !ok {
   449  			allErrs = append(allErrs, field.Invalid(serviceEndpointsPath.Index(id).Child("name"), service.Name, "not a supported override service"))
   450  		}
   451  
   452  		// Check if the service URL is valid
   453  		err := validateEndpoint(service.URL)
   454  		if err != nil {
   455  			allErrs = append(allErrs, field.Invalid(serviceEndpointsPath.Index(id).Child("url"), service.URL, err.Error()))
   456  		}
   457  	}
   458  
   459  	return allErrs.ToAggregate()
   460  }
   461  
   462  // validateEndpoint will validate an endpoint meets acceptable URI requirements.
   463  func validateEndpoint(endpoint string) error {
   464  	// Ignore local unit tests
   465  	if endpoint == "e2e.unittest.local" {
   466  		return nil
   467  	}
   468  	// NOTE(cjschaef): At this time we expect the endpoint to be an absolute URI (besides local unittests checked above)
   469  	_, err := url.Parse(endpoint)
   470  	if err != nil {
   471  		return err
   472  	}
   473  	// Verify the endpoint is accessible
   474  	_, err = http.Head(endpoint) //nolint:gosec // we expect the user to provide safe endpoints, as we only wish to validation the server responds
   475  	return err
   476  }
   477  
   478  // getMachinePoolZones will return the zones if they have been specified or return nil if the MachinePoolPlatform or values are not specified
   479  func getMachinePoolZones(mp types.MachinePool) []string {
   480  	if mp.Platform.IBMCloud == nil || mp.Platform.IBMCloud.Zones == nil {
   481  		return nil
   482  	}
   483  	return mp.Platform.IBMCloud.Zones
   484  }