sigs.k8s.io/cluster-api-provider-aws@v1.5.5/api/v1beta1/awscluster_webhook_test.go (about)

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8  	http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package v1beta1
    18  
    19  import (
    20  	"context"
    21  	"strings"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/aws/aws-sdk-go/aws"
    26  	. "github.com/onsi/gomega"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	utilfeature "k8s.io/component-base/featuregate/testing"
    29  	"sigs.k8s.io/controller-runtime/pkg/client"
    30  
    31  	"sigs.k8s.io/cluster-api-provider-aws/feature"
    32  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    33  	utildefaulting "sigs.k8s.io/cluster-api/util/defaulting"
    34  )
    35  
    36  func TestAWSClusterDefault(t *testing.T) {
    37  	cluster := &AWSCluster{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default"}}
    38  	t.Run("for AWSCluster", utildefaulting.DefaultValidateTest(cluster))
    39  	cluster.Default()
    40  	g := NewWithT(t)
    41  	g.Expect(cluster.Spec.IdentityRef).NotTo(BeNil())
    42  }
    43  
    44  func TestAWSCluster_ValidateCreate(t *testing.T) {
    45  	unsupportedIncorrectScheme := ClassicELBScheme("any-other-scheme")
    46  
    47  	tests := []struct {
    48  		name    string
    49  		cluster *AWSCluster
    50  		wantErr bool
    51  		expect  func(g *WithT, res *AWSLoadBalancerSpec)
    52  	}{
    53  		// The SSHKeyName tests were moved to sshkeyname_test.go
    54  		{
    55  			name: "Default nil scheme to `internet-facing`",
    56  			cluster: &AWSCluster{
    57  				Spec: AWSClusterSpec{},
    58  			},
    59  			expect: func(g *WithT, res *AWSLoadBalancerSpec) {
    60  				g.Expect(res.Scheme.String(), ClassicELBSchemeInternetFacing.String())
    61  			},
    62  			wantErr: false,
    63  		},
    64  		{
    65  			name: "Internet-facing ELB scheme is defaulted to internet-facing during creation",
    66  			cluster: &AWSCluster{
    67  				Spec: AWSClusterSpec{
    68  					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{Scheme: &ClassicELBSchemeIncorrectInternetFacing},
    69  				},
    70  			},
    71  			expect: func(g *WithT, res *AWSLoadBalancerSpec) {
    72  				g.Expect(res.Scheme.String(), ClassicELBSchemeInternetFacing.String())
    73  			},
    74  			wantErr: false,
    75  		},
    76  		{
    77  			name: "Supported schemes are 'internet-facing, Internet-facing, internal, or nil', rest will be rejected",
    78  			cluster: &AWSCluster{
    79  				Spec: AWSClusterSpec{
    80  					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{Scheme: &unsupportedIncorrectScheme},
    81  				},
    82  			},
    83  			wantErr: true,
    84  		},
    85  		{
    86  			name: "Invalid tags are rejected",
    87  			cluster: &AWSCluster{
    88  				Spec: AWSClusterSpec{
    89  					AdditionalTags: Tags{
    90  						"key-1":                    "value-1",
    91  						"":                         "value-2",
    92  						strings.Repeat("CAPI", 33): "value-3",
    93  						"key-4":                    strings.Repeat("CAPI", 65),
    94  					},
    95  				},
    96  			},
    97  			wantErr: true,
    98  		},
    99  		{
   100  			name: "accepts bucket name with acceptable characters",
   101  			cluster: &AWSCluster{
   102  				Spec: AWSClusterSpec{
   103  					S3Bucket: &S3Bucket{
   104  						Name:                           "abcdefghijklmnoprstuwxyz-0123456789",
   105  						ControlPlaneIAMInstanceProfile: "control-plane.cluster-api-provider-aws.sigs.k8s.io",
   106  						NodesIAMInstanceProfiles:       []string{"nodes.cluster-api-provider-aws.sigs.k8s.io"},
   107  					},
   108  				},
   109  			},
   110  		},
   111  		{
   112  			name: "rejects empty bucket name",
   113  			cluster: &AWSCluster{
   114  				Spec: AWSClusterSpec{
   115  					S3Bucket: &S3Bucket{},
   116  				},
   117  			},
   118  			wantErr: true,
   119  		},
   120  		{
   121  			name: "rejects bucket name shorter than 3 characters",
   122  			cluster: &AWSCluster{
   123  				Spec: AWSClusterSpec{
   124  					S3Bucket: &S3Bucket{
   125  						Name: "fo",
   126  					},
   127  				},
   128  			},
   129  			wantErr: true,
   130  		},
   131  		{
   132  			name: "rejects bucket name longer than 63 characters",
   133  			cluster: &AWSCluster{
   134  				Spec: AWSClusterSpec{
   135  					S3Bucket: &S3Bucket{
   136  						Name: strings.Repeat("a", 64),
   137  					},
   138  				},
   139  			},
   140  			wantErr: true,
   141  		},
   142  		{
   143  			name: "rejects bucket name starting with not letter or number",
   144  			cluster: &AWSCluster{
   145  				Spec: AWSClusterSpec{
   146  					S3Bucket: &S3Bucket{
   147  						Name: "-foo",
   148  					},
   149  				},
   150  			},
   151  			wantErr: true,
   152  		},
   153  		{
   154  			name: "rejects bucket name ending with not letter or number",
   155  			cluster: &AWSCluster{
   156  				Spec: AWSClusterSpec{
   157  					S3Bucket: &S3Bucket{
   158  						Name: "foo-",
   159  					},
   160  				},
   161  			},
   162  			wantErr: true,
   163  		},
   164  		{
   165  			name: "rejects bucket name formatted as IP address",
   166  			cluster: &AWSCluster{
   167  				Spec: AWSClusterSpec{
   168  					S3Bucket: &S3Bucket{
   169  						Name: "8.8.8.8",
   170  					},
   171  				},
   172  			},
   173  			wantErr: true,
   174  		},
   175  		{
   176  			name: "requires bucket control plane IAM instance profile to be not empty",
   177  			cluster: &AWSCluster{
   178  				Spec: AWSClusterSpec{
   179  					S3Bucket: &S3Bucket{
   180  						Name:                           "foo",
   181  						ControlPlaneIAMInstanceProfile: "",
   182  					},
   183  				},
   184  			},
   185  			wantErr: true,
   186  		},
   187  		{
   188  			name: "requires at least one bucket node IAM instance profile",
   189  			cluster: &AWSCluster{
   190  				Spec: AWSClusterSpec{
   191  					S3Bucket: &S3Bucket{
   192  						Name:                           "foo",
   193  						ControlPlaneIAMInstanceProfile: "foo",
   194  					},
   195  				},
   196  			},
   197  			wantErr: true,
   198  		},
   199  		{
   200  			name: "requires all bucket node IAM instance profiles to be not empty",
   201  			cluster: &AWSCluster{
   202  				Spec: AWSClusterSpec{
   203  					S3Bucket: &S3Bucket{
   204  						Name:                           "foo",
   205  						ControlPlaneIAMInstanceProfile: "foo",
   206  						NodesIAMInstanceProfiles:       []string{""},
   207  					},
   208  				},
   209  			},
   210  			wantErr: true,
   211  		},
   212  		{
   213  			name: "does not return error when all IAM instance profiles are populated",
   214  			cluster: &AWSCluster{
   215  				Spec: AWSClusterSpec{
   216  					S3Bucket: &S3Bucket{
   217  						Name:                           "foo",
   218  						ControlPlaneIAMInstanceProfile: "foo",
   219  						NodesIAMInstanceProfiles:       []string{"bar"},
   220  					},
   221  				},
   222  			},
   223  			wantErr: false,
   224  		},
   225  	}
   226  	for _, tt := range tests {
   227  		t.Run(tt.name, func(t *testing.T) {
   228  			defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.BootstrapFormatIgnition, true)()
   229  
   230  			cluster := tt.cluster.DeepCopy()
   231  			cluster.ObjectMeta = metav1.ObjectMeta{
   232  				GenerateName: "cluster-",
   233  				Namespace:    "default",
   234  			}
   235  			ctx := context.TODO()
   236  			if err := testEnv.Create(ctx, cluster); (err != nil) != tt.wantErr {
   237  				t.Errorf("ValidateCreate() error = %v, wantErr %v", err, tt.wantErr)
   238  			}
   239  
   240  			if tt.wantErr {
   241  				return
   242  			}
   243  
   244  			c := &AWSCluster{}
   245  			key := client.ObjectKey{
   246  				Name:      cluster.Name,
   247  				Namespace: "default",
   248  			}
   249  
   250  			g := NewWithT(t)
   251  			g.Eventually(func() bool {
   252  				err := testEnv.Get(ctx, key, c)
   253  				return err == nil
   254  			}, 10*time.Second).Should(Equal(true))
   255  
   256  			if tt.expect != nil {
   257  				tt.expect(g, c.Spec.ControlPlaneLoadBalancer)
   258  			}
   259  		})
   260  	}
   261  }
   262  
   263  func TestAWSCluster_ValidateUpdate(t *testing.T) {
   264  	tests := []struct {
   265  		name       string
   266  		oldCluster *AWSCluster
   267  		newCluster *AWSCluster
   268  		wantErr    bool
   269  	}{
   270  		{
   271  			name: "region is immutable",
   272  			oldCluster: &AWSCluster{
   273  				Spec: AWSClusterSpec{
   274  					Region: "us-east-1",
   275  				},
   276  			},
   277  			newCluster: &AWSCluster{
   278  				Spec: AWSClusterSpec{
   279  					Region: "us-east-2",
   280  				},
   281  			},
   282  			wantErr: true,
   283  		},
   284  		{
   285  			name: "controlPlaneLoadBalancer name is immutable",
   286  			oldCluster: &AWSCluster{
   287  				Spec: AWSClusterSpec{
   288  					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
   289  						Name: aws.String("old-apiserver"),
   290  					},
   291  				},
   292  			},
   293  			newCluster: &AWSCluster{
   294  				Spec: AWSClusterSpec{
   295  					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
   296  						Name: aws.String("new-apiserver"),
   297  					},
   298  				},
   299  			},
   300  			wantErr: true,
   301  		},
   302  		{
   303  			name: "controlPlaneLoadBalancer name is immutable, even if it is nil",
   304  			oldCluster: &AWSCluster{
   305  				Spec: AWSClusterSpec{
   306  					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
   307  						Name: nil,
   308  					},
   309  				},
   310  			},
   311  			newCluster: &AWSCluster{
   312  				Spec: AWSClusterSpec{
   313  					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
   314  						Name: aws.String("example-apiserver"),
   315  					},
   316  				},
   317  			},
   318  			wantErr: true,
   319  		},
   320  		{
   321  			name: "controlPlaneLoadBalancer scheme is immutable",
   322  			oldCluster: &AWSCluster{
   323  				Spec: AWSClusterSpec{
   324  					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
   325  						Scheme: &ClassicELBSchemeInternal,
   326  					},
   327  				},
   328  			},
   329  			newCluster: &AWSCluster{
   330  				Spec: AWSClusterSpec{
   331  					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
   332  						Scheme: &ClassicELBSchemeInternetFacing,
   333  					},
   334  				},
   335  			},
   336  			wantErr: true,
   337  		},
   338  		{
   339  			name: "controlPlaneLoadBalancer scheme is immutable when left empty",
   340  			oldCluster: &AWSCluster{
   341  				Spec: AWSClusterSpec{},
   342  			},
   343  			newCluster: &AWSCluster{
   344  				Spec: AWSClusterSpec{
   345  					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
   346  						Scheme: &ClassicELBSchemeInternal,
   347  					},
   348  				},
   349  			},
   350  			wantErr: true,
   351  		},
   352  		{
   353  			name: "controlPlaneLoadBalancer scheme can be set to default when left empty",
   354  			oldCluster: &AWSCluster{
   355  				Spec: AWSClusterSpec{},
   356  			},
   357  			newCluster: &AWSCluster{
   358  				Spec: AWSClusterSpec{
   359  					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
   360  						Scheme: &ClassicELBSchemeInternetFacing,
   361  					},
   362  				},
   363  			},
   364  			wantErr: false,
   365  		},
   366  		{
   367  			name: "controlPlaneLoadBalancer crossZoneLoadBalancer is mutable",
   368  			oldCluster: &AWSCluster{
   369  				Spec: AWSClusterSpec{
   370  					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
   371  						CrossZoneLoadBalancing: false,
   372  					},
   373  				},
   374  			},
   375  			newCluster: &AWSCluster{
   376  				Spec: AWSClusterSpec{
   377  					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
   378  						CrossZoneLoadBalancing: true,
   379  					},
   380  				},
   381  			},
   382  			wantErr: false,
   383  		},
   384  		{
   385  			name: "controlPlaneEndpoint is immutable",
   386  			oldCluster: &AWSCluster{
   387  				Spec: AWSClusterSpec{
   388  					ControlPlaneEndpoint: clusterv1.APIEndpoint{
   389  						Host: "example.com",
   390  						Port: int32(8000),
   391  					},
   392  				},
   393  			},
   394  			newCluster: &AWSCluster{
   395  				Spec: AWSClusterSpec{
   396  					ControlPlaneEndpoint: clusterv1.APIEndpoint{
   397  						Host: "foo.example.com",
   398  						Port: int32(9000),
   399  					},
   400  				},
   401  			},
   402  			wantErr: true,
   403  		},
   404  		{
   405  			name: "controlPlaneEndpoint can be updated if it is empty",
   406  			oldCluster: &AWSCluster{
   407  				Spec: AWSClusterSpec{
   408  					ControlPlaneEndpoint: clusterv1.APIEndpoint{},
   409  				},
   410  			},
   411  			newCluster: &AWSCluster{
   412  				Spec: AWSClusterSpec{
   413  					ControlPlaneEndpoint: clusterv1.APIEndpoint{
   414  						Host: "example.com",
   415  						Port: int32(8000),
   416  					},
   417  				},
   418  			},
   419  			wantErr: false,
   420  		},
   421  		{
   422  			name: "removal of externally managed annotation is not allowed",
   423  			oldCluster: &AWSCluster{
   424  				ObjectMeta: metav1.ObjectMeta{
   425  					Annotations: map[string]string{clusterv1.ManagedByAnnotation: ""},
   426  				},
   427  			},
   428  			newCluster: &AWSCluster{},
   429  			wantErr:    true,
   430  		},
   431  		{
   432  			name:       "adding externally managed annotation is allowed",
   433  			oldCluster: &AWSCluster{},
   434  			newCluster: &AWSCluster{
   435  				ObjectMeta: metav1.ObjectMeta{
   436  					Annotations: map[string]string{clusterv1.ManagedByAnnotation: ""},
   437  				},
   438  			},
   439  			wantErr: false,
   440  		},
   441  		{
   442  			name: "VPC id is immutable cannot be emptied once set",
   443  			oldCluster: &AWSCluster{
   444  				Spec: AWSClusterSpec{
   445  					NetworkSpec: NetworkSpec{
   446  						VPC: VPCSpec{ID: "managed-or-unmanaged-vpc"},
   447  					},
   448  				},
   449  			},
   450  			newCluster: &AWSCluster{
   451  				Spec: AWSClusterSpec{},
   452  			},
   453  			wantErr: true,
   454  		},
   455  		{
   456  			name: "VPC id is immutable, cannot be set to a different value once set",
   457  			oldCluster: &AWSCluster{
   458  				Spec: AWSClusterSpec{
   459  					NetworkSpec: NetworkSpec{
   460  						VPC: VPCSpec{ID: "managed-or-unmanaged-vpc"},
   461  					},
   462  				},
   463  			},
   464  			newCluster: &AWSCluster{
   465  				Spec: AWSClusterSpec{
   466  					NetworkSpec: NetworkSpec{
   467  						VPC: VPCSpec{ID: "a-new-vpc"},
   468  					},
   469  				},
   470  			},
   471  			wantErr: true,
   472  		},
   473  		{
   474  			name: "invalid keys are not accepted during update",
   475  			oldCluster: &AWSCluster{
   476  				Spec: AWSClusterSpec{
   477  					AdditionalTags: Tags{
   478  						"key-1": "value-1",
   479  						"key-2": "value-2",
   480  					},
   481  				},
   482  			},
   483  			newCluster: &AWSCluster{
   484  				Spec: AWSClusterSpec{
   485  					AdditionalTags: Tags{
   486  						"key-1":                    "value-1",
   487  						"":                         "value-2",
   488  						strings.Repeat("CAPI", 33): "value-3",
   489  						"key-4":                    strings.Repeat("CAPI", 65),
   490  					},
   491  				},
   492  			},
   493  			wantErr: true,
   494  		},
   495  		{
   496  			name: "Should fail if controlPlaneLoadBalancer healthcheckprotocol is updated",
   497  			oldCluster: &AWSCluster{
   498  				Spec: AWSClusterSpec{
   499  					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
   500  						HealthCheckProtocol: &ClassicELBProtocolTCP,
   501  					},
   502  				},
   503  			},
   504  			newCluster: &AWSCluster{
   505  				Spec: AWSClusterSpec{
   506  					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
   507  						HealthCheckProtocol: &ClassicELBProtocolSSL,
   508  					},
   509  				},
   510  			},
   511  			wantErr: true,
   512  		},
   513  		{
   514  			name: "Should pass if controlPlaneLoadBalancer healthcheckprotocol is same after update",
   515  			oldCluster: &AWSCluster{
   516  				Spec: AWSClusterSpec{
   517  					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
   518  						HealthCheckProtocol: &ClassicELBProtocolTCP,
   519  					},
   520  				},
   521  			},
   522  			newCluster: &AWSCluster{
   523  				Spec: AWSClusterSpec{
   524  					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
   525  						HealthCheckProtocol: &ClassicELBProtocolTCP,
   526  					},
   527  				},
   528  			},
   529  			wantErr: false,
   530  		},
   531  		{
   532  			name: "Should fail if controlPlaneLoadBalancer healthcheckprotocol is changed to non-default if it was not set before update",
   533  			oldCluster: &AWSCluster{
   534  				Spec: AWSClusterSpec{},
   535  			},
   536  			newCluster: &AWSCluster{
   537  				Spec: AWSClusterSpec{
   538  					ControlPlaneLoadBalancer: &AWSLoadBalancerSpec{
   539  						HealthCheckProtocol: &ClassicELBProtocolTCP,
   540  					},
   541  				},
   542  			},
   543  			wantErr: true,
   544  		},
   545  	}
   546  	for _, tt := range tests {
   547  		t.Run(tt.name, func(t *testing.T) {
   548  			ctx := context.TODO()
   549  			cluster := tt.oldCluster.DeepCopy()
   550  			cluster.ObjectMeta.GenerateName = "cluster-"
   551  			cluster.ObjectMeta.Namespace = "default"
   552  
   553  			if err := testEnv.Create(ctx, cluster); err != nil {
   554  				t.Errorf("failed to create cluster: %v", err)
   555  			}
   556  			cluster.ObjectMeta.Annotations = tt.newCluster.Annotations
   557  			cluster.Spec = tt.newCluster.Spec
   558  			if err := testEnv.Update(ctx, cluster); (err != nil) != tt.wantErr {
   559  				t.Errorf("ValidateUpdate() error = %v, wantErr %v", err, tt.wantErr)
   560  			}
   561  		},
   562  		)
   563  	}
   564  }
   565  
   566  func TestAWSCluster_DefaultCNIIngressRules(t *testing.T) {
   567  	AZUsageLimit := 3
   568  	defaultVPCSpec := VPCSpec{
   569  		AvailabilityZoneUsageLimit: &AZUsageLimit,
   570  		AvailabilityZoneSelection:  &AZSelectionSchemeOrdered,
   571  	}
   572  	g := NewWithT(t)
   573  	tests := []struct {
   574  		name          string
   575  		beforeCluster *AWSCluster
   576  		afterCluster  *AWSCluster
   577  	}{
   578  		{
   579  			name: "CNI ingressRules are updated cni spec undefined",
   580  			beforeCluster: &AWSCluster{
   581  				Spec: AWSClusterSpec{},
   582  			},
   583  			afterCluster: &AWSCluster{
   584  				Spec: AWSClusterSpec{
   585  					NetworkSpec: NetworkSpec{
   586  						VPC: defaultVPCSpec,
   587  						CNI: &CNISpec{
   588  							CNIIngressRules: CNIIngressRules{
   589  								{
   590  									Description: "bgp (calico)",
   591  									Protocol:    SecurityGroupProtocolTCP,
   592  									FromPort:    179,
   593  									ToPort:      179,
   594  								},
   595  								{
   596  									Description: "IP-in-IP (calico)",
   597  									Protocol:    SecurityGroupProtocolIPinIP,
   598  									FromPort:    -1,
   599  									ToPort:      65535,
   600  								},
   601  							},
   602  						},
   603  					},
   604  				},
   605  			},
   606  		},
   607  		{
   608  			name: "CNIIngressRules are not added for empty CNISpec",
   609  			beforeCluster: &AWSCluster{
   610  				Spec: AWSClusterSpec{
   611  					NetworkSpec: NetworkSpec{
   612  						VPC: defaultVPCSpec,
   613  						CNI: &CNISpec{},
   614  					},
   615  				},
   616  			},
   617  			afterCluster: &AWSCluster{
   618  				Spec: AWSClusterSpec{
   619  					NetworkSpec: NetworkSpec{
   620  						VPC: defaultVPCSpec,
   621  						CNI: &CNISpec{},
   622  					},
   623  				},
   624  			},
   625  		},
   626  		{
   627  			name: "CNI ingressRules are unmodified when they exist",
   628  			beforeCluster: &AWSCluster{
   629  				Spec: AWSClusterSpec{
   630  					NetworkSpec: NetworkSpec{
   631  						VPC: defaultVPCSpec,
   632  						CNI: &CNISpec{
   633  							CNIIngressRules: CNIIngressRules{
   634  								{
   635  									Description: "Antrea 1",
   636  									Protocol:    SecurityGroupProtocolTCP,
   637  									FromPort:    10349,
   638  									ToPort:      10349,
   639  								},
   640  							},
   641  						},
   642  					},
   643  				},
   644  			},
   645  			afterCluster: &AWSCluster{
   646  				Spec: AWSClusterSpec{
   647  					NetworkSpec: NetworkSpec{
   648  						VPC: defaultVPCSpec,
   649  						CNI: &CNISpec{
   650  							CNIIngressRules: CNIIngressRules{
   651  								{
   652  									Description: "Antrea 1",
   653  									Protocol:    SecurityGroupProtocolTCP,
   654  									FromPort:    10349,
   655  									ToPort:      10349,
   656  								},
   657  							},
   658  						},
   659  					},
   660  				},
   661  			},
   662  		},
   663  	}
   664  
   665  	for _, tt := range tests {
   666  		t.Run(tt.name, func(t *testing.T) {
   667  			ctx := context.TODO()
   668  			cluster := tt.beforeCluster.DeepCopy()
   669  			cluster.ObjectMeta = metav1.ObjectMeta{
   670  				GenerateName: "cluster-",
   671  				Namespace:    "default",
   672  			}
   673  			g.Expect(testEnv.Create(ctx, cluster)).To(Succeed())
   674  			g.Expect(cluster.Spec.NetworkSpec).To(Equal(tt.afterCluster.Spec.NetworkSpec))
   675  		})
   676  	}
   677  }
   678  
   679  func TestAWSCluster_ValidateAllowedCIDRBlocks(t *testing.T) {
   680  	tests := []struct {
   681  		name    string
   682  		awsc    *AWSCluster
   683  		wantErr bool
   684  	}{
   685  		{
   686  			name: "allow valid CIDRs",
   687  			awsc: &AWSCluster{
   688  				Spec: AWSClusterSpec{
   689  					Bastion: Bastion{
   690  						AllowedCIDRBlocks: []string{
   691  							"192.168.0.0/16",
   692  							"192.168.0.1/32",
   693  						},
   694  					},
   695  				},
   696  			},
   697  			wantErr: false,
   698  		},
   699  		{
   700  			name: "disableIngressRules allowed with empty CIDR block",
   701  			awsc: &AWSCluster{
   702  				Spec: AWSClusterSpec{
   703  					Bastion: Bastion{
   704  						AllowedCIDRBlocks:   []string{},
   705  						DisableIngressRules: true,
   706  					},
   707  				},
   708  			},
   709  			wantErr: false,
   710  		},
   711  		{
   712  			name: "disableIngressRules not allowed with CIDR blocks",
   713  			awsc: &AWSCluster{
   714  				Spec: AWSClusterSpec{
   715  					Bastion: Bastion{
   716  						AllowedCIDRBlocks: []string{
   717  							"192.168.0.0/16",
   718  							"192.168.0.1/32",
   719  						},
   720  						DisableIngressRules: true,
   721  					},
   722  				},
   723  			},
   724  			wantErr: true,
   725  		},
   726  		{
   727  			name: "invalid CIDR block with invalid network",
   728  			awsc: &AWSCluster{
   729  				Spec: AWSClusterSpec{
   730  					Bastion: Bastion{
   731  						AllowedCIDRBlocks: []string{
   732  							"100.200.300.400/99",
   733  						},
   734  					},
   735  				},
   736  			},
   737  			wantErr: true,
   738  		},
   739  		{
   740  			name: "invalid CIDR block with garbage string",
   741  			awsc: &AWSCluster{
   742  				Spec: AWSClusterSpec{
   743  					Bastion: Bastion{
   744  						AllowedCIDRBlocks: []string{
   745  							"abcdefg",
   746  						},
   747  					},
   748  				},
   749  			},
   750  			wantErr: true,
   751  		},
   752  	}
   753  	for _, tt := range tests {
   754  		t.Run(tt.name, func(t *testing.T) {
   755  			ctx := context.TODO()
   756  			cluster := tt.awsc.DeepCopy()
   757  			cluster.ObjectMeta = metav1.ObjectMeta{
   758  				GenerateName: "cluster-",
   759  				Namespace:    "default",
   760  			}
   761  			if err := testEnv.Create(ctx, cluster); (err != nil) != tt.wantErr {
   762  				t.Errorf("ValidateAllowedCIDRBlocks() error = %v, wantErr %v", err, tt.wantErr)
   763  			}
   764  		})
   765  	}
   766  }
   767  
   768  func TestAWSCluster_DefaultAllowedCIDRBlocks(t *testing.T) {
   769  	g := NewWithT(t)
   770  	tests := []struct {
   771  		name          string
   772  		beforeCluster *AWSCluster
   773  		afterCluster  *AWSCluster
   774  	}{
   775  		{
   776  			name: "empty AllowedCIDRBlocks is defaulted to allow open ingress to bastion host",
   777  			beforeCluster: &AWSCluster{
   778  				Spec: AWSClusterSpec{},
   779  			},
   780  			afterCluster: &AWSCluster{
   781  				Spec: AWSClusterSpec{
   782  					Bastion: Bastion{
   783  						AllowedCIDRBlocks: []string{
   784  							"0.0.0.0/0",
   785  						},
   786  					},
   787  				},
   788  			},
   789  		},
   790  		{
   791  			name: "AllowedCIDRBlocks change not allowed if DisableIngressRules is true",
   792  			beforeCluster: &AWSCluster{
   793  				Spec: AWSClusterSpec{
   794  					Bastion: Bastion{
   795  						AllowedCIDRBlocks:   []string{"0.0.0.0/0"},
   796  						DisableIngressRules: true,
   797  						Enabled:             true,
   798  					},
   799  				},
   800  			},
   801  			afterCluster: &AWSCluster{
   802  				Spec: AWSClusterSpec{
   803  					Bastion: Bastion{
   804  						DisableIngressRules: true,
   805  						Enabled:             true,
   806  					},
   807  				},
   808  			},
   809  		},
   810  	}
   811  
   812  	for _, tt := range tests {
   813  		t.Run(tt.name, func(t *testing.T) {
   814  			ctx := context.TODO()
   815  			cluster := tt.beforeCluster.DeepCopy()
   816  			cluster.ObjectMeta = metav1.ObjectMeta{
   817  				GenerateName: "cluster-",
   818  				Namespace:    "default",
   819  			}
   820  			err := testEnv.Create(ctx, cluster)
   821  			if err != nil {
   822  				g.Expect(err).To(HaveOccurred())
   823  			} else {
   824  				g.Expect(cluster.Spec.Bastion).To(Equal(tt.afterCluster.Spec.Bastion))
   825  			}
   826  		})
   827  	}
   828  }