sigs.k8s.io/cluster-api-provider-aws@v1.5.5/pkg/cloud/services/network/routetables_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 "fmt" 21 "strings" 22 "testing" 23 24 "github.com/aws/aws-sdk-go/aws" 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 TestReconcileRouteTables(t *testing.T) { 41 mockCtrl := gomock.NewController(t) 42 defer mockCtrl.Finish() 43 44 testCases := []struct { 45 name string 46 input *infrav1.NetworkSpec 47 expect func(m *mocks.MockEC2APIMockRecorder) 48 err error 49 }{ 50 { 51 name: "no routes existing, single private and single public, same AZ", 52 input: &infrav1.NetworkSpec{ 53 VPC: infrav1.VPCSpec{ 54 ID: "vpc-routetables", 55 InternetGatewayID: aws.String("igw-01"), 56 Tags: infrav1.Tags{ 57 infrav1.ClusterTagKey("test-cluster"): "owned", 58 }, 59 }, 60 Subnets: infrav1.Subnets{ 61 infrav1.SubnetSpec{ 62 ID: "subnet-routetables-private", 63 IsPublic: false, 64 AvailabilityZone: "us-east-1a", 65 }, 66 infrav1.SubnetSpec{ 67 ID: "subnet-routetables-public", 68 IsPublic: true, 69 NatGatewayID: aws.String("nat-01"), 70 AvailabilityZone: "us-east-1a", 71 }, 72 }, 73 }, 74 expect: func(m *mocks.MockEC2APIMockRecorder) { 75 m.DescribeRouteTables(gomock.AssignableToTypeOf(&ec2.DescribeRouteTablesInput{})). 76 Return(&ec2.DescribeRouteTablesOutput{}, nil) 77 78 privateRouteTable := m.CreateRouteTable(matchRouteTableInput(&ec2.CreateRouteTableInput{VpcId: aws.String("vpc-routetables")})). 79 Return(&ec2.CreateRouteTableOutput{RouteTable: &ec2.RouteTable{RouteTableId: aws.String("rt-1")}}, nil) 80 81 m.CreateRoute(gomock.Eq(&ec2.CreateRouteInput{ 82 NatGatewayId: aws.String("nat-01"), 83 DestinationCidrBlock: aws.String("0.0.0.0/0"), 84 RouteTableId: aws.String("rt-1"), 85 })). 86 After(privateRouteTable) 87 88 m.AssociateRouteTable(gomock.Eq(&ec2.AssociateRouteTableInput{ 89 RouteTableId: aws.String("rt-1"), 90 SubnetId: aws.String("subnet-routetables-private"), 91 })). 92 Return(&ec2.AssociateRouteTableOutput{}, nil). 93 After(privateRouteTable) 94 95 publicRouteTable := m.CreateRouteTable(matchRouteTableInput(&ec2.CreateRouteTableInput{VpcId: aws.String("vpc-routetables")})). 96 Return(&ec2.CreateRouteTableOutput{RouteTable: &ec2.RouteTable{RouteTableId: aws.String("rt-2")}}, nil) 97 98 m.CreateRoute(gomock.Eq(&ec2.CreateRouteInput{ 99 GatewayId: aws.String("igw-01"), 100 DestinationCidrBlock: aws.String("0.0.0.0/0"), 101 RouteTableId: aws.String("rt-2"), 102 })). 103 After(publicRouteTable) 104 105 m.AssociateRouteTable(gomock.Eq(&ec2.AssociateRouteTableInput{ 106 RouteTableId: aws.String("rt-2"), 107 SubnetId: aws.String("subnet-routetables-public"), 108 })). 109 Return(&ec2.AssociateRouteTableOutput{}, nil). 110 After(publicRouteTable) 111 }, 112 }, 113 { 114 name: "subnets in different availability zones, returns error", 115 input: &infrav1.NetworkSpec{ 116 VPC: infrav1.VPCSpec{ 117 InternetGatewayID: aws.String("igw-01"), 118 ID: "vpc-routetables", 119 Tags: infrav1.Tags{ 120 infrav1.ClusterTagKey("test-cluster"): "owned", 121 }, 122 }, 123 Subnets: infrav1.Subnets{ 124 infrav1.SubnetSpec{ 125 ID: "subnet-routetables-private", 126 IsPublic: false, 127 AvailabilityZone: "us-east-1a", 128 }, 129 infrav1.SubnetSpec{ 130 ID: "subnet-routetables-public", 131 IsPublic: true, 132 NatGatewayID: aws.String("nat-01"), 133 AvailabilityZone: "us-east-1b", 134 }, 135 }, 136 }, 137 expect: func(m *mocks.MockEC2APIMockRecorder) { 138 m.DescribeRouteTables(gomock.AssignableToTypeOf(&ec2.DescribeRouteTablesInput{})). 139 Return(&ec2.DescribeRouteTablesOutput{}, nil) 140 }, 141 err: errors.New(`no nat gateways available in "us-east-1a"`), 142 }, 143 { 144 name: "routes exist, but the nat gateway ID is incorrect, replaces it", 145 input: &infrav1.NetworkSpec{ 146 VPC: infrav1.VPCSpec{ 147 InternetGatewayID: aws.String("igw-01"), 148 ID: "vpc-routetables", 149 Tags: infrav1.Tags{ 150 infrav1.ClusterTagKey("test-cluster"): "owned", 151 }, 152 }, 153 Subnets: infrav1.Subnets{ 154 infrav1.SubnetSpec{ 155 ID: "subnet-routetables-private", 156 IsPublic: false, 157 AvailabilityZone: "us-east-1a", 158 }, 159 infrav1.SubnetSpec{ 160 ID: "subnet-routetables-public", 161 IsPublic: true, 162 NatGatewayID: aws.String("nat-01"), 163 AvailabilityZone: "us-east-1a", 164 RouteTableID: aws.String("route-table-1"), 165 }, 166 }, 167 }, 168 expect: func(m *mocks.MockEC2APIMockRecorder) { 169 m.DescribeRouteTables(gomock.AssignableToTypeOf(&ec2.DescribeRouteTablesInput{})). 170 Return(&ec2.DescribeRouteTablesOutput{ 171 RouteTables: []*ec2.RouteTable{ 172 { 173 RouteTableId: aws.String("route-table-private"), 174 Associations: []*ec2.RouteTableAssociation{ 175 { 176 SubnetId: aws.String("subnet-routetables-private"), 177 }, 178 }, 179 Routes: []*ec2.Route{ 180 { 181 DestinationCidrBlock: aws.String("0.0.0.0/0"), 182 NatGatewayId: aws.String("outdated-nat-01"), 183 }, 184 }, 185 Tags: []*ec2.Tag{ 186 { 187 Key: aws.String("sigs.k8s.io/cluster-api-provider-aws/role"), 188 Value: aws.String("common"), 189 }, 190 { 191 Key: aws.String("Name"), 192 Value: aws.String("test-cluster-rt-private-us-east-1a"), 193 }, 194 { 195 Key: aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster"), 196 Value: aws.String("owned"), 197 }, 198 }, 199 }, 200 { 201 RouteTableId: aws.String("route-table-public"), 202 Associations: []*ec2.RouteTableAssociation{ 203 { 204 SubnetId: aws.String("subnet-routetables-public"), 205 }, 206 }, 207 Routes: []*ec2.Route{ 208 { 209 DestinationCidrBlock: aws.String("0.0.0.0/0"), 210 GatewayId: aws.String("igw-01"), 211 }, 212 }, 213 Tags: []*ec2.Tag{ 214 { 215 Key: aws.String("sigs.k8s.io/cluster-api-provider-aws/role"), 216 Value: aws.String("common"), 217 }, 218 { 219 Key: aws.String("Name"), 220 Value: aws.String("test-cluster-rt-public-us-east-1a"), 221 }, 222 { 223 Key: aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster"), 224 Value: aws.String("owned"), 225 }, 226 }, 227 }, 228 }, 229 }, nil) 230 231 m.ReplaceRoute(gomock.Eq( 232 &ec2.ReplaceRouteInput{ 233 DestinationCidrBlock: aws.String("0.0.0.0/0"), 234 RouteTableId: aws.String("route-table-private"), 235 NatGatewayId: aws.String("nat-01"), 236 }, 237 )). 238 Return(nil, nil) 239 }, 240 }, 241 { 242 name: "extra routes exist, do nothing", 243 input: &infrav1.NetworkSpec{ 244 VPC: infrav1.VPCSpec{ 245 InternetGatewayID: aws.String("igw-01"), 246 ID: "vpc-routetables", 247 Tags: infrav1.Tags{ 248 infrav1.ClusterTagKey("test-cluster"): "owned", 249 }, 250 }, 251 Subnets: infrav1.Subnets{ 252 infrav1.SubnetSpec{ 253 ID: "subnet-routetables-private", 254 IsPublic: false, 255 AvailabilityZone: "us-east-1a", 256 }, 257 infrav1.SubnetSpec{ 258 ID: "subnet-routetables-public", 259 IsPublic: true, 260 NatGatewayID: aws.String("nat-01"), 261 AvailabilityZone: "us-east-1a", 262 }, 263 }, 264 }, 265 expect: func(m *mocks.MockEC2APIMockRecorder) { 266 m.DescribeRouteTables(gomock.AssignableToTypeOf(&ec2.DescribeRouteTablesInput{})). 267 Return(&ec2.DescribeRouteTablesOutput{ 268 RouteTables: []*ec2.RouteTable{ 269 { 270 RouteTableId: aws.String("route-table-private"), 271 Associations: []*ec2.RouteTableAssociation{ 272 { 273 SubnetId: aws.String("subnet-routetables-private"), 274 }, 275 }, 276 Routes: []*ec2.Route{ 277 { 278 DestinationCidrBlock: aws.String("0.0.0.0/0"), 279 NatGatewayId: aws.String("nat-01"), 280 }, 281 // Extra (managed outside of CAPA) route with Managed Prefix List destination. 282 { 283 DestinationPrefixListId: aws.String("pl-foobar"), 284 }, 285 }, 286 Tags: []*ec2.Tag{ 287 { 288 Key: aws.String("sigs.k8s.io/cluster-api-provider-aws/role"), 289 Value: aws.String("common"), 290 }, 291 { 292 Key: aws.String("Name"), 293 Value: aws.String("test-cluster-rt-private-us-east-1a"), 294 }, 295 { 296 Key: aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster"), 297 Value: aws.String("owned"), 298 }, 299 }, 300 }, 301 { 302 RouteTableId: aws.String("route-table-public"), 303 Associations: []*ec2.RouteTableAssociation{ 304 { 305 SubnetId: aws.String("subnet-routetables-public"), 306 }, 307 }, 308 Routes: []*ec2.Route{ 309 { 310 DestinationCidrBlock: aws.String("0.0.0.0/0"), 311 GatewayId: aws.String("igw-01"), 312 }, 313 }, 314 Tags: []*ec2.Tag{ 315 { 316 Key: aws.String("sigs.k8s.io/cluster-api-provider-aws/role"), 317 Value: aws.String("common"), 318 }, 319 { 320 Key: aws.String("Name"), 321 Value: aws.String("test-cluster-rt-public-us-east-1a"), 322 }, 323 { 324 Key: aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster"), 325 Value: aws.String("owned"), 326 }, 327 }, 328 }, 329 }, 330 }, nil) 331 }, 332 }, 333 } 334 335 for _, tc := range testCases { 336 t.Run(tc.name, func(t *testing.T) { 337 ec2Mock := mocks.NewMockEC2API(mockCtrl) 338 339 scheme := runtime.NewScheme() 340 _ = infrav1.AddToScheme(scheme) 341 client := fake.NewClientBuilder().WithScheme(scheme).Build() 342 scope, err := scope.NewClusterScope(scope.ClusterScopeParams{ 343 Client: client, 344 Cluster: &clusterv1.Cluster{ 345 ObjectMeta: metav1.ObjectMeta{Name: "test-cluster"}, 346 }, 347 AWSCluster: &infrav1.AWSCluster{ 348 ObjectMeta: metav1.ObjectMeta{Name: "test"}, 349 Spec: infrav1.AWSClusterSpec{ 350 NetworkSpec: *tc.input, 351 }, 352 }, 353 }) 354 if err != nil { 355 t.Fatalf("Failed to create test context: %v", err) 356 } 357 358 tc.expect(ec2Mock.EXPECT()) 359 360 s := NewService(scope) 361 s.EC2Client = ec2Mock 362 363 if err := s.reconcileRouteTables(); err != nil && tc.err != nil { 364 if !strings.Contains(err.Error(), tc.err.Error()) { 365 t.Fatalf("was expecting error to look like '%v', but got '%v'", tc.err, err) 366 } 367 } else if err != nil { 368 t.Fatalf("got an unexpected error: %v", err) 369 } 370 }) 371 } 372 } 373 374 func TestDeleteRouteTables(t *testing.T) { 375 mockCtrl := gomock.NewController(t) 376 defer mockCtrl.Finish() 377 378 describeRouteTableOutput := &ec2.DescribeRouteTablesOutput{ 379 RouteTables: []*ec2.RouteTable{ 380 { 381 RouteTableId: aws.String("route-table-private"), 382 Associations: []*ec2.RouteTableAssociation{ 383 { 384 SubnetId: nil, 385 }, 386 }, 387 Routes: []*ec2.Route{ 388 { 389 DestinationCidrBlock: aws.String("0.0.0.0/0"), 390 NatGatewayId: aws.String("outdated-nat-01"), 391 }, 392 }, 393 }, 394 { 395 RouteTableId: aws.String("route-table-public"), 396 Associations: []*ec2.RouteTableAssociation{ 397 { 398 SubnetId: aws.String("subnet-routetables-public"), 399 RouteTableAssociationId: aws.String("route-table-public"), 400 }, 401 }, 402 Routes: []*ec2.Route{ 403 { 404 DestinationCidrBlock: aws.String("0.0.0.0/0"), 405 GatewayId: aws.String("igw-01"), 406 }, 407 }, 408 Tags: []*ec2.Tag{ 409 { 410 Key: aws.String("sigs.k8s.io/cluster-api-provider-aws/role"), 411 Value: aws.String("common"), 412 }, 413 { 414 Key: aws.String("Name"), 415 Value: aws.String("test-cluster-rt-public-us-east-1a"), 416 }, 417 { 418 Key: aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster"), 419 Value: aws.String("owned"), 420 }, 421 }, 422 }, 423 }, 424 } 425 426 testCases := []struct { 427 name string 428 input *infrav1.NetworkSpec 429 expect func(m *mocks.MockEC2APIMockRecorder) 430 wantErr bool 431 }{ 432 { 433 name: "Should skip deletion if vpc is unmanaged", 434 input: &infrav1.NetworkSpec{ 435 VPC: infrav1.VPCSpec{ 436 ID: "vpc-routetables", 437 Tags: infrav1.Tags{}, 438 }, 439 }, 440 }, 441 { 442 name: "Should delete route table successfully", 443 input: &infrav1.NetworkSpec{}, 444 expect: func(m *mocks.MockEC2APIMockRecorder) { 445 m.DescribeRouteTables(gomock.AssignableToTypeOf(&ec2.DescribeRouteTablesInput{})). 446 Return(describeRouteTableOutput, nil) 447 448 m.DeleteRouteTable(gomock.Eq(&ec2.DeleteRouteTableInput{ 449 RouteTableId: aws.String("route-table-private"), 450 })).Return(&ec2.DeleteRouteTableOutput{}, nil) 451 452 m.DisassociateRouteTable(gomock.Eq(&ec2.DisassociateRouteTableInput{ 453 AssociationId: aws.String("route-table-public"), 454 })).Return(&ec2.DisassociateRouteTableOutput{}, nil) 455 456 m.DeleteRouteTable(gomock.Eq(&ec2.DeleteRouteTableInput{ 457 RouteTableId: aws.String("route-table-public"), 458 })).Return(&ec2.DeleteRouteTableOutput{}, nil) 459 }, 460 }, 461 { 462 name: "Should return error if describe route table fails", 463 input: &infrav1.NetworkSpec{}, 464 expect: func(m *mocks.MockEC2APIMockRecorder) { 465 m.DescribeRouteTables(gomock.AssignableToTypeOf(&ec2.DescribeRouteTablesInput{})). 466 Return(nil, awserrors.NewFailedDependency("failed dependency")) 467 }, 468 wantErr: true, 469 }, 470 { 471 name: "Should return error if delete route table fails", 472 input: &infrav1.NetworkSpec{}, 473 expect: func(m *mocks.MockEC2APIMockRecorder) { 474 m.DescribeRouteTables(gomock.AssignableToTypeOf(&ec2.DescribeRouteTablesInput{})). 475 Return(describeRouteTableOutput, nil) 476 477 m.DeleteRouteTable(gomock.Eq(&ec2.DeleteRouteTableInput{ 478 RouteTableId: aws.String("route-table-private"), 479 })).Return(nil, awserrors.NewNotFound("not found")) 480 }, 481 wantErr: true, 482 }, 483 { 484 name: "Should return error if disassociate route table fails", 485 input: &infrav1.NetworkSpec{}, 486 expect: func(m *mocks.MockEC2APIMockRecorder) { 487 m.DescribeRouteTables(gomock.AssignableToTypeOf(&ec2.DescribeRouteTablesInput{})). 488 Return(describeRouteTableOutput, nil) 489 490 m.DeleteRouteTable(gomock.Eq(&ec2.DeleteRouteTableInput{ 491 RouteTableId: aws.String("route-table-private"), 492 })).Return(&ec2.DeleteRouteTableOutput{}, nil) 493 494 m.DisassociateRouteTable(gomock.Eq(&ec2.DisassociateRouteTableInput{ 495 AssociationId: aws.String("route-table-public"), 496 })).Return(nil, awserrors.NewNotFound("not found")) 497 }, 498 wantErr: true, 499 }, 500 } 501 502 for _, tc := range testCases { 503 t.Run(tc.name, func(t *testing.T) { 504 g := NewWithT(t) 505 ec2Mock := mocks.NewMockEC2API(mockCtrl) 506 507 scheme := runtime.NewScheme() 508 _ = infrav1.AddToScheme(scheme) 509 client := fake.NewClientBuilder().WithScheme(scheme).Build() 510 scope, err := scope.NewClusterScope(scope.ClusterScopeParams{ 511 Client: client, 512 Cluster: &clusterv1.Cluster{ 513 ObjectMeta: metav1.ObjectMeta{Name: "test-cluster"}, 514 }, 515 AWSCluster: &infrav1.AWSCluster{ 516 ObjectMeta: metav1.ObjectMeta{Name: "test"}, 517 Spec: infrav1.AWSClusterSpec{ 518 NetworkSpec: *tc.input, 519 }, 520 }, 521 }) 522 g.Expect(err).NotTo(HaveOccurred()) 523 if tc.expect != nil { 524 tc.expect(ec2Mock.EXPECT()) 525 } 526 527 s := NewService(scope) 528 s.EC2Client = ec2Mock 529 530 err = s.deleteRouteTables() 531 if tc.wantErr { 532 g.Expect(err).To(HaveOccurred()) 533 return 534 } 535 g.Expect(err).NotTo(HaveOccurred()) 536 }) 537 } 538 } 539 540 type routeTableInputMatcher struct { 541 routeTableInput *ec2.CreateRouteTableInput 542 } 543 544 func (r routeTableInputMatcher) Matches(x interface{}) bool { 545 actual, ok := x.(*ec2.CreateRouteTableInput) 546 if !ok { 547 fmt.Println("heeeeyy") 548 return false 549 } 550 if *actual.VpcId != *r.routeTableInput.VpcId { 551 return false 552 } 553 554 return true 555 } 556 557 func (r routeTableInputMatcher) String() string { 558 return fmt.Sprintf("partially matches %v", r.routeTableInput) 559 } 560 561 func matchRouteTableInput(input *ec2.CreateRouteTableInput) gomock.Matcher { 562 return routeTableInputMatcher{routeTableInput: input} 563 }