sigs.k8s.io/cluster-api-provider-aws@v1.5.5/pkg/cloud/services/ec2/bastion_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  	"context"
    21  	"fmt"
    22  	"testing"
    23  
    24  	"github.com/aws/aws-sdk-go/aws"
    25  	"github.com/aws/aws-sdk-go/service/ec2"
    26  	"github.com/golang/mock/gomock"
    27  	. "github.com/onsi/gomega"
    28  	"github.com/pkg/errors"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    31  
    32  	infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1beta1"
    33  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/filter"
    34  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/scope"
    35  	"sigs.k8s.io/cluster-api-provider-aws/test/mocks"
    36  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    37  )
    38  
    39  func TestService_DeleteBastion(t *testing.T) {
    40  	clusterName := "cluster"
    41  
    42  	describeInput := &ec2.DescribeInstancesInput{
    43  		Filters: []*ec2.Filter{
    44  			filter.EC2.ProviderRole(infrav1.BastionRoleTagValue),
    45  			filter.EC2.Cluster(clusterName),
    46  			filter.EC2.InstanceStates(
    47  				ec2.InstanceStateNamePending,
    48  				ec2.InstanceStateNameRunning,
    49  				ec2.InstanceStateNameStopping,
    50  				ec2.InstanceStateNameStopped,
    51  			),
    52  		},
    53  	}
    54  
    55  	foundOutput := &ec2.DescribeInstancesOutput{
    56  		Reservations: []*ec2.Reservation{
    57  			{
    58  				Instances: []*ec2.Instance{
    59  					{
    60  						InstanceId: aws.String("id123"),
    61  						State: &ec2.InstanceState{
    62  							Name: aws.String(ec2.InstanceStateNameRunning),
    63  						},
    64  						Placement: &ec2.Placement{
    65  							AvailabilityZone: aws.String("us-east-1"),
    66  						},
    67  					},
    68  				},
    69  			},
    70  		},
    71  	}
    72  
    73  	tests := []struct {
    74  		name          string
    75  		expect        func(m *mocks.MockEC2APIMockRecorder)
    76  		expectError   bool
    77  		bastionStatus *infrav1.Instance
    78  	}{
    79  		{
    80  			name: "instance not found",
    81  			expect: func(m *mocks.MockEC2APIMockRecorder) {
    82  				m.
    83  					DescribeInstances(gomock.Eq(describeInput)).
    84  					Return(&ec2.DescribeInstancesOutput{}, nil)
    85  			},
    86  			expectError: false,
    87  		},
    88  		{
    89  			name: "describe error",
    90  			expect: func(m *mocks.MockEC2APIMockRecorder) {
    91  				m.
    92  					DescribeInstances(gomock.Eq(describeInput)).
    93  					Return(nil, errors.New("some error"))
    94  			},
    95  			expectError: true,
    96  		},
    97  		{
    98  			name: "terminate fails",
    99  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   100  				m.
   101  					DescribeInstances(gomock.Eq(describeInput)).
   102  					Return(foundOutput, nil)
   103  				m.
   104  					TerminateInstances(
   105  						gomock.Eq(&ec2.TerminateInstancesInput{
   106  							InstanceIds: aws.StringSlice([]string{"id123"}),
   107  						}),
   108  					).
   109  					Return(nil, errors.New("some error"))
   110  			},
   111  			expectError: true,
   112  		},
   113  		{
   114  			name: "wait after terminate fails",
   115  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   116  				m.
   117  					DescribeInstances(gomock.Eq(describeInput)).
   118  					Return(foundOutput, nil)
   119  				m.
   120  					TerminateInstances(
   121  						gomock.Eq(&ec2.TerminateInstancesInput{
   122  							InstanceIds: aws.StringSlice([]string{"id123"}),
   123  						}),
   124  					).
   125  					Return(nil, nil)
   126  				m.
   127  					WaitUntilInstanceTerminated(
   128  						gomock.Eq(&ec2.DescribeInstancesInput{
   129  							InstanceIds: aws.StringSlice([]string{"id123"}),
   130  						}),
   131  					).
   132  					Return(errors.New("some error"))
   133  			},
   134  			expectError: true,
   135  		},
   136  		{
   137  			name: "success",
   138  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   139  				m.
   140  					DescribeInstances(gomock.Eq(describeInput)).
   141  					Return(foundOutput, nil)
   142  				m.
   143  					TerminateInstances(
   144  						gomock.Eq(&ec2.TerminateInstancesInput{
   145  							InstanceIds: aws.StringSlice([]string{"id123"}),
   146  						}),
   147  					).
   148  					Return(nil, nil)
   149  				m.
   150  					WaitUntilInstanceTerminated(
   151  						gomock.Eq(&ec2.DescribeInstancesInput{
   152  							InstanceIds: aws.StringSlice([]string{"id123"}),
   153  						}),
   154  					).
   155  					Return(nil)
   156  			},
   157  			expectError:   false,
   158  			bastionStatus: nil,
   159  		},
   160  	}
   161  
   162  	for _, tc := range tests {
   163  		managedValues := []bool{false, true}
   164  		for i := range managedValues {
   165  			managed := managedValues[i]
   166  
   167  			t.Run(fmt.Sprintf("managed=%t %s", managed, tc.name), func(t *testing.T) {
   168  				g := NewWithT(t)
   169  
   170  				mockControl := gomock.NewController(t)
   171  				defer mockControl.Finish()
   172  
   173  				ec2Mock := mocks.NewMockEC2API(mockControl)
   174  
   175  				scheme, err := setupScheme()
   176  				g.Expect(err).To(BeNil())
   177  
   178  				awsCluster := &infrav1.AWSCluster{
   179  					ObjectMeta: metav1.ObjectMeta{Name: "test"},
   180  					Spec: infrav1.AWSClusterSpec{
   181  						NetworkSpec: infrav1.NetworkSpec{
   182  							VPC: infrav1.VPCSpec{
   183  								ID: "vpcID",
   184  							},
   185  						},
   186  					},
   187  				}
   188  
   189  				client := fake.NewClientBuilder().WithScheme(scheme).Build()
   190  				ctx := context.TODO()
   191  				client.Create(ctx, awsCluster)
   192  
   193  				scope, err := scope.NewClusterScope(scope.ClusterScopeParams{
   194  					Cluster: &clusterv1.Cluster{
   195  						ObjectMeta: metav1.ObjectMeta{
   196  							Namespace: "ns",
   197  							Name:      clusterName,
   198  						},
   199  					},
   200  					AWSCluster: awsCluster,
   201  					Client:     client,
   202  				})
   203  				g.Expect(err).To(BeNil())
   204  
   205  				if managed {
   206  					scope.AWSCluster.Spec.NetworkSpec.VPC.Tags = infrav1.Tags{
   207  						infrav1.ClusterTagKey(clusterName): string(infrav1.ResourceLifecycleOwned),
   208  					}
   209  				}
   210  
   211  				tc.expect(ec2Mock.EXPECT())
   212  				s := NewService(scope)
   213  				s.EC2Client = ec2Mock
   214  
   215  				err = s.DeleteBastion()
   216  				if tc.expectError {
   217  					g.Expect(err).NotTo(BeNil())
   218  					return
   219  				}
   220  
   221  				g.Expect(err).To(BeNil())
   222  
   223  				g.Expect(scope.AWSCluster.Status.Bastion).To(BeEquivalentTo(tc.bastionStatus))
   224  			})
   225  		}
   226  	}
   227  }
   228  
   229  func TestService_ReconcileBastion(t *testing.T) {
   230  	clusterName := "cluster"
   231  
   232  	describeInput := &ec2.DescribeInstancesInput{
   233  		Filters: []*ec2.Filter{
   234  			filter.EC2.ProviderRole(infrav1.BastionRoleTagValue),
   235  			filter.EC2.Cluster(clusterName),
   236  			filter.EC2.InstanceStates(
   237  				ec2.InstanceStateNamePending,
   238  				ec2.InstanceStateNameRunning,
   239  				ec2.InstanceStateNameStopping,
   240  				ec2.InstanceStateNameStopped,
   241  			),
   242  		},
   243  	}
   244  
   245  	foundOutput := &ec2.DescribeInstancesOutput{
   246  		Reservations: []*ec2.Reservation{
   247  			{
   248  				Instances: []*ec2.Instance{
   249  					{
   250  						InstanceId: aws.String("id123"),
   251  						State: &ec2.InstanceState{
   252  							Name: aws.String(ec2.InstanceStateNameRunning),
   253  						},
   254  						Placement: &ec2.Placement{
   255  							AvailabilityZone: aws.String("us-east-1"),
   256  						},
   257  					},
   258  				},
   259  			},
   260  		},
   261  	}
   262  
   263  	tests := []struct {
   264  		name           string
   265  		bastionEnabled bool
   266  		expect         func(m *mocks.MockEC2APIMockRecorder)
   267  		expectError    bool
   268  		bastionStatus  *infrav1.Instance
   269  	}{
   270  		{
   271  			name: "Should ignore reconciliation if instance not found",
   272  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   273  				m.
   274  					DescribeInstances(gomock.Eq(describeInput)).
   275  					Return(&ec2.DescribeInstancesOutput{}, nil)
   276  			},
   277  			expectError: false,
   278  		},
   279  		{
   280  			name: "Should fail reconcile if describe instance fails",
   281  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   282  				m.
   283  					DescribeInstances(gomock.Eq(describeInput)).
   284  					Return(nil, errors.New("some error"))
   285  			},
   286  			expectError: true,
   287  		},
   288  		{
   289  			name: "Should fail reconcile if terminate instance fails",
   290  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   291  				m.
   292  					DescribeInstances(gomock.Eq(describeInput)).
   293  					Return(foundOutput, nil).MinTimes(1)
   294  				m.
   295  					TerminateInstances(
   296  						gomock.Eq(&ec2.TerminateInstancesInput{
   297  							InstanceIds: aws.StringSlice([]string{"id123"}),
   298  						}),
   299  					).
   300  					Return(nil, errors.New("some error"))
   301  			},
   302  			expectError: true,
   303  		},
   304  		{
   305  			name: "Should create bastion successfully",
   306  			expect: func(m *mocks.MockEC2APIMockRecorder) {
   307  				m.DescribeInstances(gomock.Eq(describeInput)).
   308  					Return(&ec2.DescribeInstancesOutput{}, nil).MinTimes(1)
   309  				m.DescribeImages(gomock.Eq(&ec2.DescribeImagesInput{Filters: []*ec2.Filter{
   310  					{
   311  						Name:   aws.String("owner-id"),
   312  						Values: aws.StringSlice([]string{ubuntuOwnerID}),
   313  					},
   314  					{
   315  						Name:   aws.String("architecture"),
   316  						Values: aws.StringSlice([]string{"x86_64"}),
   317  					},
   318  					{
   319  						Name:   aws.String("state"),
   320  						Values: aws.StringSlice([]string{"available"}),
   321  					},
   322  					{
   323  						Name:   aws.String("virtualization-type"),
   324  						Values: aws.StringSlice([]string{"hvm"}),
   325  					},
   326  					{
   327  						Name:   aws.String("description"),
   328  						Values: aws.StringSlice([]string{ubuntuImageDescription}),
   329  					},
   330  				}})).Return(&ec2.DescribeImagesOutput{Images: images{
   331  					{
   332  						ImageId:      aws.String("ubuntu-ami-id-latest"),
   333  						CreationDate: aws.String("2019-02-08T17:02:31.000Z"),
   334  					},
   335  					{
   336  						ImageId:      aws.String("ubuntu-ami-id-old"),
   337  						CreationDate: aws.String("2014-02-08T17:02:31.000Z"),
   338  					},
   339  				}}, nil)
   340  				m.RunInstances(gomock.Any()).
   341  					Return(&ec2.Reservation{
   342  						Instances: []*ec2.Instance{
   343  							{
   344  								State: &ec2.InstanceState{
   345  									Name: aws.String(ec2.InstanceStateNameRunning),
   346  								},
   347  								IamInstanceProfile: &ec2.IamInstanceProfile{
   348  									Arn: aws.String("arn:aws:iam::123456789012:instance-profile/foo"),
   349  								},
   350  								InstanceId:     aws.String("id123"),
   351  								InstanceType:   aws.String("t3.micro"),
   352  								SubnetId:       aws.String("subnet-1"),
   353  								ImageId:        aws.String("ubuntu-ami-id-latest"),
   354  								RootDeviceName: aws.String("device-1"),
   355  								BlockDeviceMappings: []*ec2.InstanceBlockDeviceMapping{
   356  									{
   357  										DeviceName: aws.String("device-1"),
   358  										Ebs: &ec2.EbsInstanceBlockDevice{
   359  											VolumeId: aws.String("volume-1"),
   360  										},
   361  									},
   362  								},
   363  								Placement: &ec2.Placement{
   364  									AvailabilityZone: aws.String("us-east-1"),
   365  								},
   366  							},
   367  						},
   368  					}, nil)
   369  				m.WaitUntilInstanceRunningWithContext(gomock.Any(), gomock.Any(), gomock.Any()).
   370  					Return(nil)
   371  			},
   372  			bastionEnabled: true,
   373  			expectError:    false,
   374  			bastionStatus: &infrav1.Instance{
   375  				ID:               "id123",
   376  				State:            "running",
   377  				Type:             "t3.micro",
   378  				SubnetID:         "subnet-1",
   379  				ImageID:          "ubuntu-ami-id-latest",
   380  				IAMProfile:       "foo",
   381  				Addresses:        []clusterv1.MachineAddress{},
   382  				AvailabilityZone: "us-east-1",
   383  				VolumeIDs:        []string{"volume-1"},
   384  			},
   385  		},
   386  	}
   387  
   388  	for _, tc := range tests {
   389  		managedValues := []bool{false, true}
   390  		for i := range managedValues {
   391  			managed := managedValues[i]
   392  
   393  			t.Run(fmt.Sprintf("managed=%t %s", managed, tc.name), func(t *testing.T) {
   394  				g := NewWithT(t)
   395  
   396  				mockControl := gomock.NewController(t)
   397  				defer mockControl.Finish()
   398  
   399  				ec2Mock := mocks.NewMockEC2API(mockControl)
   400  
   401  				scheme, err := setupScheme()
   402  				g.Expect(err).To(BeNil())
   403  
   404  				awsCluster := &infrav1.AWSCluster{
   405  					ObjectMeta: metav1.ObjectMeta{Name: "test"},
   406  					Spec: infrav1.AWSClusterSpec{
   407  						NetworkSpec: infrav1.NetworkSpec{
   408  							VPC: infrav1.VPCSpec{
   409  								ID: "vpcID",
   410  							},
   411  							Subnets: infrav1.Subnets{
   412  								{
   413  									ID: "subnet-1",
   414  								},
   415  								{
   416  									ID:       "subnet-2",
   417  									IsPublic: true,
   418  								},
   419  							},
   420  						},
   421  						Bastion: infrav1.Bastion{Enabled: tc.bastionEnabled},
   422  					},
   423  				}
   424  
   425  				client := fake.NewClientBuilder().WithScheme(scheme).Build()
   426  				ctx := context.TODO()
   427  				client.Create(ctx, awsCluster)
   428  
   429  				scope, err := scope.NewClusterScope(scope.ClusterScopeParams{
   430  					Cluster: &clusterv1.Cluster{
   431  						ObjectMeta: metav1.ObjectMeta{
   432  							Namespace: "ns",
   433  							Name:      clusterName,
   434  						},
   435  					},
   436  					AWSCluster: awsCluster,
   437  					Client:     client,
   438  				})
   439  				g.Expect(err).To(BeNil())
   440  
   441  				if managed {
   442  					scope.AWSCluster.Spec.NetworkSpec.VPC.Tags = infrav1.Tags{
   443  						infrav1.ClusterTagKey(clusterName): string(infrav1.ResourceLifecycleOwned),
   444  					}
   445  				}
   446  
   447  				tc.expect(ec2Mock.EXPECT())
   448  				s := NewService(scope)
   449  				s.EC2Client = ec2Mock
   450  
   451  				err = s.ReconcileBastion()
   452  				if tc.expectError {
   453  					g.Expect(err).NotTo(BeNil())
   454  					return
   455  				}
   456  
   457  				g.Expect(err).To(BeNil())
   458  
   459  				g.Expect(scope.AWSCluster.Status.Bastion).To(BeEquivalentTo(tc.bastionStatus))
   460  			})
   461  		}
   462  	}
   463  }