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 }