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  }