sigs.k8s.io/cluster-api-provider-aws@v1.5.5/pkg/cloud/services/network/vpc_test.go (about)

     1  /*
     2  Copyright 2018 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 network
    18  
    19  import (
    20  	"context"
    21  	"testing"
    22  
    23  	"github.com/aws/aws-sdk-go/aws"
    24  	"github.com/aws/aws-sdk-go/aws/awserr"
    25  	"github.com/aws/aws-sdk-go/service/ec2"
    26  	"github.com/golang/mock/gomock"
    27  	. "github.com/onsi/gomega"
    28  	"github.com/pkg/errors"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/runtime"
    31  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    32  
    33  	infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1beta1"
    34  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/awserrors"
    35  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/scope"
    36  	"sigs.k8s.io/cluster-api-provider-aws/test/mocks"
    37  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    38  )
    39  
    40  func describeVpcAttributeTrue(input *ec2.DescribeVpcAttributeInput) (*ec2.DescribeVpcAttributeOutput, error) {
    41  	result := &ec2.DescribeVpcAttributeOutput{
    42  		VpcId: input.VpcId,
    43  	}
    44  	switch aws.StringValue(input.Attribute) {
    45  	case "enableDnsHostnames":
    46  		result.EnableDnsHostnames = &ec2.AttributeBooleanValue{Value: aws.Bool(true)}
    47  	case "enableDnsSupport":
    48  		result.EnableDnsSupport = &ec2.AttributeBooleanValue{Value: aws.Bool(true)}
    49  	}
    50  	return result, nil
    51  }
    52  
    53  func describeVpcAttributeFalse(input *ec2.DescribeVpcAttributeInput) (*ec2.DescribeVpcAttributeOutput, error) {
    54  	result := &ec2.DescribeVpcAttributeOutput{
    55  		VpcId: input.VpcId,
    56  	}
    57  	switch aws.StringValue(input.Attribute) {
    58  	case "enableDnsHostnames":
    59  		result.EnableDnsHostnames = &ec2.AttributeBooleanValue{Value: aws.Bool(false)}
    60  	case "enableDnsSupport":
    61  		result.EnableDnsSupport = &ec2.AttributeBooleanValue{Value: aws.Bool(false)}
    62  	}
    63  	return result, nil
    64  }
    65  
    66  func TestReconcileVPC(t *testing.T) {
    67  	mockCtrl := gomock.NewController(t)
    68  	defer mockCtrl.Finish()
    69  
    70  	usageLimit := 3
    71  	selection := infrav1.AZSelectionSchemeOrdered
    72  	tags := []*ec2.Tag{
    73  		{
    74  			Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/role"),
    75  			Value: aws.String("common"),
    76  		},
    77  		{
    78  			Key:   aws.String("Name"),
    79  			Value: aws.String("test-cluster-vpc"),
    80  		},
    81  		{
    82  			Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster"),
    83  			Value: aws.String("owned"),
    84  		},
    85  	}
    86  
    87  	testCases := []struct {
    88  		name    string
    89  		input   *infrav1.VPCSpec
    90  		want    *infrav1.VPCSpec
    91  		expect  func(m *mocks.MockEC2APIMockRecorder)
    92  		wantErr bool
    93  	}{
    94  		{
    95  			name:  "Should update tags with aws VPC resource tags, if managed vpc exists",
    96  			input: &infrav1.VPCSpec{ID: "vpc-exists", AvailabilityZoneUsageLimit: &usageLimit, AvailabilityZoneSelection: &selection},
    97  			want: &infrav1.VPCSpec{
    98  				ID:        "vpc-exists",
    99  				CidrBlock: "10.0.0.0/8",
   100  				Tags: map[string]string{
   101  					"sigs.k8s.io/cluster-api-provider-aws/role": "common",
   102  					"Name": "test-cluster-vpc",
   103  					"sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster": "owned",
   104  				},
   105  				AvailabilityZoneUsageLimit: &usageLimit,
   106  				AvailabilityZoneSelection:  &selection,
   107  			},
   108  			wantErr: false,
   109  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   110  				m.DescribeVpcs(gomock.Eq(&ec2.DescribeVpcsInput{
   111  					VpcIds: []*string{
   112  						aws.String("vpc-exists"),
   113  					},
   114  					Filters: []*ec2.Filter{
   115  						{
   116  							Name:   aws.String("state"),
   117  							Values: aws.StringSlice([]string{ec2.VpcStatePending, ec2.VpcStateAvailable}),
   118  						},
   119  					},
   120  				})).Return(&ec2.DescribeVpcsOutput{
   121  					Vpcs: []*ec2.Vpc{
   122  						{
   123  							State:     aws.String("available"),
   124  							VpcId:     aws.String("vpc-exists"),
   125  							CidrBlock: aws.String("10.0.0.0/8"),
   126  							Tags:      tags,
   127  						},
   128  					},
   129  				}, nil)
   130  
   131  				m.DescribeVpcAttribute(gomock.AssignableToTypeOf(&ec2.DescribeVpcAttributeInput{})).
   132  					DoAndReturn(describeVpcAttributeTrue).AnyTimes()
   133  			},
   134  		},
   135  		{
   136  			name:    "Should create a new VPC if managed vpc does not exist",
   137  			input:   &infrav1.VPCSpec{AvailabilityZoneUsageLimit: &usageLimit, AvailabilityZoneSelection: &selection},
   138  			wantErr: false,
   139  			want: &infrav1.VPCSpec{
   140  				ID:        "vpc-new",
   141  				CidrBlock: "10.1.0.0/16",
   142  				Tags: map[string]string{
   143  					"sigs.k8s.io/cluster-api-provider-aws/role": "common",
   144  					"Name": "test-cluster-vpc",
   145  					"sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster": "owned",
   146  				},
   147  				AvailabilityZoneUsageLimit: &usageLimit,
   148  				AvailabilityZoneSelection:  &selection,
   149  			},
   150  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   151  				m.CreateVpc(gomock.AssignableToTypeOf(&ec2.CreateVpcInput{})).Return(&ec2.CreateVpcOutput{
   152  					Vpc: &ec2.Vpc{
   153  						State:     aws.String("available"),
   154  						VpcId:     aws.String("vpc-new"),
   155  						CidrBlock: aws.String("10.1.0.0/16"),
   156  						Tags:      tags,
   157  					},
   158  				}, nil)
   159  
   160  				m.DescribeVpcAttribute(gomock.AssignableToTypeOf(&ec2.DescribeVpcAttributeInput{})).
   161  					DoAndReturn(describeVpcAttributeFalse).MinTimes(1)
   162  
   163  				m.ModifyVpcAttribute(gomock.AssignableToTypeOf(&ec2.ModifyVpcAttributeInput{})).Return(&ec2.ModifyVpcAttributeOutput{}, nil).Times(2)
   164  			},
   165  		},
   166  		{
   167  			name:    "managed vpc id exists, but vpc resource is missing",
   168  			input:   &infrav1.VPCSpec{ID: "vpc-exists", AvailabilityZoneUsageLimit: &usageLimit, AvailabilityZoneSelection: &selection},
   169  			wantErr: true,
   170  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   171  				m.DescribeVpcs(gomock.Eq(&ec2.DescribeVpcsInput{
   172  					VpcIds: []*string{
   173  						aws.String("vpc-exists"),
   174  					},
   175  					Filters: []*ec2.Filter{
   176  						{
   177  							Name:   aws.String("state"),
   178  							Values: aws.StringSlice([]string{ec2.VpcStatePending, ec2.VpcStateAvailable}),
   179  						},
   180  					},
   181  				})).Return(nil, awserr.New("404", "http not found err", errors.New("err")))
   182  			},
   183  		},
   184  		{
   185  			name:  "Should patch vpc spec successfully, if unmanaged vpc exists",
   186  			input: &infrav1.VPCSpec{ID: "unmanaged-vpc-exists", AvailabilityZoneUsageLimit: &usageLimit, AvailabilityZoneSelection: &selection},
   187  			want: &infrav1.VPCSpec{
   188  				ID:        "unmanaged-vpc-exists",
   189  				CidrBlock: "10.0.0.0/8",
   190  				Tags: map[string]string{
   191  					"sigs.k8s.io/cluster-api-provider-aws/role": "common",
   192  					"Name": "test-cluster-vpc",
   193  				},
   194  				AvailabilityZoneUsageLimit: &usageLimit,
   195  				AvailabilityZoneSelection:  &selection,
   196  			},
   197  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   198  				m.DescribeVpcs(gomock.AssignableToTypeOf(&ec2.DescribeVpcsInput{})).Return(&ec2.DescribeVpcsOutput{
   199  					Vpcs: []*ec2.Vpc{
   200  						{
   201  							State:     aws.String("available"),
   202  							VpcId:     aws.String("unmanaged-vpc-exists"),
   203  							CidrBlock: aws.String("10.0.0.0/8"),
   204  							Tags: []*ec2.Tag{
   205  								{
   206  									Key:   aws.String("sigs.k8s.io/cluster-api-provider-aws/role"),
   207  									Value: aws.String("common"),
   208  								},
   209  								{
   210  									Key:   aws.String("Name"),
   211  									Value: aws.String("test-cluster-vpc"),
   212  								},
   213  							},
   214  						},
   215  					},
   216  				}, nil)
   217  			},
   218  		},
   219  		{
   220  			name:  "Should retry if vpc not found error occurs during attributes configuration for managed vpc",
   221  			input: &infrav1.VPCSpec{ID: "managed-vpc-exists", AvailabilityZoneUsageLimit: &usageLimit, AvailabilityZoneSelection: &selection},
   222  			want: &infrav1.VPCSpec{
   223  				ID:        "managed-vpc-exists",
   224  				CidrBlock: "10.0.0.0/8",
   225  				Tags: map[string]string{
   226  					"sigs.k8s.io/cluster-api-provider-aws/role": "common",
   227  					"Name": "test-cluster-vpc",
   228  					"sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster": "owned",
   229  				},
   230  				AvailabilityZoneUsageLimit: &usageLimit,
   231  				AvailabilityZoneSelection:  &selection,
   232  			},
   233  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   234  				m.DescribeVpcs(gomock.AssignableToTypeOf(&ec2.DescribeVpcsInput{})).Return(&ec2.DescribeVpcsOutput{
   235  					Vpcs: []*ec2.Vpc{
   236  						{
   237  							State:     aws.String("available"),
   238  							VpcId:     aws.String("unmanaged-vpc-exists"),
   239  							CidrBlock: aws.String("10.0.0.0/8"),
   240  							Tags:      tags,
   241  						},
   242  					},
   243  				}, nil)
   244  				m.DescribeVpcAttribute(gomock.AssignableToTypeOf(&ec2.DescribeVpcAttributeInput{})).Return(nil, awserr.New("InvalidVpcID.NotFound", "not found", nil))
   245  				m.DescribeVpcAttribute(gomock.AssignableToTypeOf(&ec2.DescribeVpcAttributeInput{})).
   246  					DoAndReturn(describeVpcAttributeTrue).AnyTimes()
   247  			},
   248  		},
   249  		{
   250  			name:    "Should return error if failed to set vpc attributes for managed vpc",
   251  			input:   &infrav1.VPCSpec{ID: "managed-vpc-exists", AvailabilityZoneUsageLimit: &usageLimit, AvailabilityZoneSelection: &selection},
   252  			wantErr: true,
   253  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   254  				m.DescribeVpcs(gomock.Eq(&ec2.DescribeVpcsInput{
   255  					VpcIds: []*string{
   256  						aws.String("managed-vpc-exists"),
   257  					},
   258  					Filters: []*ec2.Filter{
   259  						{
   260  							Name:   aws.String("state"),
   261  							Values: aws.StringSlice([]string{ec2.VpcStatePending, ec2.VpcStateAvailable}),
   262  						},
   263  					},
   264  				})).Return(&ec2.DescribeVpcsOutput{
   265  					Vpcs: []*ec2.Vpc{
   266  						{
   267  							State:     aws.String("available"),
   268  							VpcId:     aws.String("unmanaged-vpc-exists"),
   269  							CidrBlock: aws.String("10.0.0.0/8"),
   270  							Tags:      tags,
   271  						},
   272  					},
   273  				}, nil)
   274  				m.DescribeVpcAttribute(gomock.AssignableToTypeOf(&ec2.DescribeVpcAttributeInput{})).AnyTimes().Return(nil, awserrors.NewFailedDependency("failed dependency"))
   275  			},
   276  		},
   277  		{
   278  			name:    "Should return error if failed to create vpc",
   279  			input:   &infrav1.VPCSpec{AvailabilityZoneUsageLimit: &usageLimit, AvailabilityZoneSelection: &selection},
   280  			wantErr: true,
   281  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   282  				m.CreateVpc(gomock.AssignableToTypeOf(&ec2.CreateVpcInput{})).Return(nil, awserrors.NewFailedDependency("failed dependency"))
   283  			},
   284  		},
   285  		{
   286  			name:    "Should return error if describe vpc returns empty list",
   287  			input:   &infrav1.VPCSpec{ID: "managed-vpc-exists", AvailabilityZoneUsageLimit: &usageLimit, AvailabilityZoneSelection: &selection},
   288  			wantErr: true,
   289  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   290  				m.DescribeVpcs(gomock.AssignableToTypeOf(&ec2.DescribeVpcsInput{})).Return(&ec2.DescribeVpcsOutput{
   291  					Vpcs: []*ec2.Vpc{},
   292  				}, nil)
   293  			},
   294  		},
   295  		{
   296  			name:    "Should return error if describe vpc returns more than 1 vpcs",
   297  			input:   &infrav1.VPCSpec{ID: "managed-vpc-exists", AvailabilityZoneUsageLimit: &usageLimit, AvailabilityZoneSelection: &selection},
   298  			wantErr: true,
   299  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   300  				m.DescribeVpcs(gomock.AssignableToTypeOf(&ec2.DescribeVpcsInput{})).Return(&ec2.DescribeVpcsOutput{
   301  					Vpcs: []*ec2.Vpc{
   302  						{
   303  							VpcId: aws.String("vpc_1"),
   304  						},
   305  						{
   306  							VpcId: aws.String("vpc_2"),
   307  						},
   308  					},
   309  				}, nil)
   310  			},
   311  		},
   312  		{
   313  			name:    "Should return error if vpc state is not available/pending",
   314  			input:   &infrav1.VPCSpec{ID: "managed-vpc-exists", AvailabilityZoneUsageLimit: &usageLimit, AvailabilityZoneSelection: &selection},
   315  			wantErr: true,
   316  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   317  				m.DescribeVpcs(gomock.AssignableToTypeOf(&ec2.DescribeVpcsInput{})).Return(&ec2.DescribeVpcsOutput{
   318  					Vpcs: []*ec2.Vpc{
   319  						{
   320  							VpcId: aws.String("vpc"),
   321  							State: aws.String("deleting"),
   322  						},
   323  					},
   324  				}, nil)
   325  			},
   326  		},
   327  	}
   328  	for _, tc := range testCases {
   329  		t.Run(tc.name, func(t *testing.T) {
   330  			g := NewWithT(t)
   331  			clusterScope, err := getClusterScope(tc.input)
   332  			g.Expect(err).NotTo(HaveOccurred())
   333  			ec2Mock := mocks.NewMockEC2API(mockCtrl)
   334  			tc.expect(ec2Mock.EXPECT())
   335  			s := NewService(clusterScope)
   336  			s.EC2Client = ec2Mock
   337  
   338  			err = s.reconcileVPC()
   339  			if tc.wantErr {
   340  				g.Expect(err).ToNot(BeNil())
   341  				return
   342  			} else {
   343  				g.Expect(err).To(BeNil())
   344  			}
   345  			g.Expect(tc.want).To(Equal(&clusterScope.AWSCluster.Spec.NetworkSpec.VPC))
   346  		})
   347  	}
   348  }
   349  
   350  func Test_DeleteVPC(t *testing.T) {
   351  	mockCtrl := gomock.NewController(t)
   352  	defer mockCtrl.Finish()
   353  
   354  	tags := map[string]string{
   355  		"sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster": "owned",
   356  	}
   357  
   358  	testCases := []struct {
   359  		name    string
   360  		input   *infrav1.VPCSpec
   361  		wantErr bool
   362  		expect  func(m *mocks.MockEC2APIMockRecorder)
   363  	}{
   364  		{
   365  			name:  "Should not delete vpc if vpc is unmanaged",
   366  			input: &infrav1.VPCSpec{ID: "unmanaged-vpc"},
   367  		},
   368  		{
   369  			name: "Should return error if delete vpc failed",
   370  			input: &infrav1.VPCSpec{
   371  				ID:   "managed-vpc",
   372  				Tags: tags,
   373  			},
   374  			wantErr: true,
   375  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   376  				m.DeleteVpc(gomock.Eq(&ec2.DeleteVpcInput{
   377  					VpcId: aws.String("managed-vpc"),
   378  				})).Return(nil, awserrors.NewFailedDependency("failed dependency"))
   379  			},
   380  		},
   381  		{
   382  			name: "Should return without error if delete vpc succeeded",
   383  			input: &infrav1.VPCSpec{
   384  				ID:   "managed-vpc",
   385  				Tags: tags,
   386  			},
   387  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   388  				m.DeleteVpc(gomock.Eq(&ec2.DeleteVpcInput{
   389  					VpcId: aws.String("managed-vpc"),
   390  				})).Return(&ec2.DeleteVpcOutput{}, nil)
   391  			},
   392  		},
   393  		{
   394  			name: "Should not delete vpc if vpc not found",
   395  			input: &infrav1.VPCSpec{
   396  				ID:   "managed-vpc",
   397  				Tags: tags,
   398  			},
   399  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   400  				m.DeleteVpc(gomock.Eq(&ec2.DeleteVpcInput{
   401  					VpcId: aws.String("managed-vpc"),
   402  				})).Return(nil, awserr.New("InvalidVpcID.NotFound", "not found", nil))
   403  			},
   404  		},
   405  	}
   406  	for _, tc := range testCases {
   407  		t.Run(tc.name, func(t *testing.T) {
   408  			g := NewWithT(t)
   409  			ec2Mock := mocks.NewMockEC2API(mockCtrl)
   410  			clusterScope, err := getClusterScope(tc.input)
   411  			g.Expect(err).NotTo(HaveOccurred())
   412  			if tc.expect != nil {
   413  				tc.expect(ec2Mock.EXPECT())
   414  			}
   415  			s := NewService(clusterScope)
   416  			s.EC2Client = ec2Mock
   417  
   418  			err = s.deleteVPC()
   419  			if tc.wantErr {
   420  				g.Expect(err).ToNot(BeNil())
   421  				return
   422  			}
   423  			g.Expect(err).To(BeNil())
   424  		})
   425  	}
   426  }
   427  
   428  func getClusterScope(vpcSpec *infrav1.VPCSpec) (*scope.ClusterScope, error) {
   429  	scheme := runtime.NewScheme()
   430  	_ = infrav1.AddToScheme(scheme)
   431  	client := fake.NewClientBuilder().WithScheme(scheme).Build()
   432  	awsCluster := &infrav1.AWSCluster{
   433  		ObjectMeta: metav1.ObjectMeta{Name: "test"},
   434  		Spec: infrav1.AWSClusterSpec{
   435  			NetworkSpec: infrav1.NetworkSpec{
   436  				VPC: *vpcSpec,
   437  			},
   438  		},
   439  	}
   440  	client.Create(context.TODO(), awsCluster)
   441  	return scope.NewClusterScope(scope.ClusterScopeParams{
   442  		Cluster: &clusterv1.Cluster{
   443  			ObjectMeta: metav1.ObjectMeta{Name: "test-cluster"},
   444  		},
   445  		AWSCluster: awsCluster,
   446  		Client:     client,
   447  	})
   448  }