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

     1  package gcp
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"net"
     8  	"strings"
     9  
    10  	"github.com/pkg/errors"
    11  	"github.com/sirupsen/logrus"
    12  	compute "google.golang.org/api/compute/v1"
    13  	"google.golang.org/api/dns/v1"
    14  	"google.golang.org/api/googleapi"
    15  	"k8s.io/apimachinery/pkg/util/sets"
    16  	"k8s.io/apimachinery/pkg/util/validation/field"
    17  
    18  	"github.com/openshift/installer/pkg/types"
    19  	"github.com/openshift/installer/pkg/types/gcp"
    20  	"github.com/openshift/installer/pkg/validate"
    21  	mapiutil "github.com/openshift/machine-api-provider-gcp/pkg/cloud/gcp/actuators/util"
    22  )
    23  
    24  type resourceRequirements struct {
    25  	minimumVCpus  int64
    26  	minimumMemory int64
    27  }
    28  
    29  var controlPlaneReq = resourceRequirements{
    30  	minimumVCpus:  4,
    31  	minimumMemory: 15360,
    32  }
    33  
    34  var computeReq = resourceRequirements{
    35  	minimumVCpus:  2,
    36  	minimumMemory: 7680,
    37  }
    38  
    39  var (
    40  	apiRecordType = func(ic *types.InstallConfig) string {
    41  		return fmt.Sprintf("api.%s.", strings.TrimSuffix(ic.ClusterDomain(), "."))
    42  	}
    43  	apiIntRecordName = func(ic *types.InstallConfig) string {
    44  		return fmt.Sprintf("api-int.%s.", strings.TrimSuffix(ic.ClusterDomain(), "."))
    45  	}
    46  )
    47  
    48  const unknownArchitecture = ""
    49  
    50  // Validate executes platform-specific validation.
    51  func Validate(client API, ic *types.InstallConfig) error {
    52  	allErrs := field.ErrorList{}
    53  
    54  	if err := validate.GCPClusterName(ic.ObjectMeta.Name); err != nil {
    55  		allErrs = append(allErrs, field.Invalid(field.NewPath("clusterName"), ic.ObjectMeta.Name, err.Error()))
    56  	}
    57  
    58  	allErrs = append(allErrs, validateProject(client, ic, field.NewPath("platform").Child("gcp"))...)
    59  	allErrs = append(allErrs, validateNetworkProject(client, ic, field.NewPath("platform").Child("gcp"))...)
    60  	allErrs = append(allErrs, validateRegion(client, ic, field.NewPath("platform").Child("gcp"))...)
    61  	allErrs = append(allErrs, validateZones(client, ic)...)
    62  	allErrs = append(allErrs, validateNetworks(client, ic, field.NewPath("platform").Child("gcp"))...)
    63  	allErrs = append(allErrs, validateInstanceTypes(client, ic)...)
    64  	allErrs = append(allErrs, ValidateCredentialMode(client, ic)...)
    65  	allErrs = append(allErrs, validatePreexistingServiceAccountXpn(client, ic)...)
    66  	allErrs = append(allErrs, validateServiceAccountPresent(client, ic)...)
    67  	allErrs = append(allErrs, validateMarketplaceImages(client, ic)...)
    68  
    69  	if err := validateUserTags(client, ic.Platform.GCP.ProjectID, ic.Platform.GCP.UserTags); err != nil {
    70  		allErrs = append(allErrs, field.Invalid(field.NewPath("platform").Child("gcp").Child("userTags"), ic.Platform.GCP.UserTags, err.Error()))
    71  	}
    72  
    73  	return allErrs.ToAggregate()
    74  }
    75  
    76  // ValidateInstanceType ensures the instance type has sufficient Vcpu and Memory.
    77  func ValidateInstanceType(client API, fieldPath *field.Path, project, region string, zones []string, diskType string, instanceType string, req resourceRequirements, arch string) field.ErrorList {
    78  	allErrs := field.ErrorList{}
    79  
    80  	typeMeta, typeZones, err := client.GetMachineTypeWithZones(context.TODO(), project, region, instanceType)
    81  	if err != nil {
    82  		if _, ok := err.(*googleapi.Error); ok {
    83  			return append(allErrs, field.Invalid(fieldPath.Child("type"), instanceType, err.Error()))
    84  		}
    85  		return append(allErrs, field.InternalError(nil, err))
    86  	}
    87  
    88  	if diskType == "hyperdisk-balanced" {
    89  		family, _, _ := strings.Cut(instanceType, "-")
    90  		families := sets.NewString("c3", "c3d", "m1", "n4")
    91  		if !families.Has(family) {
    92  			allErrs = append(allErrs, field.NotSupported(fieldPath.Child("diskType"), family, families.List()))
    93  		}
    94  	}
    95  
    96  	userZones := sets.New(zones...)
    97  	if len(userZones) == 0 {
    98  		userZones = typeZones
    99  	}
   100  	if diff := userZones.Difference(typeZones); len(diff) > 0 {
   101  		errMsg := fmt.Sprintf("instance type not available in zones: %v", sets.List(diff))
   102  		allErrs = append(allErrs, field.Invalid(fieldPath.Child("type"), instanceType, errMsg))
   103  	}
   104  
   105  	if typeMeta.GuestCpus < req.minimumVCpus {
   106  		errMsg := fmt.Sprintf("instance type does not meet minimum resource requirements of %d vCPUs", req.minimumVCpus)
   107  		allErrs = append(allErrs, field.Invalid(fieldPath.Child("type"), instanceType, errMsg))
   108  	}
   109  	if typeMeta.MemoryMb < req.minimumMemory {
   110  		errMsg := fmt.Sprintf("instance type does not meet minimum resource requirements of %d MB Memory", req.minimumMemory)
   111  		allErrs = append(allErrs, field.Invalid(fieldPath.Child("type"), instanceType, errMsg))
   112  	}
   113  
   114  	if arch != unknownArchitecture {
   115  		if typeArch := mapiutil.CPUArchitecture(instanceType); string(typeArch) != arch {
   116  			errMsg := fmt.Sprintf("instance type architecture %s does not match specified architecture %s", typeArch, arch)
   117  			allErrs = append(allErrs, field.Invalid(fieldPath.Child("type"), instanceType, errMsg))
   118  		}
   119  	}
   120  
   121  	return allErrs
   122  }
   123  
   124  func validateServiceAccountPresent(client API, ic *types.InstallConfig) field.ErrorList {
   125  	allErrs := field.ErrorList{}
   126  
   127  	if ic.GCP.NetworkProjectID != "" {
   128  		creds := client.GetCredentials()
   129  		if creds != nil && creds.JSON == nil {
   130  			if ic.ControlPlane.Platform.GCP != nil && ic.ControlPlane.Platform.GCP.ServiceAccount == "" {
   131  				errMsg := "service account must be provided when authentication credentials do not provide a service account"
   132  				allErrs = append(allErrs, field.Required(field.NewPath("controlPlane").Child("platform").Child("gcp").Child("serviceAccount"), errMsg))
   133  			}
   134  		}
   135  	}
   136  	return allErrs
   137  }
   138  
   139  // DefaultInstanceTypeForArch returns the appropriate instance type based on the target architecture.
   140  func DefaultInstanceTypeForArch(arch types.Architecture) string {
   141  	if arch == types.ArchitectureARM64 {
   142  		return "t2a-standard-4"
   143  	}
   144  	return "n2-standard-4"
   145  }
   146  
   147  // validateInstanceTypes checks that the user-provided instance types are valid.
   148  func validateInstanceTypes(client API, ic *types.InstallConfig) field.ErrorList {
   149  	allErrs := field.ErrorList{}
   150  
   151  	defaultInstanceType := ""
   152  	defaultZones := []string{}
   153  
   154  	// Default requirements need to be sufficient to support Control Plane instances.
   155  	defaultInstanceReq := controlPlaneReq
   156  	if ic.ControlPlane != nil && ic.ControlPlane.Platform.GCP != nil && ic.ControlPlane.Platform.GCP.InstanceType != "" {
   157  		// Default requirements can be relaxed when the controlPlane type is set explicitly.
   158  		defaultInstanceReq = computeReq
   159  	}
   160  
   161  	if ic.GCP.DefaultMachinePlatform != nil {
   162  		defaultZones = ic.GCP.DefaultMachinePlatform.Zones
   163  		defaultInstanceType = ic.GCP.DefaultMachinePlatform.InstanceType
   164  
   165  		if ic.GCP.DefaultMachinePlatform.InstanceType != "" {
   166  			allErrs = append(allErrs,
   167  				ValidateInstanceType(
   168  					client,
   169  					field.NewPath("platform", "gcp", "defaultMachinePlatform"),
   170  					ic.GCP.ProjectID,
   171  					ic.GCP.Region,
   172  					ic.GCP.DefaultMachinePlatform.Zones,
   173  					ic.GCP.DefaultMachinePlatform.DiskType,
   174  					ic.GCP.DefaultMachinePlatform.InstanceType,
   175  					defaultInstanceReq,
   176  					unknownArchitecture,
   177  				)...)
   178  		}
   179  	}
   180  
   181  	zones := defaultZones
   182  	instanceType := defaultInstanceType
   183  	arch := types.ArchitectureAMD64
   184  	if ic.ControlPlane != nil {
   185  		arch = string(ic.ControlPlane.Architecture)
   186  		if instanceType == "" {
   187  			instanceType = DefaultInstanceTypeForArch(ic.ControlPlane.Architecture)
   188  		}
   189  		if ic.ControlPlane.Platform.GCP != nil {
   190  			if ic.ControlPlane.Platform.GCP.InstanceType != "" {
   191  				instanceType = ic.ControlPlane.Platform.GCP.InstanceType
   192  			}
   193  			if len(ic.ControlPlane.Platform.GCP.Zones) > 0 {
   194  				zones = ic.ControlPlane.Platform.GCP.Zones
   195  			}
   196  		}
   197  	}
   198  	allErrs = append(allErrs,
   199  		ValidateInstanceType(
   200  			client,
   201  			field.NewPath("controlPlane", "platform", "gcp"),
   202  			ic.GCP.ProjectID,
   203  			ic.GCP.Region,
   204  			zones,
   205  			"", // the control plane nodes only support one disk type currently
   206  			instanceType,
   207  			controlPlaneReq,
   208  			arch,
   209  		)...)
   210  
   211  	for idx, compute := range ic.Compute {
   212  		fieldPath := field.NewPath("compute").Index(idx)
   213  		zones := defaultZones
   214  		instanceType := defaultInstanceType
   215  		if instanceType == "" {
   216  			instanceType = DefaultInstanceTypeForArch(compute.Architecture)
   217  		}
   218  		arch := compute.Architecture
   219  		if compute.Platform.GCP != nil {
   220  			if compute.Platform.GCP.InstanceType != "" {
   221  				instanceType = compute.Platform.GCP.InstanceType
   222  			}
   223  			if len(compute.Platform.GCP.Zones) > 0 {
   224  				zones = compute.Platform.GCP.Zones
   225  			}
   226  		}
   227  
   228  		diskType := ""
   229  		if compute.Platform.GCP != nil && compute.Platform.GCP.DiskType != "" {
   230  			diskType = compute.Platform.GCP.DiskType
   231  		}
   232  
   233  		allErrs = append(allErrs,
   234  			ValidateInstanceType(
   235  				client,
   236  				fieldPath.Child("platform", "gcp"),
   237  				ic.GCP.ProjectID,
   238  				ic.GCP.Region,
   239  				zones,
   240  				diskType,
   241  				instanceType,
   242  				computeReq,
   243  				string(arch),
   244  			)...)
   245  	}
   246  
   247  	return allErrs
   248  }
   249  
   250  func validatePreexistingServiceAccountXpn(client API, ic *types.InstallConfig) field.ErrorList {
   251  	allErrs := field.ErrorList{}
   252  
   253  	if ic.GCP.NetworkProjectID != "" {
   254  		if ic.ControlPlane.Platform.GCP != nil && ic.ControlPlane.Platform.GCP.ServiceAccount != "" {
   255  			fldPath := field.NewPath("controlPlane").Child("platform").Child("gcp").Child("serviceAccount")
   256  
   257  			// The service account is required for resources in the host project.
   258  			serviceAccount, err := client.GetServiceAccount(context.Background(), ic.GCP.ProjectID, ic.ControlPlane.Platform.GCP.ServiceAccount)
   259  			if err != nil {
   260  				return append(allErrs, field.InternalError(fldPath, err))
   261  			}
   262  			if serviceAccount == "" {
   263  				return append(allErrs, field.NotFound(fldPath, ic.ControlPlane.Platform.GCP.ServiceAccount))
   264  			}
   265  		}
   266  	}
   267  
   268  	return allErrs
   269  }
   270  
   271  // ValidatePreExistingPublicDNS ensure no pre-existing DNS record exists in the public
   272  // DNS zone for cluster's Kubernetes API. If a PublicDNSZone is provided, the provided
   273  // zone is verified against the BaseDomain. If no zone is provided, the base domain is
   274  // checked for any public zone that can be used.
   275  func ValidatePreExistingPublicDNS(client API, ic *types.InstallConfig) *field.Error {
   276  	// If this is an internal cluster, this check is not necessary
   277  	if ic.Publish == types.InternalPublishingStrategy {
   278  		return nil
   279  	}
   280  
   281  	zone, err := client.GetDNSZone(context.TODO(), ic.Platform.GCP.ProjectID, ic.BaseDomain, true)
   282  	if err != nil {
   283  		if IsNotFound(err) {
   284  			return field.NotFound(field.NewPath("baseDomain"), fmt.Sprintf("Public DNS Zone (%s/%s)", ic.Platform.GCP.ProjectID, ic.BaseDomain))
   285  		}
   286  		return field.InternalError(field.NewPath("baseDomain"), err)
   287  	}
   288  	return checkRecordSets(client, ic, zone, []string{apiRecordType(ic)})
   289  }
   290  
   291  // ValidatePrivateDNSZone ensure no pre-existing DNS record exists in the private dns zone
   292  // matching the name that will be used for this installation.
   293  func ValidatePrivateDNSZone(client API, ic *types.InstallConfig) *field.Error {
   294  	if ic.GCP.Network == "" || ic.GCP.NetworkProjectID == "" {
   295  		return nil
   296  	}
   297  
   298  	zone, err := client.GetDNSZone(context.TODO(), ic.GCP.ProjectID, ic.ClusterDomain(), false)
   299  	if err != nil {
   300  		logrus.Debug("No private DNS Zone found")
   301  		if IsNotFound(err) {
   302  			return field.NotFound(field.NewPath("baseDomain"), fmt.Sprintf("Private DNS Zone (%s/%s)", ic.Platform.GCP.ProjectID, ic.BaseDomain))
   303  		}
   304  		return field.InternalError(field.NewPath("baseDomain"), err)
   305  	}
   306  
   307  	// Private Zone can be nil, check to see if it was found or not
   308  	if zone != nil {
   309  		return checkRecordSets(client, ic, zone, []string{apiRecordType(ic), apiIntRecordName(ic)})
   310  	}
   311  	return nil
   312  }
   313  
   314  func checkRecordSets(client API, ic *types.InstallConfig, zone *dns.ManagedZone, records []string) *field.Error {
   315  	rrSets, err := client.GetRecordSets(context.TODO(), ic.GCP.ProjectID, zone.Name)
   316  	if err != nil {
   317  		return field.InternalError(field.NewPath("baseDomain"), err)
   318  	}
   319  
   320  	setOfReturnedRecords := sets.New[string]()
   321  	for _, r := range rrSets {
   322  		setOfReturnedRecords.Insert(r.Name)
   323  	}
   324  	preexistingRecords := sets.New[string](records...).Intersection(setOfReturnedRecords)
   325  
   326  	if preexistingRecords.Len() > 0 {
   327  		errMsg := fmt.Sprintf("record(s) %q already exists in DNS Zone (%s/%s) and might be in use by another cluster, please remove it to continue", sets.List(preexistingRecords), ic.GCP.ProjectID, zone.Name)
   328  		return field.Invalid(field.NewPath("metadata", "name"), ic.ObjectMeta.Name, errMsg)
   329  	}
   330  	return nil
   331  }
   332  
   333  // ValidateForProvisioning validates that the install config is valid for provisioning the cluster.
   334  func ValidateForProvisioning(ic *types.InstallConfig) error {
   335  	if ic.Platform.GCP.UserProvisionedDNS == gcp.UserProvisionedDNSEnabled {
   336  		return nil
   337  	}
   338  
   339  	allErrs := field.ErrorList{}
   340  
   341  	client, err := NewClient(context.TODO())
   342  	if err != nil {
   343  		return err
   344  	}
   345  
   346  	if err := ValidatePreExistingPublicDNS(client, ic); err != nil {
   347  		allErrs = append(allErrs, err)
   348  	}
   349  
   350  	if err := ValidatePrivateDNSZone(client, ic); err != nil {
   351  		allErrs = append(allErrs, err)
   352  	}
   353  
   354  	return allErrs.ToAggregate()
   355  }
   356  
   357  func validateProject(client API, ic *types.InstallConfig, fieldPath *field.Path) field.ErrorList {
   358  	allErrs := field.ErrorList{}
   359  
   360  	if ic.GCP.ProjectID != "" {
   361  		_, err := client.GetProjectByID(context.TODO(), ic.GCP.ProjectID)
   362  		if err != nil {
   363  			if IsNotFound(err) {
   364  				return append(allErrs, field.Invalid(fieldPath.Child("project"), ic.GCP.ProjectID, "invalid project ID"))
   365  			}
   366  			return append(allErrs, field.InternalError(fieldPath.Child("project"), err))
   367  		}
   368  	}
   369  
   370  	return allErrs
   371  }
   372  
   373  func validateNetworkProject(client API, ic *types.InstallConfig, fieldPath *field.Path) field.ErrorList {
   374  	allErrs := field.ErrorList{}
   375  
   376  	if ic.GCP.NetworkProjectID != "" {
   377  		_, err := client.GetProjectByID(context.TODO(), ic.GCP.NetworkProjectID)
   378  		if err != nil {
   379  			if IsNotFound(err) {
   380  				return append(allErrs, field.Invalid(fieldPath.Child("networkProjectID"), ic.GCP.NetworkProjectID, "invalid project ID"))
   381  			}
   382  			return append(allErrs, field.InternalError(fieldPath.Child("networkProjectID"), err))
   383  		}
   384  	}
   385  
   386  	return allErrs
   387  }
   388  
   389  // validateNetworks checks that the user-provided VPC is in the project and the provided subnets are valid.
   390  func validateNetworks(client API, ic *types.InstallConfig, fieldPath *field.Path) field.ErrorList {
   391  	allErrs := field.ErrorList{}
   392  
   393  	networkProjectID := ic.GCP.NetworkProjectID
   394  	if networkProjectID == "" {
   395  		networkProjectID = ic.GCP.ProjectID
   396  	}
   397  
   398  	if ic.GCP.Network != "" {
   399  		_, err := client.GetNetwork(context.TODO(), ic.GCP.Network, networkProjectID)
   400  		if err != nil {
   401  			return append(allErrs, field.Invalid(fieldPath.Child("network"), ic.GCP.Network, err.Error()))
   402  		}
   403  
   404  		subnets, err := client.GetSubnetworks(context.TODO(), ic.GCP.Network, networkProjectID, ic.GCP.Region)
   405  		if err != nil {
   406  			return append(allErrs, field.Invalid(fieldPath.Child("network"), ic.GCP.Network, "failed to retrieve subnets"))
   407  		}
   408  
   409  		allErrs = append(allErrs, validateSubnet(client, ic, fieldPath.Child("computeSubnet"), subnets, ic.GCP.ComputeSubnet)...)
   410  		allErrs = append(allErrs, validateSubnet(client, ic, fieldPath.Child("controlPlaneSubnet"), subnets, ic.GCP.ControlPlaneSubnet)...)
   411  	}
   412  
   413  	return allErrs
   414  }
   415  
   416  func validateSubnet(client API, ic *types.InstallConfig, fieldPath *field.Path, subnets []*compute.Subnetwork, name string) field.ErrorList {
   417  	allErrs := field.ErrorList{}
   418  
   419  	subnet, errMsg := findSubnet(subnets, name, ic.GCP.Network, ic.GCP.Region)
   420  	if subnet == nil {
   421  		return append(allErrs, field.Invalid(fieldPath, name, errMsg))
   422  	}
   423  
   424  	subnetIP, _, err := net.ParseCIDR(subnet.IpCidrRange)
   425  	if err != nil {
   426  		return append(allErrs, field.Invalid(fieldPath, name, "unable to parse subnet CIDR"))
   427  	}
   428  
   429  	allErrs = append(allErrs, validateMachineNetworksContainIP(fieldPath, ic.Networking.MachineNetwork, name, subnetIP)...)
   430  	return allErrs
   431  }
   432  
   433  // findSubnet checks that the subnets are in the provided VPC and region.
   434  func findSubnet(subnets []*compute.Subnetwork, userSubnet, network, region string) (*compute.Subnetwork, string) {
   435  	for _, vpcSubnet := range subnets {
   436  		if userSubnet == vpcSubnet.Name {
   437  			return vpcSubnet, ""
   438  		}
   439  	}
   440  	return nil, fmt.Sprintf("could not find subnet %s in network %s and region %s", userSubnet, network, region)
   441  }
   442  
   443  func validateMachineNetworksContainIP(fldPath *field.Path, networks []types.MachineNetworkEntry, subnetName string, ip net.IP) field.ErrorList {
   444  	for _, network := range networks {
   445  		if network.CIDR.Contains(ip) {
   446  			return nil
   447  		}
   448  	}
   449  	return field.ErrorList{field.Invalid(fldPath, subnetName, fmt.Sprintf("subnet CIDR range start %s is outside of the specified machine networks", ip))}
   450  }
   451  
   452  // ValidateEnabledServices gets all the enabled services for a project and validate if any of the required services are not enabled.
   453  // also warns the user if optional services are not enabled.
   454  func ValidateEnabledServices(ctx context.Context, client API, project string) error {
   455  	requiredServices := sets.NewString("compute.googleapis.com",
   456  		"cloudresourcemanager.googleapis.com",
   457  		"dns.googleapis.com",
   458  		"iam.googleapis.com",
   459  		"iamcredentials.googleapis.com",
   460  		"serviceusage.googleapis.com")
   461  	optionalServices := sets.NewString("cloudapis.googleapis.com",
   462  		"servicemanagement.googleapis.com",
   463  		"deploymentmanager.googleapis.com",
   464  		"storage-api.googleapis.com",
   465  		"storage-component.googleapis.com",
   466  		"file.googleapis.com")
   467  	projectServices, err := client.GetEnabledServices(ctx, project)
   468  	if err != nil {
   469  		if IsForbidden(err) {
   470  			return errors.Wrap(err, "unable to fetch enabled services for project. Make sure 'serviceusage.googleapis.com' is enabled")
   471  		}
   472  		return err
   473  	}
   474  
   475  	if remaining := requiredServices.Difference(sets.NewString(projectServices...)); remaining.Len() > 0 {
   476  		return fmt.Errorf("the following required services are not enabled in this project: %s",
   477  			strings.Join(remaining.List(), ","))
   478  	}
   479  
   480  	if remaining := optionalServices.Difference(sets.NewString(projectServices...)); remaining.Len() > 0 {
   481  		logrus.Warnf("the following optional services are not enabled in this project: %s",
   482  			strings.Join(remaining.List(), ","))
   483  	}
   484  	return nil
   485  }
   486  
   487  // ValidateProjectRegion determines whether the region is valid for the project
   488  func validateRegion(client API, ic *types.InstallConfig, fieldPath *field.Path) field.ErrorList {
   489  	allErrs := field.ErrorList{}
   490  	regionFound := false
   491  
   492  	if ic.GCP.ProjectID != "" && ic.GCP.Region != "" {
   493  		computeRegions, err := client.GetRegions(context.TODO(), ic.GCP.ProjectID)
   494  		if err != nil {
   495  			return append(allErrs, field.InternalError(fieldPath.Child("project"), err))
   496  		} else if len(computeRegions) == 0 {
   497  			return append(allErrs, field.Invalid(fieldPath.Child("project"), ic.GCP.ProjectID, "no regions found"))
   498  		}
   499  
   500  		for _, region := range computeRegions {
   501  			if regionFound = region == ic.GCP.Region; regionFound {
   502  				break
   503  			}
   504  		}
   505  	}
   506  
   507  	if !regionFound {
   508  		return append(allErrs, field.Invalid(fieldPath.Child("region"), ic.GCP.Region, "invalid region"))
   509  	}
   510  	return nil
   511  }
   512  
   513  // ValidateCredentialMode The presence of `authorized_user` in the credentials indicates that no service account
   514  // was used for authentication and requires Manual credential mode.
   515  func ValidateCredentialMode(client API, ic *types.InstallConfig) field.ErrorList {
   516  	allErrs := field.ErrorList{}
   517  	creds := client.GetCredentials()
   518  
   519  	if creds.JSON != nil {
   520  		var credsMap map[string]interface{}
   521  		err := json.Unmarshal(creds.JSON, &credsMap)
   522  		if err != nil {
   523  			return append(allErrs, field.Invalid(field.NewPath("credentials").Child("JSON"), creds.JSON, "failed to unmarshal JSON credentials"))
   524  		}
   525  
   526  		credsType, found := credsMap["type"]
   527  		if !found {
   528  			return append(allErrs, field.NotFound(field.NewPath("credentials").Child("JSON").Child("type"), "failed to find credentials type"))
   529  		}
   530  
   531  		if credsType.(string) == string(gcp.AuthorizedUserMode) && ic.CredentialsMode != types.ManualCredentialsMode {
   532  			errMsg := "environmental authentication is only supported with Manual credentials mode"
   533  			return append(allErrs, field.Forbidden(field.NewPath("credentialsMode"), errMsg))
   534  		}
   535  	} else if creds.JSON == nil && ic.CredentialsMode != types.ManualCredentialsMode {
   536  		errMsg := "Manual credentials mode needs to be enabled to use environmental authentication"
   537  		return append(allErrs, field.Forbidden(field.NewPath("credentialsMode"), errMsg))
   538  	}
   539  	return allErrs
   540  }
   541  
   542  func validateZones(client API, ic *types.InstallConfig) field.ErrorList {
   543  	allErrs := field.ErrorList{}
   544  
   545  	zones, err := client.GetZones(context.TODO(), ic.GCP.ProjectID, fmt.Sprintf("region eq .*%s", ic.GCP.Region))
   546  	if err != nil {
   547  		return append(allErrs, field.InternalError(nil, err))
   548  	} else if len(zones) == 0 {
   549  		return append(allErrs, field.InternalError(nil, fmt.Errorf("failed to fetch zones, this error usually occurs if the region is not found")))
   550  	}
   551  
   552  	projZones := sets.New[string]()
   553  	for _, zone := range zones {
   554  		projZones.Insert(zone.Name)
   555  	}
   556  
   557  	const errMsg = "zone(s) not found in region"
   558  
   559  	if ic.Platform.GCP.DefaultMachinePlatform != nil {
   560  		diff := sets.New(ic.Platform.GCP.DefaultMachinePlatform.Zones...).Difference(projZones)
   561  		if len(diff) > 0 {
   562  			allErrs = append(allErrs, field.Invalid(field.NewPath("platform", "gcp", "defaultMachinePlatform", "zones"), sets.List(diff), errMsg))
   563  		}
   564  	}
   565  
   566  	if ic.ControlPlane != nil && ic.ControlPlane.Platform.GCP != nil {
   567  		diff := sets.New(ic.ControlPlane.Platform.GCP.Zones...).Difference(projZones)
   568  		if len(diff) > 0 {
   569  			allErrs = append(allErrs, field.Invalid(field.NewPath("controlPlane", "platform", "gcp", "zones"), sets.List(diff), errMsg))
   570  		}
   571  	}
   572  
   573  	for idx, compute := range ic.Compute {
   574  		fldPath := field.NewPath("compute").Index(idx)
   575  		if compute.Platform.GCP != nil {
   576  			diff := sets.New(compute.Platform.GCP.Zones...).Difference(projZones)
   577  			if len(diff) > 0 {
   578  				allErrs = append(allErrs, field.Invalid(fldPath.Child("platform", "gcp", "zones"), sets.List(diff), errMsg))
   579  			}
   580  		}
   581  	}
   582  
   583  	return allErrs
   584  }
   585  
   586  func validateMarketplaceImages(client API, ic *types.InstallConfig) field.ErrorList {
   587  	allErrs := field.ErrorList{}
   588  
   589  	const errorMessage string = "could not find the boot image: %v"
   590  	var err error
   591  	var defaultImage *compute.Image
   592  	var defaultOsImage *gcp.OSImage
   593  
   594  	if ic.GCP.DefaultMachinePlatform != nil && ic.GCP.DefaultMachinePlatform.OSImage != nil {
   595  		defaultOsImage = ic.GCP.DefaultMachinePlatform.OSImage
   596  		defaultImage, err = client.GetImage(context.TODO(), defaultOsImage.Name, defaultOsImage.Project)
   597  		if err != nil {
   598  			allErrs = append(allErrs, field.Invalid(field.NewPath("platform", "gcp", "defaultMachinePlatform", "osImage"), *defaultOsImage, fmt.Sprintf(errorMessage, err)))
   599  		}
   600  	}
   601  
   602  	if ic.ControlPlane != nil {
   603  		image := defaultImage
   604  		osImage := defaultOsImage
   605  		if ic.ControlPlane.Platform.GCP != nil && ic.ControlPlane.Platform.GCP.OSImage != nil {
   606  			osImage = ic.ControlPlane.Platform.GCP.OSImage
   607  			image, err = client.GetImage(context.TODO(), osImage.Name, osImage.Project)
   608  			if err != nil {
   609  				allErrs = append(allErrs, field.Invalid(field.NewPath("controlPlane", "platform", "gcp", "osImage"), *osImage, fmt.Sprintf(errorMessage, err)))
   610  			}
   611  		}
   612  		if image != nil {
   613  			if errMsg := checkArchitecture(image.Architecture, ic.ControlPlane.Architecture, "controlPlane"); errMsg != "" {
   614  				allErrs = append(allErrs, field.Invalid(field.NewPath("controlPlane", "platform", "gcp", "osImage"), *osImage, errMsg))
   615  			}
   616  		}
   617  	}
   618  
   619  	for idx, compute := range ic.Compute {
   620  		image := defaultImage
   621  		osImage := defaultOsImage
   622  		fieldPath := field.NewPath("compute").Index(idx)
   623  		if compute.Platform.GCP != nil && compute.Platform.GCP.OSImage != nil {
   624  			osImage = compute.Platform.GCP.OSImage
   625  			image, err = client.GetImage(context.TODO(), osImage.Name, osImage.Project)
   626  			if err != nil {
   627  				allErrs = append(allErrs, field.Invalid(fieldPath.Child("platform", "gcp", "osImage"), *osImage, fmt.Sprintf(errorMessage, err)))
   628  			}
   629  		}
   630  		if image != nil {
   631  			if errMsg := checkArchitecture(image.Architecture, compute.Architecture, "compute"); errMsg != "" {
   632  				allErrs = append(allErrs, field.Invalid(fieldPath.Child("platform", "gcp", "osImage"), *osImage, errMsg))
   633  			}
   634  		}
   635  	}
   636  
   637  	return allErrs
   638  }
   639  
   640  func checkArchitecture(imageArch string, icArch types.Architecture, role string) string {
   641  	const unspecifiedArch string = "ARCHITECTURE_UNSPECIFIED"
   642  	// The possible architecture names from image.Architecture are of type string hence we cannot directly obtain the possible values
   643  	// In the docs the possible values are ARM64, X86_64, and ARCHITECTURE_UNSPECIFIED
   644  	// There is no simple translation between the architecture values from Google and the architecture names used in the install config so a map is used
   645  	var (
   646  		translateArchName = map[string]types.Architecture{
   647  			"ARM64":  types.ArchitectureARM64,
   648  			"X86_64": types.ArchitectureAMD64,
   649  		}
   650  	)
   651  
   652  	if imageArch == "" || imageArch == unspecifiedArch {
   653  		logrus.Warn(fmt.Sprintf("Boot image architecture is unspecified and might not be compatible with %s %s nodes", icArch, role))
   654  	} else if translateArchName[imageArch] != icArch {
   655  		return fmt.Sprintf("image architecture %s does not match %s node architecture %s", imageArch, role, icArch)
   656  	}
   657  	return ""
   658  }
   659  
   660  // validateUserTags check for existence and accessibility of user-defined tags and persists
   661  // validated tags in-memory.
   662  func validateUserTags(client API, projectID string, userTags []gcp.UserTag) error {
   663  	return NewTagManager(client).validateAndPersistUserTags(context.Background(), projectID, userTags)
   664  }