@@ -30,6 +30,7 @@ import (
3030 "github.com/aws/aws-sdk-go-v2/service/s3"
3131 "github.com/aws/aws-sdk-go-v2/service/s3/types"
3232 "github.com/aws/aws-sdk-go-v2/service/sts"
33+ "github.com/aws/smithy-go"
3334 "k8s.io/klog/v2"
3435)
3536
@@ -50,6 +51,7 @@ type BucketType string
5051const (
5152 BucketTypeStateStore BucketType = "state"
5253 BucketTypeDiscoveryStore BucketType = "discovery"
54+ BucketTypeStagingStore BucketType = "staging"
5355)
5456
5557// NewAWSClient returns a new instance of awsClient configured to work in the default region (us-east-2).
@@ -68,17 +70,14 @@ func NewClient(ctx context.Context, region string) (*Client, error) {
6870
6971// BucketName constructs an unique bucket name using the AWS account ID in the default region (us-east-2).
7072func (c Client ) BucketName (ctx context.Context , bucketType BucketType ) (string , error ) {
71- // Construct the bucket name based on the ProwJob ID (if running in Prow) or AWS account ID (if running outside
72- // Prow) and a timestamp. When BUILD_ID is set we use it in place of time.Now() so that multiple kubetest2-kops
73- // invocations within the same CI job (e.g. upgrade tests) resolve to the same bucket name.
74- var suffix string
75- if jobID := os .Getenv ("BUILD_ID" ); jobID != "" {
76- if len (jobID ) > 14 {
77- suffix = jobID [:14 ]
78- } else {
79- suffix = jobID
80- }
81- } else {
73+ // Construct the bucket name based on the ProwJob ID (if running in Prow) or AWS account ID (if
74+ // running outside Prow) and a timestamp. When BUILD_ID is set we use it in place of time.Now()
75+ // so that multiple kubetest2-kops invocations within the same CI job (e.g. upgrade tests)
76+ // resolve to the same bucket name. Use the full build ID because truncating its low-order
77+ // digits can make jobs started in the same millisecond collide, allowing one job to delete
78+ // another's bucket.
79+ suffix := os .Getenv ("BUILD_ID" )
80+ if suffix == "" {
8281 callerIdentity , err := c .stsClient .GetCallerIdentity (ctx , & sts.GetCallerIdentityInput {})
8382 if err != nil {
8483 return "" , fmt .Errorf ("building AWS STS presigned request: %w" , err )
@@ -91,14 +90,12 @@ func (c Client) BucketName(ctx context.Context, bucketType BucketType) (string,
9190 // Only allow lowercase letters, numbers, and hyphens
9291 bucket = bucketNameRegex .ReplaceAllString (bucket , "" )
9392
94- if len (bucket ) > 63 {
95- bucket = bucket [:63 ] // Max length is 63
96- }
97-
93+ // Names over the 63 character S3 limit fail bucket creation rather than being silently
94+ // truncated, which could make distinct jobs collide on one bucket.
9895 return bucket , nil
9996}
10097
101- // EnsureS3Bucket creates a new S3 bucket with the given name and public read permissions .
98+ // EnsureS3Bucket creates an S3 bucket, optionally with public read access .
10299func (c Client ) EnsureS3Bucket (ctx context.Context , region , bucketName string , publicRead bool ) error {
103100 bucketName = strings .TrimPrefix (bucketName , "s3://" )
104101 klog .Infof ("Creating bucket %s in region %s" , bucketName , region )
@@ -112,14 +109,11 @@ func (c Client) EnsureS3Bucket(ctx context.Context, region, bucketName string, p
112109 },
113110 )
114111 if err != nil {
115- var exists * types.BucketAlreadyExists
116- if errors .As (err , & exists ) {
117- klog .Infof ("Bucket %s already exists\n " , bucketName )
118- } else {
119- klog .Infof ("Error creating bucket %s, err: %v\n " , bucketName , err )
112+ var owned * types.BucketAlreadyOwnedByYou
113+ if ! errors .As (err , & owned ) {
114+ return fmt .Errorf ("creating bucket %s: %w" , bucketName , err )
120115 }
121-
122- return fmt .Errorf ("creating bucket %s: %w" , bucketName , err )
116+ klog .Infof ("Bucket %s already exists in this account" , bucketName )
123117 }
124118
125119 // Wait for the bucket to be created
@@ -134,26 +128,21 @@ func (c Client) EnsureS3Bucket(ctx context.Context, region, bucketName string, p
134128 return fmt .Errorf ("waiting for bucket %s to exist: %w" , bucketName , err )
135129 }
136130
137- klog .Infof ("Bucket %s created successfully " , bucketName )
131+ klog .Infof ("Bucket %s is ready " , bucketName )
138132
139133 if publicRead {
140- err = c .setPublicAccessBlock (ctx , bucketName )
141- if err != nil {
134+ if err := c .setPublicAccessBlock (ctx , bucketName ); err != nil {
142135 klog .Errorf ("Failed to disable public access block policies on bucket %s, err: %v" , bucketName , err )
143-
144136 return fmt .Errorf ("disabling public access block policies for bucket %s: %w" , bucketName , err )
145137 }
146138
147139 // Wait for public access block settings to propagate before setting the policy
148140 time .Sleep (10 * time .Second )
149141
150- err = c .setPublicReadPolicy (ctx , bucketName )
151- if err != nil {
142+ if err := c .setPublicReadPolicy (ctx , bucketName ); err != nil {
152143 klog .Errorf ("Failed to set public read policy on bucket %s, err: %v" , bucketName , err )
153-
154144 return fmt .Errorf ("setting public read policy for bucket %s: %w" , bucketName , err )
155145 }
156-
157146 klog .Infof ("Public read policy set on bucket %s" , bucketName )
158147 }
159148
@@ -162,28 +151,47 @@ func (c Client) EnsureS3Bucket(ctx context.Context, region, bucketName string, p
162151
163152// DeleteS3Bucket deletes a S3 bucket with the given name.
164153func (c Client ) DeleteS3Bucket (ctx context.Context , bucketName string ) error {
154+ return c .deleteS3Bucket (ctx , bucketName , false )
155+ }
156+
157+ // DeleteS3BucketAndContents deletes all objects from an S3 bucket before deleting the bucket.
158+ func (c Client ) DeleteS3BucketAndContents (ctx context.Context , bucketName string ) error {
159+ return c .deleteS3Bucket (ctx , bucketName , true )
160+ }
161+
162+ func (c Client ) deleteS3Bucket (ctx context.Context , bucketName string , empty bool ) error {
165163 bucketName = strings .TrimPrefix (bucketName , "s3://" )
166164
167165 // Resolve the bucket's actual region to avoid 301 PermanentRedirect errors.
168166 // During teardown the deployer may derive the S3 client region from random
169167 // zones, which can differ from the region where the bucket was created.
170168 bucketRegion , err := c .getBucketRegion (ctx , bucketName )
171169 if err != nil {
170+ if isNoSuchBucket (err ) {
171+ // Teardown may run without creating every bucket it knows about.
172+ return nil
173+ }
172174 klog .Infof ("Could not determine region for bucket %s: %v" , bucketName , err )
173175 }
174176
175- regionOpt := func (o * s3.Options ) {
176- if bucketRegion != "" {
177- o .Region = bucketRegion
177+ client := c .s3Client
178+ if bucketRegion != "" {
179+ options := c .s3Client .Options ()
180+ options .Region = bucketRegion
181+ client = s3 .New (options )
182+ }
183+
184+ if empty {
185+ if err := emptyS3Bucket (ctx , client , bucketName ); err != nil {
186+ return err
178187 }
179188 }
180189
181- _ , err = c . s3Client .DeleteBucket (ctx , & s3.DeleteBucketInput {
190+ _ , err = client .DeleteBucket (ctx , & s3.DeleteBucketInput {
182191 Bucket : aws .String (bucketName ),
183- }, regionOpt )
192+ })
184193 if err != nil {
185- var noBucket * types.NoSuchBucket
186- if errors .As (err , & noBucket ) {
194+ if isNoSuchBucket (err ) {
187195 klog .Infof ("Bucket %s does not exist." , bucketName )
188196
189197 return nil
@@ -193,7 +201,7 @@ func (c Client) DeleteS3Bucket(ctx context.Context, bucketName string) error {
193201 return fmt .Errorf ("deleting bucket %s: %w" , bucketName , err )
194202 }
195203
196- err = s3 .NewBucketNotExistsWaiter (c . s3Client ).Wait (
204+ err = s3 .NewBucketNotExistsWaiter (client ).Wait (
197205 ctx , & s3.HeadBucketInput {
198206 Bucket : aws .String (bucketName ),
199207 },
@@ -205,10 +213,53 @@ func (c Client) DeleteS3Bucket(ctx context.Context, bucketName string) error {
205213 }
206214
207215 klog .Infof ("Bucket %s deleted" , bucketName )
208-
209216 return nil
210217}
211218
219+ func emptyS3Bucket (ctx context.Context , client * s3.Client , bucketName string ) error {
220+ for {
221+ result , err := client .ListObjectsV2 (ctx , & s3.ListObjectsV2Input {
222+ Bucket : aws .String (bucketName ),
223+ })
224+ if err != nil {
225+ if isNoSuchBucket (err ) {
226+ return nil
227+ }
228+ return fmt .Errorf ("listing objects in bucket %s: %w" , bucketName , err )
229+ }
230+
231+ if len (result .Contents ) == 0 {
232+ return nil
233+ }
234+
235+ objects := make ([]types.ObjectIdentifier , len (result .Contents ))
236+ for i , object := range result .Contents {
237+ objects [i ] = types.ObjectIdentifier {Key : object .Key }
238+ }
239+ deleteResult , err := client .DeleteObjects (ctx , & s3.DeleteObjectsInput {
240+ Bucket : aws .String (bucketName ),
241+ Delete : & types.Delete {Objects : objects , Quiet : aws .Bool (true )},
242+ })
243+ if err != nil {
244+ return fmt .Errorf ("deleting objects from bucket %s: %w" , bucketName , err )
245+ }
246+ if len (deleteResult .Errors ) > 0 {
247+ objectError := deleteResult .Errors [0 ]
248+ return fmt .Errorf ("deleting object %q from bucket %s: %s: %s" ,
249+ aws .ToString (objectError .Key ), bucketName , aws .ToString (objectError .Code ), aws .ToString (objectError .Message ))
250+ }
251+ }
252+ }
253+
254+ func isNoSuchBucket (err error ) bool {
255+ var noBucket * types.NoSuchBucket
256+ if errors .As (err , & noBucket ) {
257+ return true
258+ }
259+ var apiErr smithy.APIError
260+ return errors .As (err , & apiErr ) && (apiErr .ErrorCode () == "NoSuchBucket" || apiErr .ErrorCode () == "NotFound" )
261+ }
262+
212263// getBucketRegion resolves the AWS region where the bucket resides.
213264// GetBucketLocation is region-agnostic and can locate buckets in any region
214265// from any endpoint. We pin to defaultRegion for consistency.
0 commit comments