Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Resource: aws_s3control_bucket #15510

Merged
merged 3 commits into from
Oct 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions aws/internal/keyvaluetags/generators/servicetags/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ var sliceServiceNames = []string{
"route53",
"route53resolver",
"s3",
"s3control",
"sagemaker",
"secretsmanager",
"serverlessapplicationrepository",
Expand Down
90 changes: 90 additions & 0 deletions aws/internal/keyvaluetags/s3control_tags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package keyvaluetags

import (
"fmt"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/arn"
"github.com/aws/aws-sdk-go/service/s3control"
"github.com/hashicorp/aws-sdk-go-base/tfawserr"
)

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

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

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

input := &s3control.GetBucketTaggingInput{
AccountId: aws.String(parsedArn.AccountID),
Bucket: aws.String(identifier),
}

output, err := conn.GetBucketTagging(input)

if tfawserr.ErrCodeEquals(err, "NoSuchTagSet") {
return New(nil), nil
}

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

return S3controlKeyValueTags(output.TagSet), nil
}

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

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

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

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

if err != nil {
return fmt.Errorf("error 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: newTags.Merge(ignoredTags).S3controlTags(),
},
}

_, err := conn.PutBucketTagging(input)

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

_, err := conn.DeleteBucketTagging(input)

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

return nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,8 @@ func ServiceTagType(serviceName string) string {
return "TagListEntry"
case "fms":
return "ResourceTag"
case "s3control":
return "S3Tag"
case "swf":
return "ResourceTag"
default:
Expand Down
28 changes: 28 additions & 0 deletions aws/internal/keyvaluetags/service_tags_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,7 @@ func Provider() *schema.Provider {
"aws_s3_bucket_notification": resourceAwsS3BucketNotification(),
"aws_s3_bucket_metric": resourceAwsS3BucketMetric(),
"aws_s3_bucket_inventory": resourceAwsS3BucketInventory(),
"aws_s3control_bucket": resourceAwsS3ControlBucket(),
"aws_security_group": resourceAwsSecurityGroup(),
"aws_network_interface_sg_attachment": resourceAwsNetworkInterfaceSGAttachment(),
"aws_default_security_group": resourceAwsDefaultSecurityGroup(),
Expand Down
232 changes: 232 additions & 0 deletions aws/resource_aws_s3control_bucket.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
package aws

import (
"fmt"
"log"
"regexp"
"strings"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/arn"
"github.com/aws/aws-sdk-go/service/s3control"
"github.com/hashicorp/aws-sdk-go-base/tfawserr"
"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"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource"
)

const (
// Maximum amount of time to wait for s3control Bucket state to propagate
s3controlBucketStatePropagationTimeout = 5 * time.Minute
)

func resourceAwsS3ControlBucket() *schema.Resource {
return &schema.Resource{
Create: resourceAwsS3ControlBucketCreate,
Read: resourceAwsS3ControlBucketRead,
Update: resourceAwsS3ControlBucketUpdate,
Delete: resourceAwsS3ControlBucketDelete,

Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},

Schema: map[string]*schema.Schema{
"arn": {
Type: schema.TypeString,
Computed: true,
},
"bucket": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.All(
validation.StringLenBetween(3, 63),
validation.StringMatch(regexp.MustCompile(`^[a-z0-9.-]+$`), "must contain only lowercase letters, numbers, periods, and hyphens"),
validation.StringMatch(regexp.MustCompile(`^[a-z0-9]`), "must begin with lowercase letter or number"),
validation.StringMatch(regexp.MustCompile(`[a-z0-9]$`), "must end with lowercase letter or number"),
),
},
"creation_date": {
Type: schema.TypeString,
Computed: true,
},
"outpost_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.StringLenBetween(1, 64),
},
"public_access_block_enabled": {
Type: schema.TypeBool,
Computed: true,
},
"tags": tagsSchema(),
},
}
}

func resourceAwsS3ControlBucketCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).s3controlconn

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)

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)
}

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

if v := d.Get("tags").(map[string]interface{}); len(v) > 0 {
if err := keyvaluetags.S3controlBucketUpdateTags(conn, d.Id(), nil, v); err != nil {
return fmt.Errorf("error adding S3 Control Bucket (%s) tags: %w", d.Id(), err)
}
}

return resourceAwsS3ControlBucketRead(d, meta)
}

func resourceAwsS3ControlBucketRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).s3controlconn
ignoreTagsConfig := meta.(*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)
}

// 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()),
}

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
}

if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, "NoSuchOutpost") {
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())
}

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 := keyvaluetags.S3controlBucketListTags(conn, d.Id())

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

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

return nil
}

func resourceAwsS3ControlBucketUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).s3controlconn

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

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

return resourceAwsS3ControlBucketRead(d, meta)
}

func resourceAwsS3ControlBucketDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*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)
}

input := &s3control.DeleteBucketInput{
AccountId: aws.String(parsedArn.AccountID),
Bucket: aws.String(d.Id()),
}

// 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(s3controlBucketStatePropagationTimeout, func() *resource.RetryError {
_, err := conn.DeleteBucket(input)

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

if err != nil {
return resource.NonRetryableError(err)
}

return nil
})

if tfresource.TimedOut(err) {
_, err = conn.DeleteBucket(input)
}

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

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

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

return nil
}
Loading