Skip to content

Commit

Permalink
r/aws_s3control_bucket: Switch to 'WithoutTimeout' CRUD handlers (#15090
Browse files Browse the repository at this point in the history
).

Acceptance test output:

% make testacc TESTARGS='-run=TestAccS3ControlBucket_' PKG=s3control ACCTEST_PARALLELISM=3
==> Checking that code complies with gofmt requirements...
TF_ACC=1 go test ./internal/service/s3control/... -v -count 1 -parallel 3  -run=TestAccS3ControlBucket_ -timeout 180m
=== RUN   TestAccS3ControlBucket_basic
=== PAUSE TestAccS3ControlBucket_basic
=== RUN   TestAccS3ControlBucket_disappears
=== PAUSE TestAccS3ControlBucket_disappears
=== RUN   TestAccS3ControlBucket_tags
    acctest.go:71: S3 Control Bucket resource tagging requires additional eventual consistency handling, see also: #15572
--- SKIP: TestAccS3ControlBucket_tags (0.00s)
=== CONT  TestAccS3ControlBucket_basic
=== CONT  TestAccS3ControlBucket_disappears
    acctest.go:1368: skipping since no Outposts found
--- SKIP: TestAccS3ControlBucket_disappears (1.00s)
=== CONT  TestAccS3ControlBucket_basic
    acctest.go:1368: skipping since no Outposts found
--- SKIP: TestAccS3ControlBucket_basic (1.07s)
PASS
ok  	github.com/hashicorp/terraform-provider-aws/internal/service/s3control	6.289s
  • Loading branch information
ewbankkit committed Dec 23, 2022
1 parent edcb1a7 commit af25c23
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 186 deletions.
1 change: 0 additions & 1 deletion internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -2005,7 +2005,6 @@ func New(ctx context.Context) (*schema.Provider, error) {
"aws_s3_object_copy": s3.ResourceObjectCopy(),
"aws_s3_bucket_object": s3.ResourceBucketObject(), // DEPRECATED: use aws_s3_object instead

"aws_s3control_bucket": s3control.ResourceBucket(),
"aws_s3control_bucket_policy": s3control.ResourceBucketPolicy(),
"aws_s3control_multi_region_access_point": s3control.ResourceMultiRegionAccessPoint(),
"aws_s3control_multi_region_access_point_policy": s3control.ResourceMultiRegionAccessPointPolicy(),
Expand Down
203 changes: 138 additions & 65 deletions internal/service/s3control/bucket.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package s3control

import (
"context"
"fmt"
"log"
"regexp"
Expand All @@ -11,6 +12,7 @@ import (
"github.com/aws/aws-sdk-go/aws/arn"
"github.com/aws/aws-sdk-go/service/s3control"
"github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
Expand All @@ -25,12 +27,16 @@ const (
bucketStatePropagationTimeout = 5 * time.Minute
)

func ResourceBucket() *schema.Resource {
func init() {
_sp.registerSDKResourceFactory("aws_s3control_bucket", resourceBucket)
}

func resourceBucket() *schema.Resource {
return &schema.Resource{
Create: resourceBucketCreate,
Read: resourceBucketRead,
Update: resourceBucketUpdate,
Delete: resourceBucketDelete,
CreateWithoutTimeout: resourceBucketCreate,
ReadWithoutTimeout: resourceBucketRead,
UpdateWithoutTimeout: resourceBucketUpdate,
DeleteWithoutTimeout: resourceBucketDelete,

Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
Expand Down Expand Up @@ -74,135 +80,113 @@ func ResourceBucket() *schema.Resource {
}
}

func resourceBucketCreate(d *schema.ResourceData, meta interface{}) error {
func resourceBucketCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
conn := meta.(*conns.AWSClient).S3ControlConn()
defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig
tags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{})))

bucket := d.Get("bucket").(string)

input := &s3control.CreateBucketInput{
Bucket: aws.String(bucket),
OutpostId: aws.String(d.Get("outpost_id").(string)),
}

output, err := conn.CreateBucket(input)
output, err := conn.CreateBucketWithContext(ctx, input)

if err != nil {
return fmt.Errorf("error creating S3 Control Bucket (%s): %w", bucket, err)
}

if output == nil {
return fmt.Errorf("error creating S3 Control Bucket (%s): empty response", bucket)
return diag.Errorf("creating S3 Control Bucket (%s): %s", bucket, err)
}

d.SetId(aws.StringValue(output.BucketArn))

if len(tags) > 0 {
if err := bucketUpdateTags(conn, d.Id(), nil, tags); err != nil {
return fmt.Errorf("error adding S3 Control Bucket (%s) tags: %w", d.Id(), err)
if err := bucketUpdateTags(ctx, conn, d.Id(), nil, tags); err != nil {
return diag.Errorf("adding S3 Control Bucket (%s) tags: %s", d.Id(), err)
}
}

return resourceBucketRead(d, meta)
return resourceBucketRead(ctx, d, meta)
}

func resourceBucketRead(d *schema.ResourceData, meta interface{}) error {
func resourceBucketRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
conn := meta.(*conns.AWSClient).S3ControlConn()
defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig
ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig

parsedArn, err := arn.Parse(d.Id())

if err != nil {
return fmt.Errorf("error parsing S3 Control Bucket ARN (%s): %w", d.Id(), err)
return diag.FromErr(err)
}

// ARN resource format: outpost/<outpost-id>/bucket/<my-bucket-name>
arnResourceParts := strings.Split(parsedArn.Resource, "/")

if parsedArn.AccountID == "" || len(arnResourceParts) != 4 {
return fmt.Errorf("error parsing S3 Control Bucket ARN (%s): unknown format", d.Id())
}

input := &s3control.GetBucketInput{
AccountId: aws.String(parsedArn.AccountID),
Bucket: aws.String(d.Id()),
return diag.Errorf("parsing S3 Control Bucket ARN (%s): unknown format", d.Id())
}

output, err := conn.GetBucket(input)

if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, "NoSuchBucket") {
log.Printf("[WARN] S3 Control Bucket (%s) not found, removing from state", d.Id())
d.SetId("")
return nil
}
output, err := FindBucketByTwoPartKey(ctx, conn, parsedArn.AccountID, d.Id())

if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, "NoSuchOutpost") {
if !d.IsNewResource() && tfresource.NotFound(err) {
log.Printf("[WARN] S3 Control Bucket (%s) not found, removing from state", d.Id())
d.SetId("")
return nil
}

if err != nil {
return fmt.Errorf("error reading S3 Control Bucket (%s): %w", d.Id(), err)
}

if output == nil {
return fmt.Errorf("error reading S3 Control Bucket (%s): empty response", d.Id())
return diag.Errorf("reading S3 Control Bucket (%s): %s", d.Id(), err)
}

d.Set("arn", d.Id())
d.Set("bucket", output.Bucket)

if output.CreationDate != nil {
d.Set("creation_date", aws.TimeValue(output.CreationDate).Format(time.RFC3339))
}

d.Set("outpost_id", arnResourceParts[1])
d.Set("public_access_block_enabled", output.PublicAccessBlockEnabled)

tags, err := bucketListTags(conn, d.Id())
tags, err := bucketListTags(ctx, conn, d.Id())

if err != nil {
return fmt.Errorf("error listing tags for S3 Control Bucket (%s): %w", d.Id(), err)
return diag.Errorf("listing tags for S3 Control Bucket (%s): %s", d.Id(), err)
}

tags = tags.IgnoreAWS().IgnoreConfig(ignoreTagsConfig)

//lintignore:AWSR002
if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil {
return fmt.Errorf("error setting tags: %w", err)
return diag.Errorf("setting tags: %s", err)
}

if err := d.Set("tags_all", tags.Map()); err != nil {
return fmt.Errorf("error setting tags_all: %w", err)
return diag.Errorf("setting tags_all: %s", err)
}

return nil
}

func resourceBucketUpdate(d *schema.ResourceData, meta interface{}) error {
func resourceBucketUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
conn := meta.(*conns.AWSClient).S3ControlConn()

if d.HasChange("tags_all") {
o, n := d.GetChange("tags_all")

if err := bucketUpdateTags(conn, d.Id(), o, n); err != nil {
return fmt.Errorf("error updating S3 Control Bucket (%s) tags: %w", d.Id(), err)
if err := bucketUpdateTags(ctx, conn, d.Id(), o, n); err != nil {
return diag.Errorf("updating S3 Control Bucket (%s) tags: %s", d.Id(), err)
}
}

return resourceBucketRead(d, meta)
return resourceBucketRead(ctx, d, meta)
}

func resourceBucketDelete(d *schema.ResourceData, meta interface{}) error {
func resourceBucketDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
conn := meta.(*conns.AWSClient).S3ControlConn()

parsedArn, err := arn.Parse(d.Id())

if err != nil {
return fmt.Errorf("error parsing S3 Control Bucket ARN (%s): %w", d.Id(), err)
return diag.FromErr(err)
}

input := &s3control.DeleteBucketInput{
Expand All @@ -213,34 +197,123 @@ func resourceBucketDelete(d *schema.ResourceData, meta interface{}) error {
// S3 Control Bucket have a backend state which cannot be checked so this error
// can occur on deletion:
// InvalidBucketState: Bucket is in an invalid state
err = resource.Retry(bucketStatePropagationTimeout, func() *resource.RetryError {
_, err := conn.DeleteBucket(input)
log.Printf("[DEBUG] Deleting S3 Control Bucket: %s", d.Id())
_, err = tfresource.RetryWhenAWSErrCodeEqualsContext(ctx, bucketStatePropagationTimeout, func() (interface{}, error) {
return conn.DeleteBucketWithContext(ctx, input)
}, errCodeInvalidBucketState)

if tfawserr.ErrCodeEquals(err, "InvalidBucketState") {
return resource.RetryableError(err)
}
if tfawserr.ErrCodeEquals(err, errCodeNoSuchBucket, errCodeNoSuchOutpost) {
return nil
}

if err != nil {
return resource.NonRetryableError(err)
if err != nil {
return diag.Errorf("deleting S3 Control Bucket (%s): %s", d.Id(), err)
}

return nil
}

func FindBucketByTwoPartKey(ctx context.Context, conn *s3control.S3Control, accountID, bucket string) (*s3control.GetBucketOutput, error) {
input := &s3control.GetBucketInput{
AccountId: aws.String(accountID),
Bucket: aws.String(bucket),
}

output, err := conn.GetBucketWithContext(ctx, input)

if tfawserr.ErrCodeEquals(err, errCodeNoSuchBucket, errCodeNoSuchOutpost) {
return nil, &resource.NotFoundError{
LastError: err,
LastRequest: input,
}
}

return nil
})
if err != nil {
return nil, err
}

if tfresource.TimedOut(err) {
_, err = conn.DeleteBucket(input)
if output == nil {
return nil, tfresource.NewEmptyResultError(input)
}

if tfawserr.ErrCodeEquals(err, "NoSuchBucket") {
return nil
return output, nil
}

// Custom S3control tagging functions using similar formatting as other service generated code.

// bucketListTags lists S3control bucket tags.
// The identifier is the bucket ARN.
func bucketListTags(ctx context.Context, conn *s3control.S3Control, identifier string) (tftags.KeyValueTags, error) {
parsedArn, err := arn.Parse(identifier)

if err != nil {
return tftags.New(nil), err
}

if tfawserr.ErrCodeEquals(err, "NoSuchOutpost") {
return nil
input := &s3control.GetBucketTaggingInput{
AccountId: aws.String(parsedArn.AccountID),
Bucket: aws.String(identifier),
}

output, err := conn.GetBucketTaggingWithContext(ctx, input)

if tfawserr.ErrCodeEquals(err, errCodeNoSuchTagSet) {
return tftags.New(nil), nil
}

if err != nil {
return tftags.New(nil), err
}

return KeyValueTags(output.TagSet), nil
}

// bucketUpdateTags updates S3control bucket tags.
// The identifier is the bucket ARN.
func bucketUpdateTags(ctx context.Context, conn *s3control.S3Control, identifier string, oldTagsMap interface{}, newTagsMap interface{}) error {
parsedArn, err := arn.Parse(identifier)

if err != nil {
return err
}

oldTags := tftags.New(oldTagsMap)
newTags := tftags.New(newTagsMap)

// We need to also consider any existing ignored tags.
allTags, err := bucketListTags(ctx, conn, identifier)

if err != nil {
return fmt.Errorf("error deleting S3 Control Bucket (%s): %w", d.Id(), err)
return fmt.Errorf("listing resource tags (%s): %w", identifier, err)
}

ignoredTags := allTags.Ignore(oldTags).Ignore(newTags)

if len(newTags)+len(ignoredTags) > 0 {
input := &s3control.PutBucketTaggingInput{
AccountId: aws.String(parsedArn.AccountID),
Bucket: aws.String(identifier),
Tagging: &s3control.Tagging{
TagSet: Tags(newTags.Merge(ignoredTags)),
},
}

_, err := conn.PutBucketTaggingWithContext(ctx, input)

if err != nil {
return fmt.Errorf("setting resource tags (%s): %s", identifier, err)
}
} else if len(oldTags) > 0 && len(ignoredTags) == 0 {
input := &s3control.DeleteBucketTaggingInput{
AccountId: aws.String(parsedArn.AccountID),
Bucket: aws.String(identifier),
}

_, err := conn.DeleteBucketTaggingWithContext(ctx, input)

if err != nil {
return fmt.Errorf("deleting resource tags (%s): %s", identifier, err)
}
}

return nil
Expand Down
7 changes: 3 additions & 4 deletions internal/service/s3control/bucket_lifecycle_configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,12 +236,11 @@ func resourceBucketLifecycleConfigurationDelete(ctx context.Context, d *schema.R
return diag.Errorf("parsing S3 Control Bucket ARN (%s): unknown format", d.Id())
}

input := &s3control.DeleteBucketLifecycleConfigurationInput{
log.Printf("[DEBUG] Deleting S3 Control Bucket Lifecycle Configuration: %s", d.Id())
_, err = conn.DeleteBucketLifecycleConfigurationWithContext(ctx, &s3control.DeleteBucketLifecycleConfigurationInput{
AccountId: aws.String(parsedArn.AccountID),
Bucket: aws.String(d.Id()),
}

_, err = conn.DeleteBucketLifecycleConfigurationWithContext(ctx, input)
})

if tfawserr.ErrCodeEquals(err, errCodeNoSuchBucket, errCodeNoSuchLifecycleConfiguration, errCodeNoSuchOutpost) {
return nil
Expand Down
Loading

0 comments on commit af25c23

Please sign in to comment.