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 }