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  }