sigs.k8s.io/cluster-api-provider-aws@v1.5.5/pkg/cloud/services/elb/loadbalancer_test.go (about) 1 /* 2 Copyright 2019 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 elb 18 19 import ( 20 "context" 21 "fmt" 22 "strings" 23 "testing" 24 25 "github.com/aws/aws-sdk-go/aws" 26 "github.com/aws/aws-sdk-go/aws/awserr" 27 "github.com/aws/aws-sdk-go/service/ec2" 28 "github.com/aws/aws-sdk-go/service/elb" 29 rgapi "github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi" 30 "github.com/golang/mock/gomock" 31 . "github.com/onsi/gomega" 32 "github.com/pkg/errors" 33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 34 "k8s.io/apimachinery/pkg/runtime" 35 "k8s.io/utils/pointer" 36 "sigs.k8s.io/controller-runtime/pkg/client/fake" 37 38 infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1beta1" 39 "sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/scope" 40 "sigs.k8s.io/cluster-api-provider-aws/test/mocks" 41 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 42 "sigs.k8s.io/cluster-api/util/conditions" 43 ) 44 45 func TestELBName(t *testing.T) { 46 tests := []struct { 47 name string 48 awsCluster infrav1.AWSCluster 49 expected string 50 }{ 51 { 52 name: "name is not defined by user, so generate the default", 53 awsCluster: infrav1.AWSCluster{ 54 ObjectMeta: metav1.ObjectMeta{ 55 Name: "example", 56 Namespace: metav1.NamespaceDefault, 57 }, 58 }, 59 expected: "example-apiserver", 60 }, 61 { 62 name: "name is defined by user, so use it", 63 awsCluster: infrav1.AWSCluster{ 64 ObjectMeta: metav1.ObjectMeta{ 65 Name: "example", 66 Namespace: metav1.NamespaceDefault, 67 }, 68 Spec: infrav1.AWSClusterSpec{ 69 ControlPlaneLoadBalancer: &infrav1.AWSLoadBalancerSpec{ 70 Name: pointer.String("myapiserver"), 71 }, 72 }, 73 }, 74 expected: "myapiserver", 75 }, 76 } 77 for _, tt := range tests { 78 t.Run(tt.name, func(t *testing.T) { 79 scheme := runtime.NewScheme() 80 _ = infrav1.AddToScheme(scheme) 81 client := fake.NewClientBuilder().WithScheme(scheme).Build() 82 83 scope, err := scope.NewClusterScope(scope.ClusterScopeParams{ 84 Client: client, 85 Cluster: &clusterv1.Cluster{ 86 ObjectMeta: metav1.ObjectMeta{ 87 Name: tt.awsCluster.Name, 88 Namespace: tt.awsCluster.Namespace, 89 }, 90 }, 91 AWSCluster: &tt.awsCluster, 92 }) 93 if err != nil { 94 t.Fatalf("failed to create scope: %s", err) 95 } 96 97 elbName, err := ELBName(scope) 98 if err != nil { 99 t.Fatalf("unable to get ELB name: %v", err) 100 } 101 if elbName != tt.expected { 102 t.Fatalf("expected ELB name: %v, got name: %v", tt.expected, elbName) 103 } 104 }) 105 } 106 } 107 108 func TestGenerateELBName(t *testing.T) { 109 tests := []struct { 110 name string 111 expected string 112 }{ 113 { 114 name: "test", 115 expected: "test-apiserver", 116 }, 117 { 118 name: "0123456789012345678901", 119 expected: "0123456789012345678901-apiserver", 120 }, 121 { 122 name: "01234567890123456789012", 123 expected: "26o3cjil5at5qn27vukn5x09b3ql-k8s", 124 }, 125 { 126 name: "anotherverylongtoolongname", 127 expected: "t8gnrbbifaaf5d0k4xmwui3xwvip-k8s", 128 }, 129 { 130 name: "anotherverylongtoolongnameanotherverylongtoolongname", 131 expected: "tph1huzox1f10z9ow1inrootjws8-k8s", 132 }, 133 } 134 for _, tt := range tests { 135 t.Run(tt.name, func(t *testing.T) { 136 elbName, err := GenerateELBName(tt.name) 137 if err != nil { 138 t.Error(err) 139 } 140 141 if elbName != tt.expected { 142 t.Errorf("expected ELB name: %v, got name: %v", tt.expected, elbName) 143 } 144 145 if len(elbName) > 32 { 146 t.Errorf("ELB name too long: %v vs. %s", len(elbName), "32") 147 } 148 }) 149 } 150 } 151 152 func TestGetAPIServerClassicELBSpec_ControlPlaneLoadBalancer(t *testing.T) { 153 tests := []struct { 154 name string 155 lb *infrav1.AWSLoadBalancerSpec 156 mocks func(m *mocks.MockEC2APIMockRecorder) 157 expect func(t *testing.T, g *WithT, res *infrav1.ClassicELB) 158 }{ 159 { 160 name: "nil load balancer config", 161 lb: nil, 162 mocks: func(m *mocks.MockEC2APIMockRecorder) {}, 163 expect: func(t *testing.T, g *WithT, res *infrav1.ClassicELB) { 164 t.Helper() 165 if res.Attributes.CrossZoneLoadBalancing { 166 t.Error("Expected load balancer not to have cross-zone load balancing enabled") 167 } 168 }, 169 }, 170 { 171 name: "load balancer config with cross zone enabled", 172 lb: &infrav1.AWSLoadBalancerSpec{ 173 CrossZoneLoadBalancing: true, 174 }, 175 mocks: func(m *mocks.MockEC2APIMockRecorder) {}, 176 expect: func(t *testing.T, g *WithT, res *infrav1.ClassicELB) { 177 t.Helper() 178 if !res.Attributes.CrossZoneLoadBalancing { 179 t.Error("Expected load balancer to have cross-zone load balancing enabled") 180 } 181 }, 182 }, 183 { 184 name: "load balancer config with subnets specified", 185 lb: &infrav1.AWSLoadBalancerSpec{ 186 Subnets: []string{"subnet-1", "subnet-2"}, 187 }, 188 mocks: func(m *mocks.MockEC2APIMockRecorder) { 189 m.DescribeSubnets(gomock.Eq(&ec2.DescribeSubnetsInput{ 190 SubnetIds: []*string{ 191 aws.String("subnet-1"), 192 aws.String("subnet-2"), 193 }, 194 })). 195 Return(&ec2.DescribeSubnetsOutput{ 196 Subnets: []*ec2.Subnet{ 197 { 198 SubnetId: aws.String("subnet-1"), 199 AvailabilityZone: aws.String("us-east-1a"), 200 }, 201 { 202 SubnetId: aws.String("subnet-2"), 203 AvailabilityZone: aws.String("us-east-1b"), 204 }, 205 }, 206 }, nil) 207 }, 208 expect: func(t *testing.T, g *WithT, res *infrav1.ClassicELB) { 209 t.Helper() 210 if len(res.SubnetIDs) != 2 { 211 t.Errorf("Expected load balancer to be configured for 2 subnets, got %v", len(res.SubnetIDs)) 212 } 213 if len(res.AvailabilityZones) != 2 { 214 t.Errorf("Expected load balancer to be configured for 2 availability zones, got %v", len(res.AvailabilityZones)) 215 } 216 }, 217 }, 218 { 219 name: "load balancer config with additional security groups specified", 220 lb: &infrav1.AWSLoadBalancerSpec{ 221 AdditionalSecurityGroups: []string{"sg-00001", "sg-00002"}, 222 }, 223 mocks: func(m *mocks.MockEC2APIMockRecorder) {}, 224 expect: func(t *testing.T, g *WithT, res *infrav1.ClassicELB) { 225 t.Helper() 226 if len(res.SecurityGroupIDs) != 3 { 227 t.Errorf("Expected load balancer to be configured for 3 security groups, got %v", len(res.SecurityGroupIDs)) 228 } 229 }, 230 }, 231 { 232 name: "Should create load balancer spec if elb health check protocol specified in config", 233 lb: &infrav1.AWSLoadBalancerSpec{ 234 HealthCheckProtocol: &infrav1.ClassicELBProtocolTCP, 235 }, 236 mocks: func(m *mocks.MockEC2APIMockRecorder) {}, 237 expect: func(t *testing.T, g *WithT, res *infrav1.ClassicELB) { 238 t.Helper() 239 expectedTarget := fmt.Sprintf("%v:%d", infrav1.ClassicELBProtocolTCP, 6443) 240 g.Expect(expectedTarget, res.HealthCheck.Target) 241 }, 242 }, 243 { 244 name: "Should create load balancer spec with default elb health check protocol", 245 lb: &infrav1.AWSLoadBalancerSpec{}, 246 mocks: func(m *mocks.MockEC2APIMockRecorder) {}, 247 expect: func(t *testing.T, g *WithT, res *infrav1.ClassicELB) { 248 t.Helper() 249 expectedTarget := fmt.Sprintf("%v:%d", infrav1.ClassicELBProtocolTCP, 6443) 250 g.Expect(expectedTarget, res.HealthCheck.Target) 251 }, 252 }, 253 } 254 255 for _, tc := range tests { 256 t.Run(tc.name, func(t *testing.T) { 257 g := NewWithT(t) 258 mockCtrl := gomock.NewController(t) 259 defer mockCtrl.Finish() 260 ec2Mock := mocks.NewMockEC2API(mockCtrl) 261 262 scheme := runtime.NewScheme() 263 _ = infrav1.AddToScheme(scheme) 264 client := fake.NewClientBuilder().WithScheme(scheme).Build() 265 clusterScope, err := scope.NewClusterScope(scope.ClusterScopeParams{ 266 Client: client, 267 Cluster: &clusterv1.Cluster{ 268 ObjectMeta: metav1.ObjectMeta{ 269 Namespace: "foo", 270 Name: "bar", 271 }, 272 }, 273 AWSCluster: &infrav1.AWSCluster{ 274 ObjectMeta: metav1.ObjectMeta{Name: "test"}, 275 Spec: infrav1.AWSClusterSpec{ 276 ControlPlaneLoadBalancer: tc.lb, 277 }, 278 }, 279 }) 280 if err != nil { 281 t.Fatal(err) 282 } 283 284 tc.mocks(ec2Mock.EXPECT()) 285 286 s := &Service{ 287 scope: clusterScope, 288 EC2Client: ec2Mock, 289 } 290 291 spec, err := s.getAPIServerClassicELBSpec(clusterScope.Name()) 292 if err != nil { 293 t.Fatal(err) 294 } 295 296 tc.expect(t, g, spec) 297 }) 298 } 299 } 300 301 func TestRegisterInstanceWithAPIServerELB(t *testing.T) { 302 const ( 303 namespace = "foo" 304 clusterName = "bar" 305 clusterSubnetID = "subnet-1" 306 elbName = "bar-apiserver" 307 elbSubnetID = "elb-subnet" 308 instanceID = "test-instance" 309 az = "us-west-1a" 310 differentAZ = "us-east-2c" 311 ) 312 313 tests := []struct { 314 name string 315 awsCluster *infrav1.AWSCluster 316 elbAPIMocks func(m *mocks.MockELBAPIMockRecorder) 317 ec2Mocks func(m *mocks.MockEC2APIMockRecorder) 318 check func(t *testing.T, err error) 319 }{ 320 { 321 name: "no load balancer subnets specified", 322 awsCluster: &infrav1.AWSCluster{ 323 ObjectMeta: metav1.ObjectMeta{Name: clusterName}, 324 Spec: infrav1.AWSClusterSpec{ 325 ControlPlaneLoadBalancer: &infrav1.AWSLoadBalancerSpec{ 326 Name: aws.String(elbName), 327 }, 328 NetworkSpec: infrav1.NetworkSpec{ 329 Subnets: infrav1.Subnets{{ 330 ID: clusterSubnetID, 331 AvailabilityZone: az, 332 }}, 333 }, 334 }, 335 }, 336 elbAPIMocks: func(m *mocks.MockELBAPIMockRecorder) { 337 m.DescribeLoadBalancers(gomock.Eq(&elb.DescribeLoadBalancersInput{ 338 LoadBalancerNames: aws.StringSlice([]string{elbName}), 339 })). 340 Return(&elb.DescribeLoadBalancersOutput{ 341 LoadBalancerDescriptions: []*elb.LoadBalancerDescription{ 342 { 343 LoadBalancerName: aws.String(elbName), 344 Scheme: aws.String(string(infrav1.ClassicELBSchemeInternetFacing)), 345 Subnets: []*string{aws.String(clusterSubnetID)}, 346 }, 347 }, 348 }, nil) 349 m.DescribeLoadBalancerAttributes(gomock.Eq(&elb.DescribeLoadBalancerAttributesInput{ 350 LoadBalancerName: aws.String(elbName), 351 })). 352 Return(&elb.DescribeLoadBalancerAttributesOutput{ 353 LoadBalancerAttributes: &elb.LoadBalancerAttributes{ 354 CrossZoneLoadBalancing: &elb.CrossZoneLoadBalancing{ 355 Enabled: aws.Bool(false), 356 }, 357 }, 358 }, nil) 359 m.DescribeTags(&elb.DescribeTagsInput{LoadBalancerNames: []*string{aws.String(elbName)}}).Return( 360 &elb.DescribeTagsOutput{ 361 TagDescriptions: []*elb.TagDescription{ 362 { 363 LoadBalancerName: aws.String(elbName), 364 Tags: []*elb.Tag{{ 365 Key: aws.String(infrav1.ClusterTagKey(clusterName)), 366 Value: aws.String(string(infrav1.ResourceLifecycleOwned)), 367 }}, 368 }, 369 }, 370 }, nil) 371 372 m.RegisterInstancesWithLoadBalancer(gomock.Eq(&elb.RegisterInstancesWithLoadBalancerInput{ 373 Instances: []*elb.Instance{{InstanceId: aws.String(instanceID)}}, 374 LoadBalancerName: aws.String(elbName), 375 })). 376 Return(&elb.RegisterInstancesWithLoadBalancerOutput{ 377 Instances: []*elb.Instance{{InstanceId: aws.String(instanceID)}}, 378 }, nil) 379 }, 380 ec2Mocks: func(m *mocks.MockEC2APIMockRecorder) {}, 381 check: func(t *testing.T, err error) { 382 t.Helper() 383 if err != nil { 384 t.Fatalf("did not expect error: %v", err) 385 } 386 }, 387 }, 388 { 389 name: "load balancer subnets specified in the same az from the instance", 390 awsCluster: &infrav1.AWSCluster{ 391 ObjectMeta: metav1.ObjectMeta{Name: clusterName}, 392 Spec: infrav1.AWSClusterSpec{ 393 NetworkSpec: infrav1.NetworkSpec{ 394 Subnets: infrav1.Subnets{{ 395 ID: clusterSubnetID, 396 AvailabilityZone: az, 397 }}, 398 }, 399 ControlPlaneLoadBalancer: &infrav1.AWSLoadBalancerSpec{ 400 Name: aws.String("bar-apiserver"), 401 Subnets: []string{elbSubnetID}, 402 }, 403 }, 404 }, 405 elbAPIMocks: func(m *mocks.MockELBAPIMockRecorder) { 406 m.DescribeLoadBalancers(gomock.Eq(&elb.DescribeLoadBalancersInput{ 407 LoadBalancerNames: aws.StringSlice([]string{elbName}), 408 })). 409 Return(&elb.DescribeLoadBalancersOutput{ 410 LoadBalancerDescriptions: []*elb.LoadBalancerDescription{ 411 { 412 Scheme: aws.String(string(infrav1.ClassicELBSchemeInternetFacing)), 413 Subnets: []*string{aws.String(elbSubnetID)}, 414 AvailabilityZones: []*string{aws.String(az)}, 415 }, 416 }, 417 }, nil) 418 m.DescribeLoadBalancerAttributes(gomock.Eq(&elb.DescribeLoadBalancerAttributesInput{ 419 LoadBalancerName: aws.String(elbName), 420 })). 421 Return(&elb.DescribeLoadBalancerAttributesOutput{ 422 LoadBalancerAttributes: &elb.LoadBalancerAttributes{ 423 CrossZoneLoadBalancing: &elb.CrossZoneLoadBalancing{ 424 Enabled: aws.Bool(false), 425 }, 426 }, 427 }, nil) 428 m.DescribeTags(&elb.DescribeTagsInput{LoadBalancerNames: []*string{aws.String(elbName)}}).Return( 429 &elb.DescribeTagsOutput{ 430 TagDescriptions: []*elb.TagDescription{ 431 { 432 LoadBalancerName: aws.String(elbName), 433 Tags: []*elb.Tag{{ 434 Key: aws.String(infrav1.ClusterTagKey(clusterName)), 435 Value: aws.String(string(infrav1.ResourceLifecycleOwned)), 436 }}, 437 }, 438 }, 439 }, nil) 440 441 m.RegisterInstancesWithLoadBalancer(gomock.Eq(&elb.RegisterInstancesWithLoadBalancerInput{ 442 Instances: []*elb.Instance{{InstanceId: aws.String(instanceID)}}, 443 LoadBalancerName: aws.String(elbName), 444 })). 445 Return(&elb.RegisterInstancesWithLoadBalancerOutput{ 446 Instances: []*elb.Instance{{InstanceId: aws.String(instanceID)}}, 447 }, nil) 448 }, 449 ec2Mocks: func(m *mocks.MockEC2APIMockRecorder) { 450 m.DescribeSubnets(gomock.Eq(&ec2.DescribeSubnetsInput{ 451 SubnetIds: []*string{ 452 aws.String(elbSubnetID), 453 }, 454 })). 455 Return(&ec2.DescribeSubnetsOutput{ 456 Subnets: []*ec2.Subnet{ 457 { 458 SubnetId: aws.String(elbSubnetID), 459 AvailabilityZone: aws.String(az), 460 }, 461 }, 462 }, nil) 463 }, 464 check: func(t *testing.T, err error) { 465 t.Helper() 466 if err != nil { 467 t.Fatalf("did not expect error: %v", err) 468 } 469 }, 470 }, 471 { 472 name: "load balancer subnets specified in a different az from the instance", 473 awsCluster: &infrav1.AWSCluster{ 474 ObjectMeta: metav1.ObjectMeta{Name: clusterName}, 475 Spec: infrav1.AWSClusterSpec{ 476 NetworkSpec: infrav1.NetworkSpec{ 477 Subnets: infrav1.Subnets{{ 478 ID: clusterSubnetID, 479 AvailabilityZone: az, 480 }}, 481 }, 482 ControlPlaneLoadBalancer: &infrav1.AWSLoadBalancerSpec{ 483 Name: aws.String(elbName), 484 Subnets: []string{elbSubnetID}, 485 }, 486 }, 487 }, 488 elbAPIMocks: func(m *mocks.MockELBAPIMockRecorder) { 489 m.DescribeLoadBalancers(gomock.Eq(&elb.DescribeLoadBalancersInput{ 490 LoadBalancerNames: aws.StringSlice([]string{elbName}), 491 })). 492 Return(&elb.DescribeLoadBalancersOutput{ 493 LoadBalancerDescriptions: []*elb.LoadBalancerDescription{ 494 { 495 Scheme: aws.String(string(infrav1.ClassicELBSchemeInternetFacing)), 496 Subnets: []*string{aws.String(elbSubnetID)}, 497 AvailabilityZones: []*string{aws.String(differentAZ)}, 498 }, 499 }, 500 }, nil) 501 m.DescribeLoadBalancerAttributes(gomock.Eq(&elb.DescribeLoadBalancerAttributesInput{ 502 LoadBalancerName: aws.String(elbName), 503 })). 504 Return(&elb.DescribeLoadBalancerAttributesOutput{ 505 LoadBalancerAttributes: &elb.LoadBalancerAttributes{ 506 CrossZoneLoadBalancing: &elb.CrossZoneLoadBalancing{ 507 Enabled: aws.Bool(false), 508 }, 509 }, 510 }, nil) 511 m.DescribeTags(&elb.DescribeTagsInput{LoadBalancerNames: []*string{aws.String(elbName)}}).Return( 512 &elb.DescribeTagsOutput{ 513 TagDescriptions: []*elb.TagDescription{ 514 { 515 LoadBalancerName: aws.String(elbName), 516 Tags: []*elb.Tag{{ 517 Key: aws.String(infrav1.ClusterTagKey(clusterName)), 518 Value: aws.String(string(infrav1.ResourceLifecycleOwned)), 519 }}, 520 }, 521 }, 522 }, nil) 523 }, 524 ec2Mocks: func(m *mocks.MockEC2APIMockRecorder) { 525 m.DescribeSubnets(gomock.Eq(&ec2.DescribeSubnetsInput{ 526 SubnetIds: []*string{ 527 aws.String(elbSubnetID), 528 }, 529 })). 530 Return(&ec2.DescribeSubnetsOutput{ 531 Subnets: []*ec2.Subnet{ 532 { 533 SubnetId: aws.String(elbSubnetID), 534 AvailabilityZone: aws.String(differentAZ), 535 }, 536 }, 537 }, nil) 538 }, 539 check: func(t *testing.T, err error) { 540 t.Helper() 541 expectedErrMsg := "failed to register instance with APIServer ELB \"bar-apiserver\": instance is in availability zone \"us-west-1a\", no public subnets attached to the ELB in the same zone" 542 if err == nil { 543 t.Fatalf("Expected error, but got nil") 544 } 545 546 if !strings.Contains(err.Error(), expectedErrMsg) { 547 t.Fatalf("Expected error: %s\nInstead got: %s", expectedErrMsg, err.Error()) 548 } 549 }, 550 }, 551 } 552 553 for _, tc := range tests { 554 t.Run(tc.name, func(t *testing.T) { 555 mockCtrl := gomock.NewController(t) 556 defer mockCtrl.Finish() 557 elbAPIMocks := mocks.NewMockELBAPI(mockCtrl) 558 ec2Mock := mocks.NewMockEC2API(mockCtrl) 559 560 scheme, err := setupScheme() 561 if err != nil { 562 t.Fatal(err) 563 } 564 565 client := fake.NewClientBuilder().WithScheme(scheme).Build() 566 clusterScope, err := scope.NewClusterScope(scope.ClusterScopeParams{ 567 Client: client, 568 Cluster: &clusterv1.Cluster{ 569 ObjectMeta: metav1.ObjectMeta{ 570 Namespace: namespace, 571 Name: clusterName, 572 }, 573 }, 574 AWSCluster: tc.awsCluster, 575 }) 576 if err != nil { 577 t.Fatal(err) 578 } 579 580 instance := &infrav1.Instance{ 581 ID: instanceID, 582 SubnetID: clusterSubnetID, 583 } 584 585 tc.elbAPIMocks(elbAPIMocks.EXPECT()) 586 tc.ec2Mocks(ec2Mock.EXPECT()) 587 588 s := &Service{ 589 scope: clusterScope, 590 EC2Client: ec2Mock, 591 ELBClient: elbAPIMocks, 592 } 593 594 err = s.RegisterInstanceWithAPIServerELB(instance) 595 tc.check(t, err) 596 }) 597 } 598 } 599 600 func TestDeleteAPIServerELB(t *testing.T) { 601 clusterName := "bar" //nolint:goconst // does not need to be a package-level const 602 elbName := "bar-apiserver" 603 tests := []struct { 604 name string 605 elbAPIMocks func(m *mocks.MockELBAPIMockRecorder) 606 verifyAWSCluster func(*infrav1.AWSCluster) 607 }{ 608 { 609 name: "if control plane ELB is not found, do nothing", 610 elbAPIMocks: func(m *mocks.MockELBAPIMockRecorder) { 611 m.DescribeLoadBalancers(gomock.Eq(&elb.DescribeLoadBalancersInput{ 612 LoadBalancerNames: aws.StringSlice([]string{elbName}), 613 })).Return(nil, awserr.New(elb.ErrCodeAccessPointNotFoundException, "", nil)) 614 }, 615 verifyAWSCluster: func(awsCluster *infrav1.AWSCluster) { 616 loadBalancerConditionReady := conditions.IsTrue(awsCluster, infrav1.LoadBalancerReadyCondition) 617 if loadBalancerConditionReady { 618 t.Fatalf("Expected LoadBalancerReady condition to be False, but was True") 619 } 620 loadBalancerConditionReason := conditions.GetReason(awsCluster, infrav1.LoadBalancerReadyCondition) 621 if loadBalancerConditionReason != clusterv1.DeletedReason { 622 t.Fatalf("Expected LoadBalancerReady condition reason to be Deleted, but was %s", loadBalancerConditionReason) 623 } 624 }, 625 }, 626 { 627 name: "if control plane ELB is found, and it is not managed, do nothing", 628 elbAPIMocks: func(m *mocks.MockELBAPIMockRecorder) { 629 m.DescribeLoadBalancers(&elb.DescribeLoadBalancersInput{LoadBalancerNames: []*string{aws.String(elbName)}}).Return( 630 &elb.DescribeLoadBalancersOutput{ 631 LoadBalancerDescriptions: []*elb.LoadBalancerDescription{ 632 { 633 LoadBalancerName: aws.String(elbName), 634 Scheme: aws.String(string(infrav1.ClassicELBSchemeInternetFacing)), 635 }, 636 }, 637 }, 638 nil, 639 ) 640 641 m.DescribeLoadBalancerAttributes(&elb.DescribeLoadBalancerAttributesInput{LoadBalancerName: aws.String(elbName)}).Return( 642 &elb.DescribeLoadBalancerAttributesOutput{ 643 LoadBalancerAttributes: &elb.LoadBalancerAttributes{ 644 CrossZoneLoadBalancing: &elb.CrossZoneLoadBalancing{ 645 Enabled: aws.Bool(false), 646 }, 647 }, 648 }, 649 nil, 650 ) 651 652 m.DescribeTags(&elb.DescribeTagsInput{LoadBalancerNames: []*string{aws.String(elbName)}}).Return( 653 &elb.DescribeTagsOutput{ 654 TagDescriptions: []*elb.TagDescription{ 655 { 656 LoadBalancerName: aws.String(elbName), 657 Tags: []*elb.Tag{}, 658 }, 659 }, 660 }, 661 nil, 662 ) 663 }, 664 verifyAWSCluster: func(awsCluster *infrav1.AWSCluster) { 665 loadBalancerConditionReady := conditions.IsTrue(awsCluster, infrav1.LoadBalancerReadyCondition) 666 if loadBalancerConditionReady { 667 t.Fatalf("Expected LoadBalancerReady condition to be False, but was True") 668 } 669 loadBalancerConditionReason := conditions.GetReason(awsCluster, infrav1.LoadBalancerReadyCondition) 670 if loadBalancerConditionReason != clusterv1.DeletedReason { 671 t.Fatalf("Expected LoadBalancerReady condition reason to be Deleted, but was %s", loadBalancerConditionReason) 672 } 673 }, 674 }, 675 { 676 name: "if control plane ELB is found, and it is managed, delete the ELB", 677 elbAPIMocks: func(m *mocks.MockELBAPIMockRecorder) { 678 m.DescribeLoadBalancers(&elb.DescribeLoadBalancersInput{LoadBalancerNames: []*string{aws.String(elbName)}}).Return( 679 &elb.DescribeLoadBalancersOutput{ 680 LoadBalancerDescriptions: []*elb.LoadBalancerDescription{ 681 { 682 LoadBalancerName: aws.String(elbName), 683 Scheme: aws.String(string(infrav1.ClassicELBSchemeInternetFacing)), 684 }, 685 }, 686 }, 687 nil, 688 ) 689 690 m.DescribeLoadBalancerAttributes(&elb.DescribeLoadBalancerAttributesInput{LoadBalancerName: aws.String(elbName)}).Return( 691 &elb.DescribeLoadBalancerAttributesOutput{ 692 LoadBalancerAttributes: &elb.LoadBalancerAttributes{ 693 CrossZoneLoadBalancing: &elb.CrossZoneLoadBalancing{ 694 Enabled: aws.Bool(false), 695 }, 696 }, 697 }, 698 nil, 699 ) 700 701 m.DescribeTags(&elb.DescribeTagsInput{LoadBalancerNames: []*string{aws.String(elbName)}}).Return( 702 &elb.DescribeTagsOutput{ 703 TagDescriptions: []*elb.TagDescription{ 704 { 705 LoadBalancerName: aws.String(elbName), 706 Tags: []*elb.Tag{{ 707 Key: aws.String(infrav1.ClusterTagKey(clusterName)), 708 Value: aws.String(string(infrav1.ResourceLifecycleOwned)), 709 }}, 710 }, 711 }, 712 }, 713 nil, 714 ) 715 716 m.DeleteLoadBalancer(&elb.DeleteLoadBalancerInput{LoadBalancerName: aws.String(elbName)}).Return( 717 &elb.DeleteLoadBalancerOutput{}, nil) 718 719 m.DescribeLoadBalancers(&elb.DescribeLoadBalancersInput{LoadBalancerNames: []*string{aws.String(elbName)}}).Return( 720 &elb.DescribeLoadBalancersOutput{ 721 LoadBalancerDescriptions: []*elb.LoadBalancerDescription{}, 722 }, 723 nil, 724 ) 725 }, 726 verifyAWSCluster: func(awsCluster *infrav1.AWSCluster) { 727 loadBalancerConditionReady := conditions.IsTrue(awsCluster, infrav1.LoadBalancerReadyCondition) 728 if loadBalancerConditionReady { 729 t.Fatalf("Expected LoadBalancerReady condition to be False, but was True") 730 } 731 loadBalancerConditionReason := conditions.GetReason(awsCluster, infrav1.LoadBalancerReadyCondition) 732 if loadBalancerConditionReason != clusterv1.DeletedReason { 733 t.Fatalf("Expected LoadBalancerReady condition reason to be Deleted, but was %s", loadBalancerConditionReason) 734 } 735 }, 736 }, 737 } 738 739 for _, tc := range tests { 740 t.Run(tc.name, func(t *testing.T) { 741 mockCtrl := gomock.NewController(t) 742 defer mockCtrl.Finish() 743 rgapiMock := mocks.NewMockResourceGroupsTaggingAPIAPI(mockCtrl) 744 elbapiMock := mocks.NewMockELBAPI(mockCtrl) 745 746 scheme, err := setupScheme() 747 if err != nil { 748 t.Fatal(err) 749 } 750 751 awsCluster := &infrav1.AWSCluster{ 752 ObjectMeta: metav1.ObjectMeta{Name: "test"}, 753 Spec: infrav1.AWSClusterSpec{ 754 ControlPlaneLoadBalancer: &infrav1.AWSLoadBalancerSpec{ 755 Name: aws.String(elbName), 756 }, 757 }, 758 } 759 760 client := fake.NewClientBuilder().WithScheme(scheme).Build() 761 ctx := context.TODO() 762 client.Create(ctx, awsCluster) 763 764 clusterScope, err := scope.NewClusterScope(scope.ClusterScopeParams{ 765 Cluster: &clusterv1.Cluster{ 766 ObjectMeta: metav1.ObjectMeta{ 767 Namespace: "foo", 768 Name: clusterName, 769 }, 770 }, 771 AWSCluster: awsCluster, 772 Client: client, 773 }) 774 if err != nil { 775 t.Fatal(err) 776 } 777 778 tc.elbAPIMocks(elbapiMock.EXPECT()) 779 780 s := &Service{ 781 scope: clusterScope, 782 ResourceTaggingClient: rgapiMock, 783 ELBClient: elbapiMock, 784 } 785 786 err = s.deleteAPIServerELB() 787 if err != nil { 788 t.Fatal(err) 789 } 790 791 tc.verifyAWSCluster(awsCluster) 792 }) 793 } 794 } 795 796 func TestDeleteAWSCloudProviderELBs(t *testing.T) { 797 clusterName := "bar" 798 tests := []struct { 799 name string 800 rgAPIMocks func(m *mocks.MockResourceGroupsTaggingAPIAPIMockRecorder) 801 elbAPIMocks func(m *mocks.MockELBAPIMockRecorder) 802 postDeleteRGAPIMocks func(m *mocks.MockResourceGroupsTaggingAPIAPIMockRecorder) 803 postDeleteElbAPIMocks func(m *mocks.MockELBAPIMockRecorder) 804 }{ 805 { 806 name: "discover ELBs with Resource Groups Tagging API and then delete successfully", 807 rgAPIMocks: func(m *mocks.MockResourceGroupsTaggingAPIAPIMockRecorder) { 808 m.GetResourcesPages(&rgapi.GetResourcesInput{ 809 ResourceTypeFilters: aws.StringSlice([]string{elbResourceType}), 810 TagFilters: []*rgapi.TagFilter{ 811 { 812 Key: aws.String(infrav1.ClusterAWSCloudProviderTagKey(clusterName)), 813 Values: aws.StringSlice([]string{string(infrav1.ResourceLifecycleOwned)}), 814 }, 815 }, 816 }, gomock.Any()).Do(func(_, y interface{}) { 817 funct := y.(func(output *rgapi.GetResourcesOutput, lastPage bool) bool) 818 funct(&rgapi.GetResourcesOutput{ 819 ResourceTagMappingList: []*rgapi.ResourceTagMapping{ 820 { 821 ResourceARN: aws.String("arn:aws:elasticloadbalancing:eu-west-2:1234567890:loadbalancer/lb-service-name"), 822 Tags: []*rgapi.Tag{{ 823 Key: aws.String(infrav1.ClusterAWSCloudProviderTagKey(clusterName)), 824 Value: aws.String(string(infrav1.ResourceLifecycleOwned)), 825 }}, 826 }, 827 }, 828 }, true) 829 }).Return(nil) 830 }, 831 elbAPIMocks: func(m *mocks.MockELBAPIMockRecorder) { 832 m.DeleteLoadBalancer(gomock.Eq(&elb.DeleteLoadBalancerInput{LoadBalancerName: aws.String("lb-service-name")})).Return(nil, nil) 833 }, 834 postDeleteRGAPIMocks: func(m *mocks.MockResourceGroupsTaggingAPIAPIMockRecorder) { 835 m.GetResourcesPages(&rgapi.GetResourcesInput{ 836 ResourceTypeFilters: aws.StringSlice([]string{elbResourceType}), 837 TagFilters: []*rgapi.TagFilter{ 838 { 839 Key: aws.String(infrav1.ClusterAWSCloudProviderTagKey(clusterName)), 840 Values: aws.StringSlice([]string{string(infrav1.ResourceLifecycleOwned)}), 841 }, 842 }, 843 }, gomock.Any()).Do(func(_, y interface{}) { 844 funct := y.(func(output *rgapi.GetResourcesOutput, lastPage bool) bool) 845 funct(&rgapi.GetResourcesOutput{ 846 ResourceTagMappingList: []*rgapi.ResourceTagMapping{}, 847 }, true) 848 }).Return(nil) 849 }, 850 }, 851 { 852 name: "fall back to ELB API when Resource Groups Tagging API fails and then delete successfully", 853 rgAPIMocks: func(m *mocks.MockResourceGroupsTaggingAPIAPIMockRecorder) { 854 m.GetResourcesPages(gomock.Any(), gomock.Any()).Return(errors.Errorf("connection failure")).AnyTimes() 855 }, 856 elbAPIMocks: func(m *mocks.MockELBAPIMockRecorder) { 857 m.DescribeLoadBalancersPages(gomock.Any(), gomock.Any()).Do(func(_, y interface{}) { 858 funct := y.(func(output *elb.DescribeLoadBalancersOutput, lastPage bool) bool) 859 funct(&elb.DescribeLoadBalancersOutput{ 860 LoadBalancerDescriptions: []*elb.LoadBalancerDescription{ 861 { 862 LoadBalancerName: aws.String("lb-service-name"), 863 }, 864 { 865 LoadBalancerName: aws.String("another-service-not-owned"), 866 }, 867 { 868 LoadBalancerName: aws.String("service-without-tags"), 869 }, 870 }, 871 }, true) 872 }).Return(nil) 873 m.DescribeTags(&elb.DescribeTagsInput{LoadBalancerNames: []*string{aws.String("lb-service-name"), aws.String("another-service-not-owned"), aws.String("service-without-tags")}}).Return(&elb.DescribeTagsOutput{ 874 TagDescriptions: []*elb.TagDescription{ 875 { 876 LoadBalancerName: aws.String("lb-service-name"), 877 Tags: []*elb.Tag{{ 878 Key: aws.String(infrav1.ClusterAWSCloudProviderTagKey(clusterName)), 879 Value: aws.String(string(infrav1.ResourceLifecycleOwned)), 880 }}, 881 }, 882 { 883 LoadBalancerName: aws.String("another-service-not-owned"), 884 Tags: []*elb.Tag{{ 885 Key: aws.String("some-tag-key"), 886 Value: aws.String("some-tag-value"), 887 }}, 888 }, 889 { 890 LoadBalancerName: aws.String("service-without-tags"), 891 Tags: []*elb.Tag{}, 892 }, 893 }, 894 }, nil) 895 m.DeleteLoadBalancer(gomock.Eq(&elb.DeleteLoadBalancerInput{LoadBalancerName: aws.String("lb-service-name")})).Return(nil, nil) 896 }, 897 postDeleteElbAPIMocks: func(m *mocks.MockELBAPIMockRecorder) { 898 m.DescribeLoadBalancersPages(gomock.Any(), gomock.Any()).Return(nil) 899 }, 900 }, 901 } 902 903 for _, tc := range tests { 904 t.Run(tc.name, func(t *testing.T) { 905 mockCtrl := gomock.NewController(t) 906 defer mockCtrl.Finish() 907 rgapiMock := mocks.NewMockResourceGroupsTaggingAPIAPI(mockCtrl) 908 elbapiMock := mocks.NewMockELBAPI(mockCtrl) 909 910 scheme, err := setupScheme() 911 if err != nil { 912 t.Fatal(err) 913 } 914 915 awsCluster := &infrav1.AWSCluster{ 916 ObjectMeta: metav1.ObjectMeta{Name: "test"}, 917 Spec: infrav1.AWSClusterSpec{}, 918 } 919 920 client := fake.NewClientBuilder().WithScheme(scheme).Build() 921 ctx := context.TODO() 922 client.Create(ctx, awsCluster) 923 924 clusterScope, err := scope.NewClusterScope(scope.ClusterScopeParams{ 925 Cluster: &clusterv1.Cluster{ 926 ObjectMeta: metav1.ObjectMeta{ 927 Namespace: "foo", 928 Name: clusterName, 929 }, 930 }, 931 AWSCluster: awsCluster, 932 Client: client, 933 }) 934 if err != nil { 935 t.Fatal(err) 936 } 937 938 tc.rgAPIMocks(rgapiMock.EXPECT()) 939 tc.elbAPIMocks(elbapiMock.EXPECT()) 940 if tc.postDeleteElbAPIMocks != nil { 941 tc.postDeleteElbAPIMocks(elbapiMock.EXPECT()) 942 } 943 if tc.postDeleteRGAPIMocks != nil { 944 tc.postDeleteRGAPIMocks(rgapiMock.EXPECT()) 945 } 946 947 s := &Service{ 948 scope: clusterScope, 949 ResourceTaggingClient: rgapiMock, 950 ELBClient: elbapiMock, 951 } 952 953 err = s.deleteAWSCloudProviderELBs() 954 if err != nil { 955 t.Fatal(err) 956 } 957 }) 958 } 959 } 960 961 func TestDescribeLoadbalancers(t *testing.T) { 962 clusterName := "bar" 963 tests := []struct { 964 name string 965 lbName string 966 rgAPIMocks func(m *mocks.MockResourceGroupsTaggingAPIAPIMockRecorder) 967 DescribeElbAPIMocks func(m *mocks.MockELBAPIMockRecorder) 968 }{ 969 { 970 name: "Error if existing loadbalancer with same name doesn't have same scheme", 971 lbName: "bar-apiserver", 972 rgAPIMocks: func(m *mocks.MockResourceGroupsTaggingAPIAPIMockRecorder) { 973 m.GetResourcesPages(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() 974 }, 975 DescribeElbAPIMocks: func(m *mocks.MockELBAPIMockRecorder) { 976 m.DescribeLoadBalancers(gomock.Eq(&elb.DescribeLoadBalancersInput{ 977 LoadBalancerNames: aws.StringSlice([]string{"bar-apiserver"}), 978 })).Return(&elb.DescribeLoadBalancersOutput{LoadBalancerDescriptions: []*elb.LoadBalancerDescription{{Scheme: pointer.StringPtr(string(infrav1.ClassicELBSchemeInternal))}}}, nil) 979 }, 980 }, 981 } 982 983 for _, tc := range tests { 984 t.Run(tc.name, func(t *testing.T) { 985 mockCtrl := gomock.NewController(t) 986 defer mockCtrl.Finish() 987 rgapiMock := mocks.NewMockResourceGroupsTaggingAPIAPI(mockCtrl) 988 elbapiMock := mocks.NewMockELBAPI(mockCtrl) 989 990 scheme, err := setupScheme() 991 if err != nil { 992 t.Fatal(err) 993 } 994 awsCluster := &infrav1.AWSCluster{ 995 ObjectMeta: metav1.ObjectMeta{Name: "test"}, 996 Spec: infrav1.AWSClusterSpec{ControlPlaneLoadBalancer: &infrav1.AWSLoadBalancerSpec{ 997 Scheme: &infrav1.ClassicELBSchemeInternetFacing, 998 }}, 999 } 1000 1001 client := fake.NewClientBuilder().WithScheme(scheme).Build() 1002 ctx := context.TODO() 1003 client.Create(ctx, awsCluster) 1004 1005 clusterScope, err := scope.NewClusterScope(scope.ClusterScopeParams{ 1006 Cluster: &clusterv1.Cluster{ 1007 ObjectMeta: metav1.ObjectMeta{ 1008 Namespace: "foo", 1009 Name: clusterName, 1010 }, 1011 }, 1012 AWSCluster: awsCluster, 1013 Client: client, 1014 }) 1015 if err != nil { 1016 t.Fatal(err) 1017 } 1018 1019 tc.rgAPIMocks(rgapiMock.EXPECT()) 1020 tc.DescribeElbAPIMocks(elbapiMock.EXPECT()) 1021 1022 s := &Service{ 1023 scope: clusterScope, 1024 ResourceTaggingClient: rgapiMock, 1025 ELBClient: elbapiMock, 1026 } 1027 1028 _, err = s.describeClassicELB(tc.lbName) 1029 if err == nil { 1030 t.Fatal(err) 1031 } 1032 }) 1033 } 1034 } 1035 1036 func TestChunkELBs(t *testing.T) { 1037 base := "loadbalancer" 1038 var names []string 1039 for i := 0; i < 25; i++ { 1040 names = append(names, fmt.Sprintf("%s+%d", base, i)) 1041 } 1042 tests := []struct { 1043 testName string 1044 names []string 1045 expectedChunkArrayLen int 1046 }{ 1047 { 1048 testName: "When the user has more the 20 ELBs", 1049 names: names, 1050 expectedChunkArrayLen: 2, 1051 }, { 1052 testName: "When the user has less than 20 ELBs", 1053 names: []string{"loadBalancer-00"}, 1054 expectedChunkArrayLen: 1, 1055 }, 1056 } 1057 for _, tc := range tests { 1058 t.Run(tc.testName, func(t *testing.T) { 1059 ans := chunkELBs(tc.names) 1060 if len(ans) != tc.expectedChunkArrayLen { 1061 t.Errorf("got %d, want %d", len(ans), tc.expectedChunkArrayLen) 1062 } 1063 }) 1064 } 1065 } 1066 1067 func setupScheme() (*runtime.Scheme, error) { 1068 scheme := runtime.NewScheme() 1069 if err := clusterv1.AddToScheme(scheme); err != nil { 1070 return nil, err 1071 } 1072 if err := infrav1.AddToScheme(scheme); err != nil { 1073 return nil, err 1074 } 1075 return scheme, nil 1076 }