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 }