sigs.k8s.io/cluster-api-provider-aws@v1.5.5/pkg/cloud/services/ec2/launchtemplate_test.go (about) 1 /* 2 Copyright 2020 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 ec2 18 19 import ( 20 "encoding/base64" 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/aws/aws-sdk-go/service/ssm" 27 "github.com/golang/mock/gomock" 28 "github.com/google/go-cmp/cmp" 29 . "github.com/onsi/gomega" 30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 "k8s.io/utils/pointer" 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/ssm/mock_ssmiface" 39 "sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services/userdata" 40 "sigs.k8s.io/cluster-api-provider-aws/test/mocks" 41 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 42 ) 43 44 const ( 45 testUserData = `## template: jinja 46 #cloud-config 47 48 write_files: 49 - path: /tmp/kubeadm-join-config.yaml 50 owner: root:root 51 permissions: '0640' 52 content: | 53 --- 54 apiVersion: kubeadm.k8s.io/v1beta2 55 discovery: 56 bootstrapToken: 57 apiServerEndpoint: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 58 caCertHashes: 59 - sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 60 token: xxxxxx.xxxxxxxxxxxxxxxx 61 unsafeSkipCAVerification: false 62 kind: JoinConfiguration 63 nodeRegistration: 64 kubeletExtraArgs: 65 cloud-provider: aws 66 name: '{{ ds.meta_data.local_hostname }}' 67 68 runcmd: 69 - kubeadm join --config /tmp/kubeadm-join-config.yaml 70 users: 71 - name: xxxxxxxx 72 sudo: ALL=(ALL) NOPASSWD:ALL 73 ssh_authorized_keys: 74 - ssh-rsa xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx user@example.com 75 ` 76 ) 77 78 var ( 79 testUserDataHash = userdata.ComputeHash([]byte(testUserData)) 80 ) 81 82 func TestGetLaunchTemplate(t *testing.T) { 83 mockCtrl := gomock.NewController(t) 84 defer mockCtrl.Finish() 85 86 testCases := []struct { 87 name string 88 launchTemplateName string 89 expect func(m *mocks.MockEC2APIMockRecorder) 90 check func(g *WithT, launchTemplate *expinfrav1.AWSLaunchTemplate, userDataHash string, err error) 91 }{ 92 { 93 name: "Should not return launch template if empty launch template name passed", 94 check: func(g *WithT, launchTemplate *expinfrav1.AWSLaunchTemplate, userDataHash string, err error) { 95 g.Expect(err).NotTo(HaveOccurred()) 96 g.Expect(userDataHash).Should(BeEmpty()) 97 g.Expect(launchTemplate).Should(BeNil()) 98 }, 99 }, 100 { 101 name: "Should not return error if no launch template exist with given name", 102 launchTemplateName: "foo", 103 expect: func(m *mocks.MockEC2APIMockRecorder) { 104 m.DescribeLaunchTemplateVersions(gomock.Eq(&ec2.DescribeLaunchTemplateVersionsInput{ 105 LaunchTemplateName: aws.String("foo"), 106 Versions: []*string{aws.String("$Latest")}, 107 })). 108 Return(nil, awserr.New( 109 awserrors.LaunchTemplateNameNotFound, 110 "The specified launch template, with template name foo, does not exist.", 111 nil, 112 )) 113 }, 114 check: func(g *WithT, launchTemplate *expinfrav1.AWSLaunchTemplate, userDataHash string, err error) { 115 g.Expect(err).NotTo(HaveOccurred()) 116 g.Expect(userDataHash).Should(BeEmpty()) 117 g.Expect(launchTemplate).Should(BeNil()) 118 }, 119 }, 120 { 121 name: "Should return error if AWS failed during launch template fetching", 122 launchTemplateName: "foo", 123 expect: func(m *mocks.MockEC2APIMockRecorder) { 124 m.DescribeLaunchTemplateVersions(gomock.Eq(&ec2.DescribeLaunchTemplateVersionsInput{ 125 LaunchTemplateName: aws.String("foo"), 126 Versions: []*string{aws.String("$Latest")}, 127 })).Return(nil, awserrors.NewFailedDependency("dependency failure")) 128 }, 129 check: func(g *WithT, launchTemplate *expinfrav1.AWSLaunchTemplate, userDataHash string, err error) { 130 g.Expect(err).To(HaveOccurred()) 131 g.Expect(userDataHash).Should(BeEmpty()) 132 g.Expect(launchTemplate).Should(BeNil()) 133 }, 134 }, 135 { 136 name: "Should not return with error if no launch template versions received from AWS", 137 launchTemplateName: "foo", 138 expect: func(m *mocks.MockEC2APIMockRecorder) { 139 m.DescribeLaunchTemplateVersions(gomock.Eq(&ec2.DescribeLaunchTemplateVersionsInput{ 140 LaunchTemplateName: aws.String("foo"), 141 Versions: []*string{aws.String("$Latest")}, 142 })).Return(nil, nil) 143 }, 144 check: func(g *WithT, launchTemplate *expinfrav1.AWSLaunchTemplate, userDataHash string, err error) { 145 g.Expect(err).NotTo(HaveOccurred()) 146 g.Expect(userDataHash).Should(BeEmpty()) 147 g.Expect(launchTemplate).Should(BeNil()) 148 }, 149 }, 150 { 151 name: "Should successfully return launch template if exist with given name", 152 launchTemplateName: "foo", 153 expect: func(m *mocks.MockEC2APIMockRecorder) { 154 m.DescribeLaunchTemplateVersions(gomock.Eq(&ec2.DescribeLaunchTemplateVersionsInput{ 155 LaunchTemplateName: aws.String("foo"), 156 Versions: []*string{aws.String("$Latest")}, 157 })).Return(&ec2.DescribeLaunchTemplateVersionsOutput{ 158 LaunchTemplateVersions: []*ec2.LaunchTemplateVersion{ 159 { 160 LaunchTemplateId: aws.String("lt-12345"), 161 LaunchTemplateName: aws.String("foo"), 162 LaunchTemplateData: &ec2.ResponseLaunchTemplateData{ 163 SecurityGroupIds: []*string{aws.String("sg-id")}, 164 ImageId: aws.String("foo-image"), 165 IamInstanceProfile: &ec2.LaunchTemplateIamInstanceProfileSpecification{ 166 Arn: aws.String("instance-profile/foo-profile"), 167 }, 168 KeyName: aws.String("foo-keyname"), 169 BlockDeviceMappings: []*ec2.LaunchTemplateBlockDeviceMapping{ 170 { 171 DeviceName: aws.String("foo-device"), 172 Ebs: &ec2.LaunchTemplateEbsBlockDevice{ 173 Encrypted: aws.Bool(true), 174 VolumeSize: aws.Int64(16), 175 VolumeType: aws.String("cool"), 176 }, 177 }, 178 }, 179 NetworkInterfaces: []*ec2.LaunchTemplateInstanceNetworkInterfaceSpecification{ 180 { 181 DeviceIndex: aws.Int64(1), 182 Groups: []*string{aws.String("foo-group")}, 183 }, 184 }, 185 UserData: aws.String(base64.StdEncoding.EncodeToString([]byte(testUserData))), 186 }, 187 VersionNumber: aws.Int64(1), 188 }, 189 }, 190 }, nil) 191 }, 192 check: func(g *WithT, launchTemplate *expinfrav1.AWSLaunchTemplate, userDataHash string, err error) { 193 wantLT := &expinfrav1.AWSLaunchTemplate{ 194 Name: "foo", 195 AMI: infrav1.AMIReference{ 196 ID: aws.String("foo-image"), 197 }, 198 IamInstanceProfile: "foo-profile", 199 SSHKeyName: aws.String("foo-keyname"), 200 VersionNumber: aws.Int64(1), 201 AdditionalSecurityGroups: []infrav1.AWSResourceReference{{ID: aws.String("sg-id")}}, 202 } 203 204 g.Expect(err).NotTo(HaveOccurred()) 205 g.Expect(userDataHash).Should(Equal(testUserDataHash)) 206 g.Expect(launchTemplate).Should(Equal(wantLT)) 207 }, 208 }, 209 { 210 name: "Should return computed userData if AWS returns empty userData", 211 launchTemplateName: "foo", 212 expect: func(m *mocks.MockEC2APIMockRecorder) { 213 m.DescribeLaunchTemplateVersions(gomock.Eq(&ec2.DescribeLaunchTemplateVersionsInput{ 214 LaunchTemplateName: aws.String("foo"), 215 Versions: []*string{aws.String("$Latest")}, 216 })).Return(&ec2.DescribeLaunchTemplateVersionsOutput{ 217 LaunchTemplateVersions: []*ec2.LaunchTemplateVersion{ 218 { 219 LaunchTemplateId: aws.String("lt-12345"), 220 LaunchTemplateName: aws.String("foo"), 221 LaunchTemplateData: &ec2.ResponseLaunchTemplateData{ 222 SecurityGroupIds: []*string{aws.String("sg-id")}, 223 ImageId: aws.String("foo-image"), 224 IamInstanceProfile: &ec2.LaunchTemplateIamInstanceProfileSpecification{ 225 Arn: aws.String("instance-profile/foo-profile"), 226 }, 227 KeyName: aws.String("foo-keyname"), 228 BlockDeviceMappings: []*ec2.LaunchTemplateBlockDeviceMapping{ 229 { 230 DeviceName: aws.String("foo-device"), 231 Ebs: &ec2.LaunchTemplateEbsBlockDevice{ 232 Encrypted: aws.Bool(true), 233 VolumeSize: aws.Int64(16), 234 VolumeType: aws.String("cool"), 235 }, 236 }, 237 }, 238 NetworkInterfaces: []*ec2.LaunchTemplateInstanceNetworkInterfaceSpecification{ 239 { 240 DeviceIndex: aws.Int64(1), 241 Groups: []*string{aws.String("foo-group")}, 242 }, 243 }, 244 }, 245 VersionNumber: aws.Int64(1), 246 }, 247 }, 248 }, nil) 249 }, 250 check: func(g *WithT, launchTemplate *expinfrav1.AWSLaunchTemplate, userDataHash string, err error) { 251 wantLT := &expinfrav1.AWSLaunchTemplate{ 252 Name: "foo", 253 AMI: infrav1.AMIReference{ 254 ID: aws.String("foo-image"), 255 }, 256 IamInstanceProfile: "foo-profile", 257 SSHKeyName: aws.String("foo-keyname"), 258 VersionNumber: aws.Int64(1), 259 AdditionalSecurityGroups: []infrav1.AWSResourceReference{{ID: aws.String("sg-id")}}, 260 } 261 262 g.Expect(err).NotTo(HaveOccurred()) 263 g.Expect(userDataHash).Should(Equal(userdata.ComputeHash(nil))) 264 g.Expect(launchTemplate).Should(Equal(wantLT)) 265 }, 266 }, 267 } 268 for _, tc := range testCases { 269 t.Run(tc.name, func(t *testing.T) { 270 g := NewWithT(t) 271 272 scheme, err := setupScheme() 273 g.Expect(err).NotTo(HaveOccurred()) 274 client := fake.NewClientBuilder().WithScheme(scheme).Build() 275 276 cs, err := setupClusterScope(client) 277 g.Expect(err).NotTo(HaveOccurred()) 278 mockEC2Client := mocks.NewMockEC2API(mockCtrl) 279 280 s := NewService(cs) 281 s.EC2Client = mockEC2Client 282 283 if tc.expect != nil { 284 tc.expect(mockEC2Client.EXPECT()) 285 } 286 287 launchTemplate, userData, err := s.GetLaunchTemplate(tc.launchTemplateName) 288 tc.check(g, launchTemplate, userData, err) 289 }) 290 } 291 } 292 293 func TestService_SDKToLaunchTemplate(t *testing.T) { 294 tests := []struct { 295 name string 296 input *ec2.LaunchTemplateVersion 297 wantLT *expinfrav1.AWSLaunchTemplate 298 wantHash string 299 wantErr bool 300 }{ 301 { 302 name: "lots of input", 303 input: &ec2.LaunchTemplateVersion{ 304 LaunchTemplateId: aws.String("lt-12345"), 305 LaunchTemplateName: aws.String("foo"), 306 LaunchTemplateData: &ec2.ResponseLaunchTemplateData{ 307 ImageId: aws.String("foo-image"), 308 IamInstanceProfile: &ec2.LaunchTemplateIamInstanceProfileSpecification{ 309 Arn: aws.String("instance-profile/foo-profile"), 310 }, 311 KeyName: aws.String("foo-keyname"), 312 BlockDeviceMappings: []*ec2.LaunchTemplateBlockDeviceMapping{ 313 { 314 DeviceName: aws.String("foo-device"), 315 Ebs: &ec2.LaunchTemplateEbsBlockDevice{ 316 Encrypted: aws.Bool(true), 317 VolumeSize: aws.Int64(16), 318 VolumeType: aws.String("cool"), 319 }, 320 }, 321 }, 322 NetworkInterfaces: []*ec2.LaunchTemplateInstanceNetworkInterfaceSpecification{ 323 { 324 DeviceIndex: aws.Int64(1), 325 Groups: []*string{aws.String("foo-group")}, 326 }, 327 }, 328 UserData: aws.String(base64.StdEncoding.EncodeToString([]byte(testUserData))), 329 }, 330 VersionNumber: aws.Int64(1), 331 }, 332 wantLT: &expinfrav1.AWSLaunchTemplate{ 333 Name: "foo", 334 AMI: infrav1.AMIReference{ 335 ID: aws.String("foo-image"), 336 }, 337 IamInstanceProfile: "foo-profile", 338 SSHKeyName: aws.String("foo-keyname"), 339 VersionNumber: aws.Int64(1), 340 }, 341 wantHash: testUserDataHash, 342 }, 343 } 344 for _, tt := range tests { 345 t.Run(tt.name, func(t *testing.T) { 346 s := &Service{} 347 gotLT, gotHash, err := s.SDKToLaunchTemplate(tt.input) 348 if (err != nil) != tt.wantErr { 349 t.Fatalf("error mismatch: got %v, wantErr %v", err, tt.wantErr) 350 } 351 if !cmp.Equal(gotLT, tt.wantLT) { 352 t.Fatalf("launchTemplate mismatch: got %v, want %v", gotLT, tt.wantLT) 353 } 354 if !cmp.Equal(gotHash, tt.wantHash) { 355 t.Fatalf("userDataHash mismatch: got %v, want %v", gotHash, tt.wantHash) 356 } 357 }) 358 } 359 } 360 361 func TestService_LaunchTemplateNeedsUpdate(t *testing.T) { 362 mockCtrl := gomock.NewController(t) 363 defer mockCtrl.Finish() 364 365 tests := []struct { 366 name string 367 incoming *expinfrav1.AWSLaunchTemplate 368 existing *expinfrav1.AWSLaunchTemplate 369 expect func(m *mocks.MockEC2APIMockRecorder) 370 want bool 371 wantErr bool 372 }{ 373 { 374 name: "the same security groups", 375 incoming: &expinfrav1.AWSLaunchTemplate{ 376 AdditionalSecurityGroups: []infrav1.AWSResourceReference{ 377 {ID: aws.String("sg-999")}, 378 }, 379 }, 380 existing: &expinfrav1.AWSLaunchTemplate{ 381 AdditionalSecurityGroups: []infrav1.AWSResourceReference{ 382 {ID: aws.String("sg-111")}, 383 {ID: aws.String("sg-222")}, 384 {ID: aws.String("sg-999")}, 385 }, 386 }, 387 want: false, 388 wantErr: false, 389 }, 390 { 391 name: "core security group removed externally", 392 incoming: &expinfrav1.AWSLaunchTemplate{ 393 AdditionalSecurityGroups: []infrav1.AWSResourceReference{ 394 {ID: aws.String("sg-999")}, 395 }, 396 }, 397 existing: &expinfrav1.AWSLaunchTemplate{ 398 AdditionalSecurityGroups: []infrav1.AWSResourceReference{ 399 {ID: aws.String("sg-222")}, 400 {ID: aws.String("sg-999")}, 401 }, 402 }, 403 want: true, 404 wantErr: false, 405 }, 406 { 407 name: "new additional security group", 408 incoming: &expinfrav1.AWSLaunchTemplate{ 409 AdditionalSecurityGroups: []infrav1.AWSResourceReference{ 410 {ID: aws.String("sg-999")}, 411 {ID: aws.String("sg-000")}, 412 }, 413 }, 414 existing: &expinfrav1.AWSLaunchTemplate{ 415 AdditionalSecurityGroups: []infrav1.AWSResourceReference{ 416 {ID: aws.String("sg-111")}, 417 {ID: aws.String("sg-222")}, 418 {ID: aws.String("sg-999")}, 419 }, 420 }, 421 want: true, 422 wantErr: false, 423 }, 424 { 425 name: "Should return true if incoming IamInstanceProfile is not same as existing IamInstanceProfile", 426 incoming: &expinfrav1.AWSLaunchTemplate{ 427 IamInstanceProfile: DefaultAmiNameFormat, 428 }, 429 existing: &expinfrav1.AWSLaunchTemplate{ 430 IamInstanceProfile: "some-other-profile", 431 }, 432 want: true, 433 }, 434 { 435 name: "Should return true if incoming InstanceType is not same as existing InstanceType", 436 incoming: &expinfrav1.AWSLaunchTemplate{ 437 InstanceType: "t3.micro", 438 }, 439 existing: &expinfrav1.AWSLaunchTemplate{ 440 InstanceType: "t3.large", 441 }, 442 want: true, 443 }, 444 { 445 name: "new additional security group with filters", 446 incoming: &expinfrav1.AWSLaunchTemplate{ 447 AdditionalSecurityGroups: []infrav1.AWSResourceReference{ 448 {Filters: []infrav1.Filter{{Name: "sg-1", Values: []string{"test-1"}}}}, 449 }, 450 }, 451 existing: &expinfrav1.AWSLaunchTemplate{ 452 AdditionalSecurityGroups: []infrav1.AWSResourceReference{ 453 {Filters: []infrav1.Filter{{Name: "sg-2", Values: []string{"test-2"}}}}, 454 }, 455 }, 456 expect: func(m *mocks.MockEC2APIMockRecorder) { 457 m.DescribeSecurityGroups(gomock.Eq(&ec2.DescribeSecurityGroupsInput{Filters: []*ec2.Filter{{Name: aws.String("sg-1"), Values: aws.StringSlice([]string{"test-1"})}}})). 458 Return(&ec2.DescribeSecurityGroupsOutput{SecurityGroups: []*ec2.SecurityGroup{{GroupId: aws.String("sg-1")}}}, nil) 459 m.DescribeSecurityGroups(gomock.Eq(&ec2.DescribeSecurityGroupsInput{Filters: []*ec2.Filter{{Name: aws.String("sg-2"), Values: aws.StringSlice([]string{"test-2"})}}})). 460 Return(&ec2.DescribeSecurityGroupsOutput{SecurityGroups: []*ec2.SecurityGroup{{GroupId: aws.String("sg-2")}}}, nil) 461 }, 462 want: true, 463 wantErr: false, 464 }, 465 } 466 for _, tt := range tests { 467 t.Run(tt.name, func(t *testing.T) { 468 g := NewWithT(t) 469 ac := &infrav1.AWSCluster{ 470 ObjectMeta: metav1.ObjectMeta{Name: "test"}, 471 Status: infrav1.AWSClusterStatus{ 472 Network: infrav1.NetworkStatus{ 473 SecurityGroups: map[infrav1.SecurityGroupRole]infrav1.SecurityGroup{ 474 infrav1.SecurityGroupNode: { 475 ID: "sg-111", 476 }, 477 infrav1.SecurityGroupLB: { 478 ID: "sg-222", 479 }, 480 }, 481 }, 482 }, 483 } 484 s := &Service{ 485 scope: &scope.ClusterScope{ 486 AWSCluster: ac, 487 }, 488 } 489 machinePoolScope := &scope.MachinePoolScope{ 490 InfraCluster: &scope.ClusterScope{ 491 AWSCluster: ac, 492 }, 493 } 494 mockEC2Client := mocks.NewMockEC2API(mockCtrl) 495 s.EC2Client = mockEC2Client 496 497 if tt.expect != nil { 498 tt.expect(mockEC2Client.EXPECT()) 499 } 500 501 got, err := s.LaunchTemplateNeedsUpdate(machinePoolScope, tt.incoming, tt.existing) 502 if tt.wantErr { 503 g.Expect(err).To(HaveOccurred()) 504 return 505 } 506 g.Expect(err).NotTo(HaveOccurred()) 507 g.Expect(got).Should(Equal(tt.want)) 508 }) 509 } 510 } 511 512 func TestGetLaunchTemplateID(t *testing.T) { 513 mockCtrl := gomock.NewController(t) 514 defer mockCtrl.Finish() 515 516 testCases := []struct { 517 name string 518 launchTemplateName string 519 expect func(m *mocks.MockEC2APIMockRecorder) 520 check func(g *WithT, launchTemplateID string, err error) 521 }{ 522 { 523 name: "Should return with no error if empty launch template name passed", 524 expect: func(m *mocks.MockEC2APIMockRecorder) {}, 525 check: func(g *WithT, launchTemplateID string, err error) { 526 g.Expect(err).NotTo(HaveOccurred()) 527 g.Expect(launchTemplateID).Should(BeEmpty()) 528 }, 529 }, 530 { 531 name: "Should not return error if launch template does not exist", 532 launchTemplateName: "foo", 533 expect: func(m *mocks.MockEC2APIMockRecorder) { 534 m.DescribeLaunchTemplateVersions(gomock.Eq(&ec2.DescribeLaunchTemplateVersionsInput{ 535 LaunchTemplateName: aws.String("foo"), 536 Versions: []*string{aws.String("$Latest")}, 537 })).Return(nil, awserr.New( 538 awserrors.LaunchTemplateNameNotFound, 539 "The specified launch template, with template name foo, does not exist.", 540 nil, 541 )) 542 }, 543 check: func(g *WithT, launchTemplateID string, err error) { 544 g.Expect(err).NotTo(HaveOccurred()) 545 g.Expect(launchTemplateID).Should(BeEmpty()) 546 }, 547 }, 548 { 549 name: "Should return with error if AWS failed to fetch launch template", 550 launchTemplateName: "foo", 551 expect: func(m *mocks.MockEC2APIMockRecorder) { 552 m.DescribeLaunchTemplateVersions(gomock.Eq(&ec2.DescribeLaunchTemplateVersionsInput{ 553 LaunchTemplateName: aws.String("foo"), 554 Versions: []*string{aws.String("$Latest")}, 555 })).Return(nil, awserrors.NewFailedDependency("Dependency issue from AWS")) 556 }, 557 check: func(g *WithT, launchTemplateID string, err error) { 558 g.Expect(err).To(HaveOccurred()) 559 g.Expect(launchTemplateID).Should(BeEmpty()) 560 }, 561 }, 562 { 563 name: "Should not return error if AWS returns no launch template versions info in output", 564 launchTemplateName: "foo", 565 expect: func(m *mocks.MockEC2APIMockRecorder) { 566 m.DescribeLaunchTemplateVersions(gomock.Eq(&ec2.DescribeLaunchTemplateVersionsInput{ 567 LaunchTemplateName: aws.String("foo"), 568 Versions: []*string{aws.String("$Latest")}, 569 })).Return(nil, nil) 570 }, 571 check: func(g *WithT, launchTemplateID string, err error) { 572 g.Expect(err).NotTo(HaveOccurred()) 573 g.Expect(launchTemplateID).Should(BeEmpty()) 574 }, 575 }, 576 { 577 name: "Should successfully return launch template ID for given name if exists", 578 launchTemplateName: "foo", 579 expect: func(m *mocks.MockEC2APIMockRecorder) { 580 m.DescribeLaunchTemplateVersions(gomock.Eq(&ec2.DescribeLaunchTemplateVersionsInput{ 581 LaunchTemplateName: aws.String("foo"), 582 Versions: []*string{aws.String("$Latest")}, 583 })).Return(&ec2.DescribeLaunchTemplateVersionsOutput{ 584 LaunchTemplateVersions: []*ec2.LaunchTemplateVersion{ 585 { 586 LaunchTemplateId: aws.String("lt-12345"), 587 LaunchTemplateName: aws.String("foo"), 588 LaunchTemplateData: &ec2.ResponseLaunchTemplateData{ 589 ImageId: aws.String("foo-image"), 590 IamInstanceProfile: &ec2.LaunchTemplateIamInstanceProfileSpecification{ 591 Arn: aws.String("instance-profile/foo-profile"), 592 }, 593 KeyName: aws.String("foo-keyname"), 594 BlockDeviceMappings: []*ec2.LaunchTemplateBlockDeviceMapping{ 595 { 596 DeviceName: aws.String("foo-device"), 597 Ebs: &ec2.LaunchTemplateEbsBlockDevice{ 598 Encrypted: aws.Bool(true), 599 VolumeSize: aws.Int64(16), 600 VolumeType: aws.String("cool"), 601 }, 602 }, 603 }, 604 NetworkInterfaces: []*ec2.LaunchTemplateInstanceNetworkInterfaceSpecification{ 605 { 606 DeviceIndex: aws.Int64(1), 607 Groups: []*string{aws.String("foo-group")}, 608 }, 609 }, 610 UserData: aws.String(base64.StdEncoding.EncodeToString([]byte(testUserData))), 611 }, 612 VersionNumber: aws.Int64(1), 613 }, 614 }, 615 }, nil) 616 }, 617 check: func(g *WithT, launchTemplateID string, err error) { 618 g.Expect(err).NotTo(HaveOccurred()) 619 g.Expect(launchTemplateID).Should(Equal("lt-12345")) 620 }, 621 }, 622 } 623 624 for _, tc := range testCases { 625 t.Run(tc.name, func(t *testing.T) { 626 g := NewWithT(t) 627 628 scheme, err := setupScheme() 629 g.Expect(err).NotTo(HaveOccurred()) 630 client := fake.NewClientBuilder().WithScheme(scheme).Build() 631 632 cs, err := setupClusterScope(client) 633 g.Expect(err).NotTo(HaveOccurred()) 634 mockEC2Client := mocks.NewMockEC2API(mockCtrl) 635 636 s := NewService(cs) 637 s.EC2Client = mockEC2Client 638 639 if tc.expect != nil { 640 tc.expect(mockEC2Client.EXPECT()) 641 } 642 launchTemplate, err := s.GetLaunchTemplateID(tc.launchTemplateName) 643 tc.check(g, launchTemplate, err) 644 }) 645 } 646 } 647 648 func TestDeleteLaunchTemplate(t *testing.T) { 649 mockCtrl := gomock.NewController(t) 650 defer mockCtrl.Finish() 651 652 testCases := []struct { 653 name string 654 versionID string 655 expect func(m *mocks.MockEC2APIMockRecorder) 656 wantErr bool 657 }{ 658 { 659 name: "Should not return error if successfully deletes given launch template ID", 660 versionID: "1", 661 expect: func(m *mocks.MockEC2APIMockRecorder) { 662 m.DeleteLaunchTemplate(gomock.Eq(&ec2.DeleteLaunchTemplateInput{ 663 LaunchTemplateId: aws.String("1"), 664 })).Return(&ec2.DeleteLaunchTemplateOutput{}, nil) 665 }, 666 }, 667 { 668 name: "Should return error if failed to delete given launch template ID", 669 versionID: "1", 670 expect: func(m *mocks.MockEC2APIMockRecorder) { 671 m.DeleteLaunchTemplate(gomock.Eq(&ec2.DeleteLaunchTemplateInput{ 672 LaunchTemplateId: aws.String("1"), 673 })).Return(nil, awserrors.NewFailedDependency("dependency failure")) 674 }, 675 wantErr: true, 676 }, 677 } 678 for _, tc := range testCases { 679 t.Run(tc.name, func(t *testing.T) { 680 g := NewWithT(t) 681 682 scheme, err := setupScheme() 683 g.Expect(err).NotTo(HaveOccurred()) 684 client := fake.NewClientBuilder().WithScheme(scheme).Build() 685 686 cs, err := setupClusterScope(client) 687 g.Expect(err).NotTo(HaveOccurred()) 688 mockEC2Client := mocks.NewMockEC2API(mockCtrl) 689 690 s := NewService(cs) 691 s.EC2Client = mockEC2Client 692 tc.expect(mockEC2Client.EXPECT()) 693 694 err = s.DeleteLaunchTemplate(tc.versionID) 695 if tc.wantErr { 696 g.Expect(err).To(HaveOccurred()) 697 return 698 } 699 g.Expect(err).NotTo(HaveOccurred()) 700 }) 701 } 702 } 703 704 func TestCreateLaunchTemplate(t *testing.T) { 705 mockCtrl := gomock.NewController(t) 706 defer mockCtrl.Finish() 707 708 var formatTagsInput = func(arg *ec2.CreateLaunchTemplateInput) { 709 sortTags(arg.TagSpecifications[0].Tags) 710 711 for index := range arg.LaunchTemplateData.TagSpecifications { 712 sortTags(arg.LaunchTemplateData.TagSpecifications[index].Tags) 713 } 714 } 715 716 var userData = []byte{1, 0, 0} 717 testCases := []struct { 718 name string 719 awsResourceReference []infrav1.AWSResourceReference 720 expect func(g *WithT, m *mocks.MockEC2APIMockRecorder) 721 check func(g *WithT, s string, e error) 722 }{ 723 { 724 name: "Should not return error if successfully created launch template id", 725 awsResourceReference: []infrav1.AWSResourceReference{{ID: aws.String("1")}}, 726 expect: func(g *WithT, m *mocks.MockEC2APIMockRecorder) { 727 sgMap := make(map[infrav1.SecurityGroupRole]infrav1.SecurityGroup) 728 sgMap[infrav1.SecurityGroupNode] = infrav1.SecurityGroup{ID: "1"} 729 sgMap[infrav1.SecurityGroupLB] = infrav1.SecurityGroup{ID: "2"} 730 731 var expectedInput = &ec2.CreateLaunchTemplateInput{ 732 LaunchTemplateData: &ec2.RequestLaunchTemplateData{ 733 InstanceType: aws.String("t3.large"), 734 IamInstanceProfile: &ec2.LaunchTemplateIamInstanceProfileSpecificationRequest{ 735 Name: aws.String("instance-profile"), 736 }, 737 KeyName: aws.String("default"), 738 UserData: pointer.StringPtr(base64.StdEncoding.EncodeToString(userData)), 739 SecurityGroupIds: aws.StringSlice([]string{"nodeSG", "lbSG", "1"}), 740 ImageId: aws.String("imageID"), 741 TagSpecifications: []*ec2.LaunchTemplateTagSpecificationRequest{ 742 { 743 ResourceType: aws.String(ec2.ResourceTypeInstance), 744 Tags: defaultEC2Tags("aws-mp-name", "cluster-name"), 745 }, 746 { 747 ResourceType: aws.String(ec2.ResourceTypeVolume), 748 Tags: defaultEC2Tags("aws-mp-name", "cluster-name"), 749 }, 750 }, 751 }, 752 LaunchTemplateName: aws.String("aws-mp-name"), 753 TagSpecifications: []*ec2.TagSpecification{ 754 { 755 ResourceType: aws.String(ec2.ResourceTypeLaunchTemplate), 756 Tags: defaultEC2Tags("aws-mp-name", "cluster-name"), 757 }, 758 }, 759 } 760 m.CreateLaunchTemplate(gomock.AssignableToTypeOf(expectedInput)).Return(&ec2.CreateLaunchTemplateOutput{ 761 LaunchTemplate: &ec2.LaunchTemplate{ 762 LaunchTemplateId: aws.String("launch-template-id"), 763 }, 764 }, nil).Do(func(arg *ec2.CreateLaunchTemplateInput) { 765 // formatting added to match arrays during cmp.Equal 766 formatTagsInput(arg) 767 if !cmp.Equal(expectedInput, arg) { 768 t.Fatalf("mismatch in input expected: %+v, got: %+v", expectedInput, arg) 769 } 770 }) 771 }, 772 check: func(g *WithT, id string, err error) { 773 g.Expect(id).Should(Equal("launch-template-id")) 774 g.Expect(err).NotTo(HaveOccurred()) 775 }, 776 }, 777 { 778 name: "Should successfully create launch template id with AdditionalSecurityGroups Filter", 779 awsResourceReference: []infrav1.AWSResourceReference{{Filters: []infrav1.Filter{{Name: "sg-1", Values: []string{"test"}}}}}, 780 expect: func(g *WithT, m *mocks.MockEC2APIMockRecorder) { 781 sgMap := make(map[infrav1.SecurityGroupRole]infrav1.SecurityGroup) 782 sgMap[infrav1.SecurityGroupNode] = infrav1.SecurityGroup{ID: "1"} 783 sgMap[infrav1.SecurityGroupLB] = infrav1.SecurityGroup{ID: "2"} 784 785 var expectedInput = &ec2.CreateLaunchTemplateInput{ 786 LaunchTemplateData: &ec2.RequestLaunchTemplateData{ 787 InstanceType: aws.String("t3.large"), 788 IamInstanceProfile: &ec2.LaunchTemplateIamInstanceProfileSpecificationRequest{ 789 Name: aws.String("instance-profile"), 790 }, 791 KeyName: aws.String("default"), 792 UserData: pointer.StringPtr(base64.StdEncoding.EncodeToString(userData)), 793 SecurityGroupIds: aws.StringSlice([]string{"nodeSG", "lbSG", "sg-1"}), 794 ImageId: aws.String("imageID"), 795 TagSpecifications: []*ec2.LaunchTemplateTagSpecificationRequest{ 796 { 797 ResourceType: aws.String(ec2.ResourceTypeInstance), 798 Tags: defaultEC2Tags("aws-mp-name", "cluster-name"), 799 }, 800 { 801 ResourceType: aws.String(ec2.ResourceTypeVolume), 802 Tags: defaultEC2Tags("aws-mp-name", "cluster-name"), 803 }, 804 }, 805 }, 806 LaunchTemplateName: aws.String("aws-mp-name"), 807 TagSpecifications: []*ec2.TagSpecification{ 808 { 809 ResourceType: aws.String(ec2.ResourceTypeLaunchTemplate), 810 Tags: defaultEC2Tags("aws-mp-name", "cluster-name"), 811 }, 812 }, 813 } 814 m.CreateLaunchTemplate(gomock.AssignableToTypeOf(expectedInput)).Return(&ec2.CreateLaunchTemplateOutput{ 815 LaunchTemplate: &ec2.LaunchTemplate{ 816 LaunchTemplateId: aws.String("launch-template-id"), 817 }, 818 }, nil).Do(func(arg *ec2.CreateLaunchTemplateInput) { 819 // formatting added to match arrays during reflect.DeepEqual 820 formatTagsInput(arg) 821 if !cmp.Equal(expectedInput, arg) { 822 t.Fatalf("mismatch in input expected: %+v, got: %+v", expectedInput, arg) 823 } 824 }) 825 m.DescribeSecurityGroups(gomock.Eq(&ec2.DescribeSecurityGroupsInput{Filters: []*ec2.Filter{{Name: aws.String("sg-1"), Values: aws.StringSlice([]string{"test"})}}})). 826 Return(&ec2.DescribeSecurityGroupsOutput{SecurityGroups: []*ec2.SecurityGroup{{GroupId: aws.String("sg-1")}}}, nil) 827 }, 828 check: func(g *WithT, id string, err error) { 829 g.Expect(id).Should(Equal("launch-template-id")) 830 g.Expect(err).NotTo(HaveOccurred()) 831 }, 832 }, 833 { 834 name: "Should return with error if failed to create launch template id", 835 awsResourceReference: []infrav1.AWSResourceReference{{ID: aws.String("1")}}, 836 expect: func(g *WithT, m *mocks.MockEC2APIMockRecorder) { 837 sgMap := make(map[infrav1.SecurityGroupRole]infrav1.SecurityGroup) 838 sgMap[infrav1.SecurityGroupNode] = infrav1.SecurityGroup{ID: "1"} 839 sgMap[infrav1.SecurityGroupLB] = infrav1.SecurityGroup{ID: "2"} 840 841 var expectedInput = &ec2.CreateLaunchTemplateInput{ 842 LaunchTemplateData: &ec2.RequestLaunchTemplateData{ 843 InstanceType: aws.String("t3.large"), 844 IamInstanceProfile: &ec2.LaunchTemplateIamInstanceProfileSpecificationRequest{ 845 Name: aws.String("instance-profile"), 846 }, 847 KeyName: aws.String("default"), 848 UserData: pointer.StringPtr(base64.StdEncoding.EncodeToString(userData)), 849 SecurityGroupIds: aws.StringSlice([]string{"nodeSG", "lbSG", "1"}), 850 ImageId: aws.String("imageID"), 851 TagSpecifications: []*ec2.LaunchTemplateTagSpecificationRequest{ 852 { 853 ResourceType: aws.String(ec2.ResourceTypeInstance), 854 Tags: defaultEC2Tags("aws-mp-name", "cluster-name"), 855 }, 856 { 857 ResourceType: aws.String(ec2.ResourceTypeVolume), 858 Tags: defaultEC2Tags("aws-mp-name", "cluster-name"), 859 }, 860 }, 861 }, 862 LaunchTemplateName: aws.String("aws-mp-name"), 863 TagSpecifications: []*ec2.TagSpecification{ 864 { 865 ResourceType: aws.String(ec2.ResourceTypeLaunchTemplate), 866 Tags: defaultEC2Tags("aws-mp-name", "cluster-name"), 867 }, 868 }, 869 } 870 m.CreateLaunchTemplate(gomock.AssignableToTypeOf(expectedInput)).Return(nil, 871 awserrors.NewFailedDependency("dependency failure")).Do(func(arg *ec2.CreateLaunchTemplateInput) { 872 // formatting added to match arrays during cmp.Equal 873 formatTagsInput(arg) 874 if !cmp.Equal(expectedInput, arg) { 875 t.Fatalf("mismatch in input expected: %+v, got: %+v", expectedInput, arg) 876 } 877 }) 878 }, 879 check: func(g *WithT, id string, err error) { 880 g.Expect(id).Should(BeEmpty()) 881 g.Expect(err).To(HaveOccurred()) 882 }, 883 }, 884 } 885 for _, tc := range testCases { 886 t.Run(tc.name, func(t *testing.T) { 887 g := NewWithT(t) 888 889 scheme, err := setupScheme() 890 g.Expect(err).NotTo(HaveOccurred()) 891 client := fake.NewClientBuilder().WithScheme(scheme).Build() 892 893 cs, err := setupClusterScope(client) 894 g.Expect(err).NotTo(HaveOccurred()) 895 mockEC2Client := mocks.NewMockEC2API(mockCtrl) 896 897 ms, err := setupMachinePoolScope(client, cs) 898 g.Expect(err).NotTo(HaveOccurred()) 899 900 ms.AWSMachinePool.Spec.AWSLaunchTemplate.AdditionalSecurityGroups = tc.awsResourceReference 901 902 s := NewService(cs) 903 s.EC2Client = mockEC2Client 904 905 if tc.expect != nil { 906 tc.expect(g, mockEC2Client.EXPECT()) 907 } 908 909 launchTemplate, err := s.CreateLaunchTemplate(ms, aws.String("imageID"), userData) 910 tc.check(g, launchTemplate, err) 911 }) 912 } 913 } 914 915 func Test_LaunchTemplateDataCreation(t *testing.T) { 916 mockCtrl := gomock.NewController(t) 917 defer mockCtrl.Finish() 918 t.Run("Should return error if failed to create launch template data", func(t *testing.T) { 919 g := NewWithT(t) 920 scheme, err := setupScheme() 921 g.Expect(err).NotTo(HaveOccurred()) 922 client := fake.NewClientBuilder().WithScheme(scheme).Build() 923 924 cs, err := setupClusterScope(client) 925 g.Expect(err).NotTo(HaveOccurred()) 926 cs.AWSCluster.Status.Network.SecurityGroups[infrav1.SecurityGroupBastion] = infrav1.SecurityGroup{ID: "1"} 927 928 ms, err := setupMachinePoolScope(client, cs) 929 g.Expect(err).NotTo(HaveOccurred()) 930 931 s := NewService(cs) 932 933 launchTemplate, err := s.CreateLaunchTemplate(ms, aws.String("imageID"), nil) 934 g.Expect(err).To(HaveOccurred()) 935 g.Expect(launchTemplate).Should(BeEmpty()) 936 }) 937 } 938 939 func TestCreateLaunchTemplateVersion(t *testing.T) { 940 mockCtrl := gomock.NewController(t) 941 defer mockCtrl.Finish() 942 943 var formatTagsInput = func(arg *ec2.CreateLaunchTemplateVersionInput) { 944 for index := range arg.LaunchTemplateData.TagSpecifications { 945 sortTags(arg.LaunchTemplateData.TagSpecifications[index].Tags) 946 } 947 } 948 var userData = []byte{1, 0, 0} 949 testCases := []struct { 950 name string 951 imageID *string 952 awsResourceReference []infrav1.AWSResourceReference 953 expect func(m *mocks.MockEC2APIMockRecorder) 954 wantErr bool 955 }{ 956 { 957 name: "Should successfully creates launch template version", 958 awsResourceReference: []infrav1.AWSResourceReference{{ID: aws.String("1")}}, 959 expect: func(m *mocks.MockEC2APIMockRecorder) { 960 sgMap := make(map[infrav1.SecurityGroupRole]infrav1.SecurityGroup) 961 sgMap[infrav1.SecurityGroupNode] = infrav1.SecurityGroup{ID: "1"} 962 sgMap[infrav1.SecurityGroupLB] = infrav1.SecurityGroup{ID: "2"} 963 964 var expectedInput = &ec2.CreateLaunchTemplateVersionInput{ 965 LaunchTemplateData: &ec2.RequestLaunchTemplateData{ 966 InstanceType: aws.String("t3.large"), 967 IamInstanceProfile: &ec2.LaunchTemplateIamInstanceProfileSpecificationRequest{ 968 Name: aws.String("instance-profile"), 969 }, 970 KeyName: aws.String("default"), 971 UserData: pointer.StringPtr(base64.StdEncoding.EncodeToString(userData)), 972 SecurityGroupIds: aws.StringSlice([]string{"nodeSG", "lbSG", "1"}), 973 ImageId: aws.String("imageID"), 974 TagSpecifications: []*ec2.LaunchTemplateTagSpecificationRequest{ 975 { 976 ResourceType: aws.String(ec2.ResourceTypeInstance), 977 Tags: defaultEC2Tags("aws-mp-name", "cluster-name"), 978 }, 979 { 980 ResourceType: aws.String(ec2.ResourceTypeVolume), 981 Tags: defaultEC2Tags("aws-mp-name", "cluster-name"), 982 }, 983 }, 984 }, 985 LaunchTemplateId: aws.String("launch-template-id"), 986 } 987 m.CreateLaunchTemplateVersion(gomock.AssignableToTypeOf(expectedInput)).Return(&ec2.CreateLaunchTemplateVersionOutput{ 988 LaunchTemplateVersion: &ec2.LaunchTemplateVersion{ 989 LaunchTemplateId: aws.String("launch-template-id"), 990 }, 991 }, nil).Do( 992 func(arg *ec2.CreateLaunchTemplateVersionInput) { 993 // formatting added to match tags slice during cmp.Equal() 994 formatTagsInput(arg) 995 if !cmp.Equal(expectedInput, arg) { 996 t.Fatalf("mismatch in input expected: %+v, but got %+v", expectedInput, arg) 997 } 998 }) 999 }, 1000 }, 1001 { 1002 name: "Should return error if AWS failed during launch template version creation", 1003 awsResourceReference: []infrav1.AWSResourceReference{{ID: aws.String("1")}}, 1004 expect: func(m *mocks.MockEC2APIMockRecorder) { 1005 sgMap := make(map[infrav1.SecurityGroupRole]infrav1.SecurityGroup) 1006 sgMap[infrav1.SecurityGroupNode] = infrav1.SecurityGroup{ID: "1"} 1007 sgMap[infrav1.SecurityGroupLB] = infrav1.SecurityGroup{ID: "2"} 1008 1009 var expectedInput = &ec2.CreateLaunchTemplateVersionInput{ 1010 LaunchTemplateData: &ec2.RequestLaunchTemplateData{ 1011 InstanceType: aws.String("t3.large"), 1012 IamInstanceProfile: &ec2.LaunchTemplateIamInstanceProfileSpecificationRequest{ 1013 Name: aws.String("instance-profile"), 1014 }, 1015 KeyName: aws.String("default"), 1016 UserData: pointer.StringPtr(base64.StdEncoding.EncodeToString(userData)), 1017 SecurityGroupIds: aws.StringSlice([]string{"nodeSG", "lbSG", "1"}), 1018 ImageId: aws.String("imageID"), 1019 TagSpecifications: []*ec2.LaunchTemplateTagSpecificationRequest{ 1020 { 1021 ResourceType: aws.String(ec2.ResourceTypeInstance), 1022 Tags: defaultEC2Tags("aws-mp-name", "cluster-name"), 1023 }, 1024 { 1025 ResourceType: aws.String(ec2.ResourceTypeVolume), 1026 Tags: defaultEC2Tags("aws-mp-name", "cluster-name"), 1027 }, 1028 }, 1029 }, 1030 LaunchTemplateId: aws.String("launch-template-id"), 1031 } 1032 m.CreateLaunchTemplateVersion(gomock.AssignableToTypeOf(expectedInput)).Return(nil, 1033 awserrors.NewFailedDependency("dependency failure")).Do( 1034 func(arg *ec2.CreateLaunchTemplateVersionInput) { 1035 // formatting added to match tags slice during cmp.Equal() 1036 formatTagsInput(arg) 1037 if !cmp.Equal(expectedInput, arg) { 1038 t.Fatalf("mismatch in input expected: %+v, got: %+v", expectedInput, arg) 1039 } 1040 }) 1041 }, 1042 wantErr: true, 1043 }, 1044 } 1045 for _, tc := range testCases { 1046 t.Run(tc.name, func(t *testing.T) { 1047 g := NewWithT(t) 1048 1049 scheme, err := setupScheme() 1050 g.Expect(err).NotTo(HaveOccurred()) 1051 client := fake.NewClientBuilder().WithScheme(scheme).Build() 1052 1053 cs, err := setupClusterScope(client) 1054 g.Expect(err).NotTo(HaveOccurred()) 1055 1056 mpScope, err := setupMachinePoolScope(client, cs) 1057 g.Expect(err).NotTo(HaveOccurred()) 1058 1059 mpScope.AWSMachinePool.Spec.AWSLaunchTemplate.AdditionalSecurityGroups = tc.awsResourceReference 1060 1061 mockEC2Client := mocks.NewMockEC2API(mockCtrl) 1062 s := NewService(cs) 1063 s.EC2Client = mockEC2Client 1064 1065 if tc.expect != nil { 1066 tc.expect(mockEC2Client.EXPECT()) 1067 } 1068 if tc.wantErr { 1069 g.Expect(s.CreateLaunchTemplateVersion(mpScope, aws.String("imageID"), userData)).To(HaveOccurred()) 1070 return 1071 } 1072 g.Expect(s.CreateLaunchTemplateVersion(mpScope, aws.String("imageID"), userData)).NotTo(HaveOccurred()) 1073 }) 1074 } 1075 } 1076 1077 func TestBuildLaunchTemplateTagSpecificationRequest(t *testing.T) { 1078 mockCtrl := gomock.NewController(t) 1079 defer mockCtrl.Finish() 1080 1081 testCases := []struct { 1082 name string 1083 check func(g *WithT, m []*ec2.LaunchTemplateTagSpecificationRequest) 1084 }{ 1085 { 1086 name: "Should create tag specification request for building Launch template tags", 1087 check: func(g *WithT, res []*ec2.LaunchTemplateTagSpecificationRequest) { 1088 expected := []*ec2.LaunchTemplateTagSpecificationRequest{ 1089 { 1090 ResourceType: aws.String(ec2.ResourceTypeInstance), 1091 Tags: defaultEC2Tags("aws-mp-name", "cluster-name"), 1092 }, 1093 { 1094 ResourceType: aws.String(ec2.ResourceTypeVolume), 1095 Tags: defaultEC2Tags("aws-mp-name", "cluster-name"), 1096 }, 1097 } 1098 // sorting tags for comparing each request tags during cmp.Equal() 1099 for _, each := range res { 1100 sortTags(each.Tags) 1101 } 1102 g.Expect(res).Should(Equal(expected)) 1103 }, 1104 }, 1105 } 1106 for _, tc := range testCases { 1107 t.Run(tc.name, func(t *testing.T) { 1108 g := NewWithT(t) 1109 1110 scheme, err := setupScheme() 1111 g.Expect(err).NotTo(HaveOccurred()) 1112 client := fake.NewClientBuilder().WithScheme(scheme).Build() 1113 1114 cs, err := setupClusterScope(client) 1115 g.Expect(err).NotTo(HaveOccurred()) 1116 1117 mpScope, err := setupMachinePoolScope(client, cs) 1118 g.Expect(err).NotTo(HaveOccurred()) 1119 1120 s := NewService(cs) 1121 tc.check(g, s.buildLaunchTemplateTagSpecificationRequest(mpScope)) 1122 }) 1123 } 1124 } 1125 1126 func TestDiscoverLaunchTemplateAMI(t *testing.T) { 1127 mockCtrl := gomock.NewController(t) 1128 defer mockCtrl.Finish() 1129 1130 testCases := []struct { 1131 name string 1132 awsLaunchTemplate expinfrav1.AWSLaunchTemplate 1133 machineTemplate clusterv1.MachineTemplateSpec 1134 expect func(m *mocks.MockEC2APIMockRecorder) 1135 check func(*WithT, *string, error) 1136 }{ 1137 { 1138 name: "Should return default AMI for non EKS managed cluster if Image lookup format, org and BaseOS passed", 1139 awsLaunchTemplate: expinfrav1.AWSLaunchTemplate{ 1140 Name: "aws-launch-tmpl", 1141 ImageLookupFormat: "ilf", 1142 ImageLookupOrg: "ilo", 1143 ImageLookupBaseOS: "ilbo", 1144 }, 1145 machineTemplate: clusterv1.MachineTemplateSpec{ 1146 Spec: clusterv1.MachineSpec{ 1147 Version: aws.String(DefaultAmiNameFormat), 1148 }, 1149 }, 1150 expect: func(m *mocks.MockEC2APIMockRecorder) { 1151 m.DescribeImages(gomock.AssignableToTypeOf(&ec2.DescribeImagesInput{})). 1152 Return(&ec2.DescribeImagesOutput{ 1153 Images: []*ec2.Image{ 1154 { 1155 ImageId: aws.String("ancient"), 1156 CreationDate: aws.String("2011-02-08T17:02:31.000Z"), 1157 }, 1158 { 1159 ImageId: aws.String("latest"), 1160 CreationDate: aws.String("2019-02-08T17:02:31.000Z"), 1161 }, 1162 { 1163 ImageId: aws.String("oldest"), 1164 CreationDate: aws.String("2014-02-08T17:02:31.000Z"), 1165 }, 1166 }, 1167 }, nil) 1168 }, 1169 check: func(g *WithT, res *string, err error) { 1170 g.Expect(res).Should(Equal(aws.String("latest"))) 1171 g.Expect(err).NotTo(HaveOccurred()) 1172 }, 1173 }, 1174 { 1175 name: "Should return AMI and use infra cluster image details, if not passed in aws launchtemplate", 1176 awsLaunchTemplate: expinfrav1.AWSLaunchTemplate{ 1177 Name: "aws-launch-tmpl", 1178 }, 1179 machineTemplate: clusterv1.MachineTemplateSpec{ 1180 Spec: clusterv1.MachineSpec{ 1181 Version: aws.String(DefaultAmiNameFormat), 1182 }, 1183 }, 1184 expect: func(m *mocks.MockEC2APIMockRecorder) { 1185 m.DescribeImages(gomock.AssignableToTypeOf(&ec2.DescribeImagesInput{})). 1186 Return(&ec2.DescribeImagesOutput{ 1187 Images: []*ec2.Image{ 1188 { 1189 ImageId: aws.String("ancient"), 1190 CreationDate: aws.String("2011-02-08T17:02:31.000Z"), 1191 }, 1192 { 1193 ImageId: aws.String("latest"), 1194 CreationDate: aws.String("2019-02-08T17:02:31.000Z"), 1195 }, 1196 { 1197 ImageId: aws.String("oldest"), 1198 CreationDate: aws.String("2014-02-08T17:02:31.000Z"), 1199 }, 1200 }, 1201 }, nil) 1202 }, 1203 check: func(g *WithT, res *string, err error) { 1204 g.Expect(res).Should(Equal(aws.String("latest"))) 1205 g.Expect(err).NotTo(HaveOccurred()) 1206 }, 1207 }, 1208 { 1209 name: "Should return AWSlaunchtemplate ID if provided", 1210 awsLaunchTemplate: expinfrav1.AWSLaunchTemplate{ 1211 Name: "aws-launch-tmpl", 1212 AMI: infrav1.AMIReference{ID: aws.String("id")}, 1213 }, 1214 check: func(g *WithT, res *string, err error) { 1215 g.Expect(res).Should(Equal(aws.String("id"))) 1216 g.Expect(err).NotTo(HaveOccurred()) 1217 }, 1218 }, 1219 { 1220 name: "Should return with error if both AWSlaunchtemplate ID and machinePool version is not provided", 1221 awsLaunchTemplate: expinfrav1.AWSLaunchTemplate{ 1222 Name: "aws-launch-tmpl", 1223 }, 1224 check: func(g *WithT, res *string, err error) { 1225 g.Expect(err).To(HaveOccurred()) 1226 g.Expect(res).To(BeNil()) 1227 }, 1228 }, 1229 { 1230 name: "Should return error if AWS failed while describing images", 1231 awsLaunchTemplate: expinfrav1.AWSLaunchTemplate{ 1232 Name: "aws-launch-tmpl", 1233 }, 1234 machineTemplate: clusterv1.MachineTemplateSpec{ 1235 Spec: clusterv1.MachineSpec{ 1236 Version: aws.String(DefaultAmiNameFormat), 1237 }, 1238 }, 1239 expect: func(m *mocks.MockEC2APIMockRecorder) { 1240 m.DescribeImages(gomock.AssignableToTypeOf(&ec2.DescribeImagesInput{})). 1241 Return(nil, awserrors.NewFailedDependency("dependency-failure")) 1242 }, 1243 check: func(g *WithT, res *string, err error) { 1244 g.Expect(res).To(BeNil()) 1245 g.Expect(err).To(HaveOccurred()) 1246 }, 1247 }, 1248 } 1249 for _, tc := range testCases { 1250 t.Run(tc.name, func(t *testing.T) { 1251 g := NewWithT(t) 1252 1253 ec2Mock := mocks.NewMockEC2API(mockCtrl) 1254 1255 scheme, err := setupScheme() 1256 g.Expect(err).NotTo(HaveOccurred()) 1257 client := fake.NewClientBuilder().WithScheme(scheme).Build() 1258 1259 cs, err := setupClusterScope(client) 1260 g.Expect(err).NotTo(HaveOccurred()) 1261 1262 ms, err := setupMachinePoolScope(client, cs) 1263 g.Expect(err).NotTo(HaveOccurred()) 1264 1265 ms.AWSMachinePool.Spec.AWSLaunchTemplate = tc.awsLaunchTemplate 1266 ms.MachinePool.Spec.Template = tc.machineTemplate 1267 1268 if tc.expect != nil { 1269 tc.expect(ec2Mock.EXPECT()) 1270 } 1271 1272 s := NewService(cs) 1273 s.EC2Client = ec2Mock 1274 1275 id, err := s.DiscoverLaunchTemplateAMI(ms) 1276 tc.check(g, id, err) 1277 }) 1278 } 1279 } 1280 1281 func TestDiscoverLaunchTemplateAMI_ForEKS(t *testing.T) { 1282 mockCtrl := gomock.NewController(t) 1283 defer mockCtrl.Finish() 1284 1285 testCases := []struct { 1286 name string 1287 awsLaunchTemplate expinfrav1.AWSLaunchTemplate 1288 machineTemplate clusterv1.MachineTemplateSpec 1289 expect func(m *mock_ssmiface.MockSSMAPIMockRecorder) 1290 check func(*WithT, *string, error) 1291 }{ 1292 { 1293 name: "Should return AMI and use EKS infra cluster image details, if not passed in aws launch template", 1294 expect: func(m *mock_ssmiface.MockSSMAPIMockRecorder) { 1295 m.GetParameter(gomock.AssignableToTypeOf(&ssm.GetParameterInput{})). 1296 Return(&ssm.GetParameterOutput{ 1297 Parameter: &ssm.Parameter{ 1298 Value: aws.String("latest"), 1299 }, 1300 }, nil) 1301 }, 1302 check: func(g *WithT, res *string, err error) { 1303 g.Expect(res).Should(Equal(aws.String("latest"))) 1304 g.Expect(err).NotTo(HaveOccurred()) 1305 }, 1306 }, 1307 } 1308 for _, tc := range testCases { 1309 t.Run(tc.name, func(t *testing.T) { 1310 g := NewWithT(t) 1311 1312 ssmMock := mock_ssmiface.NewMockSSMAPI(mockCtrl) 1313 1314 scheme, err := setupScheme() 1315 g.Expect(err).NotTo(HaveOccurred()) 1316 client := fake.NewClientBuilder().WithScheme(scheme).Build() 1317 1318 mcps, err := setupNewManagedControlPlaneScope(client) 1319 g.Expect(err).NotTo(HaveOccurred()) 1320 1321 ms, err := setupMachinePoolScope(client, mcps) 1322 g.Expect(err).NotTo(HaveOccurred()) 1323 1324 if tc.expect != nil { 1325 tc.expect(ssmMock.EXPECT()) 1326 } 1327 1328 s := NewService(mcps) 1329 s.SSMClient = ssmMock 1330 1331 id, err := s.DiscoverLaunchTemplateAMI(ms) 1332 tc.check(g, id, err) 1333 }) 1334 } 1335 } 1336 1337 func TestDeleteLaunchTemplateVersion(t *testing.T) { 1338 mockCtrl := gomock.NewController(t) 1339 defer mockCtrl.Finish() 1340 1341 type args struct { 1342 id string 1343 version *int64 1344 } 1345 testCases := []struct { 1346 name string 1347 args args 1348 expect func(m *mocks.MockEC2APIMockRecorder) 1349 wantErr bool 1350 }{ 1351 { 1352 name: "Should return error if version is nil", 1353 wantErr: true, 1354 }, 1355 { 1356 name: "Should return error if AWS unable to delete launch template version", 1357 args: args{ 1358 id: "id", 1359 version: aws.Int64(12), 1360 }, 1361 expect: func(m *mocks.MockEC2APIMockRecorder) { 1362 m.DeleteLaunchTemplateVersions(gomock.Eq( 1363 &ec2.DeleteLaunchTemplateVersionsInput{ 1364 LaunchTemplateId: aws.String("id"), 1365 Versions: aws.StringSlice([]string{"12"}), 1366 }, 1367 )).Return(nil, awserrors.NewFailedDependency("dependency-failure")) 1368 }, 1369 wantErr: true, 1370 }, 1371 { 1372 name: "Should successfully deletes launch template version if AWS call passed", 1373 args: args{ 1374 id: "id", 1375 version: aws.Int64(12), 1376 }, 1377 expect: func(m *mocks.MockEC2APIMockRecorder) { 1378 m.DeleteLaunchTemplateVersions(gomock.Eq( 1379 &ec2.DeleteLaunchTemplateVersionsInput{ 1380 LaunchTemplateId: aws.String("id"), 1381 Versions: aws.StringSlice([]string{"12"}), 1382 }, 1383 )).Return(nil, nil) 1384 }, 1385 }, 1386 } 1387 1388 for _, tc := range testCases { 1389 t.Run(tc.name, func(t *testing.T) { 1390 g := NewWithT(t) 1391 1392 scheme, err := setupScheme() 1393 g.Expect(err).NotTo(HaveOccurred()) 1394 client := fake.NewClientBuilder().WithScheme(scheme).Build() 1395 1396 cs, err := setupClusterScope(client) 1397 g.Expect(err).NotTo(HaveOccurred()) 1398 1399 ec2Mock := mocks.NewMockEC2API(mockCtrl) 1400 s := NewService(cs) 1401 s.EC2Client = ec2Mock 1402 1403 if tc.expect != nil { 1404 tc.expect(ec2Mock.EXPECT()) 1405 } 1406 1407 if tc.wantErr { 1408 g.Expect(s.deleteLaunchTemplateVersion(tc.args.id, tc.args.version)).To(HaveOccurred()) 1409 return 1410 } 1411 g.Expect(s.deleteLaunchTemplateVersion(tc.args.id, tc.args.version)).NotTo(HaveOccurred()) 1412 }) 1413 } 1414 }