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 }