github.com/openshift/installer@v1.4.17/pkg/asset/installconfig/azure/validation.go (about) 1 package azure 2 3 import ( 4 "context" 5 "fmt" 6 "net" 7 "net/url" 8 "os" 9 "sort" 10 "strconv" 11 "strings" 12 13 azdns "github.com/Azure/azure-sdk-for-go/profiles/2018-03-01/dns/mgmt/dns" 14 aznetwork "github.com/Azure/azure-sdk-for-go/profiles/2020-09-01/network/mgmt/network" 15 azenc "github.com/Azure/azure-sdk-for-go/profiles/latest/compute/mgmt/compute" 16 "github.com/Azure/go-autorest/autorest/to" 17 "github.com/sirupsen/logrus" 18 "k8s.io/apimachinery/pkg/util/sets" 19 "k8s.io/apimachinery/pkg/util/validation/field" 20 21 "github.com/openshift/installer/pkg/types" 22 aztypes "github.com/openshift/installer/pkg/types/azure" 23 "github.com/openshift/installer/pkg/types/azure/defaults" 24 ) 25 26 type resourceRequirements struct { 27 minimumVCpus int64 28 minimumMemory int64 29 } 30 31 var controlPlaneReq = resourceRequirements{ 32 minimumVCpus: 4, 33 minimumMemory: 16, 34 } 35 36 var computeReq = resourceRequirements{ 37 minimumVCpus: 2, 38 minimumMemory: 8, 39 } 40 41 // Validate executes platform-specific validation. 42 func Validate(client API, ic *types.InstallConfig) error { 43 allErrs := field.ErrorList{} 44 45 allErrs = append(allErrs, validateNetworks(client, ic.Azure, ic.Networking.MachineNetwork, field.NewPath("platform").Child("azure"))...) 46 allErrs = append(allErrs, validateRegion(client, field.NewPath("platform").Child("azure").Child("region"), ic.Azure)...) 47 if ic.Azure.CloudName == aztypes.StackCloud { 48 allErrs = append(allErrs, validateAzureStackDiskType(client, ic)...) 49 } 50 allErrs = append(allErrs, validateInstanceTypes(client, ic)...) 51 if ic.Azure.CloudName == aztypes.StackCloud && ic.Azure.ClusterOSImage != "" { 52 StorageEndpointSuffix, err := client.GetStorageEndpointSuffix(context.TODO()) 53 if err != nil { 54 return err 55 } 56 allErrs = append(allErrs, validateAzureStackClusterOSImage(StorageEndpointSuffix, ic.Azure.ClusterOSImage, field.NewPath("platform").Child("azure"))...) 57 } 58 allErrs = append(allErrs, validateMarketplaceImages(client, ic)...) 59 return allErrs.ToAggregate() 60 } 61 62 // ValidateDiskEncryptionSet ensures the disk encryption set exists and is valid. 63 func ValidateDiskEncryptionSet(client API, ic *types.InstallConfig) field.ErrorList { 64 allErrs := field.ErrorList{} 65 66 if ic.Platform.Azure.DefaultMachinePlatform != nil && ic.Platform.Azure.DefaultMachinePlatform.OSDisk.DiskEncryptionSet != nil { 67 diskEncryptionSet := ic.Platform.Azure.DefaultMachinePlatform.OSDisk.DiskEncryptionSet 68 _, err := client.GetDiskEncryptionSet(context.TODO(), diskEncryptionSet.SubscriptionID, diskEncryptionSet.ResourceGroup, diskEncryptionSet.Name) 69 if err != nil { 70 allErrs = append(allErrs, field.Invalid(field.NewPath("platform").Child("azure", "defaultMachinePlatform", "osDisk", "diskEncryptionSet"), diskEncryptionSet, err.Error())) 71 } 72 } 73 74 if ic.ControlPlane != nil && ic.ControlPlane.Platform.Azure != nil && ic.ControlPlane.Platform.Azure.OSDisk.DiskEncryptionSet != nil { 75 diskEncryptionSet := ic.ControlPlane.Platform.Azure.OSDisk.DiskEncryptionSet 76 _, err := client.GetDiskEncryptionSet(context.TODO(), diskEncryptionSet.SubscriptionID, diskEncryptionSet.ResourceGroup, diskEncryptionSet.Name) 77 if err != nil { 78 allErrs = append(allErrs, field.Invalid(field.NewPath("platform").Child("azure", "osDisk", "diskEncryptionSet"), diskEncryptionSet, err.Error())) 79 } 80 } 81 82 for idx, compute := range ic.Compute { 83 fieldPath := field.NewPath("compute").Index(idx) 84 if compute.Platform.Azure != nil && compute.Platform.Azure.OSDisk.DiskEncryptionSet != nil { 85 diskEncryptionSet := compute.Platform.Azure.OSDisk.DiskEncryptionSet 86 _, err := client.GetDiskEncryptionSet(context.TODO(), diskEncryptionSet.SubscriptionID, diskEncryptionSet.ResourceGroup, diskEncryptionSet.Name) 87 if err != nil { 88 allErrs = append(allErrs, field.Invalid(fieldPath.Child("platform", "azure", "osDisk", "diskEncryptionSet"), diskEncryptionSet, err.Error())) 89 } 90 } 91 } 92 93 return allErrs 94 } 95 96 func validateConfidentialDiskEncryptionSet(client API, diskEncryptionSet *aztypes.DiskEncryptionSet, desFieldPath *field.Path) error { 97 resp, requestErr := client.GetDiskEncryptionSet(context.TODO(), diskEncryptionSet.SubscriptionID, diskEncryptionSet.ResourceGroup, diskEncryptionSet.Name) 98 if requestErr != nil { 99 return requestErr 100 } else if resp == nil || resp.EncryptionSetProperties == nil || resp.EncryptionSetProperties.EncryptionType != azenc.ConfidentialVMEncryptedWithCustomerKey { 101 return fmt.Errorf("the disk encryption set should be created with type %s", azenc.ConfidentialVMEncryptedWithCustomerKey) 102 } 103 return nil 104 } 105 106 // ValidateSecurityProfileDiskEncryptionSet ensures the security profile disk encryption set exists and is valid. 107 func ValidateSecurityProfileDiskEncryptionSet(client API, ic *types.InstallConfig) field.ErrorList { 108 allErrs := field.ErrorList{} 109 110 if ic.Platform.Azure.DefaultMachinePlatform != nil && 111 ic.Platform.Azure.DefaultMachinePlatform.OSDisk.SecurityProfile != nil && 112 ic.Platform.Azure.DefaultMachinePlatform.OSDisk.SecurityProfile.DiskEncryptionSet != nil { 113 desFieldPath := field.NewPath("platform").Child("azure", "defaultMachinePlatform", "osDisk", "securityProfile", "diskEncryptionSet") 114 diskEncryptionSet := ic.Platform.Azure.DefaultMachinePlatform.OSDisk.SecurityProfile.DiskEncryptionSet 115 err := validateConfidentialDiskEncryptionSet(client, diskEncryptionSet, desFieldPath) 116 if err != nil { 117 allErrs = append(allErrs, field.Invalid(desFieldPath, diskEncryptionSet, err.Error())) 118 } 119 } 120 121 if ic.ControlPlane != nil && 122 ic.ControlPlane.Platform.Azure != nil && 123 ic.ControlPlane.Platform.Azure.OSDisk.SecurityProfile != nil && 124 ic.ControlPlane.Platform.Azure.OSDisk.SecurityProfile.DiskEncryptionSet != nil { 125 desFieldPath := field.NewPath("platform").Child("azure", "osDisk", "securityProfile", "diskEncryptionSet") 126 diskEncryptionSet := ic.ControlPlane.Platform.Azure.OSDisk.SecurityProfile.DiskEncryptionSet 127 err := validateConfidentialDiskEncryptionSet(client, diskEncryptionSet, desFieldPath) 128 if err != nil { 129 allErrs = append(allErrs, field.Invalid(desFieldPath, diskEncryptionSet, err.Error())) 130 } 131 } 132 133 for idx, compute := range ic.Compute { 134 fieldPath := field.NewPath("compute").Index(idx) 135 if compute.Platform.Azure != nil && 136 compute.Platform.Azure.OSDisk.SecurityProfile != nil && 137 compute.Platform.Azure.OSDisk.SecurityProfile.DiskEncryptionSet != nil { 138 desFieldPath := fieldPath.Child("platform", "azure", "osDisk", "securityProfile", "diskEncryptionSet") 139 diskEncryptionSet := compute.Platform.Azure.OSDisk.SecurityProfile.DiskEncryptionSet 140 err := validateConfidentialDiskEncryptionSet(client, diskEncryptionSet, desFieldPath) 141 if err != nil { 142 allErrs = append(allErrs, field.Invalid(desFieldPath, diskEncryptionSet, err.Error())) 143 } 144 } 145 } 146 147 return allErrs 148 } 149 150 func validatePremiumDisk(fieldPath *field.Path, diskType string, instanceType string, capabilities map[string]string) field.ErrorList { 151 fldPath := fieldPath.Child("osDisk", "diskType") 152 val, ok := capabilities["PremiumIO"] 153 if !ok { 154 return field.ErrorList{field.Invalid(fldPath, diskType, "capability not found: PremiumIO")} 155 } 156 if strings.EqualFold(val, "False") { 157 errMsg := fmt.Sprintf("PremiumIO not supported for instance type %s", instanceType) 158 return field.ErrorList{field.Invalid(fldPath, diskType, errMsg)} 159 } 160 return field.ErrorList{} 161 } 162 163 func validateVMArchitecture(fieldPath *field.Path, instanceType string, architecture types.Architecture, capabilities map[string]string) field.ErrorList { 164 allErrs := field.ErrorList{} 165 166 val, ok := capabilities["CpuArchitectureType"] 167 if ok { 168 if architecture != types.ArchitectureARM64 && architecture != types.ArchitectureAMD64 { 169 errMsg := fmt.Sprintf("invalid install config architecture %s", architecture) 170 allErrs = append(allErrs, field.Invalid(fieldPath, instanceType, errMsg)) 171 } else if (architecture == types.ArchitectureARM64 && !strings.EqualFold(val, "Arm64")) || (architecture == types.ArchitectureAMD64 && !strings.EqualFold(val, "x64")) { 172 errMsg := fmt.Sprintf("instance type architecture '%s' does not match install config architecture %s", val, architecture) 173 allErrs = append(allErrs, field.Invalid(fieldPath, instanceType, errMsg)) 174 } 175 } else { 176 logrus.Warnf("Could not determine VM's architecture from its capabilities. Assuming it is %v", architecture) 177 } 178 179 return allErrs 180 } 181 182 func validateMininumRequirements(fieldPath *field.Path, req resourceRequirements, instanceType string, capabilities map[string]string) field.ErrorList { 183 allErrs := field.ErrorList{} 184 185 val, ok := capabilities["vCPUsAvailable"] 186 if ok { 187 cpus, err := strconv.ParseFloat(val, 0) 188 if err != nil { 189 return append(allErrs, field.InternalError(fieldPath, err)) 190 } 191 if cpus < float64(req.minimumVCpus) { 192 errMsg := fmt.Sprintf("instance type does not meet minimum resource requirements of %d vCPUsAvailable", req.minimumVCpus) 193 allErrs = append(allErrs, field.Invalid(fieldPath, instanceType, errMsg)) 194 } 195 } else { 196 logrus.Warnf("could not find vCPUsAvailable information for instance type %s", instanceType) 197 } 198 199 val, ok = capabilities["MemoryGB"] 200 if ok { 201 memory, err := strconv.ParseFloat(val, 0) 202 if err != nil { 203 return append(allErrs, field.InternalError(fieldPath, err)) 204 } 205 if memory < float64(req.minimumMemory) { 206 errMsg := fmt.Sprintf("instance type does not meet minimum resource requirements of %d GB Memory", req.minimumMemory) 207 allErrs = append(allErrs, field.Invalid(fieldPath, instanceType, errMsg)) 208 } 209 } else { 210 logrus.Warnf("could not find MemoryGB information for instance type %s", instanceType) 211 } 212 213 return allErrs 214 } 215 216 func validateSecurityType(fieldPath *field.Path, securityType aztypes.SecurityTypes, instanceType string, capabilities map[string]string) field.ErrorList { 217 allErrs := field.ErrorList{} 218 219 _, hasTrustedLaunchDisabled := capabilities["TrustedLaunchDisabled"] 220 confidentialComputingType, hasConfidentialComputingType := capabilities["ConfidentialComputingType"] 221 isConfidentialComputingTypeSNP := confidentialComputingType == "SNP" 222 223 var reason string 224 supportedSecurityType := true 225 switch securityType { 226 case aztypes.SecurityTypesConfidentialVM: 227 supportedSecurityType = hasConfidentialComputingType && isConfidentialComputingTypeSNP 228 229 if !hasConfidentialComputingType { 230 reason = "no support for Confidential Computing" 231 } else if !isConfidentialComputingTypeSNP { 232 reason = "no support for AMD-SEV SNP" 233 } 234 case aztypes.SecurityTypesTrustedLaunch: 235 supportedSecurityType = !(hasTrustedLaunchDisabled || hasConfidentialComputingType) 236 237 if hasTrustedLaunchDisabled { 238 reason = "no support for Trusted Launch" 239 } else if hasConfidentialComputingType { 240 reason = "confidential VMs do not support Trusted Launch for VMs" 241 } 242 } 243 244 if !supportedSecurityType { 245 errMsg := fmt.Sprintf("this security type is not supported for instance type %s, %s", instanceType, reason) 246 allErrs = append(allErrs, field.Invalid(fieldPath, securityType, errMsg)) 247 } 248 249 return allErrs 250 } 251 252 func validateFamily(fieldPath *field.Path, instanceType, family string) field.ErrorList { 253 windowsVMFamilies := sets.NewString( 254 "standardNVSv4Family", 255 ) 256 diskNVMeVMFamilies := sets.NewString( 257 "standardEIBDSv5Family", 258 "standardEIBSv5Family", 259 ) 260 allErrs := field.ErrorList{} 261 if windowsVMFamilies.Has(family) { 262 errMsg := fmt.Sprintf("%s is currently only supported on Windows", family) 263 allErrs = append(allErrs, field.Invalid(fieldPath, instanceType, errMsg)) 264 } 265 // FIXME: remove when supported has been added to the provider 266 // https://github.com/hashicorp/terraform-provider-azurerm/issues/22058 267 if diskNVMeVMFamilies.Has(family) { 268 errMsg := fmt.Sprintf("%s is not currently supported but might be in a future release", family) 269 allErrs = append(allErrs, field.Invalid(fieldPath, instanceType, errMsg)) 270 } 271 272 return allErrs 273 } 274 275 func validateAcceleratedNetworking(fieldPath *field.Path, vmNetworkingType string, instanceType string, capabilities map[string]string) field.ErrorList { 276 val, ok := capabilities[string(aztypes.AcceleratedNetworkingEnabled)] 277 if ok { 278 if !strings.EqualFold(val, "True") { 279 errMsg := fmt.Sprintf("vm networking type is not supported for instance type %s", instanceType) 280 return field.ErrorList{field.Invalid(fieldPath.Child("vmNetworkingType"), vmNetworkingType, errMsg)} 281 } 282 } else { 283 return field.ErrorList{field.Invalid(fieldPath.Child("type"), instanceType, "capability not found: AcceleratedNetworkingEnabled")} 284 } 285 286 return field.ErrorList{} 287 } 288 289 func validateUltraSSD(client API, fieldPath *field.Path, icZones []string, region string, instanceType string, capabilities map[string]string) field.ErrorList { 290 allErrs := field.ErrorList{} 291 292 locationInfo, err := client.GetLocationInfo(context.TODO(), region, instanceType) 293 if err != nil { 294 errMsg := fmt.Sprintf("could not determine Availability Zones support in the %s region: %v", region, err) 295 return append(allErrs, field.Invalid(fieldPath, instanceType, errMsg)) 296 } 297 // If Availability Zones not supported 298 if locationInfo == nil || len(to.StringSlice(locationInfo.Zones)) == 0 { 299 errMsg := fmt.Sprintf("UltraSSD capability is not compatible with Availability Sets which are used because region %s does not support Availability Zones", region) 300 return append(allErrs, field.Invalid(fieldPath, instanceType, errMsg)) 301 } 302 allZones := to.StringSlice(locationInfo.Zones) 303 304 // The UltraSSDAvailable capability might not be present at all, in which case it must assumed to be false 305 ultraSSDAvailable := false 306 if val, ok := capabilities["UltraSSDAvailable"]; ok { 307 ultraSSDAvailable = strings.EqualFold(val, "True") 308 } 309 for _, zoneDetails := range *locationInfo.ZoneDetails { 310 for _, capability := range *zoneDetails.Capabilities { 311 if !strings.EqualFold(to.String(capability.Name), "UltraSSDAvailable") { 312 continue 313 } 314 if strings.EqualFold(to.String(capability.Value), "True") { 315 zones := icZones 316 // If no zones are configured in the install config, then all available zones in the region are used 317 if len(zones) == 0 { 318 zones = allZones 319 } 320 capZones := to.StringSlice(zoneDetails.Name) 321 ultraSSDZones := sets.NewString(capZones...) 322 if !ultraSSDZones.HasAll(zones...) { 323 errMsg := fmt.Sprintf("UltraSSD capability only supported in zones %v for this instance type in the %s region", capZones, region) 324 return append(allErrs, field.Invalid(fieldPath, instanceType, errMsg)) 325 } 326 ultraSSDAvailable = true 327 } 328 } 329 } 330 if !ultraSSDAvailable { 331 errMsg := fmt.Sprintf("UltraSSD capability not supported for this instance type in the %s region", region) 332 allErrs = append(allErrs, field.Invalid(fieldPath, instanceType, errMsg)) 333 } 334 335 return allErrs 336 } 337 338 // ValidateInstanceType ensures the instance type has sufficient Vcpu, Memory, and a valid family type. 339 func ValidateInstanceType(client API, fieldPath *field.Path, region, instanceType, diskType string, req resourceRequirements, ultraSSDEnabled bool, vmNetworkingType string, icZones []string, architecture types.Architecture, securityType aztypes.SecurityTypes) field.ErrorList { 340 allErrs := field.ErrorList{} 341 342 capabilities, err := client.GetVMCapabilities(context.TODO(), instanceType, region) 343 if err != nil { 344 return append(allErrs, field.Invalid(fieldPath.Child("type"), instanceType, err.Error())) 345 } 346 347 allErrs = append(allErrs, validateMininumRequirements(fieldPath.Child("type"), req, instanceType, capabilities)...) 348 allErrs = append(allErrs, validateVMArchitecture(fieldPath.Child("type"), instanceType, architecture, capabilities)...) 349 allErrs = append(allErrs, validateSecurityType(fieldPath.Child("settings", "securityType"), securityType, instanceType, capabilities)...) 350 351 family, _ := client.GetVirtualMachineFamily(context.TODO(), instanceType, region) 352 if family != "" { 353 allErrs = append(allErrs, validateFamily(fieldPath.Child("type"), instanceType, family)...) 354 } 355 356 if diskType == "Premium_LRS" { 357 allErrs = append(allErrs, validatePremiumDisk(fieldPath, diskType, instanceType, capabilities)...) 358 } 359 360 if vmNetworkingType == string(aztypes.VMnetworkingTypeAccelerated) { 361 allErrs = append(allErrs, validateAcceleratedNetworking(fieldPath, vmNetworkingType, instanceType, capabilities)...) 362 } 363 364 if ultraSSDEnabled { 365 allErrs = append(allErrs, validateUltraSSD(client, fieldPath.Child("type"), icZones, region, instanceType, capabilities)...) 366 } 367 368 return allErrs 369 } 370 371 // validateInstanceTypes checks that the user-provided instance types are valid. 372 func validateInstanceTypes(client API, ic *types.InstallConfig) field.ErrorList { 373 allErrs := field.ErrorList{} 374 375 var securityType aztypes.SecurityTypes 376 377 defaultDiskType := aztypes.DefaultDiskType 378 defaultInstanceType := "" 379 defaultUltraSSDCapability := "Disabled" 380 defaultVMNetworkingType := "" 381 defaultZones := []string{} 382 useDefaultInstanceType := false 383 384 if ic.Platform.Azure.DefaultMachinePlatform != nil { 385 if ic.Platform.Azure.DefaultMachinePlatform.OSDisk.DiskType != "" { 386 defaultDiskType = ic.Platform.Azure.DefaultMachinePlatform.OSDisk.DiskType 387 } 388 if ic.Platform.Azure.DefaultMachinePlatform.InstanceType != "" { 389 defaultInstanceType = ic.Platform.Azure.DefaultMachinePlatform.InstanceType 390 } 391 if ic.Platform.Azure.DefaultMachinePlatform.UltraSSDCapability != "" { 392 defaultUltraSSDCapability = ic.Platform.Azure.DefaultMachinePlatform.UltraSSDCapability 393 } 394 if ic.Platform.Azure.DefaultMachinePlatform.VMNetworkingType != "" { 395 defaultVMNetworkingType = ic.Platform.Azure.DefaultMachinePlatform.VMNetworkingType 396 } 397 if ic.Platform.Azure.DefaultMachinePlatform.Zones != nil { 398 defaultZones = ic.Platform.Azure.DefaultMachinePlatform.Zones 399 } 400 if ic.Platform.Azure.DefaultMachinePlatform.Settings != nil { 401 securityType = ic.Platform.Azure.DefaultMachinePlatform.Settings.SecurityType 402 } 403 } 404 405 if ic.ControlPlane != nil && ic.ControlPlane.Platform.Azure != nil { 406 fieldPath := field.NewPath("controlPlane", "platform", "azure") 407 diskType := ic.ControlPlane.Platform.Azure.OSDisk.DiskType 408 instanceType := ic.ControlPlane.Platform.Azure.InstanceType 409 ultraSSDCapability := ic.ControlPlane.Platform.Azure.UltraSSDCapability 410 vmNetworkingType := ic.ControlPlane.Platform.Azure.VMNetworkingType 411 zones := ic.ControlPlane.Platform.Azure.Zones 412 architecture := ic.ControlPlane.Architecture 413 414 if ic.ControlPlane.Platform.Azure.Settings != nil { 415 securityType = ic.ControlPlane.Platform.Azure.Settings.SecurityType 416 } 417 if diskType == "" { 418 diskType = defaultDiskType 419 } 420 if instanceType == "" { 421 instanceType = defaultInstanceType 422 useDefaultInstanceType = true 423 } 424 if instanceType == "" { 425 instanceType = defaults.ControlPlaneInstanceType(ic.Azure.CloudName, ic.Azure.Region, architecture) 426 } 427 if ultraSSDCapability == "" { 428 ultraSSDCapability = defaultUltraSSDCapability 429 } 430 if vmNetworkingType == "" { 431 vmNetworkingType = defaultVMNetworkingType 432 } 433 if len(zones) == 0 { 434 zones = defaultZones 435 } 436 ultraSSDEnabled := strings.EqualFold(ultraSSDCapability, "Enabled") 437 allErrs = append(allErrs, ValidateInstanceType(client, fieldPath, ic.Azure.Region, instanceType, diskType, controlPlaneReq, ultraSSDEnabled, vmNetworkingType, zones, architecture, securityType)...) 438 } 439 440 for idx, compute := range ic.Compute { 441 fieldPath := field.NewPath("compute").Index(idx) 442 if compute.Platform.Azure != nil { 443 diskType := compute.Platform.Azure.OSDisk.DiskType 444 instanceType := compute.Platform.Azure.InstanceType 445 ultraSSDCapability := compute.Platform.Azure.UltraSSDCapability 446 vmNetworkingType := compute.Platform.Azure.VMNetworkingType 447 zones := compute.Platform.Azure.Zones 448 architecture := compute.Architecture 449 450 if compute.Platform.Azure.Settings != nil { 451 securityType = compute.Platform.Azure.Settings.SecurityType 452 } 453 if diskType == "" { 454 diskType = defaultDiskType 455 } 456 if instanceType == "" { 457 instanceType = defaultInstanceType 458 useDefaultInstanceType = true 459 } 460 if instanceType == "" { 461 instanceType = defaults.ComputeInstanceType(ic.Azure.CloudName, ic.Azure.Region, architecture) 462 } 463 if ultraSSDCapability == "" { 464 ultraSSDCapability = defaultUltraSSDCapability 465 } 466 if vmNetworkingType == "" { 467 vmNetworkingType = defaultVMNetworkingType 468 } 469 if len(zones) == 0 { 470 zones = defaultZones 471 } 472 ultraSSDEnabled := strings.EqualFold(ultraSSDCapability, "Enabled") 473 allErrs = append(allErrs, ValidateInstanceType(client, fieldPath.Child("platform", "azure"), 474 ic.Azure.Region, instanceType, diskType, computeReq, ultraSSDEnabled, vmNetworkingType, zones, architecture, securityType)...) 475 } 476 } 477 478 // checking here if the defaultInstanceType is present and not used previously. If so, check it now. 479 // The assumption here is that since cp and compute arches cannot differ today, it's ok to not check the 480 // default instance as long as it is used in any one place. 481 if !useDefaultInstanceType && defaultInstanceType != "" { 482 architecture := types.Architecture(types.ArchitectureAMD64) 483 if ic.ControlPlane != nil { 484 architecture = ic.ControlPlane.Architecture 485 } 486 minReq := computeReq 487 if ic.ControlPlane == nil || ic.ControlPlane.Platform.Azure == nil { 488 minReq = controlPlaneReq 489 } 490 fieldPath := field.NewPath("platform", "azure", "defaultMachinePlatform") 491 ultraSSDEnabled := strings.EqualFold(defaultUltraSSDCapability, "Enabled") 492 allErrs = append(allErrs, ValidateInstanceType(client, fieldPath, 493 ic.Azure.Region, defaultInstanceType, defaultDiskType, minReq, ultraSSDEnabled, defaultVMNetworkingType, defaultZones, architecture, securityType)...) 494 } 495 return allErrs 496 } 497 498 // validateNetworks checks that the user-provided VNet and subnets are valid. 499 func validateNetworks(client API, p *aztypes.Platform, machineNetworks []types.MachineNetworkEntry, fieldPath *field.Path) field.ErrorList { 500 allErrs := field.ErrorList{} 501 502 if p.VirtualNetwork != "" { 503 _, err := client.GetVirtualNetwork(context.TODO(), p.NetworkResourceGroupName, p.VirtualNetwork) 504 if err != nil { 505 return append(allErrs, field.Invalid(fieldPath.Child("virtualNetwork"), p.VirtualNetwork, err.Error())) 506 } 507 508 computeSubnet, err := client.GetComputeSubnet(context.TODO(), p.NetworkResourceGroupName, p.VirtualNetwork, p.ComputeSubnet) 509 if err != nil { 510 return append(allErrs, field.Invalid(fieldPath.Child("computeSubnet"), p.ComputeSubnet, "failed to retrieve compute subnet")) 511 } 512 513 allErrs = append(allErrs, validateSubnet(client, fieldPath.Child("computeSubnet"), computeSubnet, p.ComputeSubnet, machineNetworks)...) 514 515 controlPlaneSubnet, err := client.GetControlPlaneSubnet(context.TODO(), p.NetworkResourceGroupName, p.VirtualNetwork, p.ControlPlaneSubnet) 516 if err != nil { 517 return append(allErrs, field.Invalid(fieldPath.Child("controlPlaneSubnet"), p.ControlPlaneSubnet, "failed to retrieve control plane subnet")) 518 } 519 520 allErrs = append(allErrs, validateSubnet(client, fieldPath.Child("controlPlaneSubnet"), controlPlaneSubnet, p.ControlPlaneSubnet, machineNetworks)...) 521 } 522 523 return allErrs 524 } 525 526 // validateSubnet checks that the subnet is in the same network as the machine CIDR 527 func validateSubnet(client API, fieldPath *field.Path, subnet *aznetwork.Subnet, subnetName string, networks []types.MachineNetworkEntry) field.ErrorList { 528 allErrs := field.ErrorList{} 529 530 var addressPrefix string 531 switch { 532 case subnet.AddressPrefix != nil: 533 addressPrefix = *subnet.AddressPrefix 534 // NOTE: if the subscription has the `AllowMultipleAddressPrefixesOnSubnet` feature, the Azure API will return a 535 // `addressPrefixes` field with a slice of addresses instead of a single value via `addressPrefix`. 536 case subnet.AddressPrefixes != nil && len(*subnet.AddressPrefixes) > 0: 537 addressPrefix = (*subnet.AddressPrefixes)[0] 538 default: 539 return append(allErrs, field.Invalid(fieldPath, subnetName, "subnet does not have an address prefix")) 540 } 541 542 subnetIP, _, err := net.ParseCIDR(addressPrefix) 543 if err != nil { 544 return append(allErrs, field.Invalid(fieldPath, subnetName, "unable to parse subnet CIDR")) 545 } 546 547 allErrs = append(allErrs, validateMachineNetworksContainIP(fieldPath, networks, *subnet.Name, subnetIP)...) 548 return allErrs 549 } 550 551 func validateMachineNetworksContainIP(fldPath *field.Path, networks []types.MachineNetworkEntry, subnetName string, ip net.IP) field.ErrorList { 552 for _, network := range networks { 553 if network.CIDR.Contains(ip) { 554 return nil 555 } 556 } 557 return field.ErrorList{field.Invalid(fldPath, subnetName, fmt.Sprintf("subnet %s address prefix is outside of the specified machine networks", ip))} 558 } 559 560 // validateRegion checks that the desired region is valid and available to the user 561 func validateRegion(client API, fieldPath *field.Path, p *aztypes.Platform) field.ErrorList { 562 locations, err := client.ListLocations(context.TODO()) 563 if err != nil { 564 return field.ErrorList{field.InternalError(fieldPath, fmt.Errorf("failed to retrieve available regions: %w", err))} 565 } 566 567 availableRegions := map[string]string{} 568 for _, location := range *locations { 569 availableRegions[to.String(location.Name)] = to.String(location.DisplayName) 570 } 571 572 displayName, ok := availableRegions[p.Region] 573 if !ok { 574 errMsg := fmt.Sprintf("region %q is not valid or not available for this account", p.Region) 575 576 normalizedRegion := strings.Replace(strings.ToLower(p.Region), " ", "", -1) 577 if _, ok := availableRegions[normalizedRegion]; ok { 578 errMsg += fmt.Sprintf(", did you mean %q?", normalizedRegion) 579 } 580 581 return field.ErrorList{field.Invalid(fieldPath, p.Region, errMsg)} 582 583 } 584 585 provider, err := client.GetResourcesProvider(context.TODO(), "Microsoft.Resources") 586 if err != nil { 587 return field.ErrorList{field.InternalError(fieldPath, fmt.Errorf("failed to retrieve resource capable regions: %w", err))} 588 } 589 590 for _, resType := range *provider.ResourceTypes { 591 if *resType.ResourceType == "resourceGroups" { 592 for _, resourceCapableRegion := range *resType.Locations { 593 if resourceCapableRegion == displayName { 594 return field.ErrorList{} 595 } 596 } 597 } 598 } 599 600 return field.ErrorList{field.Invalid(fieldPath, p.Region, fmt.Sprintf("region %q does not support resource creation", p.Region))} 601 } 602 603 // ValidatePublicDNS checks DNS for CNAME, A, and AAA records for 604 // api.zoneName. If a record exists, it's likely a cluster already exists. 605 func ValidatePublicDNS(ic *types.InstallConfig, azureDNS *DNSConfig) error { 606 // If this is an internal cluster, this check is not necessary 607 if ic.Publish == types.InternalPublishingStrategy { 608 return nil 609 } 610 611 clusterName := ic.ObjectMeta.Name 612 record := fmt.Sprintf("api.%s", clusterName) 613 rgName := ic.Azure.BaseDomainResourceGroupName 614 zoneName := ic.BaseDomain 615 fmtStr := "api.%s %s record already exists in %s and might be in use by another cluster, please remove it to continue" 616 617 // Look for an existing CNAME first 618 rs, err := azureDNS.GetDNSRecordSet(rgName, zoneName, record, azdns.CNAME) 619 if err == nil && rs.CnameRecord != nil { 620 return fmt.Errorf(fmtStr, zoneName, azdns.CNAME, clusterName) 621 } 622 623 // Look for an A record 624 rs, err = azureDNS.GetDNSRecordSet(rgName, zoneName, record, azdns.A) 625 if err == nil && rs.ARecords != nil && len(*rs.ARecords) > 0 { 626 return fmt.Errorf(fmtStr, zoneName, azdns.A, clusterName) 627 } 628 629 // Look for an AAAA record 630 rs, err = azureDNS.GetDNSRecordSet(rgName, zoneName, record, azdns.AAAA) 631 if err == nil && rs.AaaaRecords != nil && len(*rs.AaaaRecords) > 0 { 632 return fmt.Errorf(fmtStr, zoneName, azdns.AAAA, clusterName) 633 } 634 635 return nil 636 } 637 638 // ValidateForProvisioning validates if the install config is valid for provisioning the cluster. 639 func ValidateForProvisioning(client API, ic *types.InstallConfig) error { 640 allErrs := field.ErrorList{} 641 allErrs = append(allErrs, validateResourceGroup(client, field.NewPath("platform").Child("azure"), ic.Azure)...) 642 allErrs = append(allErrs, ValidateDiskEncryptionSet(client, ic)...) 643 allErrs = append(allErrs, ValidateSecurityProfileDiskEncryptionSet(client, ic)...) 644 allErrs = append(allErrs, validateSkipImageUpload(field.NewPath("image"), ic)...) 645 if ic.Azure.CloudName == aztypes.StackCloud { 646 allErrs = append(allErrs, checkAzureStackClusterOSImageSet(ic.Azure.ClusterOSImage, field.NewPath("platform").Child("azure"))...) 647 } 648 return allErrs.ToAggregate() 649 } 650 651 func validateSkipImageUpload(fieldPath *field.Path, ic *types.InstallConfig) field.ErrorList { 652 allErrs := field.ErrorList{} 653 defaultOSImage := aztypes.OSImage{} 654 if ic.Azure.DefaultMachinePlatform != nil { 655 defaultOSImage = aztypes.OSImage{ 656 Plan: ic.Azure.DefaultMachinePlatform.OSImage.Plan, 657 Publisher: ic.Azure.DefaultMachinePlatform.OSImage.Publisher, 658 SKU: ic.Azure.DefaultMachinePlatform.OSImage.SKU, 659 Version: ic.Azure.DefaultMachinePlatform.OSImage.Version, 660 Offer: ic.Azure.DefaultMachinePlatform.OSImage.Offer, 661 } 662 } 663 controlPlaneOSImage := defaultOSImage 664 if ic.ControlPlane.Platform.Azure != nil { 665 controlPlaneOSImage = ic.ControlPlane.Platform.Azure.OSImage 666 } 667 allErrs = append(allErrs, validateOSImage(fieldPath.Child("controlplane"), controlPlaneOSImage)...) 668 computeOSImage := defaultOSImage 669 if len(ic.Compute) > 0 && ic.Compute[0].Platform.Azure != nil { 670 computeOSImage = ic.Compute[0].Platform.Azure.OSImage 671 } 672 allErrs = append(allErrs, validateOSImage(fieldPath.Child("compute"), computeOSImage)...) 673 return allErrs 674 } 675 func validateOSImage(fieldPath *field.Path, osImage aztypes.OSImage) field.ErrorList { 676 if _, ok := os.LookupEnv("OPENSHIFT_INSTALL_SKIP_IMAGE_UPLOAD"); ok { 677 if len(osImage.SKU) > 0 { 678 return nil 679 } 680 return field.ErrorList{field.Invalid(fieldPath, "image", "cannot skip image upload without marketplace image specified")} 681 } 682 return nil 683 } 684 func validateResourceGroup(client API, fieldPath *field.Path, platform *aztypes.Platform) field.ErrorList { 685 allErrs := field.ErrorList{} 686 if len(platform.ResourceGroupName) == 0 { 687 return allErrs 688 } 689 group, err := client.GetGroup(context.TODO(), platform.ResourceGroupName) 690 if err != nil { 691 return append(allErrs, field.InternalError(fieldPath.Child("resourceGroupName"), fmt.Errorf("failed to get resource group: %w", err))) 692 } 693 694 normalizedRegion := strings.Replace(strings.ToLower(to.String(group.Location)), " ", "", -1) 695 if !strings.EqualFold(normalizedRegion, platform.Region) { 696 allErrs = append(allErrs, field.Invalid(fieldPath.Child("resourceGroupName"), platform.ResourceGroupName, fmt.Sprintf("expected to in region %s, but found it to be in %s", platform.Region, normalizedRegion))) 697 } 698 699 tagKeys := make([]string, 0, len(group.Tags)) 700 for key := range group.Tags { 701 tagKeys = append(tagKeys, key) 702 } 703 sort.Strings(tagKeys) 704 conflictingTagKeys := tagKeys[:0] 705 for _, key := range tagKeys { 706 if strings.HasPrefix(key, "kubernetes.io_cluster") { 707 conflictingTagKeys = append(conflictingTagKeys, key) 708 } 709 } 710 if len(conflictingTagKeys) > 0 { 711 allErrs = append(allErrs, field.Invalid(fieldPath.Child("resourceGroupName"), platform.ResourceGroupName, fmt.Sprintf("resource group has conflicting tags %s", strings.Join(conflictingTagKeys, ", ")))) 712 } 713 714 // ARO provisions Azure resources before resolving the asset graph. 715 if !platform.IsARO() { 716 ids, err := client.ListResourceIDsByGroup(context.TODO(), platform.ResourceGroupName) 717 if err != nil { 718 return append(allErrs, field.InternalError(fieldPath.Child("resourceGroupName"), fmt.Errorf("failed to list resources in the resource group: %w", err))) 719 } 720 if l := len(ids); l > 0 { 721 if len(ids) > 2 { 722 ids = ids[:2] 723 } 724 allErrs = append(allErrs, field.Invalid(fieldPath.Child("resourceGroupName"), platform.ResourceGroupName, fmt.Sprintf("resource group must be empty but it has %d resources like %s ...", l, strings.Join(ids, ", ")))) 725 } 726 } 727 return allErrs 728 } 729 730 func checkAzureStackClusterOSImageSet(ClusterOSImage string, fldPath *field.Path) field.ErrorList { 731 var allErrs field.ErrorList 732 if ClusterOSImage == "" { 733 allErrs = append(allErrs, field.Required(fldPath.Child("clusterOSImage"), "clusterOSImage must be set when installing on Azure Stack")) 734 } 735 return allErrs 736 } 737 738 func validateAzureStackClusterOSImage(StorageEndpointSuffix string, ClusterOSImage string, fldPath *field.Path) field.ErrorList { 739 var allErrs field.ErrorList 740 imageParsedURL, err := url.Parse(ClusterOSImage) 741 if err != nil { 742 allErrs = append(allErrs, field.Invalid(fldPath.Child("clusterOSImage"), ClusterOSImage, fmt.Errorf("clusterOSImage URL is invalid: %w", err).Error())) 743 } 744 // If the URL for the image isn't in the Azure Stack environment we can't use it. 745 if !strings.HasSuffix(imageParsedURL.Host, StorageEndpointSuffix) { 746 allErrs = append(allErrs, field.Invalid(fldPath.Child("clusterOSImage"), ClusterOSImage, "clusterOSImage must be in the Azure Stack environment")) 747 } 748 return allErrs 749 } 750 751 func validateMarketplaceImages(client API, installConfig *types.InstallConfig) field.ErrorList { 752 var allErrs field.ErrorList 753 754 region := installConfig.Azure.Region 755 cloudName := installConfig.Azure.CloudName 756 757 var defaultInstanceType string 758 var defaultOSImage aztypes.OSImage 759 if installConfig.Azure.DefaultMachinePlatform != nil { 760 defaultInstanceType = installConfig.Azure.DefaultMachinePlatform.InstanceType 761 defaultOSImage = installConfig.Azure.DefaultMachinePlatform.OSImage 762 } 763 764 // Validate ControlPlane marketplace images 765 if installConfig.ControlPlane != nil { 766 platform := installConfig.ControlPlane.Platform.Azure 767 fldPath := field.NewPath("controlPlane") 768 769 // Determine instance type 770 instanceType := "" 771 if platform != nil { 772 instanceType = platform.InstanceType 773 } 774 if instanceType == "" { 775 instanceType = defaultInstanceType 776 } 777 if instanceType == "" { 778 instanceType = defaults.ControlPlaneInstanceType(cloudName, region, installConfig.ControlPlane.Architecture) 779 } 780 781 capabilities, err := client.GetVMCapabilities(context.Background(), instanceType, region) 782 if err != nil { 783 allErrs = append(allErrs, field.Invalid(fldPath.Child("platform", "azure", "type"), instanceType, err.Error())) 784 } 785 786 generations, err := GetHyperVGenerationVersions(capabilities) 787 if err != nil { 788 allErrs = append(allErrs, field.Invalid(fldPath.Child("platform", "azure", "type"), instanceType, err.Error())) 789 } 790 791 // If not set, try to use the OS Image definition from the default machine pool 792 var osImage aztypes.OSImage 793 if platform != nil { 794 osImage = platform.OSImage 795 } 796 if osImage.Publisher == "" { 797 osImage = defaultOSImage 798 } 799 800 imgErr := validateMarketplaceImage(client, region, generations, &osImage, fldPath) 801 if imgErr != nil { 802 allErrs = append(allErrs, imgErr) 803 } 804 } 805 806 // Validate Compute marketplace images 807 for i, compute := range installConfig.Compute { 808 platform := compute.Platform.Azure 809 fldPath := field.NewPath("compute").Index(i) 810 811 // Determine instance type 812 instanceType := "" 813 if platform != nil { 814 instanceType = platform.InstanceType 815 } 816 if instanceType == "" { 817 instanceType = defaultInstanceType 818 } 819 if instanceType == "" { 820 instanceType = defaults.ComputeInstanceType(cloudName, region, compute.Architecture) 821 } 822 823 capabilities, err := client.GetVMCapabilities(context.Background(), instanceType, region) 824 if err != nil { 825 allErrs = append(allErrs, field.Invalid(fldPath.Child("platform", "azure", "type"), instanceType, err.Error())) 826 continue 827 } 828 829 generations, err := GetHyperVGenerationVersions(capabilities) 830 if err != nil { 831 allErrs = append(allErrs, field.Invalid(fldPath.Child("platform", "azure", "type"), instanceType, err.Error())) 832 continue 833 } 834 835 // If not set, try to use the OS Image definition from the default machine pool 836 var osImage aztypes.OSImage 837 if platform != nil { 838 osImage = platform.OSImage 839 } 840 if osImage.Publisher == "" { 841 osImage = defaultOSImage 842 } 843 imgErr := validateMarketplaceImage(client, region, generations, &osImage, fldPath) 844 if imgErr != nil { 845 allErrs = append(allErrs, imgErr) 846 } 847 } 848 849 return allErrs 850 } 851 852 func validateMarketplaceImage(client API, region string, instanceHyperVGenSet sets.Set[string], osImage *aztypes.OSImage, fldPath *field.Path) *field.Error { 853 // Marketplace image not specified 854 if osImage.Publisher == "" { 855 return nil 856 } 857 858 osImageFieldPath := fldPath.Child("platform", "azure", "osImage") 859 vmImage, err := client.GetMarketplaceImage( 860 context.Background(), 861 region, 862 osImage.Publisher, 863 osImage.Offer, 864 osImage.SKU, 865 osImage.Version, 866 ) 867 if err != nil { 868 return field.Invalid(osImageFieldPath, osImage, err.Error()) 869 } 870 imageHyperVGen := string(vmImage.HyperVGeneration) 871 if !instanceHyperVGenSet.Has(imageHyperVGen) { 872 errMsg := fmt.Sprintf("instance type supports HyperVGenerations %v but the specified image is for HyperVGeneration %s; to correct this issue either specify a compatible instance type or change the HyperVGeneration for the image by using a different SKU", instanceHyperVGenSet.UnsortedList(), imageHyperVGen) 873 return field.Invalid(osImageFieldPath, osImage.SKU, errMsg) 874 } 875 876 // Image has license terms to be accepted 877 osImagePlan := osImage.Plan 878 if len(osImagePlan) == 0 { 879 // Use the default if not set in the install-config 880 osImagePlan = aztypes.ImageWithPurchasePlan 881 } 882 if plan := vmImage.Plan; plan != nil { 883 if osImagePlan == aztypes.ImageNoPurchasePlan { 884 return field.Invalid(osImageFieldPath, osImage, "marketplace image requires license terms to be accepted") 885 } 886 termsAccepted, err := client.AreMarketplaceImageTermsAccepted(context.Background(), osImage.Publisher, osImage.Offer, osImage.SKU) 887 if err != nil { 888 return field.Invalid(osImageFieldPath, osImage, fmt.Sprintf("could not determine if the license terms for the marketplace image have been accepted: %v", err)) 889 } 890 if !termsAccepted { 891 return field.Invalid(osImageFieldPath, osImage, "the license terms for the marketplace image have not been accepted") 892 } 893 } else if osImagePlan == aztypes.ImageWithPurchasePlan { 894 return field.Invalid(osImageFieldPath, osImage, "image has no license terms. Set Plan to \"NoPurchasePlan\" to continue.") 895 } 896 897 return nil 898 } 899 900 func validateAzureStackDiskType(_ API, installConfig *types.InstallConfig) field.ErrorList { 901 var allErrs field.ErrorList 902 903 supportedTypes := sets.New("Premium_LRS", "Standard_LRS") 904 errMsg := fmt.Sprintf("disk format not supported. Must be one of %v", sets.List(supportedTypes)) 905 906 defaultDiskType := aztypes.DefaultDiskType 907 if installConfig.Azure.DefaultMachinePlatform != nil && 908 installConfig.Azure.DefaultMachinePlatform.DiskType != "" { 909 defaultDiskType = installConfig.Azure.DefaultMachinePlatform.DiskType 910 } 911 912 diskType := defaultDiskType 913 if installConfig.ControlPlane != nil && 914 installConfig.ControlPlane.Platform.Azure != nil && 915 installConfig.ControlPlane.Platform.Azure.DiskType != "" { 916 diskType = installConfig.ControlPlane.Platform.Azure.DiskType 917 } 918 if !supportedTypes.Has(diskType) { 919 allErrs = append(allErrs, field.Invalid(field.NewPath("controlPlane", "platform", "azure", "OSDisk", "diskType"), diskType, errMsg)) 920 } 921 922 for idx, compute := range installConfig.Compute { 923 fieldPath := field.NewPath("compute").Index(idx) 924 diskType := defaultDiskType 925 if compute.Platform.Azure != nil && compute.Platform.Azure.DiskType != "" { 926 diskType = compute.Platform.Azure.DiskType 927 } 928 if !supportedTypes.Has(diskType) { 929 allErrs = append(allErrs, field.Invalid(fieldPath.Child("platform", "azure", "OSDisk", "diskType"), diskType, errMsg)) 930 } 931 } 932 933 return allErrs 934 }