sigs.k8s.io/cluster-api-provider-aws@v1.5.5/pkg/cloud/services/ec2/ami_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 ec2 18 19 import ( 20 "testing" 21 22 "github.com/aws/aws-sdk-go/aws" 23 "github.com/aws/aws-sdk-go/service/ec2" 24 "github.com/aws/aws-sdk-go/service/ssm" 25 "github.com/golang/mock/gomock" 26 . "github.com/onsi/gomega" 27 "sigs.k8s.io/controller-runtime/pkg/client/fake" 28 29 infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1beta1" 30 "sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/awserrors" 31 "sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services/ssm/mock_ssmiface" 32 "sigs.k8s.io/cluster-api-provider-aws/test/mocks" 33 ) 34 35 func Test_DefaultAMILookup(t *testing.T) { 36 mockCtrl := gomock.NewController(t) 37 defer mockCtrl.Finish() 38 39 type args struct { 40 ownerID string 41 baseOS string 42 kubernetesVersion string 43 amiNameFormat string 44 } 45 46 testCases := []struct { 47 name string 48 args args 49 expect func(m *mocks.MockEC2APIMockRecorder) 50 check func(g *WithT, img *ec2.Image, err error) 51 }{ 52 { 53 name: "Should return latest AMI in case of valid inputs", 54 args: args{ 55 ownerID: "ownerID", 56 baseOS: "baseOS", 57 kubernetesVersion: "v1.0.0", 58 amiNameFormat: "ami-name", 59 }, 60 expect: func(m *mocks.MockEC2APIMockRecorder) { 61 m.DescribeImages(gomock.AssignableToTypeOf(&ec2.DescribeImagesInput{})). 62 Return(&ec2.DescribeImagesOutput{ 63 Images: []*ec2.Image{ 64 { 65 ImageId: aws.String("ancient"), 66 CreationDate: aws.String("2011-02-08T17:02:31.000Z"), 67 }, 68 { 69 ImageId: aws.String("latest"), 70 CreationDate: aws.String("2019-02-08T17:02:31.000Z"), 71 }, 72 { 73 ImageId: aws.String("oldest"), 74 CreationDate: aws.String("2014-02-08T17:02:31.000Z"), 75 }, 76 }, 77 }, nil) 78 }, 79 check: func(g *WithT, img *ec2.Image, err error) { 80 g.Expect(err).NotTo(HaveOccurred()) 81 g.Expect(*img.ImageId).Should(ContainSubstring("latest")) 82 }, 83 }, 84 { 85 name: "Should return with error if AWS DescribeImages call failed with some error", 86 expect: func(m *mocks.MockEC2APIMockRecorder) { 87 m.DescribeImages(gomock.AssignableToTypeOf(&ec2.DescribeImagesInput{})). 88 Return(nil, awserrors.NewFailedDependency("dependency failure")) 89 }, 90 check: func(g *WithT, img *ec2.Image, err error) { 91 g.Expect(err).To(HaveOccurred()) 92 g.Expect(img).To(BeNil()) 93 }, 94 }, 95 { 96 name: "Should return with error if empty list of images returned from AWS ", 97 expect: func(m *mocks.MockEC2APIMockRecorder) { 98 m.DescribeImages(gomock.AssignableToTypeOf(&ec2.DescribeImagesInput{})). 99 Return(&ec2.DescribeImagesOutput{}, nil) 100 }, 101 check: func(g *WithT, img *ec2.Image, err error) { 102 g.Expect(err).To(HaveOccurred()) 103 g.Expect(img).To(BeNil()) 104 }, 105 }, 106 } 107 108 for _, tc := range testCases { 109 t.Run(tc.name, func(t *testing.T) { 110 g := NewWithT(t) 111 112 ec2Mock := mocks.NewMockEC2API(mockCtrl) 113 tc.expect(ec2Mock.EXPECT()) 114 115 img, err := DefaultAMILookup(ec2Mock, tc.args.ownerID, tc.args.baseOS, tc.args.kubernetesVersion, tc.args.amiNameFormat) 116 tc.check(g, img, err) 117 }) 118 } 119 } 120 121 func TestAMIs(t *testing.T) { 122 mockCtrl := gomock.NewController(t) 123 defer mockCtrl.Finish() 124 125 testCases := []struct { 126 name string 127 expect func(m *mocks.MockEC2APIMockRecorder) 128 check func(g *WithT, id string, err error) 129 }{ 130 { 131 name: "Should return latest AMI in case of valid inputs", 132 expect: func(m *mocks.MockEC2APIMockRecorder) { 133 m.DescribeImages(gomock.AssignableToTypeOf(&ec2.DescribeImagesInput{})). 134 Return(&ec2.DescribeImagesOutput{ 135 Images: []*ec2.Image{ 136 { 137 ImageId: aws.String("ancient"), 138 CreationDate: aws.String("2011-02-08T17:02:31.000Z"), 139 }, 140 { 141 ImageId: aws.String("latest"), 142 CreationDate: aws.String("2019-02-08T17:02:31.000Z"), 143 }, 144 { 145 ImageId: aws.String("oldest"), 146 CreationDate: aws.String("2014-02-08T17:02:31.000Z"), 147 }, 148 }, 149 }, nil) 150 }, 151 check: func(g *WithT, id string, err error) { 152 g.Expect(err).NotTo(HaveOccurred()) 153 g.Expect(id).Should(ContainSubstring("latest")) 154 }, 155 }, 156 { 157 name: "Should return error if invalid creation date passed", 158 expect: func(m *mocks.MockEC2APIMockRecorder) { 159 m.DescribeImages(gomock.AssignableToTypeOf(&ec2.DescribeImagesInput{})). 160 Return(&ec2.DescribeImagesOutput{ 161 Images: []*ec2.Image{ 162 { 163 ImageId: aws.String("ancient"), 164 CreationDate: aws.String("2011-02-08T17:02:31.000Z"), 165 }, 166 { 167 ImageId: aws.String("latest"), 168 CreationDate: aws.String("invalid creation date"), 169 }, 170 { 171 ImageId: aws.String("oldest"), 172 CreationDate: aws.String("2014-02-08T17:02:31.000Z"), 173 }, 174 }, 175 }, nil) 176 }, 177 check: func(g *WithT, id string, err error) { 178 g.Expect(err).To(HaveOccurred()) 179 g.Expect(id).Should(BeEmpty()) 180 }, 181 }, 182 } 183 184 for _, tc := range testCases { 185 t.Run(tc.name, func(t *testing.T) { 186 g := NewWithT(t) 187 188 scheme, err := setupScheme() 189 g.Expect(err).NotTo(HaveOccurred()) 190 client := fake.NewClientBuilder().WithScheme(scheme).Build() 191 192 ec2Mock := mocks.NewMockEC2API(mockCtrl) 193 tc.expect(ec2Mock.EXPECT()) 194 195 clusterScope, err := setupClusterScope(client) 196 g.Expect(err).NotTo(HaveOccurred()) 197 198 s := NewService(clusterScope) 199 s.EC2Client = ec2Mock 200 201 id, err := s.defaultAMIIDLookup("", "", "base os-baseos version", "v1.11.1") 202 tc.check(g, id, err) 203 }) 204 } 205 } 206 207 func TestFormatVersionForEKS(t *testing.T) { 208 tests := []struct { 209 name string 210 version string 211 want string 212 wantErr bool 213 }{ 214 { 215 name: "Should remove non zero patch from version", 216 version: "v1.23.2", 217 want: "1.23", 218 wantErr: false, 219 }, 220 { 221 name: "Should return major.minor in case patch is nil", 222 version: "v1.23", 223 want: "1.23", 224 wantErr: false, 225 }, 226 { 227 name: "Should return minor as zero if only major is present in version", 228 version: "v1", 229 want: "1.0", 230 wantErr: false, 231 }, 232 { 233 name: "Should return error if invalid version is given", 234 version: "v1-23.3", 235 wantErr: true, 236 }, 237 } 238 for _, tt := range tests { 239 t.Run(tt.name, func(t *testing.T) { 240 g := NewWithT(t) 241 got, err := formatVersionForEKS(tt.version) 242 if tt.wantErr { 243 g.Expect(err).To(HaveOccurred()) 244 return 245 } 246 g.Expect(err).NotTo(HaveOccurred()) 247 g.Expect(got).Should(BeEquivalentTo(tt.want)) 248 }) 249 } 250 } 251 252 func TestGenerateAmiName(t *testing.T) { 253 type args struct { 254 amiNameFormat string 255 baseOS string 256 kubernetesVersion string 257 } 258 tests := []struct { 259 name string 260 args args 261 want string 262 }{ 263 { 264 name: "Should return image name even if OS and amiNameFormat is empty", 265 args: args{ 266 kubernetesVersion: "v1.23.3", 267 }, 268 want: "capa-ami--?1.23.3-*", 269 }, 270 { 271 name: "Should return valid amiName if default AMI name format passed", 272 args: args{ 273 amiNameFormat: DefaultAmiNameFormat, 274 baseOS: "centos-7", 275 kubernetesVersion: "1.23.3", 276 }, 277 want: "capa-ami-centos-7-?1.23.3-*", 278 }, 279 { 280 name: "Should return valid amiName if custom AMI name format passed", 281 args: args{ 282 amiNameFormat: "random-{{.BaseOS}}-?{{.K8sVersion}}-*", 283 baseOS: "centos-7", 284 kubernetesVersion: "1.23.3", 285 }, 286 want: "random-centos-7-?1.23.3-*", 287 }, 288 } 289 for _, tt := range tests { 290 t.Run(tt.name, func(t *testing.T) { 291 g := NewWithT(t) 292 got, err := GenerateAmiName(tt.args.amiNameFormat, tt.args.baseOS, tt.args.kubernetesVersion) 293 g.Expect(err).To(BeNil()) 294 g.Expect(got).Should(Equal(tt.want)) 295 }) 296 } 297 } 298 299 func TestGetLatestImage(t *testing.T) { 300 tests := []struct { 301 name string 302 imgs []*ec2.Image 303 want *ec2.Image 304 wantErr bool 305 }{ 306 { 307 name: "Should return image with latest creation date", 308 imgs: []*ec2.Image{ 309 { 310 ImageId: aws.String("ancient"), 311 CreationDate: aws.String("2011-02-08T17:02:31.000Z"), 312 }, 313 { 314 ImageId: aws.String("latest"), 315 CreationDate: aws.String("2019-02-08T17:02:31.000Z"), 316 }, 317 { 318 ImageId: aws.String("oldest"), 319 CreationDate: aws.String("2014-02-08T17:02:31.000Z"), 320 }, 321 }, 322 want: &ec2.Image{ 323 ImageId: aws.String("latest"), 324 CreationDate: aws.String("2019-02-08T17:02:31.000Z"), 325 }, 326 wantErr: false, 327 }, 328 { 329 name: "Should return last image if all images have same creation date", 330 imgs: []*ec2.Image{ 331 { 332 ImageId: aws.String("image 1"), 333 CreationDate: aws.String("2019-02-08T17:02:31.000Z"), 334 }, 335 { 336 ImageId: aws.String("image 2"), 337 CreationDate: aws.String("2019-02-08T17:02:31.000Z"), 338 }, 339 { 340 ImageId: aws.String("image 3"), 341 CreationDate: aws.String("2019-02-08T17:02:31.000Z"), 342 }, 343 }, 344 want: &ec2.Image{ 345 ImageId: aws.String("image 3"), 346 CreationDate: aws.String("2019-02-08T17:02:31.000Z"), 347 }, 348 wantErr: false, 349 }, 350 { 351 name: "Should return error if creation date is given in wrong format", 352 imgs: []*ec2.Image{ 353 { 354 ImageId: aws.String("image 1"), 355 CreationDate: aws.String("2019-02-08"), 356 }, 357 { 358 ImageId: aws.String("image 2"), 359 CreationDate: aws.String("2019-02-08"), 360 }, 361 }, 362 want: nil, 363 wantErr: true, 364 }, 365 } 366 for _, tt := range tests { 367 t.Run(tt.name, func(t *testing.T) { 368 g := NewWithT(t) 369 got, err := GetLatestImage(tt.imgs) 370 if tt.wantErr { 371 g.Expect(err).To(HaveOccurred()) 372 return 373 } 374 g.Expect(got).Should(Equal(tt.want)) 375 }) 376 } 377 } 378 379 func TestEKSAMILookUp(t *testing.T) { 380 mockCtrl := gomock.NewController(t) 381 defer mockCtrl.Finish() 382 383 gpuAMI := infrav1.AmazonLinuxGPU 384 tests := []struct { 385 name string 386 k8sVersion string 387 amiType *infrav1.EKSAMILookupType 388 expect func(m *mock_ssmiface.MockSSMAPIMockRecorder) 389 want string 390 wantErr bool 391 }{ 392 { 393 name: "Should return an id corresponding to GPU if GPU based AMI type passed", 394 k8sVersion: "v1.23.3", 395 amiType: &gpuAMI, 396 expect: func(m *mock_ssmiface.MockSSMAPIMockRecorder) { 397 m.GetParameter(gomock.Eq(&ssm.GetParameterInput{ 398 Name: aws.String("/aws/service/eks/optimized-ami/1.23/amazon-linux-2-gpu/recommended/image_id"), 399 })).Return(&ssm.GetParameterOutput{ 400 Parameter: &ssm.Parameter{ 401 Value: aws.String("id"), 402 }, 403 }, nil) 404 }, 405 want: "id", 406 wantErr: false, 407 }, 408 { 409 name: "Should return an id not corresponding to GPU if AMI type is default", 410 k8sVersion: "v1.23.3", 411 expect: func(m *mock_ssmiface.MockSSMAPIMockRecorder) { 412 m.GetParameter(gomock.Eq(&ssm.GetParameterInput{ 413 Name: aws.String("/aws/service/eks/optimized-ami/1.23/amazon-linux-2/recommended/image_id"), 414 })).Return(&ssm.GetParameterOutput{ 415 Parameter: &ssm.Parameter{ 416 Value: aws.String("id"), 417 }, 418 }, nil) 419 }, 420 want: "id", 421 wantErr: false, 422 }, 423 { 424 name: "Should return an error if GetParameter call fails with some AWS error", 425 k8sVersion: "v1.23.3", 426 expect: func(m *mock_ssmiface.MockSSMAPIMockRecorder) { 427 m.GetParameter(gomock.Eq(&ssm.GetParameterInput{ 428 Name: aws.String("/aws/service/eks/optimized-ami/1.23/amazon-linux-2/recommended/image_id"), 429 })).Return(nil, awserrors.NewFailedDependency("dependency failure")) 430 }, 431 wantErr: true, 432 }, 433 { 434 name: "Should return an error if invalid Kubernetes version passed", 435 k8sVersion: "__$__", 436 wantErr: true, 437 }, 438 { 439 name: "Should return an error if no SSM parameter found", 440 k8sVersion: "v1.23.3", 441 expect: func(m *mock_ssmiface.MockSSMAPIMockRecorder) { 442 m.GetParameter(gomock.Eq(&ssm.GetParameterInput{ 443 Name: aws.String("/aws/service/eks/optimized-ami/1.23/amazon-linux-2/recommended/image_id"), 444 })).Return(&ssm.GetParameterOutput{}, nil) 445 }, 446 wantErr: true, 447 }, 448 } 449 for _, tt := range tests { 450 t.Run(tt.name, func(t *testing.T) { 451 g := NewWithT(t) 452 453 scheme, err := setupScheme() 454 g.Expect(err).NotTo(HaveOccurred()) 455 client := fake.NewClientBuilder().WithScheme(scheme).Build() 456 457 ssmMock := mock_ssmiface.NewMockSSMAPI(mockCtrl) 458 if tt.expect != nil { 459 tt.expect(ssmMock.EXPECT()) 460 } 461 462 clusterScope, err := setupClusterScope(client) 463 g.Expect(err).NotTo(HaveOccurred()) 464 465 s := NewService(clusterScope) 466 s.SSMClient = ssmMock 467 468 got, err := s.eksAMILookup(tt.k8sVersion, tt.amiType) 469 if tt.wantErr { 470 g.Expect(err).To(HaveOccurred()) 471 return 472 } 473 g.Expect(got).Should(Equal(tt.want)) 474 }) 475 } 476 }