sigs.k8s.io/cluster-api-provider-aws@v1.5.5/pkg/cloud/services/s3/s3_test.go (about) 1 /* 2 Copyright 2021 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 s3_test 18 19 import ( 20 "errors" 21 "fmt" 22 "io" 23 "net/url" 24 "reflect" 25 "strings" 26 "testing" 27 28 "github.com/aws/aws-sdk-go/aws" 29 "github.com/aws/aws-sdk-go/aws/awserr" 30 s3svc "github.com/aws/aws-sdk-go/service/s3" 31 "github.com/aws/aws-sdk-go/service/sts" 32 "github.com/golang/mock/gomock" 33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 34 "k8s.io/apimachinery/pkg/runtime" 35 "sigs.k8s.io/controller-runtime/pkg/client/fake" 36 37 infrav1 "sigs.k8s.io/cluster-api-provider-aws/api/v1beta1" 38 iamv1 "sigs.k8s.io/cluster-api-provider-aws/iam/api/v1beta1" 39 "sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/scope" 40 "sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services/s3" 41 "sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services/s3/mock_s3iface" 42 "sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/services/s3/mock_stsiface" 43 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 44 ) 45 46 const ( 47 testClusterName = "test-cluster" 48 testClusterNamespace = "test-namespace" 49 ) 50 51 func Test_Reconcile_bucket(t *testing.T) { 52 t.Parallel() 53 54 t.Run("does_nothing_when_bucket_management_is_disabled", func(t *testing.T) { 55 t.Parallel() 56 57 svc, _ := testService(t, nil) 58 59 if err := svc.ReconcileBucket(); err != nil { 60 t.Fatalf("Unexpected error: %v", err) 61 } 62 }) 63 64 t.Run("creates_bucket_with_configured_name", func(t *testing.T) { 65 t.Parallel() 66 67 expectedBucketName := "baz" 68 69 svc, s3Mock := testService(t, &infrav1.S3Bucket{ 70 Name: expectedBucketName, 71 }) 72 73 input := &s3svc.CreateBucketInput{ 74 Bucket: aws.String(expectedBucketName), 75 } 76 77 s3Mock.EXPECT().CreateBucket(gomock.Eq(input)).Return(nil, nil).Times(1) 78 s3Mock.EXPECT().PutBucketPolicy(gomock.Any()).Return(nil, nil).Times(1) 79 80 if err := svc.ReconcileBucket(); err != nil { 81 t.Fatalf("Unexpected error: %v", err) 82 } 83 }) 84 85 t.Run("hashes_default_bucket_name_if_name_exceeds_maximum_length", func(t *testing.T) { 86 t.Parallel() 87 88 mockCtrl := gomock.NewController(t) 89 s3Mock := mock_s3iface.NewMockS3API(mockCtrl) 90 stsMock := mock_stsiface.NewMockSTSAPI(mockCtrl) 91 92 getCallerIdentityResult := &sts.GetCallerIdentityOutput{Account: aws.String("foo")} 93 stsMock.EXPECT().GetCallerIdentity(gomock.Any()).Return(getCallerIdentityResult, nil).AnyTimes() 94 95 scheme := runtime.NewScheme() 96 _ = infrav1.AddToScheme(scheme) 97 client := fake.NewClientBuilder().WithScheme(scheme).Build() 98 99 longName := strings.Repeat("a", 40) 100 scope, err := scope.NewClusterScope(scope.ClusterScopeParams{ 101 Client: client, 102 Cluster: &clusterv1.Cluster{ 103 ObjectMeta: metav1.ObjectMeta{ 104 Name: longName, 105 Namespace: longName, 106 }, 107 }, 108 AWSCluster: &infrav1.AWSCluster{ 109 Spec: infrav1.AWSClusterSpec{ 110 S3Bucket: &infrav1.S3Bucket{}, 111 }, 112 }, 113 }) 114 if err != nil { 115 t.Fatalf("Failed to create test context: %v", err) 116 } 117 118 svc := s3.NewService(scope) 119 svc.S3Client = s3Mock 120 svc.STSClient = stsMock 121 122 s3Mock.EXPECT().CreateBucket(gomock.Any()).Do(func(input *s3svc.CreateBucketInput) { 123 if input.Bucket == nil { 124 t.Fatalf("CreateBucket request must have Bucket specified") 125 } 126 127 if strings.Contains(*input.Bucket, longName) { 128 t.Fatalf("Default bucket name be hashed when it's very long, got: %q", *input.Bucket) 129 } 130 }).Return(nil, nil).Times(1) 131 132 s3Mock.EXPECT().PutBucketPolicy(gomock.Any()).Return(nil, nil).Times(1) 133 134 if err := svc.ReconcileBucket(); err != nil { 135 t.Fatalf("Unexpected error: %v", err) 136 } 137 }) 138 139 t.Run("creates_bucket_with_policy_allowing_controlplane_and_worker_nodes_to_read_their_secrets", func(t *testing.T) { 140 t.Parallel() 141 142 bucketName := "bar" 143 144 svc, s3Mock := testService(t, &infrav1.S3Bucket{ 145 Name: bucketName, 146 ControlPlaneIAMInstanceProfile: fmt.Sprintf("control-plane%s", iamv1.DefaultNameSuffix), 147 NodesIAMInstanceProfiles: []string{ 148 fmt.Sprintf("nodes%s", iamv1.DefaultNameSuffix), 149 }, 150 }) 151 152 s3Mock.EXPECT().CreateBucket(gomock.Any()).Return(nil, nil).Times(1) 153 s3Mock.EXPECT().PutBucketPolicy(gomock.Any()).Do(func(input *s3svc.PutBucketPolicyInput) { 154 if input.Policy == nil { 155 t.Fatalf("Policy must be defined") 156 } 157 158 policy := *input.Policy 159 160 if !strings.Contains(policy, "role/control-plane") { 161 t.Errorf("At least one policy should include a reference to control-plane role, got: %v", policy) 162 } 163 164 if !strings.Contains(policy, "role/node") { 165 t.Errorf("At least one policy should include a reference to node role, got: %v", policy) 166 } 167 168 if !strings.Contains(policy, fmt.Sprintf("%s/control-plane/*", bucketName)) { 169 t.Errorf("At least one policy should apply for all objects with %q prefix, got: %v", "control-plane", policy) 170 } 171 172 if !strings.Contains(policy, fmt.Sprintf("%s/node/*", bucketName)) { 173 t.Errorf("At least one policy should apply for all objects with %q prefix, got: %v", "node", policy) 174 } 175 }).Return(nil, nil).Times(1) 176 177 if err := svc.ReconcileBucket(); err != nil { 178 t.Fatalf("Unexpected error: %v", err) 179 } 180 }) 181 182 t.Run("is_idempotent", func(t *testing.T) { 183 t.Parallel() 184 185 svc, s3Mock := testService(t, &infrav1.S3Bucket{}) 186 187 s3Mock.EXPECT().CreateBucket(gomock.Any()).Return(nil, nil).Times(2) 188 s3Mock.EXPECT().PutBucketPolicy(gomock.Any()).Return(nil, nil).Times(2) 189 190 if err := svc.ReconcileBucket(); err != nil { 191 t.Fatalf("Unexpected error: %v", err) 192 } 193 194 if err := svc.ReconcileBucket(); err != nil { 195 t.Fatalf("Unexpected error: %v", err) 196 } 197 }) 198 199 t.Run("ignores_when_bucket_already_exists_but_its_owned_by_the_same_account", func(t *testing.T) { 200 t.Parallel() 201 202 svc, s3Mock := testService(t, &infrav1.S3Bucket{}) 203 204 err := awserr.New(s3svc.ErrCodeBucketAlreadyOwnedByYou, "err", errors.New("err")) 205 206 s3Mock.EXPECT().CreateBucket(gomock.Any()).Return(nil, err).Times(1) 207 s3Mock.EXPECT().PutBucketPolicy(gomock.Any()).Return(nil, nil).Times(1) 208 209 if err := svc.ReconcileBucket(); err != nil { 210 t.Fatalf("Unexpected error, got: %v", err) 211 } 212 }) 213 214 t.Run("returns_error_when", func(t *testing.T) { 215 t.Parallel() 216 217 t.Run("bucket_creation_fails", func(t *testing.T) { 218 t.Parallel() 219 220 svc, s3Mock := testService(t, &infrav1.S3Bucket{}) 221 222 s3Mock.EXPECT().CreateBucket(gomock.Any()).Return(nil, errors.New("error")).Times(1) 223 224 if err := svc.ReconcileBucket(); err == nil { 225 t.Fatalf("Expected error") 226 } 227 }) 228 229 t.Run("bucket_creation_returns_unexpected_AWS_error", func(t *testing.T) { 230 t.Parallel() 231 232 svc, s3Mock := testService(t, &infrav1.S3Bucket{}) 233 234 s3Mock.EXPECT().CreateBucket(gomock.Any()).Return(nil, awserr.New("foo", "", nil)).Times(1) 235 236 if err := svc.ReconcileBucket(); err == nil { 237 t.Fatalf("Expected error") 238 } 239 }) 240 241 t.Run("generating_bucket_policy_fails", func(t *testing.T) { 242 t.Parallel() 243 244 svc, s3Mock := testService(t, &infrav1.S3Bucket{}) 245 246 s3Mock.EXPECT().CreateBucket(gomock.Any()).Return(nil, nil).Times(1) 247 248 mockCtrl := gomock.NewController(t) 249 stsMock := mock_stsiface.NewMockSTSAPI(mockCtrl) 250 stsMock.EXPECT().GetCallerIdentity(gomock.Any()).Return(nil, fmt.Errorf(t.Name())).AnyTimes() 251 svc.STSClient = stsMock 252 253 if err := svc.ReconcileBucket(); err == nil { 254 t.Fatalf("Expected error") 255 } 256 }) 257 258 t.Run("creating_bucket_policy_fails", func(t *testing.T) { 259 t.Parallel() 260 261 svc, s3Mock := testService(t, &infrav1.S3Bucket{}) 262 263 s3Mock.EXPECT().CreateBucket(gomock.Any()).Return(nil, nil).Times(1) 264 s3Mock.EXPECT().PutBucketPolicy(gomock.Any()).Return(nil, errors.New("error")).Times(1) 265 266 if err := svc.ReconcileBucket(); err == nil { 267 t.Fatalf("Expected error") 268 } 269 }) 270 }) 271 } 272 273 func Test_Delete_bucket(t *testing.T) { 274 t.Parallel() 275 276 const bucketName = "foo" 277 278 t.Run("does_nothing_when_bucket_management_is_disabled", func(t *testing.T) { 279 t.Parallel() 280 281 svc, _ := testService(t, nil) 282 283 if err := svc.DeleteBucket(); err != nil { 284 t.Fatalf("Unexpected error, got: %v", err) 285 } 286 }) 287 288 t.Run("deletes_bucket_with_configured_name", func(t *testing.T) { 289 t.Parallel() 290 291 svc, s3Mock := testService(t, &infrav1.S3Bucket{ 292 Name: bucketName, 293 }) 294 295 input := &s3svc.DeleteBucketInput{ 296 Bucket: aws.String(bucketName), 297 } 298 299 s3Mock.EXPECT().DeleteBucket(input).Return(nil, nil).Times(1) 300 301 if err := svc.DeleteBucket(); err != nil { 302 t.Fatalf("Unexpected error, got: %v", err) 303 } 304 }) 305 306 t.Run("returns_error_when_bucket_removal_returns", func(t *testing.T) { 307 t.Parallel() 308 t.Run("unexpected_error", func(t *testing.T) { 309 t.Parallel() 310 311 svc, s3Mock := testService(t, &infrav1.S3Bucket{}) 312 313 s3Mock.EXPECT().DeleteBucket(gomock.Any()).Return(nil, errors.New("err")).Times(1) 314 315 if err := svc.DeleteBucket(); err == nil { 316 t.Fatalf("Expected error") 317 } 318 }) 319 320 t.Run("unexpected_AWS_error", func(t *testing.T) { 321 t.Parallel() 322 323 svc, s3Mock := testService(t, &infrav1.S3Bucket{}) 324 325 s3Mock.EXPECT().DeleteBucket(gomock.Any()).Return(nil, awserr.New("foo", "", nil)).Times(1) 326 327 if err := svc.DeleteBucket(); err == nil { 328 t.Fatalf("Expected error") 329 } 330 }) 331 }) 332 333 t.Run("ignores_when_bucket_has_already_been_removed", func(t *testing.T) { 334 t.Parallel() 335 336 svc, s3Mock := testService(t, &infrav1.S3Bucket{}) 337 338 s3Mock.EXPECT().DeleteBucket(gomock.Any()).Return(nil, awserr.New(s3svc.ErrCodeNoSuchBucket, "", nil)).Times(1) 339 340 if err := svc.DeleteBucket(); err != nil { 341 t.Fatalf("Unexpected error: %v", err) 342 } 343 }) 344 345 t.Run("skips_bucket_removal_when_bucket_is_not_empty", func(t *testing.T) { 346 t.Parallel() 347 348 svc, s3Mock := testService(t, &infrav1.S3Bucket{}) 349 350 s3Mock.EXPECT().DeleteBucket(gomock.Any()).Return(nil, awserr.New("BucketNotEmpty", "", nil)).Times(1) 351 352 if err := svc.DeleteBucket(); err != nil { 353 t.Fatalf("Unexpected error: %v", err) 354 } 355 }) 356 } 357 358 func Test_Create_object(t *testing.T) { 359 t.Parallel() 360 361 const ( 362 bucketName = "foo" 363 nodeName = "aws-test1" 364 ) 365 366 t.Run("for_machine", func(t *testing.T) { 367 t.Parallel() 368 369 svc, s3Mock := testService(t, &infrav1.S3Bucket{ 370 Name: bucketName, 371 }) 372 373 machineScope := &scope.MachineScope{ 374 Machine: &clusterv1.Machine{}, 375 AWSMachine: &infrav1.AWSMachine{ 376 ObjectMeta: metav1.ObjectMeta{ 377 Name: nodeName, 378 }, 379 }, 380 } 381 382 bootstrapData := []byte("foobar") 383 384 s3Mock.EXPECT().PutObject(gomock.Any()).Do(func(putObjectInput *s3svc.PutObjectInput) { 385 t.Run("use_configured_bucket_name_on_cluster_level", func(t *testing.T) { 386 t.Parallel() 387 388 if *putObjectInput.Bucket != bucketName { 389 t.Fatalf("Expected object to be created in bucket %q, got %q", bucketName, *putObjectInput.Bucket) 390 } 391 }) 392 393 t.Run("use_machine_role_and_machine_name_as_key", func(t *testing.T) { 394 t.Parallel() 395 396 if !strings.HasPrefix(*putObjectInput.Key, "node") { 397 t.Errorf("Expected key to start with node role, got: %q", *putObjectInput.Key) 398 } 399 400 if !strings.HasSuffix(*putObjectInput.Key, nodeName) { 401 t.Errorf("Expected key to end with node name, got: %q", *putObjectInput.Key) 402 } 403 }) 404 405 t.Run("puts_given_bootstrap_data_untouched", func(t *testing.T) { 406 t.Parallel() 407 408 data, err := io.ReadAll(putObjectInput.Body) 409 if err != nil { 410 t.Fatalf("Reading put object body: %v", err) 411 } 412 413 if !reflect.DeepEqual(data, bootstrapData) { 414 t.Fatalf("Unexpected request body %q, expected %q", string(data), string(bootstrapData)) 415 } 416 }) 417 }).Return(nil, nil).Times(1) 418 419 bootstrapDataURL, err := svc.Create(machineScope, bootstrapData) 420 if err != nil { 421 t.Fatalf("Unexpected error, got: %v", err) 422 } 423 424 t.Run("returns_s3_url_for_created_object", func(t *testing.T) { 425 t.Parallel() 426 427 parsedURL, err := url.Parse(bootstrapDataURL) 428 if err != nil { 429 t.Fatalf("Parsing URL %q: %v", bootstrapDataURL, err) 430 } 431 432 expectedScheme := "s3" 433 if parsedURL.Scheme != expectedScheme { 434 t.Errorf("Unexpected URL scheme, expected %q, got %q", expectedScheme, parsedURL.Scheme) 435 } 436 437 if !strings.Contains(parsedURL.Host, bucketName) { 438 t.Errorf("URL Host should include bucket %q reference, got %q", bucketName, parsedURL.Host) 439 } 440 441 if !strings.HasSuffix(parsedURL.Path, nodeName) { 442 t.Errorf("URL Path should end with node name %q, got: %q", nodeName, parsedURL.Path) 443 } 444 }) 445 }) 446 447 t.Run("is_idempotent", func(t *testing.T) { 448 t.Parallel() 449 450 svc, s3Mock := testService(t, &infrav1.S3Bucket{}) 451 452 machineScope := &scope.MachineScope{ 453 Machine: &clusterv1.Machine{}, 454 AWSMachine: &infrav1.AWSMachine{ 455 ObjectMeta: metav1.ObjectMeta{ 456 Name: nodeName, 457 }, 458 }, 459 } 460 461 s3Mock.EXPECT().PutObject(gomock.Any()).Return(nil, nil).Times(2) 462 463 boostrapData := []byte("foo") 464 465 if _, err := svc.Create(machineScope, boostrapData); err != nil { 466 t.Fatalf("Unexpected error: %v", err) 467 } 468 if _, err := svc.Create(machineScope, boostrapData); err != nil { 469 t.Fatalf("Unexpected error: %v", err) 470 } 471 }) 472 473 t.Run("returns_error_when", func(t *testing.T) { 474 t.Parallel() 475 476 t.Run("object_creation_fails", func(t *testing.T) { 477 t.Parallel() 478 479 svc, s3Mock := testService(t, &infrav1.S3Bucket{}) 480 481 machineScope := &scope.MachineScope{ 482 Machine: &clusterv1.Machine{}, 483 AWSMachine: &infrav1.AWSMachine{ 484 ObjectMeta: metav1.ObjectMeta{ 485 Name: nodeName, 486 }, 487 }, 488 } 489 490 s3Mock.EXPECT().PutObject(gomock.Any()).Return(nil, errors.New("foo")).Times(1) 491 492 bootstrapDataURL, err := svc.Create(machineScope, []byte("foo")) 493 if err == nil { 494 t.Fatalf("Expected error") 495 } 496 497 if bootstrapDataURL != "" { 498 t.Fatalf("Expected empty bootstrap data URL when creation error occurs") 499 } 500 }) 501 502 t.Run("given_empty_machine_scope", func(t *testing.T) { 503 t.Parallel() 504 505 svc, _ := testService(t, &infrav1.S3Bucket{}) 506 507 bootstrapDataURL, err := svc.Create(nil, []byte("foo")) 508 if err == nil { 509 t.Fatalf("Expected error") 510 } 511 512 if bootstrapDataURL != "" { 513 t.Fatalf("Expected empty bootstrap data URL when creation error occurs") 514 } 515 }) 516 517 // If one tries to put empty bootstrap data into S3, most likely something is wrong. 518 t.Run("given_empty_bootstrap_data", func(t *testing.T) { 519 t.Parallel() 520 521 svc, _ := testService(t, &infrav1.S3Bucket{}) 522 523 machineScope := &scope.MachineScope{ 524 Machine: &clusterv1.Machine{}, 525 AWSMachine: &infrav1.AWSMachine{ 526 ObjectMeta: metav1.ObjectMeta{ 527 Name: nodeName, 528 }, 529 }, 530 } 531 532 bootstrapDataURL, err := svc.Create(machineScope, []byte{}) 533 if err == nil { 534 t.Fatalf("Expected error") 535 } 536 537 if bootstrapDataURL != "" { 538 t.Fatalf("Expected empty bootstrap data URL when creation error occurs") 539 } 540 }) 541 542 t.Run("bucket_management_is_disabled_clusterwide", func(t *testing.T) { 543 t.Parallel() 544 545 svc, _ := testService(t, nil) 546 547 machineScope := &scope.MachineScope{ 548 Machine: &clusterv1.Machine{}, 549 AWSMachine: &infrav1.AWSMachine{ 550 ObjectMeta: metav1.ObjectMeta{ 551 Name: nodeName, 552 }, 553 }, 554 } 555 556 bootstrapDataURL, err := svc.Create(machineScope, []byte("foo")) 557 if err == nil { 558 t.Fatalf("Expected error") 559 } 560 561 if bootstrapDataURL != "" { 562 t.Fatalf("Expected empty bootstrap data URL when creation error occurs") 563 } 564 }) 565 }) 566 } 567 568 func Test_Delete_object(t *testing.T) { 569 t.Parallel() 570 571 const nodeName = "aws-test1" 572 573 t.Run("for_machine", func(t *testing.T) { 574 t.Parallel() 575 576 expectedBucketName := "foo" 577 578 svc, s3Mock := testService(t, &infrav1.S3Bucket{ 579 Name: expectedBucketName, 580 }) 581 582 machineScope := &scope.MachineScope{ 583 Machine: &clusterv1.Machine{ 584 ObjectMeta: metav1.ObjectMeta{ 585 Labels: map[string]string{ 586 clusterv1.MachineControlPlaneLabelName: "", 587 }, 588 }, 589 }, 590 AWSMachine: &infrav1.AWSMachine{ 591 ObjectMeta: metav1.ObjectMeta{ 592 Name: nodeName, 593 }, 594 }, 595 } 596 597 s3Mock.EXPECT().DeleteObject(gomock.Any()).Do(func(deleteObjectInput *s3svc.DeleteObjectInput) { 598 t.Run("use_configured_bucket_name_on_cluster_level", func(t *testing.T) { 599 t.Parallel() 600 601 if *deleteObjectInput.Bucket != expectedBucketName { 602 t.Fatalf("Expected object to be deleted from bucket %q, got %q", expectedBucketName, *deleteObjectInput.Bucket) 603 } 604 }) 605 606 t.Run("use_machine_role_and_machine_name_as_key", func(t *testing.T) { 607 t.Parallel() 608 609 if !strings.HasPrefix(*deleteObjectInput.Key, "control-plane") { 610 t.Errorf("Expected key to start with control-plane role, got: %q", *deleteObjectInput.Key) 611 } 612 613 if !strings.HasSuffix(*deleteObjectInput.Key, nodeName) { 614 t.Errorf("Expected key to end with node name, got: %q", *deleteObjectInput.Key) 615 } 616 }) 617 }).Return(nil, nil).Times(1) 618 619 if err := svc.Delete(machineScope); err != nil { 620 t.Fatalf("Unexpected error, got: %v", err) 621 } 622 }) 623 624 t.Run("succeeds_when_bucket_has_already_been_removed", func(t *testing.T) { 625 t.Parallel() 626 627 svc, s3Mock := testService(t, &infrav1.S3Bucket{}) 628 629 machineScope := &scope.MachineScope{ 630 Machine: &clusterv1.Machine{}, 631 AWSMachine: &infrav1.AWSMachine{ 632 ObjectMeta: metav1.ObjectMeta{ 633 Name: nodeName, 634 }, 635 }, 636 } 637 638 s3Mock.EXPECT().DeleteObject(gomock.Any()).Return(nil, awserr.New(s3svc.ErrCodeNoSuchBucket, "", nil)).Times(1) 639 640 if err := svc.Delete(machineScope); err != nil { 641 t.Fatalf("Unexpected error, got: %v", err) 642 } 643 }) 644 645 t.Run("returns_error_when", func(t *testing.T) { 646 t.Parallel() 647 648 t.Run("object_deletion_fails", func(t *testing.T) { 649 t.Parallel() 650 651 svc, s3Mock := testService(t, &infrav1.S3Bucket{}) 652 653 machineScope := &scope.MachineScope{ 654 Machine: &clusterv1.Machine{}, 655 AWSMachine: &infrav1.AWSMachine{ 656 ObjectMeta: metav1.ObjectMeta{ 657 Name: nodeName, 658 }, 659 }, 660 } 661 662 s3Mock.EXPECT().DeleteObject(gomock.Any()).Return(nil, errors.New("foo")).Times(1) 663 664 if err := svc.Delete(machineScope); err == nil { 665 t.Fatalf("Expected error") 666 } 667 }) 668 669 t.Run("given_empty_machine_scope", func(t *testing.T) { 670 t.Parallel() 671 672 svc, _ := testService(t, &infrav1.S3Bucket{}) 673 674 if err := svc.Delete(nil); err == nil { 675 t.Fatalf("Expected error") 676 } 677 }) 678 679 t.Run("bucket_management_is_disabled_clusterwide", func(t *testing.T) { 680 t.Parallel() 681 682 svc, _ := testService(t, nil) 683 684 machineScope := &scope.MachineScope{ 685 Machine: &clusterv1.Machine{}, 686 AWSMachine: &infrav1.AWSMachine{ 687 ObjectMeta: metav1.ObjectMeta{ 688 Name: nodeName, 689 }, 690 }, 691 } 692 693 if err := svc.Delete(machineScope); err == nil { 694 t.Fatalf("Expected error") 695 } 696 }) 697 }) 698 699 t.Run("is_idempotent", func(t *testing.T) { 700 t.Parallel() 701 702 svc, s3Mock := testService(t, &infrav1.S3Bucket{}) 703 704 machineScope := &scope.MachineScope{ 705 Machine: &clusterv1.Machine{}, 706 AWSMachine: &infrav1.AWSMachine{ 707 ObjectMeta: metav1.ObjectMeta{ 708 Name: nodeName, 709 }, 710 }, 711 } 712 713 s3Mock.EXPECT().DeleteObject(gomock.Any()).Return(nil, nil).Times(2) 714 715 if err := svc.Delete(machineScope); err != nil { 716 t.Fatalf("Unexpected error: %v", err) 717 } 718 719 if err := svc.Delete(machineScope); err != nil { 720 t.Fatalf("Unexpected error: %v", err) 721 } 722 }) 723 } 724 725 func testService(t *testing.T, bucket *infrav1.S3Bucket) (*s3.Service, *mock_s3iface.MockS3API) { 726 t.Helper() 727 728 mockCtrl := gomock.NewController(t) 729 s3Mock := mock_s3iface.NewMockS3API(mockCtrl) 730 stsMock := mock_stsiface.NewMockSTSAPI(mockCtrl) 731 732 getCallerIdentityResult := &sts.GetCallerIdentityOutput{Account: aws.String("foo")} 733 stsMock.EXPECT().GetCallerIdentity(gomock.Any()).Return(getCallerIdentityResult, nil).AnyTimes() 734 735 scheme := runtime.NewScheme() 736 _ = infrav1.AddToScheme(scheme) 737 client := fake.NewClientBuilder().WithScheme(scheme).Build() 738 739 scope, err := scope.NewClusterScope(scope.ClusterScopeParams{ 740 Client: client, 741 Cluster: &clusterv1.Cluster{ 742 ObjectMeta: metav1.ObjectMeta{ 743 Name: testClusterName, 744 Namespace: testClusterNamespace, 745 }, 746 }, 747 AWSCluster: &infrav1.AWSCluster{ 748 Spec: infrav1.AWSClusterSpec{ 749 S3Bucket: bucket, 750 }, 751 }, 752 }) 753 if err != nil { 754 t.Fatalf("Failed to create test context: %v", err) 755 } 756 757 svc := s3.NewService(scope) 758 svc.S3Client = s3Mock 759 svc.STSClient = stsMock 760 761 return svc, s3Mock 762 }