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  }