github.com/openshift/installer@v1.4.17/pkg/asset/installconfig/aws/validation.go (about) 1 package aws 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "net" 8 "net/http" 9 "net/url" 10 "sort" 11 12 "github.com/aws/aws-sdk-go/aws" 13 "github.com/aws/aws-sdk-go/aws/endpoints" 14 "github.com/aws/aws-sdk-go/aws/session" 15 "github.com/aws/aws-sdk-go/service/ec2" 16 "github.com/aws/aws-sdk-go/service/iam" 17 "github.com/aws/aws-sdk-go/service/route53" 18 utilerrors "k8s.io/apimachinery/pkg/util/errors" 19 "k8s.io/apimachinery/pkg/util/sets" 20 "k8s.io/apimachinery/pkg/util/validation/field" 21 22 "github.com/openshift/installer/pkg/rhcos" 23 "github.com/openshift/installer/pkg/types" 24 awstypes "github.com/openshift/installer/pkg/types/aws" 25 ) 26 27 type resourceRequirements struct { 28 minimumVCpus int64 29 minimumMemory int64 30 } 31 32 var controlPlaneReq = resourceRequirements{ 33 minimumVCpus: 4, 34 minimumMemory: 16384, 35 } 36 37 var computeReq = resourceRequirements{ 38 minimumVCpus: 2, 39 minimumMemory: 8192, 40 } 41 42 // Validate executes platform-specific validation. 43 func Validate(ctx context.Context, meta *Metadata, config *types.InstallConfig) error { 44 allErrs := field.ErrorList{} 45 46 if config.Platform.AWS == nil { 47 return errors.New(field.Required(field.NewPath("platform", "aws"), "AWS validation requires an AWS platform configuration").Error()) 48 } 49 allErrs = append(allErrs, validateAMI(ctx, config)...) 50 allErrs = append(allErrs, validatePublicIpv4Pool(ctx, meta, field.NewPath("platform", "aws", "publicIpv4PoolId"), config)...) 51 allErrs = append(allErrs, validatePlatform(ctx, meta, field.NewPath("platform", "aws"), config.Platform.AWS, config.Networking, config.Publish)...) 52 53 if config.ControlPlane != nil { 54 arch := string(config.ControlPlane.Architecture) 55 pool := &awstypes.MachinePool{} 56 pool.Set(config.AWS.DefaultMachinePlatform) 57 pool.Set(config.ControlPlane.Platform.AWS) 58 allErrs = append(allErrs, validateMachinePool(ctx, meta, field.NewPath("controlPlane", "platform", "aws"), config.Platform.AWS, pool, controlPlaneReq, "", arch)...) 59 } 60 61 for idx, compute := range config.Compute { 62 fldPath := field.NewPath("compute").Index(idx) 63 if compute.Name == types.MachinePoolEdgeRoleName { 64 if len(config.Platform.AWS.Subnets) == 0 { 65 if compute.Platform.AWS == nil { 66 allErrs = append(allErrs, field.Required(fldPath.Child("platform", "aws"), "edge compute pools are only supported on the AWS platform")) 67 } 68 } 69 } 70 71 arch := string(compute.Architecture) 72 pool := &awstypes.MachinePool{} 73 pool.Set(config.AWS.DefaultMachinePlatform) 74 pool.Set(compute.Platform.AWS) 75 allErrs = append(allErrs, validateMachinePool(ctx, meta, fldPath.Child("platform", "aws"), config.Platform.AWS, pool, computeReq, compute.Name, arch)...) 76 } 77 return allErrs.ToAggregate() 78 } 79 80 func validatePlatform(ctx context.Context, meta *Metadata, fldPath *field.Path, platform *awstypes.Platform, networking *types.Networking, publish types.PublishingStrategy) field.ErrorList { 81 allErrs := field.ErrorList{} 82 83 allErrs = append(allErrs, validateServiceEndpoints(fldPath.Child("serviceEndpoints"), platform.Region, platform.ServiceEndpoints)...) 84 85 // Fail fast when service endpoints are invalid to avoid long timeouts. 86 if len(allErrs) > 0 { 87 return allErrs 88 } 89 90 if len(platform.Subnets) > 0 { 91 allErrs = append(allErrs, validateSubnets(ctx, meta, fldPath.Child("subnets"), platform.Subnets, networking, publish)...) 92 } 93 if platform.DefaultMachinePlatform != nil { 94 allErrs = append(allErrs, validateMachinePool(ctx, meta, fldPath.Child("defaultMachinePlatform"), platform, platform.DefaultMachinePlatform, controlPlaneReq, "", "")...) 95 } 96 return allErrs 97 } 98 99 func validateAMI(ctx context.Context, config *types.InstallConfig) field.ErrorList { 100 // accept AMI from the rhcos stream metadata 101 if rhcos.AMIRegions(config.ControlPlane.Architecture).Has(config.Platform.AWS.Region) { 102 return nil 103 } 104 105 // accept AMI specified at the platform level 106 if config.Platform.AWS.AMIID != "" { 107 return nil 108 } 109 110 // accept AMI specified for the default machine platform 111 if config.Platform.AWS.DefaultMachinePlatform != nil { 112 if config.Platform.AWS.DefaultMachinePlatform.AMIID != "" { 113 return nil 114 } 115 } 116 117 // accept AMIs specified specifically for each machine pool 118 controlPlaneHasAMISpecified := false 119 if config.ControlPlane != nil && config.ControlPlane.Platform.AWS != nil { 120 controlPlaneHasAMISpecified = config.ControlPlane.Platform.AWS.AMIID != "" 121 } 122 computesHaveAMISpecified := true 123 for _, c := range config.Compute { 124 if c.Replicas != nil && *c.Replicas == 0 { 125 continue 126 } 127 if c.Platform.AWS == nil || c.Platform.AWS.AMIID == "" { 128 computesHaveAMISpecified = false 129 } 130 } 131 if controlPlaneHasAMISpecified && computesHaveAMISpecified { 132 return nil 133 } 134 135 // accept AMI that can be copied from us-east-1 if the region is in the standard AWS partition 136 if partition, partitionFound := endpoints.PartitionForRegion(endpoints.DefaultPartitions(), config.Platform.AWS.Region); partitionFound { 137 if partition.ID() == endpoints.AwsPartitionID { 138 return nil 139 } 140 } 141 142 // fail validation since we do not have an AMI to use 143 return field.ErrorList{field.Required(field.NewPath("platform", "aws", "amiID"), "AMI must be provided")} 144 } 145 146 func validatePublicIpv4Pool(ctx context.Context, meta *Metadata, fldPath *field.Path, config *types.InstallConfig) field.ErrorList { 147 allErrs := field.ErrorList{} 148 149 if config.Platform.AWS.PublicIpv4Pool == "" { 150 return nil 151 } 152 poolID := config.Platform.AWS.PublicIpv4Pool 153 if config.Publish != types.ExternalPublishingStrategy { 154 return append(allErrs, field.Invalid(fldPath, poolID, fmt.Errorf("publish strategy %s can't be used with custom Public IPv4 Pools", config.Publish).Error())) 155 } 156 157 // Pool validations 158 // Resources claiming Public IPv4 from Pool in regular 'External' installations: 159 // 1* for Bootsrtap 160 // N*Zones for NAT Gateways 161 // N*Zones for API LB 162 // N*Zones for Ingress LB 163 allzones, err := meta.AvailabilityZones(ctx) 164 if err != nil { 165 return append(allErrs, field.InternalError(fldPath, err)) 166 } 167 totalPublicIPRequired := int64(1 + (len(allzones) * 3)) 168 169 sess, err := meta.Session(ctx) 170 if err != nil { 171 return append(allErrs, field.Invalid(fldPath, nil, fmt.Sprintf("unable to start a session: %s", err.Error()))) 172 } 173 publicIpv4Pool, err := DescribePublicIpv4Pool(ctx, sess, config.Platform.AWS.Region, poolID) 174 if err != nil { 175 return append(allErrs, field.Invalid(fldPath, poolID, err.Error())) 176 } 177 178 got := aws.Int64Value(publicIpv4Pool.TotalAvailableAddressCount) 179 if got < totalPublicIPRequired { 180 err = fmt.Errorf("required a minimum of %d Public IPv4 IPs available in the pool %s, got %d", totalPublicIPRequired, poolID, got) 181 return append(allErrs, field.InternalError(fldPath, err)) 182 } 183 184 return nil 185 } 186 187 func validateSubnets(ctx context.Context, meta *Metadata, fldPath *field.Path, subnets []string, networking *types.Networking, publish types.PublishingStrategy) field.ErrorList { 188 allErrs := field.ErrorList{} 189 privateSubnets, err := meta.PrivateSubnets(ctx) 190 if err != nil { 191 return append(allErrs, field.Invalid(fldPath, subnets, err.Error())) 192 } 193 privateSubnetsIdx := map[string]int{} 194 for idx, id := range subnets { 195 if _, ok := privateSubnets[id]; ok { 196 privateSubnetsIdx[id] = idx 197 } 198 } 199 if len(privateSubnets) == 0 { 200 allErrs = append(allErrs, field.Invalid(fldPath, subnets, "No private subnets found")) 201 } 202 203 publicSubnets, err := meta.PublicSubnets(ctx) 204 if err != nil { 205 return append(allErrs, field.Invalid(fldPath, subnets, err.Error())) 206 } 207 publicSubnetsIdx := map[string]int{} 208 for idx, id := range subnets { 209 if _, ok := publicSubnets[id]; ok { 210 publicSubnetsIdx[id] = idx 211 } 212 } 213 214 edgeSubnets, err := meta.EdgeSubnets(ctx) 215 if err != nil { 216 return append(allErrs, field.Invalid(fldPath, subnets, err.Error())) 217 } 218 edgeSubnetsIdx := map[string]int{} 219 for idx, id := range subnets { 220 if _, ok := edgeSubnets[id]; ok { 221 edgeSubnetsIdx[id] = idx 222 } 223 } 224 225 allErrs = append(allErrs, validateSubnetCIDR(fldPath, privateSubnets, privateSubnetsIdx, networking.MachineNetwork)...) 226 allErrs = append(allErrs, validateSubnetCIDR(fldPath, publicSubnets, publicSubnetsIdx, networking.MachineNetwork)...) 227 allErrs = append(allErrs, validateDuplicateSubnetZones(fldPath, privateSubnets, privateSubnetsIdx, "private")...) 228 allErrs = append(allErrs, validateDuplicateSubnetZones(fldPath, publicSubnets, publicSubnetsIdx, "public")...) 229 allErrs = append(allErrs, validateDuplicateSubnetZones(fldPath, edgeSubnets, edgeSubnetsIdx, "edge")...) 230 231 privateZones := sets.New[string]() 232 publicZones := sets.New[string]() 233 for _, subnet := range privateSubnets { 234 privateZones.Insert(subnet.Zone.Name) 235 } 236 for _, subnet := range publicSubnets { 237 publicZones.Insert(subnet.Zone.Name) 238 } 239 if publish == types.ExternalPublishingStrategy && !publicZones.IsSuperset(privateZones) { 240 errMsg := fmt.Sprintf("No public subnet provided for zones %s", sets.List(privateZones.Difference(publicZones))) 241 allErrs = append(allErrs, field.Invalid(fldPath, subnets, errMsg)) 242 } 243 244 return allErrs 245 } 246 247 func validateMachinePool(ctx context.Context, meta *Metadata, fldPath *field.Path, platform *awstypes.Platform, pool *awstypes.MachinePool, req resourceRequirements, poolName string, arch string) field.ErrorList { 248 var err error 249 allErrs := field.ErrorList{} 250 251 // Pool's specific validation. 252 // Edge Compute Pool / AWS Local Zones: 253 // - is valid when installing in existing VPC; or 254 // - is valid in new VPC when Local Zone name is defined 255 if poolName == types.MachinePoolEdgeRoleName { 256 if len(platform.Subnets) > 0 { 257 edgeSubnets, err := meta.EdgeSubnets(ctx) 258 if err != nil { 259 errMsg := fmt.Sprintf("%s pool. %v", poolName, err.Error()) 260 return append(allErrs, field.Invalid(field.NewPath("subnets"), platform.Subnets, errMsg)) 261 } 262 if len(edgeSubnets) == 0 { 263 return append(allErrs, field.Required(fldPath, "the provided subnets must include valid subnets for the specified edge zones")) 264 } 265 } else { 266 if pool.Zones == nil || len(pool.Zones) == 0 { 267 return append(allErrs, field.Required(fldPath, "zone is required when using edge machine pools")) 268 } 269 for _, zone := range pool.Zones { 270 err := validateZoneLocal(ctx, meta, fldPath.Child("zones"), zone) 271 if err != nil { 272 allErrs = append(allErrs, err) 273 } 274 } 275 if len(allErrs) > 0 { 276 return allErrs 277 } 278 } 279 } 280 281 if pool.Zones != nil && len(pool.Zones) > 0 { 282 availableZones := sets.New[string]() 283 diffErrMsgPrefix := "One or more zones are unavailable" 284 if len(platform.Subnets) > 0 { 285 diffErrMsgPrefix = "No subnets provided for zones" 286 var subnets Subnets 287 if poolName == types.MachinePoolEdgeRoleName { 288 subnets, err = meta.EdgeSubnets(ctx) 289 } else { 290 subnets, err = meta.PrivateSubnets(ctx) 291 } 292 293 if err != nil { 294 return append(allErrs, field.InternalError(fldPath, err)) 295 } 296 for _, subnet := range subnets { 297 availableZones.Insert(subnet.Zone.Name) 298 } 299 } else { 300 var allzones []string 301 if poolName == types.MachinePoolEdgeRoleName { 302 allzones, err = meta.EdgeZones(ctx) 303 } else { 304 allzones, err = meta.AvailabilityZones(ctx) 305 } 306 if err != nil { 307 return append(allErrs, field.InternalError(fldPath, err)) 308 } 309 availableZones.Insert(allzones...) 310 } 311 312 if diff := sets.New[string](pool.Zones...).Difference(availableZones); diff.Len() > 0 { 313 errMsg := fmt.Sprintf("%s %s", diffErrMsgPrefix, sets.List(diff)) 314 allErrs = append(allErrs, field.Invalid(fldPath.Child("zones"), pool.Zones, errMsg)) 315 } 316 } 317 if pool.InstanceType != "" { 318 instanceTypes, err := meta.InstanceTypes(ctx) 319 if err != nil { 320 return append(allErrs, field.InternalError(fldPath, err)) 321 } 322 if typeMeta, ok := instanceTypes[pool.InstanceType]; ok { 323 if typeMeta.DefaultVCpus < req.minimumVCpus { 324 errMsg := fmt.Sprintf("instance type does not meet minimum resource requirements of %d vCPUs", req.minimumVCpus) 325 allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), pool.InstanceType, errMsg)) 326 } 327 if typeMeta.MemInMiB < req.minimumMemory { 328 errMsg := fmt.Sprintf("instance type does not meet minimum resource requirements of %d MiB Memory", req.minimumMemory) 329 allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), pool.InstanceType, errMsg)) 330 } 331 instanceArches := translateEC2Arches(typeMeta.Arches) 332 // `arch` might not be specified (e.g, defaultMachinePool) 333 if len(arch) > 0 && !instanceArches.Has(arch) { 334 errMsg := fmt.Sprintf("instance type supported architectures %s do not match specified architecture %s", sets.List(instanceArches), arch) 335 allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), pool.InstanceType, errMsg)) 336 } 337 } else { 338 errMsg := fmt.Sprintf("instance type %s not found", pool.InstanceType) 339 allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), pool.InstanceType, errMsg)) 340 } 341 } 342 343 if len(pool.AdditionalSecurityGroupIDs) > 0 { 344 allErrs = append(allErrs, validateSecurityGroupIDs(ctx, meta, fldPath.Child("additionalSecurityGroupIDs"), platform, pool)...) 345 } 346 347 if len(pool.IAMProfile) > 0 { 348 if len(pool.IAMRole) > 0 { 349 allErrs = append(allErrs, field.Forbidden(fldPath.Child("iamRole"), "cannot be used with iamProfile")) 350 } 351 if err := validateInstanceProfile(ctx, meta, fldPath.Child("iamProfile"), pool); err != nil { 352 allErrs = append(allErrs, err) 353 } 354 } 355 356 return allErrs 357 } 358 359 func translateEC2Arches(arches []string) sets.Set[string] { 360 res := sets.New[string]() 361 for _, arch := range arches { 362 switch arch { 363 case ec2.ArchitectureTypeX8664: 364 res.Insert(types.ArchitectureAMD64) 365 case ec2.ArchitectureTypeArm64: 366 res.Insert(types.ArchitectureARM64) 367 default: 368 continue 369 } 370 } 371 return res 372 } 373 374 func validateSecurityGroupIDs(ctx context.Context, meta *Metadata, fldPath *field.Path, platform *awstypes.Platform, pool *awstypes.MachinePool) field.ErrorList { 375 allErrs := field.ErrorList{} 376 377 vpc, err := meta.VPC(ctx) 378 if err != nil { 379 errMsg := fmt.Sprintf("could not determine cluster VPC: %s", err.Error()) 380 return append(allErrs, field.Invalid(fldPath, vpc, errMsg)) 381 } 382 383 securityGroups, err := DescribeSecurityGroups(ctx, meta.session, pool.AdditionalSecurityGroupIDs, platform.Region) 384 if err != nil { 385 return append(allErrs, field.Invalid(fldPath, pool.AdditionalSecurityGroupIDs, err.Error())) 386 } 387 388 for _, sg := range securityGroups { 389 sgVpcID := *sg.VpcId 390 if sgVpcID != vpc { 391 errMsg := fmt.Sprintf("sg %s is associated with vpc %s not the provided vpc %s", *sg.GroupId, sgVpcID, vpc) 392 allErrs = append(allErrs, field.Invalid(fldPath, sgVpcID, errMsg)) 393 } 394 } 395 396 return allErrs 397 } 398 399 func validateSubnetCIDR(fldPath *field.Path, subnets Subnets, idxMap map[string]int, networks []types.MachineNetworkEntry) field.ErrorList { 400 allErrs := field.ErrorList{} 401 for id, v := range subnets { 402 fp := fldPath.Index(idxMap[id]) 403 cidr, _, err := net.ParseCIDR(v.CIDR) 404 if err != nil { 405 allErrs = append(allErrs, field.Invalid(fp, id, err.Error())) 406 continue 407 } 408 allErrs = append(allErrs, validateMachineNetworksContainIP(fp, networks, id, cidr)...) 409 } 410 return allErrs 411 } 412 413 func validateMachineNetworksContainIP(fldPath *field.Path, networks []types.MachineNetworkEntry, subnetName string, ip net.IP) field.ErrorList { 414 for _, network := range networks { 415 if network.CIDR.Contains(ip) { 416 return nil 417 } 418 } 419 return field.ErrorList{field.Invalid(fldPath, subnetName, fmt.Sprintf("subnet's CIDR range start %s is outside of the specified machine networks", ip))} 420 } 421 422 func validateDuplicateSubnetZones(fldPath *field.Path, subnets Subnets, idxMap map[string]int, typ string) field.ErrorList { 423 var keys []string 424 for id := range subnets { 425 keys = append(keys, id) 426 } 427 sort.Strings(keys) 428 429 allErrs := field.ErrorList{} 430 zones := map[string]string{} 431 for _, id := range keys { 432 subnet := subnets[id] 433 if conflictingSubnet, ok := zones[subnet.Zone.Name]; ok { 434 errMsg := fmt.Sprintf("%s subnet %s is also in zone %s", typ, conflictingSubnet, subnet.Zone.Name) 435 allErrs = append(allErrs, field.Invalid(fldPath.Index(idxMap[id]), id, errMsg)) 436 } else { 437 zones[subnet.Zone.Name] = id 438 } 439 } 440 return allErrs 441 } 442 443 func validateServiceEndpoints(fldPath *field.Path, region string, services []awstypes.ServiceEndpoint) field.ErrorList { 444 allErrs := field.ErrorList{} 445 ec2Endpoint := "" 446 for id, service := range services { 447 err := validateEndpointAccessibility(service.URL) 448 if err != nil { 449 allErrs = append(allErrs, field.Invalid(fldPath.Index(id).Child("url"), service.URL, err.Error())) 450 continue 451 } 452 if service.Name == ec2.ServiceName { 453 ec2Endpoint = service.URL 454 } 455 } 456 457 if partition, partitionFound := endpoints.PartitionForRegion(endpoints.DefaultPartitions(), region); partitionFound { 458 if _, ok := partition.Regions()[region]; !ok && ec2Endpoint == "" { 459 err := validateRegion(region) 460 if err != nil { 461 allErrs = append(allErrs, field.Invalid(fldPath.Child("region"), region, err.Error())) 462 } 463 } 464 return allErrs 465 } 466 467 resolver := newAWSResolver(region, services) 468 var errs []error 469 for _, service := range requiredServices { 470 _, err := resolver.EndpointFor(service, region, endpoints.StrictMatchingOption) 471 if err != nil { 472 errs = append(errs, fmt.Errorf("failed to find endpoint for service %q: %w", service, err)) 473 } 474 } 475 if err := utilerrors.NewAggregate(errs); err != nil { 476 allErrs = append(allErrs, field.Invalid(fldPath, services, err.Error())) 477 } 478 return allErrs 479 } 480 481 func validateRegion(region string) error { 482 ses, err := GetSessionWithOptions(func(sess *session.Options) { 483 sess.Config.Region = aws.String(region) 484 }) 485 if err != nil { 486 return err 487 } 488 ec2Session := ec2.New(ses) 489 return validateEndpointAccessibility(ec2Session.Endpoint) 490 } 491 492 func validateZoneLocal(ctx context.Context, meta *Metadata, fldPath *field.Path, zoneName string) *field.Error { 493 sess, err := meta.Session(ctx) 494 if err != nil { 495 return field.Invalid(fldPath, zoneName, fmt.Sprintf("unable to start a session: %s", err.Error())) 496 } 497 zones, err := describeFilteredZones(ctx, sess, meta.Region, []string{zoneName}) 498 if err != nil { 499 return field.Invalid(fldPath, zoneName, fmt.Sprintf("unable to get describe zone: %s", err.Error())) 500 } 501 validZone := false 502 for _, zone := range zones { 503 if aws.StringValue(zone.ZoneName) == zoneName { 504 switch aws.StringValue(zone.ZoneType) { 505 case awstypes.LocalZoneType, awstypes.WavelengthZoneType: 506 default: 507 return field.Invalid(fldPath, zoneName, fmt.Sprintf("only zone type local-zone or wavelength-zone are valid in the edge machine pool: %s", aws.StringValue(zone.ZoneType))) 508 } 509 if aws.StringValue(zone.OptInStatus) != awstypes.ZoneOptInStatusOptedIn { 510 return field.Invalid(fldPath, zoneName, fmt.Sprintf("zone group is not opted-in: %s", aws.StringValue(zone.GroupName))) 511 } 512 validZone = true 513 } 514 } 515 if !validZone { 516 return field.Invalid(fldPath, zoneName, fmt.Sprintf("invalid local zone name: %s", zoneName)) 517 } 518 return nil 519 } 520 521 func validateEndpointAccessibility(endpointURL string) error { 522 // For each provided service endpoint, verify we can resolve and connect with net.Dial. 523 // Ignore e2e.local from unit tests. 524 if endpointURL == "e2e.local" { 525 return nil 526 } 527 _, err := url.Parse(endpointURL) 528 if err != nil { 529 return err 530 } 531 _, err = http.Head(endpointURL) 532 return err 533 } 534 535 var requiredServices = []string{ 536 "ec2", 537 "elasticloadbalancing", 538 "iam", 539 "route53", 540 "s3", 541 "sts", 542 "tagging", 543 } 544 545 // ValidateForProvisioning validates if the install config is valid for provisioning the cluster. 546 func ValidateForProvisioning(client API, ic *types.InstallConfig, metadata *Metadata) error { 547 if ic.Publish == types.InternalPublishingStrategy && ic.AWS.HostedZone == "" { 548 return nil 549 } 550 551 var zoneName string 552 var zonePath *field.Path 553 var zone *route53.HostedZone 554 555 allErrs := field.ErrorList{} 556 r53cfg := GetR53ClientCfg(metadata.session, ic.AWS.HostedZoneRole) 557 558 if ic.AWS.HostedZone != "" { 559 zoneName = ic.AWS.HostedZone 560 zonePath = field.NewPath("aws", "hostedZone") 561 zoneOutput, err := client.GetHostedZone(zoneName, r53cfg) 562 if err != nil { 563 errMsg := fmt.Errorf("unable to retrieve hosted zone: %w", err).Error() 564 return field.ErrorList{ 565 field.Invalid(zonePath, zoneName, errMsg), 566 }.ToAggregate() 567 } 568 569 if errs := validateHostedZone(zoneOutput, zonePath, zoneName, metadata); len(errs) > 0 { 570 allErrs = append(allErrs, errs...) 571 } 572 573 zone = zoneOutput.HostedZone 574 } else { 575 zoneName = ic.BaseDomain 576 zonePath = field.NewPath("baseDomain") 577 baseDomainOutput, err := client.GetBaseDomain(zoneName) 578 if err != nil { 579 return field.ErrorList{ 580 field.Invalid(zonePath, zoneName, "cannot find base domain"), 581 }.ToAggregate() 582 } 583 584 zone = baseDomainOutput 585 } 586 587 if errs := client.ValidateZoneRecords(zone, zoneName, zonePath, ic, r53cfg); len(errs) > 0 { 588 allErrs = append(allErrs, errs...) 589 } 590 591 return allErrs.ToAggregate() 592 } 593 594 func validateHostedZone(hostedZoneOutput *route53.GetHostedZoneOutput, hostedZonePath *field.Path, hostedZoneName string, metadata *Metadata) field.ErrorList { 595 allErrs := field.ErrorList{} 596 597 // validate that the hosted zone is associated with the VPC containing the existing subnets for the cluster 598 vpcID, err := metadata.VPC(context.TODO()) 599 if err == nil { 600 if !isHostedZoneAssociatedWithVPC(hostedZoneOutput, vpcID) { 601 allErrs = append(allErrs, field.Invalid(hostedZonePath, hostedZoneName, "hosted zone is not associated with the VPC")) 602 } 603 } else { 604 allErrs = append(allErrs, field.Invalid(hostedZonePath, hostedZoneName, "no VPC found")) 605 } 606 607 return allErrs 608 } 609 610 func isHostedZoneAssociatedWithVPC(hostedZone *route53.GetHostedZoneOutput, vpcID string) bool { 611 if vpcID == "" { 612 return false 613 } 614 for _, vpc := range hostedZone.VPCs { 615 if aws.StringValue(vpc.VPCId) == vpcID { 616 return true 617 } 618 } 619 return false 620 } 621 622 func validateInstanceProfile(ctx context.Context, meta *Metadata, fldPath *field.Path, pool *awstypes.MachinePool) *field.Error { 623 session, err := meta.Session(ctx) 624 if err != nil { 625 return field.InternalError(fldPath, fmt.Errorf("unable to start a session: %w", err)) 626 } 627 client := iam.New(session) 628 res, err := client.GetInstanceProfileWithContext(ctx, &iam.GetInstanceProfileInput{ 629 InstanceProfileName: aws.String(pool.IAMProfile), 630 }) 631 if err != nil { 632 msg := fmt.Errorf("unable to retrieve instance profile: %w", err).Error() 633 return field.Invalid(fldPath, pool.IAMProfile, msg) 634 } 635 if len(res.InstanceProfile.Roles) == 0 || res.InstanceProfile.Roles[0] == nil { 636 return field.Invalid(fldPath, pool.IAMProfile, "no role attached to instance profile") 637 } 638 639 return nil 640 }