github.com/openshift/installer@v1.4.17/pkg/asset/installconfig/gcp/validation_test.go (about) 1 package gcp 2 3 import ( 4 "context" 5 "fmt" 6 "net" 7 "net/http" 8 "strings" 9 "testing" 10 11 "github.com/golang/mock/gomock" 12 logrusTest "github.com/sirupsen/logrus/hooks/test" 13 "github.com/stretchr/testify/assert" 14 googleoauth "golang.org/x/oauth2/google" 15 "google.golang.org/api/cloudresourcemanager/v3" 16 compute "google.golang.org/api/compute/v1" 17 dns "google.golang.org/api/dns/v1" 18 "google.golang.org/api/googleapi" 19 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 20 "k8s.io/apimachinery/pkg/util/sets" 21 "k8s.io/apimachinery/pkg/util/validation/field" 22 23 "github.com/openshift/installer/pkg/asset/installconfig/gcp/mock" 24 "github.com/openshift/installer/pkg/ipnet" 25 "github.com/openshift/installer/pkg/types" 26 "github.com/openshift/installer/pkg/types/gcp" 27 ) 28 29 type editFunctions []func(ic *types.InstallConfig) 30 31 var ( 32 validNetworkName = "valid-vpc" 33 validProjectName = "valid-project" 34 invalidProjectName = "invalid-project" 35 validRegion = "us-east1" 36 invalidRegion = "us-east4" 37 validZone = "us-east1-b" 38 validComputeSubnet = "valid-compute-subnet" 39 validCPSubnet = "valid-controlplane-subnet" 40 validCIDR = "10.0.0.0/16" 41 validClusterName = "valid-cluster" 42 validPrivateZone = "valid-short-private-zone" 43 validPublicZone = "valid-short-public-zone" 44 invalidPublicZone = "invalid-short-public-zone" 45 validBaseDomain = "example.installer.domain." 46 validXpnSA = "valid-example-sa@gcloud.serviceaccount.com" 47 invalidXpnSA = "invalid-example-sa@gcloud.serviceaccount.com" 48 49 // #nosec G101 50 fakeCreds = `{ 51 "client_id": "fake.apps.googleusercontent.com", 52 "client_secret": "fake-secret", 53 "quota_project_id": "openshift-installer-fake", 54 "refresh_token": "fake_token", 55 "type": "authorized_user" 56 }` 57 58 validPrivateDNSZone = dns.ManagedZone{ 59 Name: validPrivateZone, 60 DnsName: fmt.Sprintf("%s.%s", validClusterName, strings.TrimSuffix(validBaseDomain, ".")), 61 } 62 validPublicDNSZone = dns.ManagedZone{ 63 Name: validPublicZone, 64 DnsName: validBaseDomain, 65 } 66 67 invalidateMachineCIDR = func(ic *types.InstallConfig) { 68 _, newCidr, _ := net.ParseCIDR("192.168.111.0/24") 69 ic.MachineNetwork = []types.MachineNetworkEntry{ 70 {CIDR: ipnet.IPNet{IPNet: *newCidr}}, 71 } 72 } 73 74 validMachineTypes = func(ic *types.InstallConfig) { 75 ic.Platform.GCP.DefaultMachinePlatform.InstanceType = "n1-standard-2" 76 ic.ControlPlane.Platform.GCP.InstanceType = "n1-standard-4" 77 ic.Compute[0].Platform.GCP.InstanceType = "n1-standard-2" 78 } 79 80 invalidateDefaultMachineTypes = func(ic *types.InstallConfig) { 81 ic.Platform.GCP.DefaultMachinePlatform.InstanceType = "n1-standard-1" 82 } 83 84 invalidateControlPlaneMachineTypes = func(ic *types.InstallConfig) { 85 ic.ControlPlane.Platform.GCP.InstanceType = "n1-standard-1" 86 } 87 88 invalidateComputeMachineTypes = func(ic *types.InstallConfig) { 89 ic.Compute[0].Platform.GCP.InstanceType = "n1-standard-1" 90 } 91 92 undefinedDefaultMachineTypes = func(ic *types.InstallConfig) { 93 ic.Platform.GCP.DefaultMachinePlatform.InstanceType = "n1-dne-1" 94 } 95 96 invalidateNetwork = func(ic *types.InstallConfig) { ic.GCP.Network = "invalid-vpc" } 97 invalidateComputeSubnet = func(ic *types.InstallConfig) { ic.GCP.ComputeSubnet = "invalid-compute-subnet" } 98 invalidateCPSubnet = func(ic *types.InstallConfig) { ic.GCP.ControlPlaneSubnet = "invalid-cp-subnet" } 99 invalidateRegion = func(ic *types.InstallConfig) { ic.GCP.Region = invalidRegion } 100 invalidateProject = func(ic *types.InstallConfig) { ic.GCP.ProjectID = invalidProjectName } 101 invalidateNetworkProject = func(ic *types.InstallConfig) { ic.GCP.NetworkProjectID = invalidProjectName } 102 removeVPC = func(ic *types.InstallConfig) { ic.GCP.Network = "" } 103 removeSubnets = func(ic *types.InstallConfig) { ic.GCP.ComputeSubnet, ic.GCP.ControlPlaneSubnet = "", "" } 104 invalidClusterName = func(ic *types.InstallConfig) { ic.ObjectMeta.Name = "testgoogletest" } 105 validNetworkProject = func(ic *types.InstallConfig) { ic.GCP.NetworkProjectID = validProjectName } 106 validateXpnSA = func(ic *types.InstallConfig) { ic.ControlPlane.Platform.GCP.ServiceAccount = validXpnSA } 107 invalidateXpnSA = func(ic *types.InstallConfig) { ic.ControlPlane.Platform.GCP.ServiceAccount = invalidXpnSA } 108 109 machineTypeAPIResult = map[string]*compute.MachineType{ 110 "n1-standard-1": {GuestCpus: 1, MemoryMb: 3840}, 111 "n1-standard-2": {GuestCpus: 2, MemoryMb: 7680}, 112 "n1-standard-4": {GuestCpus: 4, MemoryMb: 15360}, 113 "n2-standard-1": {GuestCpus: 1, MemoryMb: 8192}, 114 "n2-standard-2": {GuestCpus: 2, MemoryMb: 16384}, 115 "n2-standard-4": {GuestCpus: 4, MemoryMb: 32768}, 116 "n4-standard-4": {GuestCpus: 4, MemoryMb: 32768}, 117 "t2a-standard-4": {GuestCpus: 4, MemoryMb: 16384}, 118 } 119 120 subnetAPIResult = []*compute.Subnetwork{ 121 { 122 Name: validCPSubnet, 123 IpCidrRange: validCIDR, 124 }, 125 { 126 Name: validComputeSubnet, 127 IpCidrRange: validCIDR, 128 }, 129 } 130 ) 131 132 func validInstallConfig() *types.InstallConfig { 133 return &types.InstallConfig{ 134 BaseDomain: validBaseDomain, 135 ObjectMeta: metav1.ObjectMeta{ 136 Name: validClusterName, 137 }, 138 Networking: &types.Networking{ 139 MachineNetwork: []types.MachineNetworkEntry{ 140 {CIDR: *ipnet.MustParseCIDR(validCIDR)}, 141 }, 142 }, 143 Platform: types.Platform{ 144 GCP: &gcp.Platform{ 145 DefaultMachinePlatform: &gcp.MachinePool{}, 146 ProjectID: validProjectName, 147 Region: validRegion, 148 Network: validNetworkName, 149 ComputeSubnet: validComputeSubnet, 150 ControlPlaneSubnet: validCPSubnet, 151 }, 152 }, 153 ControlPlane: &types.MachinePool{ 154 Architecture: types.ArchitectureAMD64, 155 Platform: types.MachinePoolPlatform{ 156 GCP: &gcp.MachinePool{}, 157 }, 158 }, 159 Compute: []types.MachinePool{{ 160 Architecture: types.ArchitectureAMD64, 161 Platform: types.MachinePoolPlatform{ 162 GCP: &gcp.MachinePool{}, 163 }, 164 }}, 165 // Setting to manual for testing the ValidateCredentials 166 CredentialsMode: types.ManualCredentialsMode, 167 } 168 } 169 170 func TestGCPInstallConfigValidation(t *testing.T) { 171 cases := []struct { 172 name string 173 edits editFunctions 174 expectedError bool 175 expectedErrMsg string 176 }{ 177 { 178 name: "Valid network & subnets", 179 edits: editFunctions{}, 180 expectedError: false, 181 expectedErrMsg: "", 182 }, 183 { 184 name: "Invalid ClusterName", 185 edits: editFunctions{invalidClusterName}, 186 expectedError: true, 187 expectedErrMsg: `clusterName: Invalid value: "testgoogletest": cluster name must not start with "goog" or contain variations of "google"`, 188 }, 189 { 190 name: "Valid install config without network & subnets", 191 edits: editFunctions{removeVPC, removeSubnets}, 192 expectedError: false, 193 expectedErrMsg: "", 194 }, 195 { 196 name: "Invalid subnet range", 197 edits: editFunctions{invalidateMachineCIDR}, 198 expectedError: true, 199 expectedErrMsg: "computeSubnet: Invalid value.*subnet CIDR range start 10.0.0.0 is outside of the specified machine networks", 200 }, 201 { 202 name: "Invalid network", 203 edits: editFunctions{invalidateNetwork}, 204 expectedError: true, 205 expectedErrMsg: "network: Invalid value", 206 }, 207 { 208 name: "Invalid compute subnet", 209 edits: editFunctions{invalidateComputeSubnet}, 210 expectedError: true, 211 expectedErrMsg: "computeSubnet: Invalid value", 212 }, 213 { 214 name: "Invalid control plane subnet", 215 edits: editFunctions{invalidateCPSubnet}, 216 expectedError: true, 217 expectedErrMsg: "controlPlaneSubnet: Invalid value", 218 }, 219 { 220 name: "Invalid both subnets", 221 edits: editFunctions{invalidateCPSubnet, invalidateComputeSubnet}, 222 expectedError: true, 223 expectedErrMsg: "computeSubnet: Invalid value.*controlPlaneSubnet: Invalid value", 224 }, 225 { 226 name: "Valid machine types", 227 edits: editFunctions{validMachineTypes}, 228 expectedError: false, 229 expectedErrMsg: "", 230 }, 231 { 232 name: "Invalid default machine type", 233 edits: editFunctions{invalidateDefaultMachineTypes}, 234 expectedError: true, 235 expectedErrMsg: `\[platform.gcp.defaultMachinePlatform.type: Invalid value: "n1-standard-1": instance type does not meet minimum resource requirements of 4 vCPUs, platform.gcp.defaultMachinePlatform.type: Invalid value: "n1-standard-1": instance type does not meet minimum resource requirements of 15360 MB Memory, controlPlane.platform.gcp.type: Invalid value: "n1-standard-1": instance type does not meet minimum resource requirements of 4 vCPUs, controlPlane.platform.gcp.type: Invalid value: "n1-standard-1": instance type does not meet minimum resource requirements of 15360 MB Memory, compute\[0\].platform.gcp.type: Invalid value: "n1-standard-1": instance type does not meet minimum resource requirements of 2 vCPUs, compute\[0\].platform.gcp.type: Invalid value: "n1-standard-1": instance type does not meet minimum resource requirements of 7680 MB Memory\]`, 236 }, 237 { 238 name: "Invalid control plane machine types", 239 edits: editFunctions{invalidateControlPlaneMachineTypes}, 240 expectedError: true, 241 expectedErrMsg: `[controlPlane.platform.gcp.type: Invalid value: "n1\-standard\-1": instance type does not meet minimum resource requirements of 4 vCPUs, controlPlane.platform.gcp.type: Invalid value: "n1\-standard\-1": instance type does not meet minimum resource requirements of 15361 MB Memory]`, 242 }, 243 { 244 name: "Invalid compute machine types", 245 edits: editFunctions{invalidateComputeMachineTypes}, 246 expectedError: true, 247 expectedErrMsg: `\[compute\[0\].platform.gcp.type: Invalid value: "n1-standard-1": instance type does not meet minimum resource requirements of 2 vCPUs, compute\[0\].platform.gcp.type: Invalid value: "n1-standard-1": instance type does not meet minimum resource requirements of 7680 MB Memory\]`, 248 }, 249 { 250 name: "Undefined default machine types", 251 edits: editFunctions{undefinedDefaultMachineTypes}, 252 expectedError: true, 253 expectedErrMsg: `Internal error: 404`, 254 }, 255 { 256 name: "Invalid region", 257 edits: editFunctions{invalidateRegion}, 258 expectedError: true, 259 expectedErrMsg: "could not find subnet valid-compute-subnet in network valid-vpc and region us-east4", 260 }, 261 { 262 name: "Invalid project", 263 edits: editFunctions{invalidateProject}, 264 expectedError: true, 265 expectedErrMsg: "network: Invalid value", 266 }, 267 { 268 name: "Invalid project & region", 269 edits: editFunctions{invalidateRegion, invalidateProject}, 270 expectedError: true, 271 expectedErrMsg: "network: Invalid value", 272 }, 273 { 274 name: "Invalid project ID", 275 edits: editFunctions{invalidateProject, removeSubnets, removeVPC}, 276 expectedError: true, 277 expectedErrMsg: "platform.gcp.project: Invalid value: \"invalid-project\": invalid project ID", 278 }, 279 { 280 name: "Invalid network project ID", 281 edits: editFunctions{invalidateNetworkProject}, 282 expectedError: true, 283 expectedErrMsg: "platform.gcp.networkProjectID: Invalid value: \"invalid-project\": invalid project ID", 284 }, 285 { 286 name: "Valid Region", 287 edits: editFunctions{}, 288 expectedError: false, 289 expectedErrMsg: "", 290 }, 291 { 292 name: "Invalid region not found", 293 edits: editFunctions{invalidateRegion, invalidateProject}, 294 expectedError: true, 295 expectedErrMsg: "platform.gcp.project: Invalid value: \"invalid-project\": invalid project ID", 296 }, 297 { 298 name: "Region not validated", 299 edits: editFunctions{invalidateRegion}, 300 expectedError: true, 301 expectedErrMsg: "platform.gcp.region: Invalid value: \"us-east4\": invalid region", 302 }, 303 { 304 name: "Valid XPN Service Account", 305 edits: editFunctions{validNetworkProject, validateXpnSA}, 306 expectedError: false, 307 }, 308 { 309 name: "Invalid XPN Service Account", 310 edits: editFunctions{validNetworkProject, invalidateXpnSA}, 311 expectedError: true, 312 expectedErrMsg: "controlPlane.platform.gcp.serviceAccount: Internal error\"", 313 }, 314 } 315 mockCtrl := gomock.NewController(t) 316 defer mockCtrl.Finish() 317 318 gcpClient := mock.NewMockAPI(mockCtrl) 319 320 errNotFound := &googleapi.Error{Code: http.StatusNotFound} 321 322 // Should get the list of projects. 323 gcpClient.EXPECT().GetProjects(gomock.Any()).Return(map[string]string{"valid-project": "valid-project"}, nil).AnyTimes() 324 gcpClient.EXPECT().GetProjectByID(gomock.Any(), "valid-project").Return(&cloudresourcemanager.Project{}, nil).AnyTimes() 325 gcpClient.EXPECT().GetProjectByID(gomock.Any(), "invalid-project").Return(nil, errNotFound).AnyTimes() 326 gcpClient.EXPECT().GetProjectByID(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("error")).AnyTimes() 327 328 // Should get the list of zones. 329 gcpClient.EXPECT().GetZones(gomock.Any(), gomock.Any(), gomock.Any()).Return([]*compute.Zone{{Name: validZone}}, nil).AnyTimes() 330 331 // When passed an invalid project, no regions will be returned 332 gcpClient.EXPECT().GetRegions(gomock.Any(), invalidProjectName).Return(nil, fmt.Errorf("failed to get regions for project")).AnyTimes() 333 // When passed a project that is valid but the region is not contained, an error should still occur 334 gcpClient.EXPECT().GetRegions(gomock.Any(), validProjectName).Return([]string{validRegion}, nil).AnyTimes() 335 336 // Should return the machine type as specified. 337 for key, value := range machineTypeAPIResult { 338 gcpClient.EXPECT().GetMachineTypeWithZones(gomock.Any(), gomock.Any(), gomock.Any(), key).Return(value, sets.New(validZone), nil).AnyTimes() 339 } 340 // When passed incorrect machine type, the API returns nil. 341 gcpClient.EXPECT().GetMachineTypeWithZones(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil, fmt.Errorf("404")).AnyTimes() 342 343 // When passed the correct network & project, return an empty network, which should be enough to validate ok. 344 gcpClient.EXPECT().GetNetwork(gomock.Any(), validNetworkName, validProjectName).Return(&compute.Network{}, nil).AnyTimes() 345 346 // When passed an incorrect network or incorrect project, the API returns nil 347 gcpClient.EXPECT().GetNetwork(gomock.Any(), gomock.Not(validNetworkName), gomock.Any()).Return(nil, fmt.Errorf("404")).AnyTimes() 348 gcpClient.EXPECT().GetNetwork(gomock.Any(), gomock.Any(), gomock.Not(validProjectName)).Return(nil, fmt.Errorf("404")).AnyTimes() 349 350 // When passed a correct network, project, & region, returns valid subnets. 351 // We will test incorrect subnets, by changing the install config. 352 gcpClient.EXPECT().GetSubnetworks(gomock.Any(), validNetworkName, validProjectName, validRegion).Return(subnetAPIResult, nil).AnyTimes() 353 354 // When passed an incorrect network, project or region, return empty list. 355 gcpClient.EXPECT().GetSubnetworks(gomock.Any(), gomock.Not(validNetworkName), gomock.Any(), gomock.Any()).Return([]*compute.Subnetwork{}, nil).AnyTimes() 356 gcpClient.EXPECT().GetSubnetworks(gomock.Any(), gomock.Any(), gomock.Not(validProjectName), gomock.Any()).Return([]*compute.Subnetwork{}, nil).AnyTimes() 357 gcpClient.EXPECT().GetSubnetworks(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Not(validRegion)).Return([]*compute.Subnetwork{}, nil).AnyTimes() 358 359 // Return fake credentials when asked 360 gcpClient.EXPECT().GetCredentials().Return(&googleoauth.Credentials{JSON: []byte(fakeCreds)}).AnyTimes() 361 362 // Expected results for the managed zone tests 363 gcpClient.EXPECT().GetDNSZoneByName(gomock.Any(), gomock.Any(), validPublicZone).Return(&validPublicDNSZone, nil).AnyTimes() 364 gcpClient.EXPECT().GetDNSZoneByName(gomock.Any(), gomock.Any(), validPrivateZone).Return(&validPrivateDNSZone, nil).AnyTimes() 365 gcpClient.EXPECT().GetDNSZoneByName(gomock.Any(), gomock.Any(), invalidPublicZone).Return(nil, fmt.Errorf("no matching DNS Zone found")).AnyTimes() 366 367 gcpClient.EXPECT().GetServiceAccount(gomock.Any(), validProjectName, validXpnSA).Return(validXpnSA, nil).AnyTimes() 368 gcpClient.EXPECT().GetServiceAccount(gomock.Any(), validProjectName, invalidXpnSA).Return("", fmt.Errorf("controlPlane.platform.gcp.serviceAccount: Internal error\"")).AnyTimes() 369 370 for _, tc := range cases { 371 t.Run(tc.name, func(t *testing.T) { 372 editedInstallConfig := validInstallConfig() 373 for _, edit := range tc.edits { 374 edit(editedInstallConfig) 375 } 376 377 errs := Validate(gcpClient, editedInstallConfig) 378 if tc.expectedError { 379 assert.Regexp(t, tc.expectedErrMsg, errs) 380 } else { 381 assert.Empty(t, errs) 382 } 383 }) 384 } 385 } 386 387 func TestValidatePreExistingPublicDNS(t *testing.T) { 388 cases := []struct { 389 name string 390 records []*dns.ResourceRecordSet 391 err string 392 }{{ 393 name: "no pre-existing", 394 records: nil, 395 }, { 396 name: "no pre-existing", 397 records: []*dns.ResourceRecordSet{{Name: "api.another-cluster-name.base-domain."}}, 398 }, { 399 name: "pre-existing", 400 records: []*dns.ResourceRecordSet{{Name: "api.cluster-name.base-domain."}}, 401 err: `^metadata\.name: Invalid value: "cluster-name": record\(s\) \["api\.cluster-name\.base-domain\."\] already exists in DNS Zone \(project-id/zone-name\) and might be in use by another cluster, please remove it to continue$`, 402 }, { 403 name: "pre-existing", 404 records: []*dns.ResourceRecordSet{{Name: "api.cluster-name.base-domain."}, {Name: "api.cluster-name.base-domain."}}, 405 err: `^metadata\.name: Invalid value: "cluster-name": record\(s\) \["api\.cluster-name\.base-domain\."\] already exists in DNS Zone \(project-id/zone-name\) and might be in use by another cluster, please remove it to continue$`, 406 }} 407 408 for _, test := range cases { 409 t.Run(test.name, func(t *testing.T) { 410 mockCtrl := gomock.NewController(t) 411 defer mockCtrl.Finish() 412 gcpClient := mock.NewMockAPI(mockCtrl) 413 414 gcpClient.EXPECT().GetDNSZone(gomock.Any(), "project-id", "base-domain", true).Return(&dns.ManagedZone{Name: "zone-name"}, nil).AnyTimes() 415 gcpClient.EXPECT().GetRecordSets(gomock.Any(), gomock.Eq("project-id"), gomock.Eq("zone-name")).Return(test.records, nil).AnyTimes() 416 417 err := ValidatePreExistingPublicDNS(gcpClient, &types.InstallConfig{ 418 ObjectMeta: metav1.ObjectMeta{Name: "cluster-name"}, 419 BaseDomain: "base-domain", 420 Platform: types.Platform{GCP: &gcp.Platform{ProjectID: "project-id"}}, 421 }) 422 if test.err == "" { 423 assert.True(t, err == nil) 424 } else { 425 assert.Regexp(t, test.err, err) 426 } 427 }) 428 } 429 } 430 431 func TestValidatePrivateDNSZone(t *testing.T) { 432 cases := []struct { 433 name string 434 records []*dns.ResourceRecordSet 435 err string 436 }{{ 437 name: "no pre-existing", 438 records: nil, 439 }, { 440 name: "no pre-existing", 441 records: []*dns.ResourceRecordSet{{Name: "api.another-cluster-name.base-domain."}}, 442 }, { 443 name: "pre-existing", 444 records: []*dns.ResourceRecordSet{{Name: "api.cluster-name.base-domain."}}, 445 err: `^metadata\.name: Invalid value: "cluster-name": record\(s\) \["api\.cluster-name\.base-domain\."\] already exists in DNS Zone \(project-id/zone-name\) and might be in use by another cluster, please remove it to continue$`, 446 }, { 447 name: "pre-existing", 448 records: []*dns.ResourceRecordSet{{Name: "api.cluster-name.base-domain."}, {Name: "api.cluster-name.base-domain."}}, 449 err: `^metadata\.name: Invalid value: "cluster-name": record\(s\) \["api\.cluster-name\.base-domain\."\] already exists in DNS Zone \(project-id/zone-name\) and might be in use by another cluster, please remove it to continue$`, 450 }} 451 452 for _, test := range cases { 453 t.Run(test.name, func(t *testing.T) { 454 mockCtrl := gomock.NewController(t) 455 defer mockCtrl.Finish() 456 gcpClient := mock.NewMockAPI(mockCtrl) 457 458 gcpClient.EXPECT().GetDNSZone(gomock.Any(), "project-id", "cluster-name.base-domain", false).Return(&dns.ManagedZone{Name: "zone-name"}, nil).AnyTimes() 459 gcpClient.EXPECT().GetRecordSets(gomock.Any(), gomock.Eq("project-id"), gomock.Eq("zone-name")).Return(test.records, nil).AnyTimes() 460 461 err := ValidatePrivateDNSZone(gcpClient, &types.InstallConfig{ 462 ObjectMeta: metav1.ObjectMeta{Name: "cluster-name"}, 463 BaseDomain: "base-domain", 464 Platform: types.Platform{GCP: &gcp.Platform{ProjectID: "project-id", Network: "shared-vpc", NetworkProjectID: "test-network-project"}}, 465 }) 466 if test.err == "" { 467 assert.True(t, err == nil) 468 } else { 469 assert.Regexp(t, test.err, err) 470 } 471 }) 472 } 473 } 474 475 func TestGCPEnabledServicesList(t *testing.T) { 476 cases := []struct { 477 name string 478 services []string 479 err string 480 }{{ 481 name: "No services present", 482 services: nil, 483 err: "unable to fetch enabled services for project. Make sure 'serviceusage.googleapis.com' is enabled", 484 }, { 485 name: "Service Usage missing", 486 services: []string{"compute.googleapis.com"}, 487 err: "unable to fetch enabled services for project. Make sure 'serviceusage.googleapis.com' is enabled", 488 }, { 489 name: "All pre-existing", 490 services: []string{ 491 "compute.googleapis.com", 492 "cloudresourcemanager.googleapis.com", "dns.googleapis.com", 493 "iam.googleapis.com", "iamcredentials.googleapis.com", "serviceusage.googleapis.com", 494 "deploymentmanager.googleapis.com", 495 }, 496 }, { 497 name: "Some services present", 498 services: []string{"compute.googleapis.com", "serviceusage.googleapis.com"}, 499 err: "the following required services are not enabled in this project: cloudresourcemanager.googleapis.com,dns.googleapis.com,iam.googleapis.com,iamcredentials.googleapis.com", 500 }} 501 502 errForbidden := &googleapi.Error{Code: http.StatusForbidden} 503 504 for _, test := range cases { 505 t.Run(test.name, func(t *testing.T) { 506 mockCtrl := gomock.NewController(t) 507 defer mockCtrl.Finish() 508 gcpClient := mock.NewMockAPI(mockCtrl) 509 510 if !sets.NewString(test.services...).Has("serviceusage.googleapis.com") { 511 gcpClient.EXPECT().GetEnabledServices(gomock.Any(), gomock.Any()).Return(nil, errForbidden).AnyTimes() 512 } else { 513 gcpClient.EXPECT().GetEnabledServices(gomock.Any(), gomock.Any()).Return(test.services, nil).AnyTimes() 514 } 515 err := ValidateEnabledServices(context.TODO(), gcpClient, "") 516 if test.err == "" { 517 assert.NoError(t, err) 518 } else { 519 assert.Regexp(t, test.err, err) 520 } 521 }) 522 } 523 } 524 525 func TestValidateCredentialMode(t *testing.T) { 526 cases := []struct { 527 name string 528 creds types.CredentialsMode 529 emptyCreds bool 530 err string 531 }{{ 532 name: "missing json with manual creds", 533 creds: types.ManualCredentialsMode, 534 emptyCreds: true, 535 }, { 536 name: "missing json without manual creds", 537 creds: types.PassthroughCredentialsMode, 538 emptyCreds: true, 539 err: "credentialsMode: Forbidden: Manual credentials mode needs to be enabled to use environmental authentication", 540 }, { 541 name: "supplied json with manual creds", 542 creds: types.ManualCredentialsMode, 543 emptyCreds: false, 544 }, { 545 name: "supplied json without manual creds", 546 creds: types.PassthroughCredentialsMode, 547 emptyCreds: false, 548 err: "credentialsMode: Forbidden: environmental authentication is only supported with Manual credentials mode", 549 }} 550 551 mockCtrl := gomock.NewController(t) 552 defer mockCtrl.Finish() 553 554 // Client where the creds are empty 555 gcpClientEmptyCreds := mock.NewMockAPI(mockCtrl) 556 gcpClientEmptyCreds.EXPECT().GetCredentials().Return(&googleoauth.Credentials{JSON: nil}).AnyTimes() 557 558 // Client that contains creds 559 560 gcpClientWithCreds := mock.NewMockAPI(mockCtrl) 561 gcpClientWithCreds.EXPECT().GetCredentials().Return(&googleoauth.Credentials{JSON: []byte(fakeCreds)}).AnyTimes() 562 563 for _, test := range cases { 564 t.Run(test.name, func(t *testing.T) { 565 ic := types.InstallConfig{ 566 ObjectMeta: metav1.ObjectMeta{Name: "cluster-name"}, 567 BaseDomain: "base-domain", 568 Platform: types.Platform{GCP: &gcp.Platform{ProjectID: "project-id"}}, 569 CredentialsMode: test.creds, 570 } 571 572 var err error 573 if test.emptyCreds { 574 err = ValidateCredentialMode(gcpClientEmptyCreds, &ic).ToAggregate() 575 } else { 576 err = ValidateCredentialMode(gcpClientWithCreds, &ic).ToAggregate() 577 } 578 579 if test.err == "" { 580 assert.Nil(t, err) 581 } else { 582 assert.Regexp(t, test.err, err) 583 } 584 }) 585 } 586 } 587 588 func TestValidateServiceAccountPresent(t *testing.T) { 589 cases := []struct { 590 name string 591 creds *googleoauth.Credentials 592 serviceAccount string 593 networkProjectID string 594 expectedError string 595 }{ 596 { 597 name: "Test no network project ID", 598 creds: &googleoauth.Credentials{}, 599 }, 600 { 601 name: "Test network project ID with service account", 602 creds: &googleoauth.Credentials{}, 603 serviceAccount: "test-service-account", 604 networkProjectID: "test-network-project", 605 }, 606 { 607 name: "Test network project ID service account and creds", 608 creds: &googleoauth.Credentials{JSON: []byte("{}")}, 609 serviceAccount: "test-service-account", 610 networkProjectID: "test-network-project", 611 }, 612 { 613 name: "Test network project ID no creds", 614 creds: &googleoauth.Credentials{JSON: nil}, 615 networkProjectID: "test-network-project", 616 expectedError: "controlPlane.platform.gcp.serviceAccount: Required value: service account must be provided when authentication credentials do not provide a service account", 617 }, 618 } 619 620 mockCtrl := gomock.NewController(t) 621 defer mockCtrl.Finish() 622 623 for _, test := range cases { 624 gcpClient := mock.NewMockAPI(mockCtrl) 625 if test.networkProjectID != "" { 626 gcpClient.EXPECT().GetCredentials().Return(test.creds) 627 } 628 629 t.Run(test.name, func(t *testing.T) { 630 ic := types.InstallConfig{ 631 ObjectMeta: metav1.ObjectMeta{Name: "cluster-name"}, 632 BaseDomain: "base-domain", 633 Platform: types.Platform{GCP: &gcp.Platform{ProjectID: "project-id", NetworkProjectID: test.networkProjectID}}, 634 CredentialsMode: types.PassthroughCredentialsMode, 635 ControlPlane: &types.MachinePool{ 636 Platform: types.MachinePoolPlatform{ 637 GCP: &gcp.MachinePool{ 638 ServiceAccount: test.serviceAccount, 639 }, 640 }, 641 }, 642 } 643 644 errorList := validateServiceAccountPresent(gcpClient, &ic) 645 if errorList == nil && test.expectedError == "" { 646 assert.NoError(t, errorList.ToAggregate()) 647 } else { 648 assert.Regexp(t, test.expectedError, errorList.ToAggregate()) 649 } 650 }) 651 } 652 } 653 654 func TestValidateZones(t *testing.T) { 655 validZonesDefaultMachine := func(ic *types.InstallConfig) { 656 ic.Platform.GCP.DefaultMachinePlatform.Zones = []string{"us-central1-a", "us-central1-c"} 657 } 658 validZonesControlPlane := func(ic *types.InstallConfig) { 659 ic.ControlPlane.Platform.GCP.Zones = []string{"us-central1-a", "us-central1-b"} 660 } 661 validZonesCompute := func(ic *types.InstallConfig) { 662 ic.Compute[0].Platform.GCP.Zones = []string{"us-central1-b", "us-central1-c", "us-central1-d"} 663 } 664 invalidZonesDefaultMachine := func(ic *types.InstallConfig) { 665 ic.Platform.GCP.DefaultMachinePlatform.Zones = []string{"us-central1-a", "us-central1-x", "us-central1-y"} 666 } 667 invalidZonesControlPlane := func(ic *types.InstallConfig) { 668 ic.ControlPlane.Platform.GCP.Zones = []string{"us-central1-d", "us-central1-x", "us-central1-y"} 669 } 670 invalidZonesCompute := func(ic *types.InstallConfig) { 671 ic.Compute[0].Platform.GCP.Zones = []string{"us-central1-y", "us-central1-z", "us-central1-w"} 672 } 673 674 cases := []struct { 675 name string 676 edits editFunctions 677 expectedError bool 678 expectedErrMsg string 679 }{ 680 { 681 name: "Valid zones for defaultMachine", 682 edits: editFunctions{validZonesDefaultMachine}, 683 expectedError: false, 684 expectedErrMsg: "", 685 }, 686 { 687 name: "Invalid zones for defaultMachine", 688 edits: editFunctions{invalidZonesDefaultMachine}, 689 expectedError: true, 690 expectedErrMsg: `^\[platform.gcp.defaultMachinePlatform.zones: Invalid value: \[\]string\{"us\-central1\-x", "us\-central1\-y"\}: zone\(s\) not found in region\]$`, 691 }, 692 { 693 name: "Valid zones for controlPlane", 694 edits: editFunctions{validZonesControlPlane}, 695 expectedError: false, 696 expectedErrMsg: "", 697 }, 698 { 699 name: "Invalid zones for controlPlane", 700 edits: editFunctions{invalidZonesControlPlane}, 701 expectedError: true, 702 expectedErrMsg: `^\[controlPlane.platform.gcp.zones: Invalid value: \[\]string\{"us\-central1\-x", "us\-central1\-y"\}: zone\(s\) not found in region\]$`, 703 }, 704 { 705 name: "Valid zones for compute", 706 edits: editFunctions{validZonesCompute}, 707 expectedError: false, 708 expectedErrMsg: "", 709 }, 710 { 711 name: "Invalid zones for compute", 712 edits: editFunctions{invalidZonesCompute}, 713 expectedError: true, 714 expectedErrMsg: `^\[compute\[0\].platform.gcp.zones: Invalid value: \[\]string\{"us\-central1\-w", "us\-central1\-y", "us\-central1\-z"\}: zone\(s\) not found in region\]$`, 715 }, 716 } 717 718 mockCtrl := gomock.NewController(t) 719 defer mockCtrl.Finish() 720 gcpClient := mock.NewMockAPI(mockCtrl) 721 722 validZones := []*compute.Zone{ 723 {Name: "us-central1-a"}, 724 {Name: "us-central1-b"}, 725 {Name: "us-central1-c"}, 726 {Name: "us-central1-d"}, 727 } 728 729 // Should get the list of zones. 730 gcpClient.EXPECT().GetZones(gomock.Any(), gomock.Any(), gomock.Any()).Return(validZones, nil).AnyTimes() 731 732 for _, tc := range cases { 733 t.Run(tc.name, func(t *testing.T) { 734 editedInstallConfig := validInstallConfig() 735 for _, edit := range tc.edits { 736 edit(editedInstallConfig) 737 } 738 739 errs := validateZones(gcpClient, editedInstallConfig) 740 if tc.expectedError { 741 assert.Regexp(t, tc.expectedErrMsg, errs) 742 } else { 743 assert.Empty(t, errs) 744 } 745 }) 746 } 747 } 748 749 func TestValidateInstanceType(t *testing.T) { 750 cases := []struct { 751 name string 752 zones []string 753 diskType string 754 instanceType string 755 arch string 756 expectedError bool 757 expectedErrMsg string 758 }{ 759 { 760 name: "Valid instance type with min requirements and no zones specified", 761 zones: []string{}, 762 instanceType: "n1-standard-4", 763 diskType: "pd-ssd", 764 expectedError: false, 765 expectedErrMsg: "", 766 }, 767 { 768 name: "Valid instance type with min requirements and valid zones specified", 769 zones: []string{"a", "b"}, 770 instanceType: "n1-standard-4", 771 diskType: "pd-ssd", 772 arch: "amd64", 773 expectedError: false, 774 expectedErrMsg: "", 775 }, 776 { 777 name: "Valid instance type with min requirements and invalid zones specified", 778 zones: []string{"a", "b", "d", "x", "y"}, 779 instanceType: "n1-standard-4", 780 diskType: "pd-ssd", 781 expectedError: true, 782 expectedErrMsg: `\[instance.type: Invalid value: "n1\-standard\-4": instance type not available in zones: \[x y\]\]$`, 783 }, 784 { 785 name: "Valid instance fails min requirements and no zones specified", 786 zones: []string{}, 787 instanceType: "n1-standard-2", 788 diskType: "pd-ssd", 789 expectedError: true, 790 expectedErrMsg: `^\[instance.type: Invalid value: "n1\-standard\-2": instance type does not meet minimum resource requirements of 4 vCPUs instance.type: Invalid value: "n1\-standard\-2": instance type does not meet minimum resource requirements of 15360 MB Memory\]$`, 791 }, 792 { 793 name: "Valid instance fails min requirements and valid zones specified", 794 zones: []string{"a", "b"}, 795 instanceType: "n1-standard-1", 796 diskType: "pd-ssd", 797 expectedError: true, 798 expectedErrMsg: ``, 799 }, 800 { 801 name: "Valid instance fails min requirements and invalid zones specified", 802 zones: []string{"a", "x", "y"}, 803 diskType: "pd-ssd", 804 expectedError: true, 805 expectedErrMsg: ``, 806 }, 807 { 808 name: "Invalid instance and no zones specified", 809 zones: []string{}, 810 instanceType: "invalid-instance-1", 811 diskType: "pd-ssd", 812 expectedError: true, 813 expectedErrMsg: `^\[<nil>: Internal error: 404\]$`, 814 }, 815 { 816 name: "Invalid instance and valid zones specified", 817 zones: []string{"a", "b"}, 818 instanceType: "invalid-instance-1", 819 diskType: "pd-ssd", 820 expectedError: true, 821 expectedErrMsg: `^\[<nil>: Internal error: 404\]$`, 822 }, 823 { 824 name: "Invalid instance and invalid zones specified", 825 zones: []string{"a", "x", "y", "z"}, 826 instanceType: "invalid-instance-1", 827 diskType: "pd-ssd", 828 expectedError: true, 829 expectedErrMsg: `^\[<nil>: Internal error: 404\]$`, 830 }, 831 { 832 name: "Invalid instance architecture", 833 zones: []string{"a", "b"}, 834 instanceType: "t2a-standard-4", 835 diskType: "pd-ssd", 836 arch: "amd64", 837 expectedError: true, 838 expectedErrMsg: `^\[instance.type: Invalid value: "t2a\-standard\-4": instance type architecture arm64 does not match specified architecture amd64\]$`, 839 }, 840 { 841 name: "Valid special instance type with min requirements", 842 zones: []string{"a"}, 843 instanceType: "n4-standard-4", 844 diskType: "hyperdisk-balanced", 845 expectedError: false, 846 expectedErrMsg: "", 847 }, 848 { 849 name: "Invalid special instance type with min requirements", 850 zones: []string{"a"}, 851 instanceType: "n2-standard-4", 852 diskType: "hyperdisk-balanced", 853 expectedError: true, 854 expectedErrMsg: `^\[instance.diskType: Unsupported value: \"n2\": supported values: \"c3\", \"c3d\", \"m1\", \"n4\"\]$`, 855 }, 856 } 857 858 mockCtrl := gomock.NewController(t) 859 defer mockCtrl.Finish() 860 gcpClient := mock.NewMockAPI(mockCtrl) 861 862 // Should return the machine type as specified 863 for key, value := range machineTypeAPIResult { 864 gcpClient.EXPECT().GetMachineTypeWithZones(gomock.Any(), gomock.Any(), gomock.Any(), key).Return(value, sets.New("a", "b", "c", "d"), nil).AnyTimes() 865 } 866 // When passed incorrect machine type, the API returns nil. 867 gcpClient.EXPECT().GetMachineTypeWithZones(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil, fmt.Errorf("404")).AnyTimes() 868 869 for _, test := range cases { 870 t.Run(test.name, func(t *testing.T) { 871 errs := ValidateInstanceType(gcpClient, field.NewPath("instance"), "project-id", "region", test.zones, test.diskType, test.instanceType, controlPlaneReq, test.arch) 872 if test.expectedError { 873 assert.Regexp(t, test.expectedErrMsg, errs) 874 } else { 875 assert.Empty(t, errs) 876 } 877 }) 878 } 879 } 880 881 func TestValidateMarketplaceImages(t *testing.T) { 882 var ( 883 validImage = "valid-image" 884 projectID = "project-id" 885 invalidImage = "invalid-image" 886 mismatchedArch = "mismatched-arch" 887 osImage = &gcp.OSImage{} 888 889 validDefaultMachineImage = func(ic *types.InstallConfig) { 890 ic.Platform.GCP.DefaultMachinePlatform.OSImage = osImage 891 ic.Platform.GCP.DefaultMachinePlatform.OSImage.Name = validImage 892 ic.Platform.GCP.DefaultMachinePlatform.OSImage.Project = projectID 893 } 894 validControlPlaneImage = func(ic *types.InstallConfig) { 895 ic.ControlPlane.Platform.GCP.OSImage = osImage 896 ic.ControlPlane.Platform.GCP.OSImage.Name = validImage 897 ic.ControlPlane.Platform.GCP.OSImage.Project = projectID 898 } 899 validComputeImage = func(ic *types.InstallConfig) { 900 ic.Compute[0].Platform.GCP.OSImage = osImage 901 ic.Compute[0].Platform.GCP.OSImage.Name = validImage 902 ic.Compute[0].Platform.GCP.OSImage.Project = projectID 903 } 904 905 invalidDefaultMachineImage = func(ic *types.InstallConfig) { 906 ic.Platform.GCP.DefaultMachinePlatform.OSImage = osImage 907 ic.Platform.GCP.DefaultMachinePlatform.OSImage.Name = invalidImage 908 ic.Platform.GCP.DefaultMachinePlatform.OSImage.Project = projectID 909 } 910 invalidControlPlaneImage = func(ic *types.InstallConfig) { 911 ic.ControlPlane.Platform.GCP.OSImage = osImage 912 ic.ControlPlane.Platform.GCP.OSImage.Name = invalidImage 913 ic.ControlPlane.Platform.GCP.OSImage.Project = projectID 914 } 915 invalidComputeImage = func(ic *types.InstallConfig) { 916 ic.Compute[0].Platform.GCP.OSImage = osImage 917 ic.Compute[0].Platform.GCP.OSImage.Name = invalidImage 918 ic.Compute[0].Platform.GCP.OSImage.Project = projectID 919 } 920 921 mismatchedDefaultMachineImageArchitecture = func(ic *types.InstallConfig) { 922 ic.Platform.GCP.DefaultMachinePlatform.OSImage = osImage 923 ic.Platform.GCP.DefaultMachinePlatform.OSImage.Name = mismatchedArch 924 ic.Platform.GCP.DefaultMachinePlatform.OSImage.Project = projectID 925 ic.ControlPlane.Architecture = types.ArchitectureARM64 926 ic.Compute[0].Architecture = types.ArchitectureARM64 927 } 928 mismatchedControlPlaneImageArchitecture = func(ic *types.InstallConfig) { 929 ic.ControlPlane.Platform.GCP.OSImage = osImage 930 ic.ControlPlane.Platform.GCP.OSImage.Name = mismatchedArch 931 ic.ControlPlane.Platform.GCP.OSImage.Project = projectID 932 ic.ControlPlane.Architecture = types.ArchitectureARM64 933 } 934 mismatchedComputeImageArchitecture = func(ic *types.InstallConfig) { 935 ic.Compute[0].Platform.GCP.OSImage = osImage 936 ic.Compute[0].Platform.GCP.OSImage.Name = mismatchedArch 937 ic.Compute[0].Platform.GCP.OSImage.Project = projectID 938 ic.Compute[0].Architecture = types.ArchitectureARM64 939 } 940 unspecifiedImageArchitecture = func(ic *types.InstallConfig) { 941 ic.ControlPlane.Platform.GCP.OSImage = osImage 942 ic.ControlPlane.Platform.GCP.OSImage.Name = "unspecified-arch" 943 ic.ControlPlane.Platform.GCP.OSImage.Project = projectID 944 } 945 missingImageArchitecture = func(ic *types.InstallConfig) { 946 ic.ControlPlane.Platform.GCP.OSImage = osImage 947 ic.ControlPlane.Platform.GCP.OSImage.Name = "missing-arch" 948 ic.ControlPlane.Platform.GCP.OSImage.Project = projectID 949 } 950 951 marketplaceImageAPIResult = &compute.Image{ 952 Architecture: "X86_64", 953 } 954 955 unspecifiedMarketplaceImageAPIResult = &compute.Image{ 956 Architecture: "ARCHITECTURE_UNSPECIFIED", 957 } 958 emptyMarketplaceImageAPIResult = &compute.Image{} 959 ) 960 961 cases := []struct { 962 name string 963 edits editFunctions 964 expectedError bool 965 expectedErrMsg string 966 expectedWarnMsg string //NOTE: this is a REGEXP 967 }{ 968 { 969 name: "Valid default machine image", 970 edits: editFunctions{validDefaultMachineImage}, 971 expectedError: false, 972 }, 973 { 974 name: "Valid control plane image", 975 edits: editFunctions{validControlPlaneImage}, 976 expectedError: false, 977 }, 978 { 979 name: "Valid compute image", 980 edits: editFunctions{validComputeImage}, 981 expectedError: false, 982 }, 983 { 984 name: "Invalid default machine image", 985 edits: editFunctions{invalidDefaultMachineImage}, 986 expectedError: true, 987 expectedErrMsg: `^\[platform.gcp.defaultMachinePlatform.osImage: Invalid value: gcp.OSImage{Name:"invalid-image", Project:"project-id"}: could not find the boot image: image not found\]$`, 988 }, 989 { 990 name: "Invalid control plane image", 991 edits: editFunctions{invalidControlPlaneImage}, 992 expectedError: true, 993 expectedErrMsg: `^\[controlPlane.platform.gcp.osImage: Invalid value: gcp.OSImage{Name:"invalid-image", Project:"project-id"}: could not find the boot image: image not found\]$`, 994 }, 995 { 996 name: "Invalid compute image", 997 edits: editFunctions{invalidComputeImage}, 998 expectedError: true, 999 expectedErrMsg: `^\[compute\[0\].platform.gcp.osImage: Invalid value: gcp.OSImage{Name:"invalid-image", Project:"project-id"}: could not find the boot image: image not found\]$`, 1000 }, 1001 { 1002 name: "Invalid images", 1003 edits: editFunctions{invalidDefaultMachineImage, invalidControlPlaneImage, invalidComputeImage}, 1004 expectedError: true, 1005 expectedErrMsg: `^\[(.*?\.osImage: Invalid value: gcp\.OSImage\{Name:"invalid-image", Project:"project-id"\}: could not find the boot image: image not found){3}\]$`, 1006 }, 1007 { 1008 name: "Mismatched default machine image architecture", 1009 edits: editFunctions{mismatchedDefaultMachineImageArchitecture}, 1010 expectedError: true, 1011 expectedErrMsg: `^\[controlPlane.platform.gcp.osImage: Invalid value: gcp.OSImage{Name:"mismatched-arch", Project:"project-id"}: image architecture X86_64 does not match controlPlane node architecture arm64 compute\[0\].platform.gcp.osImage: Invalid value: gcp.OSImage{Name:"mismatched-arch", Project:"project-id"}: image architecture X86_64 does not match compute node architecture arm64]$`, 1012 }, 1013 { 1014 name: "Mismatched control plane image architecture", 1015 edits: editFunctions{mismatchedControlPlaneImageArchitecture}, 1016 expectedError: true, 1017 expectedErrMsg: `^\[controlPlane.platform.gcp.osImage: Invalid value: gcp.OSImage{Name:"mismatched-arch", Project:"project-id"}: image architecture X86_64 does not match controlPlane node architecture arm64]$`, 1018 }, 1019 { 1020 name: "Mismatched compute image architecture", 1021 edits: editFunctions{mismatchedComputeImageArchitecture}, 1022 expectedError: true, 1023 expectedErrMsg: `^\[compute\[0\].platform.gcp.osImage: Invalid value: gcp.OSImage{Name:"mismatched-arch", Project:"project-id"}: image architecture X86_64 does not match compute node architecture arm64]$`, 1024 }, 1025 { 1026 name: "Missing image architecture", 1027 edits: editFunctions{missingImageArchitecture}, 1028 expectedError: false, 1029 expectedWarnMsg: "Boot image architecture is unspecified and might not be compatible with amd64 controlPlane nodes", 1030 }, 1031 { 1032 name: "Unspecified image architecture", 1033 edits: editFunctions{unspecifiedImageArchitecture}, 1034 expectedError: false, 1035 expectedWarnMsg: "Boot image architecture is unspecified and might not be compatible with amd64 controlPlane nodes", 1036 }, 1037 } 1038 1039 mockCtrl := gomock.NewController(t) 1040 defer mockCtrl.Finish() 1041 1042 gcpClient := mock.NewMockAPI(mockCtrl) 1043 1044 // Mocks: valid image with matching architecture 1045 gcpClient.EXPECT().GetImage(gomock.Any(), gomock.Eq(validImage), gomock.Any()).Return(marketplaceImageAPIResult, nil).AnyTimes() 1046 1047 //Mocks: invalid image 1048 gcpClient.EXPECT().GetImage(gomock.Any(), gomock.Eq(invalidImage), gomock.Any()).Return(marketplaceImageAPIResult, fmt.Errorf("image not found")).AnyTimes() 1049 1050 //Mocks: valid image with mismatched architecture 1051 gcpClient.EXPECT().GetImage(gomock.Any(), gomock.Eq(mismatchedArch), gomock.Any()).Return(marketplaceImageAPIResult, nil).AnyTimes() 1052 1053 //Mocks: valid image with no specified architecture 1054 gcpClient.EXPECT().GetImage(gomock.Any(), gomock.Eq("unspecified-arch"), gomock.Any()).Return(unspecifiedMarketplaceImageAPIResult, nil).AnyTimes() 1055 gcpClient.EXPECT().GetImage(gomock.Any(), gomock.Eq("missing-arch"), gomock.Any()).Return(emptyMarketplaceImageAPIResult, nil).AnyTimes() 1056 1057 for _, tc := range cases { 1058 t.Run(tc.name, func(t *testing.T) { 1059 editedInstallConfig := validInstallConfig() 1060 for _, edit := range tc.edits { 1061 edit(editedInstallConfig) 1062 } 1063 1064 hook := logrusTest.NewGlobal() 1065 errs := validateMarketplaceImages(gcpClient, editedInstallConfig) 1066 if tc.expectedError { 1067 assert.Regexp(t, tc.expectedErrMsg, errs) 1068 } else { 1069 assert.Empty(t, errs) 1070 } 1071 if len(tc.expectedWarnMsg) > 0 { 1072 assert.Regexp(t, tc.expectedWarnMsg, hook.LastEntry().Message) 1073 } 1074 }) 1075 } 1076 }