Skip to content

Commit 9b32a5c

Browse files
authored
Merge pull request #34647 from hashicorp/f-s3-express-tidyup
S3 Express post-release fixes
2 parents c809ba8 + 6c9e986 commit 9b32a5c

File tree

9 files changed

+259
-68
lines changed

9 files changed

+259
-68
lines changed

.changelog/34647.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:bug
2+
resource/aws_s3_directory_bucket: Fix `NotImplemented: This bucket does not support Object Versioning` errors on resource Delete when `force_destroy` is `true`
3+
```

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,6 @@ require (
9595
github.com/aws/aws-sdk-go-v2/service/vpclattice v1.5.1
9696
github.com/aws/aws-sdk-go-v2/service/workspaces v1.35.1
9797
github.com/aws/aws-sdk-go-v2/service/xray v1.23.1
98-
github.com/aws/smithy-go v1.18.1
9998
github.com/beevik/etree v1.2.0
10099
github.com/davecgh/go-spew v1.1.1
101100
github.com/gertd/go-pluralize v0.2.1
@@ -158,6 +157,7 @@ require (
158157
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.7 // indirect
159158
github.com/aws/aws-sdk-go-v2/service/sso v1.18.1 // indirect
160159
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.1 // indirect
160+
github.com/aws/smithy-go v1.18.1 // indirect
161161
github.com/bgentry/speakeasy v0.1.0 // indirect
162162
github.com/boombuler/barcode v1.0.1 // indirect
163163
github.com/bufbuild/protocompile v0.6.0 // indirect

internal/service/s3/bucket.go

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import (
2323
"github.com/aws/aws-sdk-go/aws/request"
2424
"github.com/aws/aws-sdk-go/service/s3"
2525
"github.com/aws/aws-sdk-go/service/s3/s3manager"
26-
smithy "github.com/aws/smithy-go"
2726
"github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr"
2827
tfawserr_sdkv2 "github.com/hashicorp/aws-sdk-go-base/v2/tfawserr"
2928
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
@@ -1442,25 +1441,13 @@ func findBucket(ctx context.Context, conn *s3_sdkv2.Client, bucket string, optFn
14421441

14431442
_, err := conn.HeadBucket(ctx, input, optFns...)
14441443

1445-
if tfawserr_sdkv2.ErrHTTPStatusCodeEquals(err, http.StatusNotFound) || tfawserr_sdkv2.ErrCodeEquals(err, errCodeNoSuchBucket) {
1444+
if tfawserr_sdkv2.ErrHTTPStatusCodeEquals(err, http.StatusNotFound) || tfawserr_sdkv2.ErrCodeEquals(err, errCodeNoSuchBucket) || errs.Contains(err, errCodeNoSuchBucket) {
14461445
return &retry.NotFoundError{
14471446
LastError: err,
14481447
LastRequest: input,
14491448
}
14501449
}
14511450

1452-
// FIXME Move to aws-sdk-go-base
1453-
// FIXME &smithy.OperationError{ServiceID:"S3", OperationName:"HeadBucket", Err:(*errors.errorString)(0xc00202bb60)}
1454-
// FIXME "operation error S3: HeadBucket, get identity: get credentials: operation error S3: CreateSession, https response error StatusCode: 404, RequestID: 0033eada6b00018c17de82890509d9eada65ba39, HostID: F31dBn, NoSuchBucket:"
1455-
if operationErr, ok := errs.As[*smithy.OperationError](err); ok {
1456-
if strings.Contains(operationErr.Err.Error(), errCodeNoSuchBucket) {
1457-
return &retry.NotFoundError{
1458-
LastError: err,
1459-
LastRequest: input,
1460-
}
1461-
}
1462-
}
1463-
14641451
return err
14651452
}
14661453

internal/service/s3/delete.go

Lines changed: 122 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,13 @@ import (
1313
"github.com/aws/aws-sdk-go-v2/service/s3"
1414
"github.com/aws/aws-sdk-go-v2/service/s3/types"
1515
"github.com/hashicorp/aws-sdk-go-base/v2/tfawserr"
16+
tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices"
1617
)
1718

18-
const (
19-
listObjectVersionsMaxKeys = 1000
20-
)
21-
22-
// emptyBucket empties the specified S3 bucket by deleting all object versions and delete markers.
19+
// emptyBucket empties the specified S3 general purpose bucket by deleting all object versions and delete markers.
2320
// If `force` is `true` then S3 Object Lock governance mode restrictions are bypassed and
2421
// an attempt is made to remove any S3 Object Lock legal holds.
25-
// Returns the number of objects deleted.
22+
// Returns the number of object versions and delete markers deleted.
2623
func emptyBucket(ctx context.Context, conn *s3.Client, bucket string, force bool) (int64, error) {
2724
nObjects, err := forEachObjectVersionsPage(ctx, conn, bucket, func(ctx context.Context, conn *s3.Client, bucket string, page *s3.ListObjectVersionsOutput) (int64, error) {
2825
return deletePageOfObjectVersions(ctx, conn, bucket, force, page)
@@ -38,13 +35,18 @@ func emptyBucket(ctx context.Context, conn *s3.Client, bucket string, force bool
3835
return nObjects, err
3936
}
4037

38+
// emptyDirectoryBucket empties the specified S3 directory bucket by deleting all objects.
39+
// Returns the number of objects deleted.
40+
func emptyDirectoryBucket(ctx context.Context, conn *s3.Client, bucket string) (int64, error) {
41+
return forEachObjectsPage(ctx, conn, bucket, deletePageOfObjects)
42+
}
43+
4144
// forEachObjectVersionsPage calls the specified function for each page returned from the S3 ListObjectVersionsPages API.
4245
func forEachObjectVersionsPage(ctx context.Context, conn *s3.Client, bucket string, fn func(ctx context.Context, conn *s3.Client, bucket string, page *s3.ListObjectVersionsOutput) (int64, error)) (int64, error) {
4346
var nObjects int64
4447

4548
input := &s3.ListObjectVersionsInput{
46-
Bucket: aws.String(bucket),
47-
MaxKeys: aws.Int32(listObjectVersionsMaxKeys),
49+
Bucket: aws.String(bucket),
4850
}
4951
var lastErr error
5052

@@ -72,21 +74,52 @@ func forEachObjectVersionsPage(ctx context.Context, conn *s3.Client, bucket stri
7274
return nObjects, nil
7375
}
7476

77+
// forEachObjectsPage calls the specified function for each page returned from the S3 ListObjectsV2 API.
78+
func forEachObjectsPage(ctx context.Context, conn *s3.Client, bucket string, fn func(ctx context.Context, conn *s3.Client, bucket string, page *s3.ListObjectsV2Output) (int64, error)) (int64, error) {
79+
var nObjects int64
80+
81+
input := &s3.ListObjectsV2Input{
82+
Bucket: aws.String(bucket),
83+
}
84+
var lastErr error
85+
86+
pages := s3.NewListObjectsV2Paginator(conn, input)
87+
for pages.HasMorePages() {
88+
page, err := pages.NextPage(ctx)
89+
90+
if err != nil {
91+
return nObjects, fmt.Errorf("listing S3 bucket (%s) objects: %w", bucket, err)
92+
}
93+
94+
n, err := fn(ctx, conn, bucket, page)
95+
nObjects += n
96+
97+
if err != nil {
98+
lastErr = err
99+
break
100+
}
101+
}
102+
103+
if lastErr != nil {
104+
return nObjects, lastErr
105+
}
106+
107+
return nObjects, nil
108+
}
109+
75110
// deletePageOfObjectVersions deletes a page (<= 1000) of S3 object versions.
76111
// If `force` is `true` then S3 Object Lock governance mode restrictions are bypassed and
77112
// an attempt is made to remove any S3 Object Lock legal holds.
78113
// Returns the number of objects deleted.
79114
func deletePageOfObjectVersions(ctx context.Context, conn *s3.Client, bucket string, force bool, page *s3.ListObjectVersionsOutput) (int64, error) {
80-
var nObjects int64
81-
82-
toDelete := make([]types.ObjectIdentifier, 0, len(page.Versions))
83-
for _, v := range page.Versions {
84-
toDelete = append(toDelete, types.ObjectIdentifier{
115+
toDelete := tfslices.ApplyToAll(page.Versions, func(v types.ObjectVersion) types.ObjectIdentifier {
116+
return types.ObjectIdentifier{
85117
Key: v.Key,
86118
VersionId: v.VersionId,
87-
})
88-
}
119+
}
120+
})
89121

122+
var nObjects int64
90123
if nObjects = int64(len(toDelete)); nObjects == 0 {
91124
return nObjects, nil
92125
}
@@ -109,16 +142,14 @@ func deletePageOfObjectVersions(ctx context.Context, conn *s3.Client, bucket str
109142
}
110143

111144
if err != nil {
112-
return nObjects, fmt.Errorf("deleting S3 bucket (%s) objects: %w", bucket, err)
145+
return nObjects, fmt.Errorf("deleting S3 bucket (%s) object versions: %w", bucket, err)
113146
}
114147

115148
nObjects -= int64(len(output.Errors))
116149

117-
var deleteErrs []error
118-
150+
var errs []error
119151
for _, v := range output.Errors {
120152
code := aws.ToString(v.Code)
121-
122153
if code == errCodeNoSuchKey {
123154
continue
124155
}
@@ -139,8 +170,8 @@ func deletePageOfObjectVersions(ctx context.Context, conn *s3.Client, bucket str
139170

140171
if err != nil {
141172
// Add the original error and the new error.
142-
deleteErrs = append(deleteErrs, newDeleteObjectVersionError(v))
143-
deleteErrs = append(deleteErrs, fmt.Errorf("removing legal hold: %w", newObjectVersionError(key, versionID, err)))
173+
errs = append(errs, newDeleteObjectVersionError(v))
174+
errs = append(errs, fmt.Errorf("removing legal hold: %w", newObjectVersionError(key, versionID, err)))
144175
} else {
145176
// Attempt to delete the object once the legal hold has been removed.
146177
_, err := conn.DeleteObject(ctx, &s3.DeleteObjectInput{
@@ -150,18 +181,18 @@ func deletePageOfObjectVersions(ctx context.Context, conn *s3.Client, bucket str
150181
})
151182

152183
if err != nil {
153-
deleteErrs = append(deleteErrs, fmt.Errorf("deleting: %w", newObjectVersionError(key, versionID, err)))
184+
errs = append(errs, fmt.Errorf("deleting: %w", newObjectVersionError(key, versionID, err)))
154185
} else {
155186
nObjects++
156187
}
157188
}
158189
} else {
159-
deleteErrs = append(deleteErrs, newDeleteObjectVersionError(v))
190+
errs = append(errs, newDeleteObjectVersionError(v))
160191
}
161192
}
162193

163-
if err := errors.Join(deleteErrs...); err != nil {
164-
return nObjects, fmt.Errorf("deleting S3 bucket (%s) objects: %w", bucket, err)
194+
if err := errors.Join(errs...); err != nil {
195+
return nObjects, fmt.Errorf("deleting S3 bucket (%s) object versions: %w", bucket, err)
165196
}
166197

167198
return nObjects, nil
@@ -170,16 +201,14 @@ func deletePageOfObjectVersions(ctx context.Context, conn *s3.Client, bucket str
170201
// deletePageOfDeleteMarkers deletes a page (<= 1000) of S3 object delete markers.
171202
// Returns the number of delete markers deleted.
172203
func deletePageOfDeleteMarkers(ctx context.Context, conn *s3.Client, bucket string, page *s3.ListObjectVersionsOutput) (int64, error) {
173-
var nObjects int64
174-
175-
toDelete := make([]types.ObjectIdentifier, 0, len(page.Versions))
176-
for _, v := range page.DeleteMarkers {
177-
toDelete = append(toDelete, types.ObjectIdentifier{
204+
toDelete := tfslices.ApplyToAll(page.Versions, func(v types.ObjectVersion) types.ObjectIdentifier {
205+
return types.ObjectIdentifier{
178206
Key: v.Key,
179207
VersionId: v.VersionId,
180-
})
181-
}
208+
}
209+
})
182210

211+
var nObjects int64
183212
if nObjects = int64(len(toDelete)); nObjects == 0 {
184213
return nObjects, nil
185214
}
@@ -204,19 +233,64 @@ func deletePageOfDeleteMarkers(ctx context.Context, conn *s3.Client, bucket stri
204233

205234
nObjects -= int64(len(output.Errors))
206235

207-
var deleteErrs []error
208-
236+
var errs []error
209237
for _, v := range output.Errors {
210-
deleteErrs = append(deleteErrs, newDeleteObjectVersionError(v))
238+
errs = append(errs, newDeleteObjectVersionError(v))
211239
}
212240

213-
if err := errors.Join(deleteErrs...); err != nil {
241+
if err := errors.Join(errs...); err != nil {
214242
return nObjects, fmt.Errorf("deleting S3 bucket (%s) delete markers: %w", bucket, err)
215243
}
216244

217245
return nObjects, nil
218246
}
219247

248+
// deletePageOfObjects deletes a page (<= 1000) of S3 objects.
249+
// Returns the number of objects deleted.
250+
func deletePageOfObjects(ctx context.Context, conn *s3.Client, bucket string, page *s3.ListObjectsV2Output) (int64, error) {
251+
toDelete := tfslices.ApplyToAll(page.Contents, func(v types.Object) types.ObjectIdentifier {
252+
return types.ObjectIdentifier{
253+
Key: v.Key,
254+
}
255+
})
256+
257+
var nObjects int64
258+
if nObjects = int64(len(toDelete)); nObjects == 0 {
259+
return nObjects, nil
260+
}
261+
262+
input := &s3.DeleteObjectsInput{
263+
Bucket: aws.String(bucket),
264+
Delete: &types.Delete{
265+
Objects: toDelete,
266+
Quiet: aws.Bool(true), // Only report errors.
267+
},
268+
}
269+
270+
output, err := conn.DeleteObjects(ctx, input)
271+
272+
if tfawserr.ErrCodeEquals(err, errCodeNoSuchBucket) {
273+
return nObjects, nil
274+
}
275+
276+
if err != nil {
277+
return nObjects, fmt.Errorf("deleting S3 bucket (%s) objects: %w", bucket, err)
278+
}
279+
280+
nObjects -= int64(len(output.Errors))
281+
282+
var errs []error
283+
for _, v := range output.Errors {
284+
errs = append(errs, newDeleteObjectVersionError(v))
285+
}
286+
287+
if err := errors.Join(errs...); err != nil {
288+
return nObjects, fmt.Errorf("deleting S3 bucket (%s) objects: %w", bucket, err)
289+
}
290+
291+
return nObjects, nil
292+
}
293+
220294
func newObjectVersionError(key, versionID string, err error) error {
221295
if err == nil {
222296
return nil
@@ -235,20 +309,21 @@ func newDeleteObjectVersionError(err types.Error) error {
235309
return fmt.Errorf("deleting: %w", newObjectVersionError(aws.ToString(err.Key), aws.ToString(err.VersionId), s3Err))
236310
}
237311

238-
// deleteAllObjectVersions deletes all versions of a specified key from an S3 bucket.
312+
// deleteAllObjectVersions deletes all versions of a specified key from an S3 general purpose bucket.
239313
// If key is empty then all versions of all objects are deleted.
240-
// Set force to true to override any S3 object lock protections on object lock enabled buckets.
314+
// Set `force` to `true` to override any S3 object lock protections on object lock enabled buckets.
241315
// Returns the number of objects deleted.
316+
// Use `emptyBucket` to delete all versions of all objects in a bucket.
242317
func deleteAllObjectVersions(ctx context.Context, conn *s3.Client, bucket, key string, force, ignoreObjectErrors bool) (int64, error) {
243-
var nObjects int64
318+
if key == "" {
319+
return 0, errors.New("use `emptyBucket` to delete all versions of all objects in an S3 general purpose bucket")
320+
}
244321

245322
input := &s3.ListObjectVersionsInput{
246-
Bucket: aws.String(bucket),
247-
MaxKeys: aws.Int32(listObjectVersionsMaxKeys),
248-
}
249-
if key != "" {
250-
input.Prefix = aws.String(key)
323+
Bucket: aws.String(bucket),
324+
Prefix: aws.String(key),
251325
}
326+
var nObjects int64
252327
var lastErr error
253328

254329
pages := s3.NewListObjectVersionsPaginator(conn, input)
@@ -267,7 +342,7 @@ func deleteAllObjectVersions(ctx context.Context, conn *s3.Client, bucket, key s
267342
objectKey := aws.ToString(objectVersion.Key)
268343
objectVersionID := aws.ToString(objectVersion.VersionId)
269344

270-
if key != "" && key != objectKey {
345+
if key != objectKey {
271346
continue
272347
}
273348

@@ -358,7 +433,7 @@ func deleteAllObjectVersions(ctx context.Context, conn *s3.Client, bucket, key s
358433
deleteMarkerKey := aws.ToString(deleteMarker.Key)
359434
deleteMarkerVersionID := aws.ToString(deleteMarker.VersionId)
360435

361-
if key != "" && key != deleteMarkerKey {
436+
if key != deleteMarkerKey {
362437
continue
363438
}
364439

@@ -383,7 +458,7 @@ func deleteAllObjectVersions(ctx context.Context, conn *s3.Client, bucket, key s
383458
}
384459

385460
// deleteObjectVersion deletes a specific object version.
386-
// Set force to true to override any S3 object lock protections.
461+
// Set `force` to `true` to override any S3 object lock protections.
387462
func deleteObjectVersion(ctx context.Context, conn *s3.Client, b, k, v string, force bool) error {
388463
input := &s3.DeleteObjectInput{
389464
Bucket: aws.String(b),

internal/service/s3/directory_bucket.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ func (r *directoryBucketResource) Delete(ctx context.Context, request resource.D
254254
if tfawserr.ErrCodeEquals(err, errCodeBucketNotEmpty) {
255255
if data.ForceDestroy.ValueBool() {
256256
// Empty the bucket and try again.
257-
_, err = emptyBucket(ctx, conn, data.ID.ValueString(), false)
257+
_, err = emptyDirectoryBucket(ctx, conn, data.ID.ValueString())
258258

259259
if err != nil {
260260
response.Diagnostics.AddError(fmt.Sprintf("emptying S3 Directory Bucket (%s)", data.ID.ValueString()), err.Error())

0 commit comments

Comments
 (0)