github.com/openshift/installer@v1.4.17/pkg/asset/manifests/aws/zones_test.go (about)

     1  package aws
     2  
     3  import (
     4  	"sort"
     5  	"testing"
     6  
     7  	"github.com/google/go-cmp/cmp"
     8  	"github.com/stretchr/testify/assert"
     9  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    10  	"k8s.io/apimachinery/pkg/util/sets"
    11  	capa "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2"
    12  
    13  	"github.com/openshift/installer/pkg/asset/installconfig"
    14  	"github.com/openshift/installer/pkg/asset/installconfig/aws"
    15  	"github.com/openshift/installer/pkg/asset/manifests/capiutils"
    16  	"github.com/openshift/installer/pkg/ipnet"
    17  	"github.com/openshift/installer/pkg/types"
    18  	awstypes "github.com/openshift/installer/pkg/types/aws"
    19  )
    20  
    21  var stubDefaultCIDR = "10.0.0.0/16"
    22  
    23  func stubClusterID() *installconfig.ClusterID {
    24  	return &installconfig.ClusterID{
    25  		InfraID: "infra-id",
    26  	}
    27  }
    28  
    29  func stubInstallConfig() *installconfig.InstallConfig {
    30  	return &installconfig.InstallConfig{}
    31  }
    32  
    33  func stubInstallConfigType() *types.InstallConfig {
    34  	return &types.InstallConfig{
    35  		TypeMeta: metav1.TypeMeta{
    36  			APIVersion: types.InstallConfigVersion,
    37  		},
    38  		ObjectMeta: metav1.ObjectMeta{
    39  			Name: "test-cluster",
    40  		},
    41  		Publish: types.ExternalPublishingStrategy,
    42  		Networking: &types.Networking{
    43  			MachineNetwork: []types.MachineNetworkEntry{
    44  				{
    45  					CIDR: *ipnet.MustParseCIDR(stubDefaultCIDR),
    46  				},
    47  			},
    48  		},
    49  	}
    50  }
    51  
    52  func stubInstallConfigPoolCompute() []types.MachinePool {
    53  	return []types.MachinePool{
    54  		{
    55  			Name: "worker",
    56  			Platform: types.MachinePoolPlatform{
    57  				AWS: &awstypes.MachinePool{
    58  					Zones: []string{"b", "c"},
    59  				},
    60  			},
    61  		},
    62  	}
    63  }
    64  
    65  func stubInstallConfigPoolComputeWithEdge() []types.MachinePool {
    66  	p := stubInstallConfigPoolCompute()
    67  	p = append(p, types.MachinePool{
    68  		Name: "edge",
    69  		Platform: types.MachinePoolPlatform{
    70  			AWS: &awstypes.MachinePool{
    71  				Zones: []string{"edge-b", "edge-c"},
    72  			},
    73  		},
    74  	})
    75  	return p
    76  }
    77  
    78  func stubInstallConfigPoolControl() *types.MachinePool {
    79  	return &types.MachinePool{
    80  		Name: "master",
    81  		Platform: types.MachinePoolPlatform{
    82  			AWS: &awstypes.MachinePool{
    83  				Zones: []string{"a", "b"},
    84  			},
    85  		},
    86  	}
    87  }
    88  
    89  func tSortCapaSubnetsByID(in capa.Subnets) capa.Subnets {
    90  	subnetIDs := []string{}
    91  	subnetsMap := make(map[string]capa.SubnetSpec, len(in))
    92  	for _, subnet := range in {
    93  		subnetsMap[subnet.ID] = subnet
    94  		subnetIDs = append(subnetIDs, subnet.ID)
    95  	}
    96  	sort.Strings(subnetIDs)
    97  	out := capa.Subnets{}
    98  	for _, sid := range subnetIDs {
    99  		out = append(out, subnetsMap[sid])
   100  	}
   101  	return out
   102  }
   103  
   104  func Test_extractZonesFromInstallConfig(t *testing.T) {
   105  	type args struct {
   106  		in *zonesInput
   107  	}
   108  	tests := []struct {
   109  		name       string
   110  		args       args
   111  		want       *ZonesCAPI
   112  		wantErrMsg string
   113  	}{
   114  		{
   115  			name: "no zones in config, use default from region",
   116  			args: args{
   117  				in: &zonesInput{
   118  					InstallConfig: func() *installconfig.InstallConfig {
   119  						ic := stubInstallConfig()
   120  						ic.Config = stubInstallConfigType()
   121  						ic.Config.AWS = &awstypes.Platform{
   122  							DefaultMachinePlatform: &awstypes.MachinePool{
   123  								Zones: []string{},
   124  							},
   125  						}
   126  						return ic
   127  					}(),
   128  					ZonesInRegion: []string{"x", "y"},
   129  				},
   130  			},
   131  			want: &ZonesCAPI{
   132  				ControlPlaneZones: sets.New("x", "y"),
   133  				ComputeZones:      sets.New("x", "y"),
   134  				EdgeZones:         sets.Set[string]{},
   135  			},
   136  		},
   137  		{
   138  			name: "no zones in config pools, use default from platform config",
   139  			args: args{
   140  				in: &zonesInput{
   141  					InstallConfig: func() *installconfig.InstallConfig {
   142  						ic := stubInstallConfig()
   143  						ic.Config = stubInstallConfigType()
   144  						ic.Config.AWS = &awstypes.Platform{
   145  							DefaultMachinePlatform: &awstypes.MachinePool{
   146  								Zones: []string{"a", "b"},
   147  							},
   148  						}
   149  						return ic
   150  					}(),
   151  					ZonesInRegion: []string{"x", "y"},
   152  				},
   153  			},
   154  			want: &ZonesCAPI{
   155  				ControlPlaneZones: sets.New("a", "b"),
   156  				ComputeZones:      sets.New("a", "b"),
   157  				EdgeZones:         sets.Set[string]{},
   158  			},
   159  		},
   160  		{
   161  			name: "custom zones control plane pool",
   162  			args: args{
   163  				in: &zonesInput{
   164  					InstallConfig: func() *installconfig.InstallConfig {
   165  						ic := stubInstallConfig()
   166  						ic.Config = &types.InstallConfig{
   167  							ControlPlane: stubInstallConfigPoolControl(),
   168  							Compute:      nil,
   169  						}
   170  						return ic
   171  					}(),
   172  					ZonesInRegion: []string{"x", "y"},
   173  				},
   174  			},
   175  			want: &ZonesCAPI{
   176  				ControlPlaneZones: sets.New("a", "b"),
   177  				ComputeZones:      sets.New("x", "y"),
   178  				EdgeZones:         sets.Set[string]{},
   179  			},
   180  		},
   181  		{
   182  			name: "custom zones compute pool",
   183  			args: args{
   184  				in: &zonesInput{
   185  					InstallConfig: func() *installconfig.InstallConfig {
   186  						ic := stubInstallConfig()
   187  						ic.Config = &types.InstallConfig{
   188  							ControlPlane: nil,
   189  							Compute:      stubInstallConfigPoolCompute(),
   190  						}
   191  						return ic
   192  					}(),
   193  					ZonesInRegion: []string{"x", "y"},
   194  				},
   195  			},
   196  			want: &ZonesCAPI{
   197  				ControlPlaneZones: sets.New("x", "y"),
   198  				ComputeZones:      sets.New("b", "c"),
   199  				EdgeZones:         sets.Set[string]{},
   200  			},
   201  		},
   202  		{
   203  			name: "custom zones control plane and compute pools",
   204  			args: args{
   205  				in: &zonesInput{
   206  					InstallConfig: func() *installconfig.InstallConfig {
   207  						ic := stubInstallConfig()
   208  						ic.Config = &types.InstallConfig{
   209  							ControlPlane: stubInstallConfigPoolControl(),
   210  							Compute:      stubInstallConfigPoolCompute(),
   211  						}
   212  						return ic
   213  					}(),
   214  					ZonesInRegion: []string{"x", "y"},
   215  				},
   216  			},
   217  			want: &ZonesCAPI{
   218  				ControlPlaneZones: sets.New("a", "b"),
   219  				ComputeZones:      sets.New("b", "c"),
   220  				EdgeZones:         sets.Set[string]{},
   221  			},
   222  		},
   223  		{
   224  			name: "custom zones control plane, compute and edge pools",
   225  			args: args{
   226  				in: &zonesInput{
   227  					InstallConfig: func() *installconfig.InstallConfig {
   228  						ic := stubInstallConfig()
   229  						ic.Config = &types.InstallConfig{
   230  							ControlPlane: stubInstallConfigPoolControl(),
   231  							Compute:      stubInstallConfigPoolComputeWithEdge(),
   232  						}
   233  						return ic
   234  					}(),
   235  					ZonesInRegion: []string{"x", "y"},
   236  				},
   237  			},
   238  			want: &ZonesCAPI{
   239  				ControlPlaneZones: sets.New("a", "b"),
   240  				ComputeZones:      sets.New("b", "c"),
   241  				EdgeZones:         sets.New("edge-b", "edge-c"),
   242  			},
   243  		},
   244  		// errors
   245  		{
   246  			name: "unexpected empty zones on config and metadata",
   247  			args: args{
   248  				in: &zonesInput{
   249  					InstallConfig: func() *installconfig.InstallConfig {
   250  						ic := stubInstallConfig()
   251  						ic.Config = stubInstallConfigType()
   252  						ic.Config.AWS = &awstypes.Platform{
   253  							DefaultMachinePlatform: &awstypes.MachinePool{
   254  								Zones: []string{},
   255  							},
   256  						}
   257  						return ic
   258  					}(),
   259  					ZonesInRegion: []string{},
   260  				},
   261  			},
   262  			want: &ZonesCAPI{
   263  				ControlPlaneZones: sets.Set[string]{},
   264  				ComputeZones:      sets.Set[string]{},
   265  				EdgeZones:         sets.Set[string]{},
   266  			},
   267  			wantErrMsg: `failed to set zones from config, got: []`,
   268  		},
   269  		{
   270  			name: "unexpected empty zones from edge compute pool",
   271  			args: args{
   272  				in: &zonesInput{
   273  					InstallConfig: func() *installconfig.InstallConfig {
   274  						ic := stubInstallConfig()
   275  						ic.Config = &types.InstallConfig{
   276  							ControlPlane: stubInstallConfigPoolControl(),
   277  							Compute: func() []types.MachinePool {
   278  								pools := stubInstallConfigPoolCompute()
   279  								// create empty zones' edge pool to force failures
   280  								pools = append(pools, types.MachinePool{
   281  									Name: "edge",
   282  									Platform: types.MachinePoolPlatform{
   283  										AWS: &awstypes.MachinePool{
   284  											Zones: []string{},
   285  										},
   286  									},
   287  								})
   288  								return pools
   289  							}(),
   290  						}
   291  						return ic
   292  					}(),
   293  					ZonesInRegion: []string{"x", "y"},
   294  				},
   295  			},
   296  			wantErrMsg: `expect one or more zones in the edge compute pool, got: []`,
   297  		},
   298  	}
   299  	for _, tt := range tests {
   300  		t.Run(tt.name, func(t *testing.T) {
   301  			got, err := extractZonesFromInstallConfig(tt.args.in)
   302  			if err != nil {
   303  				if len(tt.wantErrMsg) > 0 {
   304  					if got := err.Error(); !cmp.Equal(got, tt.wantErrMsg) {
   305  						t.Errorf("extractZonesFromInstallConfig() unexpected error message: %v", cmp.Diff(got, tt.wantErrMsg))
   306  					}
   307  					return
   308  				}
   309  				t.Errorf("extractZonesFromInstallConfig() unexpected error: %v", err)
   310  				return
   311  			}
   312  			if !cmp.Equal(got, tt.want) {
   313  				t.Errorf("extractZonesFromInstallConfig() Got unexpected results:\n%v", cmp.Diff(got, tt.want))
   314  			}
   315  		})
   316  	}
   317  }
   318  
   319  func Test_setSubnetsManagedVPC(t *testing.T) {
   320  	type args struct {
   321  		in *zonesInput
   322  	}
   323  	tests := []struct {
   324  		name    string
   325  		args    args
   326  		wantErr bool
   327  		want    capa.NetworkSpec
   328  	}{
   329  		{
   330  			name: "default availability zones in the region",
   331  			args: args{
   332  				in: &zonesInput{
   333  					ClusterID: stubClusterID(),
   334  					InstallConfig: func() *installconfig.InstallConfig {
   335  						ic := stubInstallConfig()
   336  						ic.Config = &types.InstallConfig{
   337  							Publish: types.ExternalPublishingStrategy,
   338  							Networking: &types.Networking{
   339  								MachineNetwork: []types.MachineNetworkEntry{
   340  									{
   341  										CIDR: *ipnet.MustParseCIDR(stubDefaultCIDR),
   342  									},
   343  								},
   344  							},
   345  						}
   346  						return ic
   347  					}(),
   348  					Cluster: &capa.AWSCluster{
   349  						ObjectMeta: metav1.ObjectMeta{
   350  							Name:      "infraId",
   351  							Namespace: capiutils.Namespace,
   352  						},
   353  						Spec: capa.AWSClusterSpec{},
   354  					},
   355  					ZonesInRegion: []string{"a", "b", "c"},
   356  				},
   357  			},
   358  			want: capa.NetworkSpec{
   359  				VPC: capa.VPCSpec{CidrBlock: stubDefaultCIDR},
   360  				Subnets: []capa.SubnetSpec{
   361  					{
   362  						ID:               "infra-id-subnet-private-a",
   363  						AvailabilityZone: "a",
   364  						IsPublic:         false,
   365  						CidrBlock:        "10.0.0.0/19",
   366  					}, {
   367  						ID:               "infra-id-subnet-private-b",
   368  						AvailabilityZone: "b",
   369  						IsPublic:         false,
   370  						CidrBlock:        "10.0.32.0/19",
   371  					}, {
   372  						ID:               "infra-id-subnet-private-c",
   373  						AvailabilityZone: "c",
   374  						IsPublic:         false,
   375  						CidrBlock:        "10.0.64.0/19",
   376  					}, {
   377  						ID:               "infra-id-subnet-public-a",
   378  						AvailabilityZone: "a",
   379  						IsPublic:         true,
   380  						CidrBlock:        "10.0.96.0/21",
   381  					}, {
   382  						ID:               "infra-id-subnet-public-b",
   383  						AvailabilityZone: "b",
   384  						IsPublic:         true,
   385  						CidrBlock:        "10.0.104.0/21",
   386  					}, {
   387  						ID:               "infra-id-subnet-public-c",
   388  						AvailabilityZone: "c",
   389  						IsPublic:         true,
   390  						CidrBlock:        "10.0.112.0/21",
   391  					},
   392  				},
   393  			},
   394  		},
   395  		{
   396  			name: "default availability zones in the region and edge",
   397  			args: args{
   398  				in: &zonesInput{
   399  					ClusterID: stubClusterID(),
   400  					InstallConfig: func() *installconfig.InstallConfig {
   401  						ic := stubInstallConfig()
   402  						ic.Config = &types.InstallConfig{
   403  							Publish: types.ExternalPublishingStrategy,
   404  							Networking: &types.Networking{
   405  								MachineNetwork: []types.MachineNetworkEntry{
   406  									{
   407  										CIDR: *ipnet.MustParseCIDR(stubDefaultCIDR),
   408  									},
   409  								},
   410  							},
   411  							Compute: []types.MachinePool{
   412  								{
   413  									Name: "edge",
   414  									Platform: types.MachinePoolPlatform{
   415  										AWS: &awstypes.MachinePool{
   416  											Zones: []string{"edge-a"},
   417  										},
   418  									},
   419  								},
   420  							},
   421  						}
   422  						return ic
   423  					}(),
   424  					Cluster: &capa.AWSCluster{
   425  						ObjectMeta: metav1.ObjectMeta{
   426  							Name:      "infraId",
   427  							Namespace: capiutils.Namespace,
   428  						},
   429  						Spec: capa.AWSClusterSpec{},
   430  					},
   431  					ZonesInRegion: []string{"a", "b", "c"},
   432  				},
   433  			},
   434  			want: capa.NetworkSpec{
   435  				VPC: capa.VPCSpec{CidrBlock: stubDefaultCIDR},
   436  				Subnets: []capa.SubnetSpec{
   437  					{
   438  						ID:               "infra-id-subnet-private-a",
   439  						AvailabilityZone: "a",
   440  						IsPublic:         false,
   441  						CidrBlock:        "10.0.0.0/19",
   442  					}, {
   443  						ID:               "infra-id-subnet-private-b",
   444  						AvailabilityZone: "b",
   445  						IsPublic:         false,
   446  						CidrBlock:        "10.0.32.0/19",
   447  					}, {
   448  						ID:               "infra-id-subnet-private-c",
   449  						AvailabilityZone: "c",
   450  						IsPublic:         false,
   451  						CidrBlock:        "10.0.64.0/19",
   452  					}, {
   453  						ID:               "infra-id-subnet-private-edge-a",
   454  						AvailabilityZone: "edge-a",
   455  						IsPublic:         false,
   456  						CidrBlock:        "10.0.128.0/21",
   457  					}, {
   458  						ID:               "infra-id-subnet-public-a",
   459  						AvailabilityZone: "a",
   460  						IsPublic:         true,
   461  						CidrBlock:        "10.0.96.0/21",
   462  					}, {
   463  						ID:               "infra-id-subnet-public-b",
   464  						AvailabilityZone: "b",
   465  						IsPublic:         true,
   466  						CidrBlock:        "10.0.104.0/21",
   467  					}, {
   468  						ID:               "infra-id-subnet-public-c",
   469  						AvailabilityZone: "c",
   470  						IsPublic:         true,
   471  						CidrBlock:        "10.0.112.0/21",
   472  					}, {
   473  						ID:               "infra-id-subnet-public-edge-a",
   474  						AvailabilityZone: "edge-a",
   475  						IsPublic:         true,
   476  						CidrBlock:        "10.0.136.0/21",
   477  					},
   478  				},
   479  			},
   480  		},
   481  		// TODO: error scenarios to review the coverage
   482  		// {
   483  		// 	name: "err: failed to get availability zones: expect one or more zones in the edge compute pool",
   484  		// },
   485  		// {
   486  		// 	name: "err: failed to get availability zones: failed to set zones from config",
   487  		// },
   488  		// {
   489  		// 	name: "err: unable to generate CIDR blocks for all private subnets",
   490  		// },
   491  		// {
   492  		// 	name: "err: unable to generate CIDR blocks for all public subnets",
   493  		// },
   494  		// {
   495  		// 	name: "err: unable to define CIDR blocks to all zones for private subnets",
   496  		// },
   497  		// {
   498  		// 	name: "err: unable to define CIDR blocks to all zones for public subnets",
   499  		// },
   500  		// {
   501  		// 	name: "err: unable to generate CIDR blocks for all edge subnets",
   502  		// },
   503  		// {
   504  		// 	name: "err: unable to define CIDR blocks to all edge zones for private subnets",
   505  		// },
   506  		// {
   507  		// 	name: "err: unable to define CIDR blocks to all edge zones for public subnets",
   508  		// },
   509  	}
   510  	for _, tt := range tests {
   511  		t.Run(tt.name, func(t *testing.T) {
   512  			err := setSubnetsManagedVPC(tt.args.in)
   513  			if (err != nil) != tt.wantErr {
   514  				t.Errorf("setSubnetsManagedVPC() #1 error: %+v,\nwantErr %+v\n", err, tt.wantErr)
   515  			}
   516  			if tt.args.in == nil && (err == nil) {
   517  				return
   518  			}
   519  			if tt.args.in.Cluster == nil && (err == nil) {
   520  				return
   521  			}
   522  
   523  			if len(tt.args.in.Cluster.Spec.NetworkSpec.Subnets) == 0 {
   524  				if !tt.wantErr {
   525  					t.Errorf("setSubnetsManagedVPC() #2 error: %v, wantErr: %v", err, tt.wantErr)
   526  				}
   527  				return
   528  			}
   529  			tt.args.in.Cluster.Spec.NetworkSpec.Subnets = tSortCapaSubnetsByID(tt.args.in.Cluster.Spec.NetworkSpec.Subnets)
   530  			if got := tt.args.in.Cluster.Spec.NetworkSpec; !cmp.Equal(got, tt.want) {
   531  				t.Errorf("setSubnetsManagedVPC() NetworkSpec.Subnets diff: %v", cmp.Diff(got, tt.want))
   532  			}
   533  		})
   534  	}
   535  }
   536  
   537  func Test_setSubnetsBYOVPC(t *testing.T) {
   538  	type args struct {
   539  		in *zonesInput
   540  	}
   541  	tests := []struct {
   542  		name    string
   543  		args    args
   544  		want    capa.NetworkSpec
   545  		wantErr bool
   546  	}{
   547  		{
   548  			name: "default byo vpc",
   549  			args: args{
   550  				in: &zonesInput{
   551  					Cluster: &capa.AWSCluster{},
   552  					Subnets: &subnetsInput{
   553  						vpc: "vpc-id",
   554  						privateSubnets: aws.Subnets{
   555  							"subnetId-a-private": aws.Subnet{
   556  								ID:   "subnetId-a-private",
   557  								CIDR: "10.0.1.0/24",
   558  								Zone: &aws.Zone{
   559  									Name: "a",
   560  								},
   561  								Public: false,
   562  							},
   563  							"subnetId-b-private": aws.Subnet{
   564  								ID:   "subnetId-b-private",
   565  								CIDR: "10.0.2.0/24",
   566  								Zone: &aws.Zone{
   567  									Name: "b",
   568  								},
   569  								Public: false,
   570  							},
   571  							"subnetId-c-private": aws.Subnet{
   572  								ID:   "subnetId-c-private",
   573  								CIDR: "10.0.3.0/24",
   574  								Zone: &aws.Zone{
   575  									Name: "c",
   576  								},
   577  								Public: false,
   578  							},
   579  						},
   580  						publicSubnets: aws.Subnets{
   581  							"subnetId-a-public": aws.Subnet{
   582  								ID:   "subnetId-a-public",
   583  								CIDR: "10.0.4.0/24",
   584  								Zone: &aws.Zone{
   585  									Name: "a",
   586  								},
   587  								Public: true,
   588  							},
   589  							"subnetId-b-public": aws.Subnet{
   590  								ID:   "subnetId-b-public",
   591  								CIDR: "10.0.5.0/24",
   592  								Zone: &aws.Zone{
   593  									Name: "b",
   594  								},
   595  								Public: true,
   596  							},
   597  							"subnetId-c-public": aws.Subnet{
   598  								ID:   "subnetId-c-public",
   599  								CIDR: "10.0.6.0/24",
   600  								Zone: &aws.Zone{
   601  									Name: "c",
   602  								},
   603  								Public: true,
   604  							},
   605  						},
   606  					},
   607  				},
   608  			},
   609  			want: capa.NetworkSpec{
   610  				VPC: capa.VPCSpec{ID: "vpc-id"},
   611  				Subnets: []capa.SubnetSpec{
   612  					{
   613  						ID:               "subnetId-a-private",
   614  						AvailabilityZone: "a",
   615  						IsPublic:         false,
   616  						CidrBlock:        "10.0.1.0/24",
   617  					}, {
   618  						ID:               "subnetId-a-public",
   619  						AvailabilityZone: "a",
   620  						IsPublic:         true,
   621  						CidrBlock:        "10.0.4.0/24",
   622  					}, {
   623  						ID:               "subnetId-b-private",
   624  						AvailabilityZone: "b",
   625  						IsPublic:         false,
   626  						CidrBlock:        "10.0.2.0/24",
   627  					}, {
   628  						ID:               "subnetId-b-public",
   629  						AvailabilityZone: "b",
   630  						IsPublic:         true,
   631  						CidrBlock:        "10.0.5.0/24",
   632  					}, {
   633  						ID:               "subnetId-c-private",
   634  						AvailabilityZone: "c",
   635  						IsPublic:         false,
   636  						CidrBlock:        "10.0.3.0/24",
   637  					}, {
   638  						ID:               "subnetId-c-public",
   639  						AvailabilityZone: "c",
   640  						IsPublic:         true,
   641  						CidrBlock:        "10.0.6.0/24",
   642  					},
   643  				},
   644  			},
   645  		},
   646  		{
   647  			name: "byo vpc only private subnets",
   648  			args: args{
   649  				in: &zonesInput{
   650  					Cluster: &capa.AWSCluster{},
   651  					Subnets: &subnetsInput{
   652  						vpc: "vpc-id",
   653  						privateSubnets: aws.Subnets{
   654  							"subnetId-a-private": aws.Subnet{
   655  								ID:   "subnetId-a-private",
   656  								CIDR: "10.0.1.0/24",
   657  								Zone: &aws.Zone{
   658  									Name: "a",
   659  								},
   660  								Public: false,
   661  							},
   662  							"subnetId-b-private": aws.Subnet{
   663  								ID:   "subnetId-b-private",
   664  								CIDR: "10.0.2.0/24",
   665  								Zone: &aws.Zone{
   666  									Name: "b",
   667  								},
   668  								Public: false,
   669  							},
   670  							"subnetId-c-private": aws.Subnet{
   671  								ID:   "subnetId-c-private",
   672  								CIDR: "10.0.3.0/24",
   673  								Zone: &aws.Zone{
   674  									Name: "c",
   675  								},
   676  								Public: false,
   677  							},
   678  						},
   679  					},
   680  				},
   681  			},
   682  			want: capa.NetworkSpec{
   683  				VPC: capa.VPCSpec{
   684  					ID: "vpc-id",
   685  				},
   686  				Subnets: capa.Subnets{
   687  					{
   688  						ID:               "subnetId-a-private",
   689  						AvailabilityZone: "a",
   690  						IsPublic:         false,
   691  						CidrBlock:        "10.0.1.0/24",
   692  					},
   693  					{
   694  						ID:               "subnetId-b-private",
   695  						AvailabilityZone: "b",
   696  						IsPublic:         false,
   697  						CidrBlock:        "10.0.2.0/24",
   698  					},
   699  					{
   700  						ID:               "subnetId-c-private",
   701  						AvailabilityZone: "c",
   702  						IsPublic:         false,
   703  						CidrBlock:        "10.0.3.0/24",
   704  					},
   705  				},
   706  			},
   707  		},
   708  		{
   709  			name: "byo vpc with edge",
   710  			args: args{
   711  				in: &zonesInput{
   712  					Cluster: &capa.AWSCluster{},
   713  					Subnets: &subnetsInput{
   714  						vpc: "vpc-id",
   715  						privateSubnets: aws.Subnets{
   716  							"subnetId-a-private": aws.Subnet{
   717  								ID:   "subnetId-a-private",
   718  								CIDR: "10.0.1.0/24",
   719  								Zone: &aws.Zone{
   720  									Name: "a",
   721  								},
   722  								Public: false,
   723  							},
   724  							"subnetId-b-private": aws.Subnet{
   725  								ID:   "subnetId-b-private",
   726  								CIDR: "10.0.2.0/24",
   727  								Zone: &aws.Zone{
   728  									Name: "b",
   729  								},
   730  								Public: false,
   731  							},
   732  							"subnetId-c-private": aws.Subnet{
   733  								ID:   "subnetId-c-private",
   734  								CIDR: "10.0.3.0/24",
   735  								Zone: &aws.Zone{
   736  									Name: "c",
   737  								},
   738  								Public: false,
   739  							},
   740  						},
   741  						publicSubnets: aws.Subnets{
   742  							"subnetId-a-public": aws.Subnet{
   743  								ID:   "subnetId-a-public",
   744  								CIDR: "10.0.4.0/24",
   745  								Zone: &aws.Zone{
   746  									Name: "a",
   747  								},
   748  								Public: true,
   749  							},
   750  							"subnetId-b-public": aws.Subnet{
   751  								ID:   "subnetId-b-public",
   752  								CIDR: "10.0.5.0/24",
   753  								Zone: &aws.Zone{
   754  									Name: "b",
   755  								},
   756  								Public: true,
   757  							},
   758  							"subnetId-c-public": aws.Subnet{
   759  								ID:   "subnetId-c-public",
   760  								CIDR: "10.0.6.0/24",
   761  								Zone: &aws.Zone{
   762  									Name: "c",
   763  								},
   764  								Public: true,
   765  							},
   766  						},
   767  						edgeSubnets: aws.Subnets{
   768  							"subnetId-edge-a-private": aws.Subnet{
   769  								ID:   "subnetId-edge-a-private",
   770  								CIDR: "10.0.7.0/24",
   771  								Zone: &aws.Zone{
   772  									Name: "edge-a",
   773  								},
   774  								Public: false,
   775  							},
   776  							"subnetId-edge-a-public": aws.Subnet{
   777  								ID:   "subnetId-edge-a-public",
   778  								CIDR: "10.0.8.0/24",
   779  								Zone: &aws.Zone{
   780  									Name: "edge-a",
   781  								},
   782  								Public: true,
   783  							},
   784  						},
   785  					},
   786  				},
   787  			},
   788  			want: capa.NetworkSpec{
   789  				VPC: capa.VPCSpec{ID: "vpc-id"},
   790  				Subnets: []capa.SubnetSpec{
   791  					{
   792  						ID:               "subnetId-a-private",
   793  						AvailabilityZone: "a",
   794  						IsPublic:         false,
   795  						CidrBlock:        "10.0.1.0/24",
   796  					}, {
   797  						ID:               "subnetId-a-public",
   798  						AvailabilityZone: "a",
   799  						IsPublic:         true,
   800  						CidrBlock:        "10.0.4.0/24",
   801  					}, {
   802  						ID:               "subnetId-b-private",
   803  						AvailabilityZone: "b",
   804  						IsPublic:         false,
   805  						CidrBlock:        "10.0.2.0/24",
   806  					}, {
   807  						ID:               "subnetId-b-public",
   808  						AvailabilityZone: "b",
   809  						IsPublic:         true,
   810  						CidrBlock:        "10.0.5.0/24",
   811  					}, {
   812  						ID:               "subnetId-c-private",
   813  						AvailabilityZone: "c",
   814  						IsPublic:         false,
   815  						CidrBlock:        "10.0.3.0/24",
   816  					}, {
   817  						ID:               "subnetId-c-public",
   818  						AvailabilityZone: "c",
   819  						IsPublic:         true,
   820  						CidrBlock:        "10.0.6.0/24",
   821  					}, {
   822  						ID:               "subnetId-edge-a-private",
   823  						AvailabilityZone: "edge-a",
   824  						IsPublic:         false,
   825  						CidrBlock:        "10.0.7.0/24",
   826  					}, {
   827  						ID:               "subnetId-edge-a-public",
   828  						AvailabilityZone: "edge-a",
   829  						IsPublic:         true,
   830  						CidrBlock:        "10.0.8.0/24",
   831  					},
   832  				},
   833  			},
   834  		},
   835  	}
   836  	for _, tt := range tests {
   837  		t.Run(tt.name, func(t *testing.T) {
   838  			err := setSubnetsBYOVPC(tt.args.in)
   839  			if (err != nil) != tt.wantErr {
   840  				t.Errorf("setSubnetsBYOVPC() #1 error: %v, wantErr: %v", err, tt.wantErr)
   841  				return
   842  			}
   843  			if len(tt.args.in.Cluster.Spec.NetworkSpec.Subnets) == 0 {
   844  				if !tt.wantErr {
   845  					t.Errorf("setSubnetsBYOVPC() #2 error: %v, wantErr: %v", err, tt.wantErr)
   846  				}
   847  				return
   848  			}
   849  			tt.args.in.Cluster.Spec.NetworkSpec.Subnets = tSortCapaSubnetsByID(tt.args.in.Cluster.Spec.NetworkSpec.Subnets)
   850  			if got := tt.args.in.Cluster.Spec.NetworkSpec; !cmp.Equal(got, tt.want) {
   851  				t.Errorf("setSubnetsBYOVPC() NetworkSpec.Subnets diff: %v", cmp.Diff(got, tt.want))
   852  			}
   853  		})
   854  	}
   855  }
   856  
   857  func Test_ZonesCAPI_SetAvailabilityZones(t *testing.T) {
   858  	type fields struct {
   859  		ControlPlaneZones sets.Set[string]
   860  		ComputeZones      sets.Set[string]
   861  	}
   862  	type args struct {
   863  		pool  string
   864  		zones []string
   865  	}
   866  	tests := []struct {
   867  		name   string
   868  		fields fields
   869  		args   args
   870  		want   *ZonesCAPI
   871  	}{
   872  		{
   873  			name: "empty",
   874  			fields: fields{
   875  				ControlPlaneZones: sets.Set[string]{},
   876  				ComputeZones:      sets.Set[string]{},
   877  			},
   878  			args: args{
   879  				pool:  types.MachinePoolControlPlaneRoleName,
   880  				zones: []string{},
   881  			},
   882  			want: &ZonesCAPI{
   883  				ControlPlaneZones: sets.Set[string]{},
   884  				ComputeZones:      sets.Set[string]{},
   885  			},
   886  		},
   887  		{
   888  			name: "set zones for control plane pool",
   889  			fields: fields{
   890  				ControlPlaneZones: sets.Set[string]{},
   891  				ComputeZones:      sets.Set[string]{},
   892  			},
   893  			args: args{
   894  				pool:  types.MachinePoolControlPlaneRoleName,
   895  				zones: []string{"a", "b"},
   896  			},
   897  			want: &ZonesCAPI{
   898  				ControlPlaneZones: sets.New("a", "b"),
   899  				ComputeZones:      sets.Set[string]{},
   900  			},
   901  		},
   902  		{
   903  			name: "set zones for compute pool",
   904  			fields: fields{
   905  				ControlPlaneZones: sets.Set[string]{},
   906  				ComputeZones:      sets.Set[string]{},
   907  			},
   908  			args: args{
   909  				pool:  types.MachinePoolComputeRoleName,
   910  				zones: []string{"b", "c"},
   911  			},
   912  			want: &ZonesCAPI{
   913  				ControlPlaneZones: sets.Set[string]{},
   914  				ComputeZones:      sets.New("b", "c"),
   915  			},
   916  		},
   917  	}
   918  	for _, tt := range tests {
   919  		t.Run(tt.name, func(t *testing.T) {
   920  			zo := &ZonesCAPI{
   921  				ControlPlaneZones: tt.fields.ControlPlaneZones,
   922  				ComputeZones:      tt.fields.ComputeZones,
   923  			}
   924  			zo.SetAvailabilityZones(tt.args.pool, tt.args.zones)
   925  			if tt.want != nil {
   926  				assert.EqualValuesf(t, tt.want, zo, "%v failed", tt.name)
   927  			}
   928  		})
   929  	}
   930  }
   931  
   932  func Test_ZonesCAPI_SetDefaultConfigZones(t *testing.T) {
   933  	type fields struct {
   934  		AvailabilityZones sets.Set[string]
   935  		ControlPlaneZones sets.Set[string]
   936  		ComputeZones      sets.Set[string]
   937  	}
   938  	type args struct {
   939  		pool      string
   940  		defConfig []string
   941  		defRegion []string
   942  	}
   943  	tests := []struct {
   944  		name   string
   945  		fields fields
   946  		args   args
   947  		want   *ZonesCAPI
   948  	}{
   949  		{
   950  			name: "empty",
   951  			fields: fields{
   952  				ControlPlaneZones: sets.Set[string]{},
   953  				ComputeZones:      sets.Set[string]{},
   954  			},
   955  			args: args{
   956  				pool:      types.MachinePoolControlPlaneRoleName,
   957  				defConfig: []string{},
   958  				defRegion: []string{},
   959  			},
   960  			want: &ZonesCAPI{
   961  				ControlPlaneZones: sets.Set[string]{},
   962  				ComputeZones:      sets.Set[string]{},
   963  			},
   964  		},
   965  		{
   966  			name: "platform defaults when control plane pool exists",
   967  			fields: fields{
   968  				ControlPlaneZones: sets.New("a"),
   969  				ComputeZones:      sets.Set[string]{},
   970  			},
   971  			args: args{
   972  				pool:      types.MachinePoolControlPlaneRoleName,
   973  				defConfig: []string{"d"},
   974  				defRegion: []string{"f"},
   975  			},
   976  			want: &ZonesCAPI{
   977  				ControlPlaneZones: sets.New("a"),
   978  				ComputeZones:      sets.Set[string]{},
   979  			},
   980  		},
   981  		{
   982  			name: "platform defaults when control plane pool not exists",
   983  			fields: fields{
   984  				ControlPlaneZones: sets.Set[string]{},
   985  				ComputeZones:      sets.Set[string]{},
   986  			},
   987  			args: args{
   988  				pool:      types.MachinePoolControlPlaneRoleName,
   989  				defConfig: []string{"d"},
   990  				defRegion: []string{"f"},
   991  			},
   992  			want: &ZonesCAPI{
   993  				ControlPlaneZones: sets.New("d"),
   994  				ComputeZones:      sets.Set[string]{},
   995  			},
   996  		},
   997  		{
   998  			name: "region defaults when control plane pool exists",
   999  			fields: fields{
  1000  				ControlPlaneZones: sets.New("a"),
  1001  				ComputeZones:      sets.Set[string]{},
  1002  			},
  1003  			args: args{
  1004  				pool:      types.MachinePoolControlPlaneRoleName,
  1005  				defConfig: []string{},
  1006  				defRegion: []string{"f"},
  1007  			},
  1008  			want: &ZonesCAPI{
  1009  				ControlPlaneZones: sets.New("a"),
  1010  				ComputeZones:      sets.Set[string]{},
  1011  			},
  1012  		},
  1013  		{
  1014  			name: "region defaults when control plane pool not exists",
  1015  			fields: fields{
  1016  				ControlPlaneZones: sets.Set[string]{},
  1017  				ComputeZones:      sets.Set[string]{},
  1018  			},
  1019  			args: args{
  1020  				pool:      types.MachinePoolControlPlaneRoleName,
  1021  				defConfig: []string{},
  1022  				defRegion: []string{"f"},
  1023  			},
  1024  			want: &ZonesCAPI{
  1025  				ControlPlaneZones: sets.New("f"),
  1026  				ComputeZones:      sets.Set[string]{},
  1027  			},
  1028  		},
  1029  		{
  1030  			name: "platform defaults when compute pool exists",
  1031  			fields: fields{
  1032  				ControlPlaneZones: sets.Set[string]{},
  1033  				ComputeZones:      sets.New("b"),
  1034  			},
  1035  			args: args{
  1036  				pool:      types.MachinePoolComputeRoleName,
  1037  				defConfig: []string{"d"},
  1038  				defRegion: []string{"f"},
  1039  			},
  1040  			want: &ZonesCAPI{
  1041  				ControlPlaneZones: sets.Set[string]{},
  1042  				ComputeZones:      sets.New("b"),
  1043  			},
  1044  		},
  1045  		{
  1046  			name: "platform defaults when compute pool not exists",
  1047  			fields: fields{
  1048  				AvailabilityZones: sets.Set[string]{},
  1049  				ControlPlaneZones: sets.Set[string]{},
  1050  				ComputeZones:      sets.Set[string]{},
  1051  			},
  1052  			args: args{
  1053  				pool:      types.MachinePoolComputeRoleName,
  1054  				defConfig: []string{"d"},
  1055  				defRegion: []string{"f"},
  1056  			},
  1057  			want: &ZonesCAPI{
  1058  				ControlPlaneZones: sets.Set[string]{},
  1059  				ComputeZones:      sets.New("d"),
  1060  			},
  1061  		},
  1062  		{
  1063  			name: "region defaults when compute pool exists",
  1064  			fields: fields{
  1065  				ControlPlaneZones: sets.Set[string]{},
  1066  				ComputeZones:      sets.New("b"),
  1067  			},
  1068  			args: args{
  1069  				pool:      types.MachinePoolComputeRoleName,
  1070  				defConfig: []string{},
  1071  				defRegion: []string{"f"},
  1072  			},
  1073  			want: &ZonesCAPI{
  1074  				ControlPlaneZones: sets.Set[string]{},
  1075  				ComputeZones:      sets.New("b"),
  1076  			},
  1077  		},
  1078  		{
  1079  			name: "region defaults when compute pool not exists",
  1080  			fields: fields{
  1081  				ControlPlaneZones: sets.Set[string]{},
  1082  				ComputeZones:      sets.Set[string]{},
  1083  			},
  1084  			args: args{
  1085  				pool:      types.MachinePoolComputeRoleName,
  1086  				defConfig: []string{},
  1087  				defRegion: []string{"f"},
  1088  			},
  1089  			want: &ZonesCAPI{
  1090  				ControlPlaneZones: sets.Set[string]{},
  1091  				ComputeZones:      sets.New("f"),
  1092  			},
  1093  		},
  1094  	}
  1095  	for _, tt := range tests {
  1096  		t.Run(tt.name, func(t *testing.T) {
  1097  			zo := &ZonesCAPI{
  1098  				ControlPlaneZones: tt.fields.ControlPlaneZones,
  1099  				ComputeZones:      tt.fields.ComputeZones,
  1100  			}
  1101  			zo.SetDefaultConfigZones(tt.args.pool, tt.args.defConfig, tt.args.defRegion)
  1102  			if tt.want != nil {
  1103  				assert.EqualValuesf(t, tt.want, zo, "%v failed", tt.name)
  1104  			}
  1105  		})
  1106  	}
  1107  }
  1108  
  1109  func Test_ZonesCAPI_GetAvailabilityZones(t *testing.T) {
  1110  	tests := []struct {
  1111  		name  string
  1112  		zones *ZonesCAPI
  1113  		want  []string
  1114  	}{
  1115  		{
  1116  			name:  "empty",
  1117  			zones: &ZonesCAPI{},
  1118  			want:  []string{},
  1119  		},
  1120  		{
  1121  			name: "empty az",
  1122  			zones: &ZonesCAPI{
  1123  				EdgeZones: sets.New("edge-x", "edge-y"),
  1124  			},
  1125  			want: []string{},
  1126  		},
  1127  		{
  1128  			name: "sorted",
  1129  			zones: &ZonesCAPI{
  1130  				ControlPlaneZones: sets.New("a", "b"),
  1131  				ComputeZones:      sets.New("b", "c"),
  1132  				EdgeZones:         sets.New("edge-x", "edge-y"),
  1133  			},
  1134  			want: []string{"a", "b", "c"},
  1135  		},
  1136  		{
  1137  			name: "unsorted",
  1138  			zones: &ZonesCAPI{
  1139  				ControlPlaneZones: sets.New("x", "a"),
  1140  				ComputeZones:      sets.New("b", "a"),
  1141  				EdgeZones:         sets.New("edge-x", "edge-y"),
  1142  			},
  1143  			want: []string{"a", "b", "x"},
  1144  		},
  1145  		{
  1146  			name: "control planes only",
  1147  			zones: &ZonesCAPI{
  1148  				ControlPlaneZones: sets.New("x", "a"),
  1149  				ComputeZones:      sets.Set[string]{},
  1150  				EdgeZones:         sets.New("edge-x", "edge-y"),
  1151  			},
  1152  			want: []string{"a", "x"},
  1153  		},
  1154  		{
  1155  			name: "compute only",
  1156  			zones: &ZonesCAPI{
  1157  				ControlPlaneZones: sets.Set[string]{},
  1158  				ComputeZones:      sets.New("x", "a"),
  1159  				EdgeZones:         sets.New("edge-x", "edge-y"),
  1160  			},
  1161  			want: []string{"a", "x"},
  1162  		},
  1163  	}
  1164  	for _, tt := range tests {
  1165  		t.Run(tt.name, func(t *testing.T) {
  1166  			zo := tt.zones
  1167  			if got := zo.GetAvailabilityZones(); !cmp.Equal(got, tt.want) {
  1168  				t.Errorf("ZonesCAPI.GetAvailabilityZones() unexpected results:\n%v", cmp.Diff(got, tt.want))
  1169  			}
  1170  		})
  1171  	}
  1172  }
  1173  
  1174  func Test_ZonesCAPI_EdgeZones(t *testing.T) {
  1175  	tests := []struct {
  1176  		name  string
  1177  		zones *ZonesCAPI
  1178  		want  []string
  1179  	}{
  1180  		{
  1181  			name:  "empty",
  1182  			zones: &ZonesCAPI{},
  1183  			want:  []string{},
  1184  		},
  1185  		{
  1186  			name: "empty edge",
  1187  			zones: &ZonesCAPI{
  1188  				ControlPlaneZones: sets.New("x", "y"),
  1189  			},
  1190  			want: []string{},
  1191  		},
  1192  		{
  1193  			name: "empty only",
  1194  			zones: &ZonesCAPI{
  1195  				EdgeZones: sets.New("edge-x"),
  1196  			},
  1197  			want: []string{"edge-x"},
  1198  		},
  1199  		{
  1200  			name: "sorted",
  1201  			zones: &ZonesCAPI{
  1202  				ControlPlaneZones: sets.New("a", "b"),
  1203  				ComputeZones:      sets.New("b", "c"),
  1204  				EdgeZones:         sets.New("edge-x", "edge-y"),
  1205  			},
  1206  			want: []string{"edge-x", "edge-y"},
  1207  		},
  1208  		{
  1209  			name: "unsorted",
  1210  			zones: &ZonesCAPI{
  1211  				ControlPlaneZones: sets.New("x", "a"),
  1212  				ComputeZones:      sets.New("b", "a"),
  1213  				EdgeZones:         sets.New("edge-y", "edge-a"),
  1214  			},
  1215  			want: []string{"edge-a", "edge-y"},
  1216  		},
  1217  		{
  1218  			name: "control planes only",
  1219  			zones: &ZonesCAPI{
  1220  				ControlPlaneZones: sets.New("x", "a"),
  1221  				ComputeZones:      sets.Set[string]{},
  1222  				EdgeZones:         sets.New("edge-a", "edge-y"),
  1223  			},
  1224  			want: []string{"edge-a", "edge-y"},
  1225  		},
  1226  		{
  1227  			name: "compute only",
  1228  			zones: &ZonesCAPI{
  1229  				ControlPlaneZones: sets.Set[string]{},
  1230  				ComputeZones:      sets.New("x", "a"),
  1231  				EdgeZones:         sets.New("edge-a", "edge-y"),
  1232  			},
  1233  			want: []string{"edge-a", "edge-y"},
  1234  		},
  1235  	}
  1236  	for _, tt := range tests {
  1237  		t.Run(tt.name, func(t *testing.T) {
  1238  			zo := tt.zones
  1239  			if got := zo.GetEdgeZones(); !cmp.Equal(got, tt.want) {
  1240  				t.Errorf("ZonesCAPI.EdgeZones() unexpected results:\n %v", cmp.Diff(got, tt.want))
  1241  			}
  1242  		})
  1243  	}
  1244  }