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  }