sigs.k8s.io/cluster-api-provider-aws@v1.5.5/pkg/cloud/services/autoscaling/autoscalinggroup_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 asg 18 19 import ( 20 "sort" 21 "testing" 22 23 "github.com/aws/aws-sdk-go/aws" 24 "github.com/aws/aws-sdk-go/service/autoscaling" 25 "github.com/aws/aws-sdk-go/service/ec2" 26 "github.com/golang/mock/gomock" 27 "github.com/google/go-cmp/cmp" 28 . "github.com/onsi/gomega" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/apimachinery/pkg/runtime" 31 "sigs.k8s.io/controller-runtime/pkg/client" 32 "sigs.k8s.io/controller-runtime/pkg/client/fake" 33 34 infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1beta1" 35 expinfrav1 "sigs.k8s.io/cluster-api-provider-aws/exp/api/v1beta1" 36 "sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/awserrors" 37 "sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/scope" 38 "sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services/autoscaling/mock_autoscalingiface" 39 "sigs.k8s.io/cluster-api-provider-aws/test/mocks" 40 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 41 expclusterv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1" 42 ) 43 44 func TestService_GetASGByName(t *testing.T) { 45 mockCtrl := gomock.NewController(t) 46 defer mockCtrl.Finish() 47 tests := []struct { 48 name string 49 machinePoolName string 50 wantErr bool 51 wantASG bool 52 expect func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) 53 }{ 54 { 55 name: "should return nil if ASG is not found", 56 machinePoolName: "test-asg-is-not-present", 57 wantErr: false, 58 wantASG: false, 59 expect: func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) { 60 m.DescribeAutoScalingGroups(gomock.Eq(&autoscaling.DescribeAutoScalingGroupsInput{ 61 AutoScalingGroupNames: []*string{ 62 aws.String("test-asg-is-not-present"), 63 }, 64 })). 65 Return(nil, awserrors.NewNotFound("not found")) 66 }, 67 }, 68 { 69 name: "should return error if describe asg failed", 70 machinePoolName: "dependency-failure-occurred", 71 wantErr: true, 72 wantASG: false, 73 expect: func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) { 74 m.DescribeAutoScalingGroups(gomock.Eq(&autoscaling.DescribeAutoScalingGroupsInput{ 75 AutoScalingGroupNames: []*string{ 76 aws.String("dependency-failure-occurred"), 77 }, 78 })). 79 Return(nil, awserrors.NewFailedDependency("unknown error occurred")) 80 }, 81 }, 82 { 83 name: "should return ASG, if found", 84 machinePoolName: "test-group-is-present", 85 wantErr: false, 86 wantASG: true, 87 expect: func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) { 88 m.DescribeAutoScalingGroups(gomock.Eq(&autoscaling.DescribeAutoScalingGroupsInput{ 89 AutoScalingGroupNames: []*string{ 90 aws.String("test-group-is-present"), 91 }, 92 })). 93 Return(&autoscaling.DescribeAutoScalingGroupsOutput{ 94 AutoScalingGroups: []*autoscaling.Group{ 95 { 96 AutoScalingGroupName: aws.String("test-group-is-present"), 97 MixedInstancesPolicy: &autoscaling.MixedInstancesPolicy{ 98 InstancesDistribution: &autoscaling.InstancesDistribution{ 99 OnDemandAllocationStrategy: aws.String("prioritized"), 100 }, 101 LaunchTemplate: &autoscaling.LaunchTemplate{}, 102 }, 103 }, 104 }}, nil) 105 }, 106 }, 107 } 108 for _, tt := range tests { 109 t.Run(tt.name, func(t *testing.T) { 110 g := NewWithT(t) 111 fakeClient := getFakeClient() 112 113 clusterScope, err := getClusterScope(fakeClient) 114 g.Expect(err).ToNot(HaveOccurred()) 115 asgMock := mock_autoscalingiface.NewMockAutoScalingAPI(mockCtrl) 116 tt.expect(asgMock.EXPECT()) 117 s := NewService(clusterScope) 118 s.ASGClient = asgMock 119 120 mps, err := getMachinePoolScope(fakeClient, clusterScope) 121 g.Expect(err).ToNot(HaveOccurred()) 122 mps.AWSMachinePool.Name = tt.machinePoolName 123 124 asg, err := s.GetASGByName(mps) 125 checkErr(tt.wantErr, err, g) 126 checkASG(tt.wantASG, asg, g) 127 }) 128 } 129 } 130 131 func TestService_SDKToAutoScalingGroup(t *testing.T) { 132 tests := []struct { 133 name string 134 input *autoscaling.Group 135 want *expinfrav1.AutoScalingGroup 136 wantErr bool 137 }{ 138 { 139 name: "valid input - all required fields filled", 140 input: &autoscaling.Group{ 141 AutoScalingGroupARN: aws.String("test-id"), 142 AutoScalingGroupName: aws.String("test-name"), 143 DesiredCapacity: aws.Int64(1234), 144 MaxSize: aws.Int64(1234), 145 MinSize: aws.Int64(1234), 146 CapacityRebalance: aws.Bool(true), 147 MixedInstancesPolicy: &autoscaling.MixedInstancesPolicy{ 148 InstancesDistribution: &autoscaling.InstancesDistribution{ 149 OnDemandAllocationStrategy: aws.String("prioritized"), 150 OnDemandBaseCapacity: aws.Int64(1234), 151 OnDemandPercentageAboveBaseCapacity: aws.Int64(1234), 152 SpotAllocationStrategy: aws.String("lowest-price"), 153 }, 154 LaunchTemplate: &autoscaling.LaunchTemplate{ 155 Overrides: []*autoscaling.LaunchTemplateOverrides{ 156 { 157 InstanceType: aws.String("t2.medium"), 158 WeightedCapacity: aws.String("test-weighted-cap"), 159 }, 160 }, 161 }, 162 }, 163 }, 164 want: &expinfrav1.AutoScalingGroup{ 165 ID: "test-id", 166 Name: "test-name", 167 DesiredCapacity: aws.Int32(1234), 168 MaxSize: int32(1234), 169 MinSize: int32(1234), 170 CapacityRebalance: true, 171 MixedInstancesPolicy: &expinfrav1.MixedInstancesPolicy{ 172 InstancesDistribution: &expinfrav1.InstancesDistribution{ 173 OnDemandAllocationStrategy: expinfrav1.OnDemandAllocationStrategyPrioritized, 174 OnDemandBaseCapacity: aws.Int64(1234), 175 OnDemandPercentageAboveBaseCapacity: aws.Int64(1234), 176 SpotAllocationStrategy: expinfrav1.SpotAllocationStrategyLowestPrice, 177 }, 178 Overrides: []expinfrav1.Overrides{ 179 { 180 InstanceType: "t2.medium", 181 }, 182 }, 183 }, 184 }, 185 wantErr: false, 186 }, 187 { 188 name: "valid input - all fields filled", 189 input: &autoscaling.Group{ 190 AutoScalingGroupARN: aws.String("test-id"), 191 AutoScalingGroupName: aws.String("test-name"), 192 DesiredCapacity: aws.Int64(1234), 193 MaxSize: aws.Int64(1234), 194 MinSize: aws.Int64(1234), 195 CapacityRebalance: aws.Bool(true), 196 MixedInstancesPolicy: &autoscaling.MixedInstancesPolicy{ 197 InstancesDistribution: &autoscaling.InstancesDistribution{ 198 OnDemandAllocationStrategy: aws.String("prioritized"), 199 OnDemandBaseCapacity: aws.Int64(1234), 200 OnDemandPercentageAboveBaseCapacity: aws.Int64(1234), 201 SpotAllocationStrategy: aws.String("lowest-price"), 202 }, 203 LaunchTemplate: &autoscaling.LaunchTemplate{ 204 Overrides: []*autoscaling.LaunchTemplateOverrides{ 205 { 206 InstanceType: aws.String("t2.medium"), 207 WeightedCapacity: aws.String("test-weighted-cap"), 208 }, 209 }, 210 }, 211 }, 212 Status: aws.String("status"), 213 Tags: []*autoscaling.TagDescription{ 214 { 215 Key: aws.String("key"), 216 Value: aws.String("value"), 217 }, 218 }, 219 Instances: []*autoscaling.Instance{ 220 { 221 InstanceId: aws.String("instanceId"), 222 LifecycleState: aws.String("lifecycleState"), 223 AvailabilityZone: aws.String("us-east-1a"), 224 }, 225 }, 226 }, 227 want: &expinfrav1.AutoScalingGroup{ 228 ID: "test-id", 229 Name: "test-name", 230 DesiredCapacity: aws.Int32(1234), 231 MaxSize: int32(1234), 232 MinSize: int32(1234), 233 CapacityRebalance: true, 234 MixedInstancesPolicy: &expinfrav1.MixedInstancesPolicy{ 235 InstancesDistribution: &expinfrav1.InstancesDistribution{ 236 OnDemandAllocationStrategy: expinfrav1.OnDemandAllocationStrategyPrioritized, 237 OnDemandBaseCapacity: aws.Int64(1234), 238 OnDemandPercentageAboveBaseCapacity: aws.Int64(1234), 239 SpotAllocationStrategy: expinfrav1.SpotAllocationStrategyLowestPrice, 240 }, 241 Overrides: []expinfrav1.Overrides{ 242 { 243 InstanceType: "t2.medium", 244 }, 245 }, 246 }, 247 Status: "status", 248 Tags: map[string]string{ 249 "key": "value", 250 }, 251 Instances: []infrav1.Instance{ 252 { 253 ID: "instanceId", 254 State: "lifecycleState", 255 AvailabilityZone: "us-east-1a", 256 }, 257 }, 258 }, 259 wantErr: false, 260 }, 261 { 262 name: "valid input - without mixedInstancesPolicy", 263 input: &autoscaling.Group{ 264 AutoScalingGroupARN: aws.String("test-id"), 265 AutoScalingGroupName: aws.String("test-name"), 266 DesiredCapacity: aws.Int64(1234), 267 MaxSize: aws.Int64(1234), 268 MinSize: aws.Int64(1234), 269 CapacityRebalance: aws.Bool(true), 270 MixedInstancesPolicy: nil, 271 }, 272 want: &expinfrav1.AutoScalingGroup{ 273 ID: "test-id", 274 Name: "test-name", 275 DesiredCapacity: aws.Int32(1234), 276 MaxSize: int32(1234), 277 MinSize: int32(1234), 278 CapacityRebalance: true, 279 MixedInstancesPolicy: nil, 280 }, 281 wantErr: false, 282 }, 283 } 284 for _, tt := range tests { 285 t.Run(tt.name, func(t *testing.T) { 286 s := &Service{} 287 got, err := s.SDKToAutoScalingGroup(tt.input) 288 if (err != nil) != tt.wantErr { 289 t.Errorf("Service.SDKToAutoScalingGroup() error = %v, wantErr %v", err, tt.wantErr) 290 return 291 } 292 if !cmp.Equal(got, tt.want) { 293 t.Errorf("Service.SDKToAutoScalingGroup() = %v, want %v", got, tt.want) 294 } 295 }) 296 } 297 } 298 299 func TestService_ASGIfExists(t *testing.T) { 300 mockCtrl := gomock.NewController(t) 301 defer mockCtrl.Finish() 302 303 tests := []struct { 304 name string 305 asgName *string 306 wantErr bool 307 wantASG bool 308 expect func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) 309 }{ 310 { 311 name: "should return nil if ASG name is not given", 312 asgName: nil, 313 wantErr: false, 314 wantASG: false, 315 expect: func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) {}, 316 }, 317 { 318 name: "should return without error if ASG is not found", 319 asgName: aws.String("asgName"), 320 wantErr: false, 321 wantASG: false, 322 expect: func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) { 323 m.DescribeAutoScalingGroups(gomock.Eq(&autoscaling.DescribeAutoScalingGroupsInput{ 324 AutoScalingGroupNames: []*string{ 325 aws.String("asgName"), 326 }, 327 })). 328 Return(nil, awserrors.NewNotFound("resource not found")) 329 }, 330 }, 331 { 332 name: "should return error if describe ASG fails", 333 asgName: aws.String("asgName"), 334 wantErr: true, 335 wantASG: false, 336 expect: func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) { 337 m.DescribeAutoScalingGroups(gomock.Eq(&autoscaling.DescribeAutoScalingGroupsInput{ 338 AutoScalingGroupNames: []*string{ 339 aws.String("asgName"), 340 }, 341 })). 342 Return(nil, awserrors.NewFailedDependency("unknown error occurred")) 343 }, 344 }, 345 { 346 name: "should return ASG, if found", 347 asgName: aws.String("asgName"), 348 wantErr: false, 349 wantASG: true, 350 expect: func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) { 351 m.DescribeAutoScalingGroups(gomock.Eq(&autoscaling.DescribeAutoScalingGroupsInput{ 352 AutoScalingGroupNames: []*string{ 353 aws.String("asgName"), 354 }, 355 })). 356 Return(&autoscaling.DescribeAutoScalingGroupsOutput{ 357 AutoScalingGroups: []*autoscaling.Group{ 358 { 359 AutoScalingGroupName: aws.String("asgName"), 360 MixedInstancesPolicy: &autoscaling.MixedInstancesPolicy{ 361 InstancesDistribution: &autoscaling.InstancesDistribution{ 362 OnDemandAllocationStrategy: aws.String("prioritized"), 363 }, 364 LaunchTemplate: &autoscaling.LaunchTemplate{}, 365 }, 366 }, 367 }}, nil) 368 }, 369 }, 370 } 371 for _, tt := range tests { 372 t.Run(tt.name, func(t *testing.T) { 373 g := NewWithT(t) 374 fakeClient := getFakeClient() 375 376 clusterScope, err := getClusterScope(fakeClient) 377 g.Expect(err).ToNot(HaveOccurred()) 378 asgMock := mock_autoscalingiface.NewMockAutoScalingAPI(mockCtrl) 379 tt.expect(asgMock.EXPECT()) 380 s := NewService(clusterScope) 381 s.ASGClient = asgMock 382 383 asg, err := s.ASGIfExists(tt.asgName) 384 checkErr(tt.wantErr, err, g) 385 checkASG(tt.wantASG, asg, g) 386 }) 387 } 388 } 389 390 func TestService_CreateASG(t *testing.T) { 391 mockCtrl := gomock.NewController(t) 392 defer mockCtrl.Finish() 393 tests := []struct { 394 name string 395 machinePoolName string 396 setupMachinePoolScope func(*scope.MachinePoolScope) 397 wantErr bool 398 wantASG bool 399 expect func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) 400 }{ 401 { 402 name: "should return without error if create ASG is successful", 403 machinePoolName: "create-asg-success", 404 setupMachinePoolScope: func(mps *scope.MachinePoolScope) {}, 405 wantErr: false, 406 wantASG: false, 407 expect: func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) { 408 expected := &autoscaling.CreateAutoScalingGroupInput{ 409 AutoScalingGroupName: aws.String("create-asg-success"), 410 CapacityRebalance: aws.Bool(false), 411 DefaultCooldown: aws.Int64(0), 412 MixedInstancesPolicy: &autoscaling.MixedInstancesPolicy{ 413 InstancesDistribution: &autoscaling.InstancesDistribution{ 414 OnDemandAllocationStrategy: aws.String("prioritized"), 415 OnDemandBaseCapacity: aws.Int64(0), 416 OnDemandPercentageAboveBaseCapacity: aws.Int64(100), 417 SpotAllocationStrategy: aws.String(""), 418 }, 419 LaunchTemplate: &autoscaling.LaunchTemplate{ 420 LaunchTemplateSpecification: &autoscaling.LaunchTemplateSpecification{ 421 LaunchTemplateName: aws.String("create-asg-success"), 422 Version: aws.String("$Latest"), 423 }, 424 Overrides: []*autoscaling.LaunchTemplateOverrides{ 425 { 426 InstanceType: aws.String("t1.large"), 427 }, 428 }, 429 }, 430 }, 431 MaxSize: aws.Int64(0), 432 MinSize: aws.Int64(0), 433 Tags: []*autoscaling.Tag{ 434 { 435 Key: aws.String("kubernetes.io/cluster/test"), 436 PropagateAtLaunch: aws.Bool(false), 437 ResourceId: aws.String("create-asg-success"), 438 ResourceType: aws.String("auto-scaling-group"), 439 Value: aws.String("owned"), 440 }, 441 { 442 Key: aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test"), 443 PropagateAtLaunch: aws.Bool(false), 444 ResourceId: aws.String("create-asg-success"), 445 ResourceType: aws.String("auto-scaling-group"), 446 Value: aws.String("owned"), 447 }, 448 { 449 Key: aws.String("sigs.k8s.io/cluster-api-provider-aws/role"), 450 PropagateAtLaunch: aws.Bool(false), 451 ResourceId: aws.String("create-asg-success"), 452 ResourceType: aws.String("auto-scaling-group"), 453 Value: aws.String("node"), 454 }, 455 { 456 Key: aws.String("Name"), 457 PropagateAtLaunch: aws.Bool(false), 458 ResourceId: aws.String("create-asg-success"), 459 ResourceType: aws.String("auto-scaling-group"), 460 Value: aws.String("create-asg-success"), 461 }, 462 }, 463 VPCZoneIdentifier: aws.String("subnet1"), 464 } 465 466 m.CreateAutoScalingGroup(gomock.AssignableToTypeOf(&autoscaling.CreateAutoScalingGroupInput{})).Do( 467 func(actual *autoscaling.CreateAutoScalingGroupInput) (*autoscaling.CreateAutoScalingGroupOutput, error) { 468 sortTagsByKey := func(tags []*autoscaling.Tag) { 469 sort.Slice(tags, func(i, j int) bool { 470 return *(tags[i].Key) < *(tags[j].Key) 471 }) 472 } 473 // sorting tags to avoid failure due to different ordering of tags 474 sortTagsByKey(actual.Tags) 475 sortTagsByKey(expected.Tags) 476 if !cmp.Equal(expected, actual) { 477 t.Fatalf("Actual CreateAutoScalingGroupInput did not match expected, Actual : %v, Expected: %v", actual, expected) 478 } 479 return &autoscaling.CreateAutoScalingGroupOutput{}, nil 480 }) 481 }, 482 }, 483 { 484 name: "should return error if subnet not found for asg", 485 machinePoolName: "create-asg-fail", 486 setupMachinePoolScope: func(mps *scope.MachinePoolScope) { 487 mps.AWSMachinePool.Spec.Subnets = nil 488 mps.InfraCluster.(*scope.ClusterScope).AWSCluster.Spec.NetworkSpec.Subnets = nil 489 }, 490 wantErr: true, 491 wantASG: false, 492 expect: func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) {}, 493 }, 494 { 495 name: "should return error if create ASG fails", 496 machinePoolName: "create-asg-fail", 497 setupMachinePoolScope: func(mps *scope.MachinePoolScope) { 498 mps.AWSMachinePool.Spec.MixedInstancesPolicy = nil 499 }, 500 wantErr: true, 501 wantASG: false, 502 expect: func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) { 503 m.CreateAutoScalingGroup(gomock.AssignableToTypeOf(&autoscaling.CreateAutoScalingGroupInput{})).Return(nil, awserrors.NewFailedDependency("dependency failure")) 504 }, 505 }, 506 { 507 name: "should return error if launch template is missing", 508 machinePoolName: "create-asg-fail", 509 setupMachinePoolScope: func(mps *scope.MachinePoolScope) { 510 mps.AWSMachinePool.Spec.MixedInstancesPolicy = nil 511 mps.AWSMachinePool.Status.LaunchTemplateID = "" 512 }, 513 wantErr: true, 514 wantASG: false, 515 expect: func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) {}, 516 }, 517 } 518 for _, tt := range tests { 519 t.Run(tt.name, func(t *testing.T) { 520 g := NewWithT(t) 521 fakeClient := getFakeClient() 522 523 clusterScope, err := getClusterScope(fakeClient) 524 g.Expect(err).ToNot(HaveOccurred()) 525 asgMock := mock_autoscalingiface.NewMockAutoScalingAPI(mockCtrl) 526 tt.expect(asgMock.EXPECT()) 527 s := NewService(clusterScope) 528 s.ASGClient = asgMock 529 530 mps, err := getMachinePoolScope(fakeClient, clusterScope) 531 g.Expect(err).ToNot(HaveOccurred()) 532 mps.AWSMachinePool.Name = tt.machinePoolName 533 tt.setupMachinePoolScope(mps) 534 asg, err := s.CreateASG(mps) 535 checkErr(tt.wantErr, err, g) 536 checkASG(tt.wantASG, asg, g) 537 }) 538 } 539 } 540 541 func TestService_UpdateASG(t *testing.T) { 542 mockCtrl := gomock.NewController(t) 543 defer mockCtrl.Finish() 544 545 tests := []struct { 546 name string 547 machinePoolName string 548 setupMachinePoolScope func(*scope.MachinePoolScope) 549 wantErr bool 550 expect func(e *mocks.MockEC2APIMockRecorder, m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) 551 }{ 552 { 553 name: "should return without error if update ASG is successful", 554 machinePoolName: "update-asg-success", 555 wantErr: false, 556 setupMachinePoolScope: func(mps *scope.MachinePoolScope) { 557 mps.AWSMachinePool.Spec.Subnets = nil 558 }, 559 expect: func(e *mocks.MockEC2APIMockRecorder, m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) { 560 m.UpdateAutoScalingGroup(gomock.AssignableToTypeOf(&autoscaling.UpdateAutoScalingGroupInput{})).Return(&autoscaling.UpdateAutoScalingGroupOutput{}, nil) 561 }, 562 }, 563 { 564 name: "should return error if update ASG fails", 565 machinePoolName: "update-asg-fail", 566 wantErr: true, 567 setupMachinePoolScope: func(mps *scope.MachinePoolScope) { 568 mps.AWSMachinePool.Spec.MixedInstancesPolicy = nil 569 }, 570 expect: func(e *mocks.MockEC2APIMockRecorder, m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) { 571 m.UpdateAutoScalingGroup(gomock.AssignableToTypeOf(&autoscaling.UpdateAutoScalingGroupInput{})).Return(nil, awserrors.NewFailedDependency("dependency failure")) 572 }, 573 }, 574 } 575 for _, tt := range tests { 576 t.Run(tt.name, func(t *testing.T) { 577 g := NewWithT(t) 578 fakeClient := getFakeClient() 579 580 clusterScope, err := getClusterScope(fakeClient) 581 g.Expect(err).ToNot(HaveOccurred()) 582 ec2Mock := mocks.NewMockEC2API(mockCtrl) 583 asgMock := mock_autoscalingiface.NewMockAutoScalingAPI(mockCtrl) 584 tt.expect(ec2Mock.EXPECT(), asgMock.EXPECT()) 585 s := NewService(clusterScope) 586 s.ASGClient = asgMock 587 588 mps, err := getMachinePoolScope(fakeClient, clusterScope) 589 g.Expect(err).ToNot(HaveOccurred()) 590 mps.AWSMachinePool.Name = tt.machinePoolName 591 592 err = s.UpdateASG(mps) 593 checkErr(tt.wantErr, err, g) 594 }) 595 } 596 } 597 598 func TestService_UpdateASGWithSubnetFilters(t *testing.T) { 599 mockCtrl := gomock.NewController(t) 600 defer mockCtrl.Finish() 601 602 tests := []struct { 603 name string 604 machinePoolName string 605 awsResourceReference []infrav1.AWSResourceReference 606 wantErr bool 607 expect func(e *mocks.MockEC2APIMockRecorder, m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) 608 }{ 609 { 610 name: "should return without error if update ASG is successful", 611 machinePoolName: "update-asg-success", 612 wantErr: false, 613 awsResourceReference: []infrav1.AWSResourceReference{ 614 { 615 Filters: []infrav1.Filter{{Name: "availability-zone", Values: []string{"us-east-1a"}}}, 616 }, 617 }, 618 expect: func(e *mocks.MockEC2APIMockRecorder, m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) { 619 e.DescribeSubnets(gomock.AssignableToTypeOf(&ec2.DescribeSubnetsInput{})).Return(&ec2.DescribeSubnetsOutput{ 620 Subnets: []*ec2.Subnet{{SubnetId: aws.String("subnet-02")}}, 621 }, nil) 622 m.UpdateAutoScalingGroup(gomock.AssignableToTypeOf(&autoscaling.UpdateAutoScalingGroupInput{})).Return(&autoscaling.UpdateAutoScalingGroupOutput{}, nil) 623 }, 624 }, 625 { 626 name: "should return an error if no matching subnets found", 627 machinePoolName: "update-asg-fail", 628 wantErr: true, 629 awsResourceReference: []infrav1.AWSResourceReference{ 630 { 631 Filters: []infrav1.Filter{ 632 { 633 Name: "tag:subnet-role", 634 Values: []string{"non-existent"}, 635 }, 636 }, 637 }, 638 }, 639 expect: func(e *mocks.MockEC2APIMockRecorder, m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) { 640 e.DescribeSubnets(gomock.AssignableToTypeOf(&ec2.DescribeSubnetsInput{})).Return(&ec2.DescribeSubnetsOutput{ 641 Subnets: []*ec2.Subnet{}, 642 }, nil) 643 }, 644 }, 645 { 646 name: "should return error if update ASG fails", 647 machinePoolName: "update-asg-fail", 648 wantErr: true, 649 awsResourceReference: []infrav1.AWSResourceReference{ 650 { 651 ID: aws.String("subnet-01"), 652 }, 653 }, 654 expect: func(e *mocks.MockEC2APIMockRecorder, m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) { 655 m.UpdateAutoScalingGroup(gomock.AssignableToTypeOf(&autoscaling.UpdateAutoScalingGroupInput{})).Return(nil, awserrors.NewFailedDependency("dependency failure")) 656 }, 657 }, 658 } 659 for _, tt := range tests { 660 t.Run(tt.name, func(t *testing.T) { 661 g := NewWithT(t) 662 fakeClient := getFakeClient() 663 664 clusterScope, err := getClusterScope(fakeClient) 665 g.Expect(err).ToNot(HaveOccurred()) 666 667 ec2Mock := mocks.NewMockEC2API(mockCtrl) 668 asgMock := mock_autoscalingiface.NewMockAutoScalingAPI(mockCtrl) 669 if tt.expect != nil { 670 tt.expect(ec2Mock.EXPECT(), asgMock.EXPECT()) 671 } 672 s := NewService(clusterScope) 673 s.ASGClient = asgMock 674 s.EC2Client = ec2Mock 675 676 mps, err := getMachinePoolScope(fakeClient, clusterScope) 677 g.Expect(err).ToNot(HaveOccurred()) 678 mps.AWSMachinePool.Name = tt.machinePoolName 679 mps.AWSMachinePool.Spec.Subnets = tt.awsResourceReference 680 681 err = s.UpdateASG(mps) 682 checkErr(tt.wantErr, err, g) 683 }) 684 } 685 } 686 687 func TestService_UpdateResourceTags(t *testing.T) { 688 mockCtrl := gomock.NewController(t) 689 defer mockCtrl.Finish() 690 691 type args struct { 692 resourceID *string 693 create map[string]string 694 remove map[string]string 695 } 696 697 tests := []struct { 698 name string 699 args args 700 wantErr bool 701 expect func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) 702 }{ 703 { 704 name: "should return nil if nothing to update", 705 args: args{ 706 resourceID: aws.String("mock-resource-id"), 707 }, 708 wantErr: false, 709 expect: func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) {}, 710 }, 711 { 712 name: "should create tags if new tags are passed", 713 args: args{ 714 resourceID: aws.String("mock-resource-id"), 715 create: map[string]string{ 716 "key1": "value1", 717 }, 718 }, 719 wantErr: false, 720 expect: func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) { 721 m.CreateOrUpdateTags(gomock.Eq(&autoscaling.CreateOrUpdateTagsInput{ 722 Tags: mapToTags(map[string]string{ 723 "key1": "value1", 724 }, aws.String("mock-resource-id")), 725 })). 726 Return(nil, nil) 727 }, 728 }, 729 { 730 name: "should return error if new tags creation failed", 731 args: args{ 732 resourceID: aws.String("mock-resource-id"), 733 create: map[string]string{ 734 "key1": "value1", 735 }, 736 }, 737 wantErr: true, 738 expect: func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) { 739 m.CreateOrUpdateTags(gomock.Eq(&autoscaling.CreateOrUpdateTagsInput{ 740 Tags: mapToTags(map[string]string{ 741 "key1": "value1", 742 }, aws.String("mock-resource-id")), 743 })). 744 Return(nil, awserrors.NewNotFound("not found")) 745 }, 746 }, 747 { 748 name: "should remove tags successfully if tags to be deleted", 749 args: args{ 750 resourceID: aws.String("mock-resource-id"), 751 remove: map[string]string{ 752 "key1": "value1", 753 }, 754 }, 755 wantErr: false, 756 expect: func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) { 757 m.DeleteTags(gomock.Eq(&autoscaling.DeleteTagsInput{ 758 Tags: mapToTags(map[string]string{ 759 "key1": "value1", 760 }, aws.String("mock-resource-id")), 761 })). 762 Return(nil, nil) 763 }, 764 }, 765 { 766 name: "should return error if removing existing tags failed", 767 args: args{ 768 resourceID: aws.String("mock-resource-id"), 769 remove: map[string]string{ 770 "key1": "value1", 771 }, 772 }, 773 wantErr: true, 774 expect: func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) { 775 m.DeleteTags(gomock.Eq(&autoscaling.DeleteTagsInput{ 776 Tags: mapToTags(map[string]string{ 777 "key1": "value1", 778 }, aws.String("mock-resource-id")), 779 })). 780 Return(nil, awserrors.NewNotFound("not found")) 781 }, 782 }, 783 } 784 785 for _, tt := range tests { 786 t.Run(tt.name, func(t *testing.T) { 787 g := NewWithT(t) 788 fakeClient := getFakeClient() 789 790 clusterScope, err := getClusterScope(fakeClient) 791 g.Expect(err).ToNot(HaveOccurred()) 792 asgMock := mock_autoscalingiface.NewMockAutoScalingAPI(mockCtrl) 793 tt.expect(asgMock.EXPECT()) 794 s := NewService(clusterScope) 795 s.ASGClient = asgMock 796 797 err = s.UpdateResourceTags(tt.args.resourceID, tt.args.create, tt.args.remove) 798 checkErr(tt.wantErr, err, g) 799 }) 800 } 801 } 802 803 func TestService_DeleteASG(t *testing.T) { 804 mockCtrl := gomock.NewController(t) 805 defer mockCtrl.Finish() 806 807 tests := []struct { 808 name string 809 wantErr bool 810 expect func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) 811 }{ 812 { 813 name: "Delete ASG successful", 814 wantErr: false, 815 expect: func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) { 816 m.DeleteAutoScalingGroup(gomock.Eq(&autoscaling.DeleteAutoScalingGroupInput{ 817 AutoScalingGroupName: aws.String("asgName"), 818 ForceDelete: aws.Bool(true), 819 })). 820 Return(nil, nil) 821 }, 822 }, 823 { 824 name: "Delete ASG should fail when ASG is not found", 825 wantErr: true, 826 expect: func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) { 827 m.DeleteAutoScalingGroup(gomock.Eq(&autoscaling.DeleteAutoScalingGroupInput{ 828 AutoScalingGroupName: aws.String("asgName"), 829 ForceDelete: aws.Bool(true), 830 })). 831 Return(nil, awserrors.NewNotFound("not found")) 832 }, 833 }, 834 } 835 836 for _, tt := range tests { 837 t.Run(tt.name, func(t *testing.T) { 838 g := NewWithT(t) 839 fakeClient := getFakeClient() 840 841 clusterScope, err := getClusterScope(fakeClient) 842 g.Expect(err).ToNot(HaveOccurred()) 843 asgMock := mock_autoscalingiface.NewMockAutoScalingAPI(mockCtrl) 844 tt.expect(asgMock.EXPECT()) 845 s := NewService(clusterScope) 846 s.ASGClient = asgMock 847 848 err = s.DeleteASG("asgName") 849 checkErr(tt.wantErr, err, g) 850 }) 851 } 852 } 853 854 func TestService_DeleteASGAndWait(t *testing.T) { 855 mockCtrl := gomock.NewController(t) 856 defer mockCtrl.Finish() 857 858 tests := []struct { 859 name string 860 wantErr bool 861 expect func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) 862 }{ 863 { 864 name: "Delete ASG with wait passed", 865 wantErr: false, 866 expect: func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) { 867 m.DeleteAutoScalingGroup(gomock.Eq(&autoscaling.DeleteAutoScalingGroupInput{ 868 AutoScalingGroupName: aws.String("asgName"), 869 ForceDelete: aws.Bool(true), 870 })). 871 Return(nil, nil) 872 m.WaitUntilGroupNotExists(gomock.Eq(&autoscaling.DescribeAutoScalingGroupsInput{ 873 AutoScalingGroupNames: aws.StringSlice([]string{"asgName"}), 874 })). 875 Return(nil) 876 }, 877 }, 878 { 879 name: "should return error if delete ASG failed while waiting", 880 wantErr: true, 881 expect: func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) { 882 m.DeleteAutoScalingGroup(gomock.Eq(&autoscaling.DeleteAutoScalingGroupInput{ 883 AutoScalingGroupName: aws.String("asgName"), 884 ForceDelete: aws.Bool(true), 885 })). 886 Return(nil, nil) 887 m.WaitUntilGroupNotExists(gomock.Eq(&autoscaling.DescribeAutoScalingGroupsInput{ 888 AutoScalingGroupNames: aws.StringSlice([]string{"asgName"}), 889 })). 890 Return(awserrors.NewFailedDependency("dependency error")) 891 }, 892 }, 893 { 894 name: "should return error if delete ASG failed during ASG deletion", 895 wantErr: true, 896 expect: func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) { 897 m.DeleteAutoScalingGroup(gomock.Eq(&autoscaling.DeleteAutoScalingGroupInput{ 898 AutoScalingGroupName: aws.String("asgName"), 899 ForceDelete: aws.Bool(true), 900 })). 901 Return(nil, awserrors.NewNotFound("not found")) 902 }, 903 }, 904 } 905 906 for _, tt := range tests { 907 t.Run(tt.name, func(t *testing.T) { 908 g := NewWithT(t) 909 fakeClient := getFakeClient() 910 911 clusterScope, err := getClusterScope(fakeClient) 912 g.Expect(err).ToNot(HaveOccurred()) 913 asgMock := mock_autoscalingiface.NewMockAutoScalingAPI(mockCtrl) 914 tt.expect(asgMock.EXPECT()) 915 s := NewService(clusterScope) 916 s.ASGClient = asgMock 917 918 err = s.DeleteASGAndWait("asgName") 919 checkErr(tt.wantErr, err, g) 920 }) 921 } 922 } 923 924 func TestService_CanStartASGInstanceRefresh(t *testing.T) { 925 mockCtrl := gomock.NewController(t) 926 defer mockCtrl.Finish() 927 928 tests := []struct { 929 name string 930 wantErr bool 931 canStart bool 932 expect func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) 933 }{ 934 { 935 name: "should return error if describe instance refresh failed", 936 wantErr: true, 937 canStart: false, 938 expect: func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) { 939 m.DescribeInstanceRefreshes(gomock.Eq(&autoscaling.DescribeInstanceRefreshesInput{ 940 AutoScalingGroupName: aws.String("machinePoolName"), 941 })). 942 Return(nil, awserrors.NewNotFound("not found")) 943 }, 944 }, 945 { 946 name: "should return true if no instance available for refresh", 947 wantErr: false, 948 canStart: true, 949 expect: func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) { 950 m.DescribeInstanceRefreshes(gomock.Eq(&autoscaling.DescribeInstanceRefreshesInput{ 951 AutoScalingGroupName: aws.String("machinePoolName"), 952 })). 953 Return(&autoscaling.DescribeInstanceRefreshesOutput{}, nil) 954 }, 955 }, 956 { 957 name: "should return false if some instances have unfinished refresh", 958 wantErr: false, 959 canStart: false, 960 expect: func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) { 961 m.DescribeInstanceRefreshes(gomock.Eq(&autoscaling.DescribeInstanceRefreshesInput{ 962 AutoScalingGroupName: aws.String("machinePoolName"), 963 })). 964 Return(&autoscaling.DescribeInstanceRefreshesOutput{ 965 InstanceRefreshes: []*autoscaling.InstanceRefresh{ 966 { 967 Status: aws.String(autoscaling.InstanceRefreshStatusInProgress), 968 }, 969 }, 970 }, nil) 971 }, 972 }, 973 } 974 975 for _, tt := range tests { 976 t.Run(tt.name, func(t *testing.T) { 977 g := NewWithT(t) 978 fakeClient := getFakeClient() 979 980 clusterScope, err := getClusterScope(fakeClient) 981 g.Expect(err).ToNot(HaveOccurred()) 982 asgMock := mock_autoscalingiface.NewMockAutoScalingAPI(mockCtrl) 983 tt.expect(asgMock.EXPECT()) 984 s := NewService(clusterScope) 985 s.ASGClient = asgMock 986 987 mps, err := getMachinePoolScope(fakeClient, clusterScope) 988 g.Expect(err).ToNot(HaveOccurred()) 989 mps.AWSMachinePool.Name = "machinePoolName" 990 991 out, err := s.CanStartASGInstanceRefresh(mps) 992 checkErr(tt.wantErr, err, g) 993 if tt.canStart { 994 g.Expect(out).To(BeTrue()) 995 return 996 } 997 g.Expect(out).To(BeFalse()) 998 }) 999 } 1000 } 1001 1002 func TestService_StartASGInstanceRefresh(t *testing.T) { 1003 mockCtrl := gomock.NewController(t) 1004 defer mockCtrl.Finish() 1005 1006 tests := []struct { 1007 name string 1008 wantErr bool 1009 expect func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) 1010 }{ 1011 { 1012 name: "should return error if start instance refresh failed", 1013 wantErr: true, 1014 expect: func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) { 1015 m.StartInstanceRefresh(gomock.Eq(&autoscaling.StartInstanceRefreshInput{ 1016 AutoScalingGroupName: aws.String("mpn"), 1017 Strategy: aws.String("Rolling"), 1018 Preferences: &autoscaling.RefreshPreferences{ 1019 InstanceWarmup: aws.Int64(100), 1020 MinHealthyPercentage: aws.Int64(80), 1021 }, 1022 })). 1023 Return(nil, awserrors.NewNotFound("not found")) 1024 }, 1025 }, 1026 { 1027 name: "should return nil if start instance refresh is success", 1028 wantErr: false, 1029 expect: func(m *mock_autoscalingiface.MockAutoScalingAPIMockRecorder) { 1030 m.StartInstanceRefresh(gomock.Eq(&autoscaling.StartInstanceRefreshInput{ 1031 AutoScalingGroupName: aws.String("mpn"), 1032 Strategy: aws.String("Rolling"), 1033 Preferences: &autoscaling.RefreshPreferences{ 1034 InstanceWarmup: aws.Int64(100), 1035 MinHealthyPercentage: aws.Int64(80), 1036 }, 1037 })). 1038 Return(&autoscaling.StartInstanceRefreshOutput{}, nil) 1039 }, 1040 }, 1041 } 1042 1043 for _, tt := range tests { 1044 t.Run(tt.name, func(t *testing.T) { 1045 g := NewWithT(t) 1046 fakeClient := getFakeClient() 1047 1048 clusterScope, err := getClusterScope(fakeClient) 1049 g.Expect(err).ToNot(HaveOccurred()) 1050 asgMock := mock_autoscalingiface.NewMockAutoScalingAPI(mockCtrl) 1051 tt.expect(asgMock.EXPECT()) 1052 s := NewService(clusterScope) 1053 s.ASGClient = asgMock 1054 1055 mps, err := getMachinePoolScope(fakeClient, clusterScope) 1056 g.Expect(err).ToNot(HaveOccurred()) 1057 mps.AWSMachinePool.Name = "mpn" 1058 1059 err = s.StartASGInstanceRefresh(mps) 1060 checkErr(tt.wantErr, err, g) 1061 }) 1062 } 1063 } 1064 1065 func getFakeClient() client.Client { 1066 scheme := runtime.NewScheme() 1067 _ = infrav1.AddToScheme(scheme) 1068 _ = expinfrav1.AddToScheme(scheme) 1069 _ = expclusterv1.AddToScheme(scheme) 1070 return fake.NewClientBuilder().WithScheme(scheme).Build() 1071 } 1072 1073 func checkErr(wantErr bool, err error, g *WithT) { 1074 if wantErr { 1075 g.Expect(err).To(HaveOccurred()) 1076 return 1077 } 1078 g.Expect(err).To(BeNil()) 1079 } 1080 1081 func checkASG(wantASG bool, asg *expinfrav1.AutoScalingGroup, g *WithT) { 1082 if wantASG { 1083 g.Expect(asg).To(Not(BeNil())) 1084 return 1085 } 1086 g.Expect(asg).To(BeNil()) 1087 } 1088 1089 func getClusterScope(client client.Client) (*scope.ClusterScope, error) { 1090 cluster := &clusterv1.Cluster{ 1091 ObjectMeta: metav1.ObjectMeta{ 1092 Name: "test", 1093 }, 1094 } 1095 cs, err := scope.NewClusterScope(scope.ClusterScopeParams{ 1096 Client: client, 1097 Cluster: cluster, 1098 AWSCluster: &infrav1.AWSCluster{ 1099 Spec: infrav1.AWSClusterSpec{ 1100 NetworkSpec: infrav1.NetworkSpec{ 1101 Subnets: []infrav1.SubnetSpec{ 1102 { 1103 ID: "subnetId", 1104 }, 1105 }, 1106 }, 1107 }, 1108 }, 1109 }) 1110 if err != nil { 1111 return nil, err 1112 } 1113 return cs, nil 1114 } 1115 1116 func getMachinePoolScope(client client.Client, clusterScope *scope.ClusterScope) (*scope.MachinePoolScope, error) { 1117 awsMachinePool := &expinfrav1.AWSMachinePool{ 1118 Spec: expinfrav1.AWSMachinePoolSpec{ 1119 Subnets: []infrav1.AWSResourceReference{ 1120 { 1121 ID: aws.String("subnet1"), 1122 }, 1123 }, 1124 RefreshPreferences: &expinfrav1.RefreshPreferences{ 1125 Strategy: aws.String("Rolling"), 1126 InstanceWarmup: aws.Int64(100), 1127 MinHealthyPercentage: aws.Int64(80), 1128 }, 1129 MixedInstancesPolicy: &expinfrav1.MixedInstancesPolicy{ 1130 InstancesDistribution: &expinfrav1.InstancesDistribution{ 1131 OnDemandAllocationStrategy: "prioritized", 1132 OnDemandBaseCapacity: aws.Int64(0), 1133 OnDemandPercentageAboveBaseCapacity: aws.Int64(100), 1134 }, 1135 Overrides: []expinfrav1.Overrides{ 1136 { 1137 InstanceType: "t1.large", 1138 }, 1139 }, 1140 }, 1141 }, 1142 Status: expinfrav1.AWSMachinePoolStatus{ 1143 LaunchTemplateID: "launchTemplateID", 1144 }, 1145 } 1146 mps, err := scope.NewMachinePoolScope(scope.MachinePoolScopeParams{ 1147 Client: client, 1148 Cluster: clusterScope.Cluster, 1149 MachinePool: &expclusterv1.MachinePool{}, 1150 InfraCluster: clusterScope, 1151 AWSMachinePool: awsMachinePool, 1152 }) 1153 if err != nil { 1154 return nil, err 1155 } 1156 return mps, nil 1157 }