diff --git a/aws/provider.go b/aws/provider.go index 79388178cd6..74b661956e2 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -855,6 +855,7 @@ func Provider() *schema.Provider { "aws_s3_bucket_notification": resourceAwsS3BucketNotification(), "aws_s3_bucket_metric": resourceAwsS3BucketMetric(), "aws_s3_bucket_inventory": resourceAwsS3BucketInventory(), + "aws_s3control_bucket_lifecycle_configuration": resourceAwsS3ControlBucketLifecycleConfiguration(), "aws_security_group": resourceAwsSecurityGroup(), "aws_network_interface_sg_attachment": resourceAwsNetworkInterfaceSGAttachment(), "aws_default_security_group": resourceAwsDefaultSecurityGroup(), diff --git a/aws/resource_aws_s3control_bucket_lifecycle_configuration.go b/aws/resource_aws_s3control_bucket_lifecycle_configuration.go new file mode 100644 index 00000000000..7ba8f832ef7 --- /dev/null +++ b/aws/resource_aws_s3control_bucket_lifecycle_configuration.go @@ -0,0 +1,529 @@ +package aws + +import ( + "fmt" + "log" + "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/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" +) + +func resourceAwsS3ControlBucketLifecycleConfiguration() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsS3ControlBucketLifecycleConfigurationCreate, + Read: resourceAwsS3ControlBucketLifecycleConfigurationRead, + Update: resourceAwsS3ControlBucketLifecycleConfigurationUpdate, + Delete: resourceAwsS3ControlBucketLifecycleConfigurationDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "bucket": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateArn, + }, + "rule": { + Type: schema.TypeSet, + Required: true, + MinItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "abort_incomplete_multipart_upload": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "days_after_initiation": { + Type: schema.TypeInt, + Required: true, + }, + }, + }, + }, + "expiration": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "date": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + + _, err := time.Parse("2006-01-02", value) + + if err != nil { + errors = append(errors, fmt.Errorf("%q should be in YYYY-MM-DD date format", value)) + } + + return + }, + }, + "days": { + Type: schema.TypeInt, + Optional: true, + }, + "expired_object_delete_marker": { + Type: schema.TypeBool, + Optional: true, + Default: false, // Prevent SDK TypeSet difference issues + }, + }, + }, + }, + "filter": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "prefix": { + Type: schema.TypeString, + Optional: true, + }, + "tags": tagsSchema(), + }, + }, + }, + "id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + "status": { + Type: schema.TypeString, + Optional: true, + Default: s3control.ExpirationStatusEnabled, + }, + }, + }, + }, + }, + } +} + +func resourceAwsS3ControlBucketLifecycleConfigurationCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).s3controlconn + + bucket := d.Get("bucket").(string) + + parsedArn, err := arn.Parse(bucket) + + if err != nil { + return fmt.Errorf("error parsing S3 Control Bucket ARN (%s): %w", bucket, err) + } + + if parsedArn.AccountID == "" { + return fmt.Errorf("error parsing S3 Control Bucket ARN (%s): unknown format", d.Id()) + } + + input := &s3control.PutBucketLifecycleConfigurationInput{ + AccountId: aws.String(parsedArn.AccountID), + Bucket: aws.String(bucket), + LifecycleConfiguration: &s3control.LifecycleConfiguration{ + Rules: expandS3controlLifecycleRules(d.Get("rule").(*schema.Set).List()), + }, + } + + _, err = conn.PutBucketLifecycleConfiguration(input) + + if err != nil { + return fmt.Errorf("error creating S3 Control Lifecycle Configuration (%s): %w", bucket, err) + } + + d.SetId(bucket) + + return resourceAwsS3ControlBucketLifecycleConfigurationRead(d, meta) +} + +func resourceAwsS3ControlBucketLifecycleConfigurationRead(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) + } + + if parsedArn.AccountID == "" { + return fmt.Errorf("error parsing S3 Control Bucket ARN (%s): unknown format", d.Id()) + } + + input := &s3control.GetBucketLifecycleConfigurationInput{ + AccountId: aws.String(parsedArn.AccountID), + Bucket: aws.String(d.Id()), + } + + output, err := conn.GetBucketLifecycleConfiguration(input) + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, "NoSuchBucket") { + log.Printf("[WARN] S3 Control Lifecycle Configuration (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, "NoSuchLifecycleConfiguration") { + log.Printf("[WARN] S3 Control Lifecycle Configuration (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, "NoSuchOutpost") { + log.Printf("[WARN] S3 Control Lifecycle Configuration (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading S3 Control Lifecycle Configuration (%s): %w", d.Id(), err) + } + + if output == nil { + return fmt.Errorf("error reading S3 Control Lifecycle Configuration (%s): empty response", d.Id()) + } + + d.Set("bucket", d.Id()) + + if err := d.Set("rule", flattenS3controlLifecycleRules(output.Rules)); err != nil { + return fmt.Errorf("error setting rule: %w", err) + } + + return nil +} + +func resourceAwsS3ControlBucketLifecycleConfigurationUpdate(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) + } + + if parsedArn.AccountID == "" { + return fmt.Errorf("error parsing S3 Control Bucket ARN (%s): unknown format", d.Id()) + } + + input := &s3control.PutBucketLifecycleConfigurationInput{ + AccountId: aws.String(parsedArn.AccountID), + Bucket: aws.String(d.Id()), + LifecycleConfiguration: &s3control.LifecycleConfiguration{ + Rules: expandS3controlLifecycleRules(d.Get("rule").(*schema.Set).List()), + }, + } + + _, err = conn.PutBucketLifecycleConfiguration(input) + + if err != nil { + return fmt.Errorf("error updating S3 Control Lifecycle Configuration (%s): %w", d.Id(), err) + } + + return resourceAwsS3ControlBucketLifecycleConfigurationRead(d, meta) +} + +func resourceAwsS3ControlBucketLifecycleConfigurationDelete(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) + } + + if parsedArn.AccountID == "" { + return fmt.Errorf("error parsing S3 Control Bucket ARN (%s): unknown format", d.Id()) + } + + input := &s3control.DeleteBucketLifecycleConfigurationInput{ + AccountId: aws.String(parsedArn.AccountID), + Bucket: aws.String(d.Id()), + } + + _, err = conn.DeleteBucketLifecycleConfiguration(input) + + if tfawserr.ErrCodeEquals(err, "NoSuchBucket") { + return nil + } + + if tfawserr.ErrCodeEquals(err, "NoSuchLifecycleConfiguration") { + return nil + } + + if tfawserr.ErrCodeEquals(err, "NoSuchOutpost") { + return nil + } + + if err != nil { + return fmt.Errorf("error deleting S3 Control Lifecycle Configuration (%s): %w", d.Id(), err) + } + + return nil +} + +func expandS3controlAbortIncompleteMultipartUpload(tfList []interface{}) *s3control.AbortIncompleteMultipartUpload { + if len(tfList) == 0 || tfList[0] == nil { + return nil + } + + tfMap, ok := tfList[0].(map[string]interface{}) + + if !ok { + return nil + } + + apiObject := &s3control.AbortIncompleteMultipartUpload{} + + if v, ok := tfMap["days_after_initiation"].(int); ok && v != 0 { + apiObject.DaysAfterInitiation = aws.Int64(int64(v)) + } + + return apiObject +} + +func expandS3controlLifecycleExpiration(tfList []interface{}) *s3control.LifecycleExpiration { + if len(tfList) == 0 || tfList[0] == nil { + return nil + } + + tfMap, ok := tfList[0].(map[string]interface{}) + + if !ok { + return nil + } + + apiObject := &s3control.LifecycleExpiration{} + + if v, ok := tfMap["date"].(string); ok && v != "" { + parsedDate, err := time.Parse("2006-01-02", v) + + if err == nil { + apiObject.Date = aws.Time(parsedDate) + } + } + + if v, ok := tfMap["days"].(int); ok && v != 0 { + apiObject.Days = aws.Int64(int64(v)) + } + + if v, ok := tfMap["expired_object_delete_marker"].(bool); ok && v { + apiObject.ExpiredObjectDeleteMarker = aws.Bool(v) + } + + return apiObject +} + +func expandS3controlLifecycleRules(tfList []interface{}) []*s3control.LifecycleRule { + var apiObjects []*s3control.LifecycleRule + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + apiObject := expandS3controlLifecycleRule(tfMap) + + if apiObject == nil { + continue + } + + apiObjects = append(apiObjects, apiObject) + } + + return apiObjects +} + +func expandS3controlLifecycleRule(tfMap map[string]interface{}) *s3control.LifecycleRule { + if len(tfMap) == 0 { + return nil + } + + apiObject := &s3control.LifecycleRule{} + + if v, ok := tfMap["abort_incomplete_multipart_upload"].([]interface{}); ok && len(v) > 0 { + apiObject.AbortIncompleteMultipartUpload = expandS3controlAbortIncompleteMultipartUpload(v) + } + + if v, ok := tfMap["expiration"].([]interface{}); ok && len(v) > 0 { + apiObject.Expiration = expandS3controlLifecycleExpiration(v) + } + + if v, ok := tfMap["filter"].([]interface{}); ok && len(v) > 0 { + apiObject.Filter = expandS3controlLifecycleRuleFilter(v) + } + + if v, ok := tfMap["id"].(string); ok && v != "" { + apiObject.ID = aws.String(v) + } + + if v, ok := tfMap["status"].(string); ok && v != "" { + apiObject.Status = aws.String(v) + } + + // Terraform Plugin SDK sometimes sends map with only empty configuration blocks: + // map[abort_incomplete_multipart_upload:[] expiration:[] filter:[] id: status:] + // This is to prevent this error: InvalidParameter: 1 validation error(s) found. + // - missing required field, PutBucketLifecycleConfigurationInput.LifecycleConfiguration.Rules[0].Status. + if apiObject.ID == nil && apiObject.Status == nil { + return nil + } + + return apiObject +} + +func expandS3controlLifecycleRuleFilter(tfList []interface{}) *s3control.LifecycleRuleFilter { + if len(tfList) == 0 || tfList[0] == nil { + return nil + } + + tfMap, ok := tfList[0].(map[string]interface{}) + + if !ok { + return nil + } + + apiObject := &s3control.LifecycleRuleFilter{} + + if v, ok := tfMap["prefix"].(string); ok && v != "" { + apiObject.Prefix = aws.String(v) + } + + if v, ok := tfMap["tags"].(map[string]interface{}); ok && len(v) > 0 { + // See also aws_s3_bucket ReplicationRule.Filter handling + if len(v) == 1 { + apiObject.Tag = keyvaluetags.New(v).S3controlTags()[0] + } else { + apiObject.And = &s3control.LifecycleRuleAndOperator{ + Prefix: apiObject.Prefix, + Tags: keyvaluetags.New(v).S3controlTags(), + } + apiObject.Prefix = nil + } + } + + return apiObject +} + +func flattenS3controlAbortIncompleteMultipartUpload(apiObject *s3control.AbortIncompleteMultipartUpload) []interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.DaysAfterInitiation; v != nil { + tfMap["days_after_initiation"] = aws.Int64Value(v) + } + + return []interface{}{tfMap} +} + +func flattenS3controlLifecycleExpiration(apiObject *s3control.LifecycleExpiration) []interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.Date; v != nil { + tfMap["date"] = aws.TimeValue(v).Format("2006-01-02") + } + + if v := apiObject.Days; v != nil { + tfMap["days"] = aws.Int64Value(v) + } + + if v := apiObject.ExpiredObjectDeleteMarker; v != nil { + tfMap["expired_object_delete_marker"] = aws.BoolValue(v) + } + + return []interface{}{tfMap} +} + +func flattenS3controlLifecycleRules(apiObjects []*s3control.LifecycleRule) []interface{} { + var tfMaps []interface{} + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + tfMaps = append(tfMaps, flattenS3controlLifecycleRule(apiObject)) + } + + return tfMaps +} + +func flattenS3controlLifecycleRule(apiObject *s3control.LifecycleRule) map[string]interface{} { + if apiObject == nil { + return nil + } + + log.Printf("[BFLAD] flattenS3controlLifecycleRule() apiObject: %+v", apiObject) + + tfMap := map[string]interface{}{} + + if v := apiObject.AbortIncompleteMultipartUpload; v != nil { + tfMap["abort_incomplete_multipart_upload"] = flattenS3controlAbortIncompleteMultipartUpload(v) + } + + if v := apiObject.Expiration; v != nil { + tfMap["expiration"] = flattenS3controlLifecycleExpiration(v) + } + + if v := apiObject.Filter; v != nil { + tfMap["filter"] = flattenS3controlLifecycleRuleFilter(v) + } + + if v := apiObject.ID; v != nil { + tfMap["id"] = aws.StringValue(v) + } + + if v := apiObject.Status; v != nil { + tfMap["status"] = aws.StringValue(v) + } + + return tfMap +} + +func flattenS3controlLifecycleRuleFilter(apiObject *s3control.LifecycleRuleFilter) []interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if apiObject.And != nil { + if v := apiObject.And.Prefix; v != nil { + tfMap["prefix"] = aws.StringValue(v) + } + + if v := apiObject.And.Tags; v != nil { + tfMap["tags"] = keyvaluetags.S3controlKeyValueTags(v).IgnoreAws().Map() + } + } else { + if v := apiObject.Prefix; v != nil { + tfMap["prefix"] = aws.StringValue(v) + } + + if v := apiObject.Tag; v != nil { + tfMap["tags"] = keyvaluetags.S3controlKeyValueTags([]*s3control.S3Tag{v}).IgnoreAws().Map() + } + } + + return []interface{}{tfMap} +} diff --git a/aws/resource_aws_s3control_bucket_lifecycle_configuration_test.go b/aws/resource_aws_s3control_bucket_lifecycle_configuration_test.go new file mode 100644 index 00000000000..7d63f8c1677 --- /dev/null +++ b/aws/resource_aws_s3control_bucket_lifecycle_configuration_test.go @@ -0,0 +1,750 @@ +package aws + +import ( + "fmt" + "testing" + "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/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfawsresource" +) + +func TestAccAWSS3ControlBucketLifecycleConfiguration_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_s3control_bucket_lifecycle_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSS3ControlBucketLifecycleConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSS3ControlBucketLifecycleConfigurationConfig_Rule_Id(rName, "test"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3ControlBucketLifecycleConfigurationExists(resourceName), + resource.TestCheckResourceAttrPair(resourceName, "bucket", "aws_s3control_bucket.test", "arn"), + resource.TestCheckResourceAttr(resourceName, "rule.#", "1"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ + "expiration.#": "1", + "expiration.0.days": "365", + "id": "test", + "status": s3control.ExpirationStatusEnabled, + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSS3ControlBucketLifecycleConfiguration_disappears(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_s3control_bucket_lifecycle_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSS3ControlBucketLifecycleConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSS3ControlBucketLifecycleConfigurationConfig_Rule_Id(rName, "test"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3ControlBucketLifecycleConfigurationExists(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsS3ControlBucketLifecycleConfiguration(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAWSS3ControlBucketLifecycleConfiguration_Rule_AbortIncompleteMultipartUpload_DaysAfterInitiation(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_s3control_bucket_lifecycle_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSS3ControlBucketLifecycleConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSS3ControlBucketLifecycleConfigurationConfig_Rule_AbortIncompleteMultipartUpload_DaysAfterInitiation(rName, 1), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3ControlBucketLifecycleConfigurationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "rule.#", "1"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ + "abort_incomplete_multipart_upload.#": "1", + "abort_incomplete_multipart_upload.0.days_after_initiation": "1", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSS3ControlBucketLifecycleConfigurationConfig_Rule_AbortIncompleteMultipartUpload_DaysAfterInitiation(rName, 2), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3ControlBucketLifecycleConfigurationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "rule.#", "1"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ + "abort_incomplete_multipart_upload.#": "1", + "abort_incomplete_multipart_upload.0.days_after_initiation": "2", + }), + ), + }, + }, + }) +} + +func TestAccAWSS3ControlBucketLifecycleConfiguration_Rule_Expiration_Date(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_s3control_bucket_lifecycle_configuration.test" + date1 := time.Now().AddDate(0, 0, 1).Format("2006-01-02") + date2 := time.Now().AddDate(0, 0, 2).Format("2006-01-02") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSS3ControlBucketLifecycleConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSS3ControlBucketLifecycleConfigurationConfig_Rule_Expiration_Date(rName, date1), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3ControlBucketLifecycleConfigurationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "rule.#", "1"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ + "expiration.#": "1", + "expiration.0.date": date1, + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSS3ControlBucketLifecycleConfigurationConfig_Rule_Expiration_Date(rName, date2), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3ControlBucketLifecycleConfigurationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "rule.#", "1"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ + "expiration.#": "1", + "expiration.0.date": date2, + }), + ), + }, + }, + }) +} + +func TestAccAWSS3ControlBucketLifecycleConfiguration_Rule_Expiration_Days(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_s3control_bucket_lifecycle_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSS3ControlBucketLifecycleConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSS3ControlBucketLifecycleConfigurationConfig_Rule_Expiration_Days(rName, 7), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3ControlBucketLifecycleConfigurationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "rule.#", "1"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ + "expiration.#": "1", + "expiration.0.days": "7", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSS3ControlBucketLifecycleConfigurationConfig_Rule_Expiration_Days(rName, 30), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3ControlBucketLifecycleConfigurationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "rule.#", "1"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ + "expiration.#": "1", + "expiration.0.days": "30", + }), + ), + }, + }, + }) +} + +func TestAccAWSS3ControlBucketLifecycleConfiguration_Rule_Expiration_ExpiredObjectDeleteMarker(t *testing.T) { + TestAccSkip(t, "S3 on Outposts does not error or save it in the API when receiving this parameter") + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_s3control_bucket_lifecycle_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSS3ControlBucketLifecycleConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSS3ControlBucketLifecycleConfigurationConfig_Rule_Expiration_ExpiredObjectDeleteMarker(rName, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3ControlBucketLifecycleConfigurationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "rule.#", "1"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ + "expiration.#": "1", + "expiration.0.expired_object_delete_marker": "true", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSS3ControlBucketLifecycleConfigurationConfig_Rule_Expiration_ExpiredObjectDeleteMarker(rName, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3ControlBucketLifecycleConfigurationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "rule.#", "1"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ + "expiration.#": "1", + "expiration.0.expired_object_delete_marker": "false", + }), + ), + }, + }, + }) +} + +func TestAccAWSS3ControlBucketLifecycleConfiguration_Rule_Filter_Prefix(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_s3control_bucket_lifecycle_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSS3ControlBucketLifecycleConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSS3ControlBucketLifecycleConfigurationConfig_Rule_Filter_Prefix(rName, "test1/"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3ControlBucketLifecycleConfigurationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "rule.#", "1"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ + "filter.#": "1", + "filter.0.prefix": "test1/", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSS3ControlBucketLifecycleConfigurationConfig_Rule_Filter_Prefix(rName, "test2/"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3ControlBucketLifecycleConfigurationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "rule.#", "1"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ + "filter.#": "1", + "filter.0.prefix": "test2/", + }), + ), + }, + }, + }) +} + +func TestAccAWSS3ControlBucketLifecycleConfiguration_Rule_Filter_Tags(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_s3control_bucket_lifecycle_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSS3ControlBucketLifecycleConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSS3ControlBucketLifecycleConfigurationConfig_Rule_Filter_Tags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3ControlBucketLifecycleConfigurationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "rule.#", "1"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ + "filter.#": "1", + "filter.0.tags.%": "1", + "filter.0.tags.key1": "value1", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + // There is currently an API model or AWS Go SDK bug where LifecycleFilter.And.Tags + // does not get populated from the XML response. Reference: + // https://github.com/aws/aws-sdk-go/issues/3591 + // { + // Config: testAccAWSS3ControlBucketLifecycleConfigurationConfig_Rule_Filter_Tags2(rName, "key1", "value1updated", "key2", "value2"), + // Check: resource.ComposeTestCheckFunc( + // testAccCheckAWSS3ControlBucketLifecycleConfigurationExists(resourceName), + // resource.TestCheckResourceAttr(resourceName, "rule.#", "1"), + // tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ + // "filter.#": "1", + // "filter.0.tags.%": "2", + // "filter.0.tags.key1": "value1updated", + // "filter.0.tags.key2": "value2", + // }), + // ), + // }, + { + Config: testAccAWSS3ControlBucketLifecycleConfigurationConfig_Rule_Filter_Tags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3ControlBucketLifecycleConfigurationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "rule.#", "1"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ + "filter.#": "1", + "filter.0.tags.%": "1", + "filter.0.tags.key2": "value2", + }), + ), + }, + }, + }) +} + +func TestAccAWSS3ControlBucketLifecycleConfiguration_Rule_Id(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_s3control_bucket_lifecycle_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSS3ControlBucketLifecycleConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSS3ControlBucketLifecycleConfigurationConfig_Rule_Id(rName, "test1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3ControlBucketLifecycleConfigurationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "rule.#", "1"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ + "id": "test1", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSS3ControlBucketLifecycleConfigurationConfig_Rule_Id(rName, "test2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3ControlBucketLifecycleConfigurationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "rule.#", "1"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ + "id": "test2", + }), + ), + }, + }, + }) +} + +func TestAccAWSS3ControlBucketLifecycleConfiguration_Rule_Status(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_s3control_bucket_lifecycle_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSS3ControlBucketLifecycleConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSS3ControlBucketLifecycleConfigurationConfig_Rule_Status(rName, s3control.ExpirationStatusDisabled), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3ControlBucketLifecycleConfigurationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "rule.#", "1"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ + "status": s3control.ExpirationStatusDisabled, + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSS3ControlBucketLifecycleConfigurationConfig_Rule_Status(rName, s3control.ExpirationStatusEnabled), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3ControlBucketLifecycleConfigurationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "rule.#", "1"), + tfawsresource.TestCheckTypeSetElemNestedAttrs(resourceName, "rule.*", map[string]string{ + "status": s3control.ExpirationStatusEnabled, + }), + ), + }, + }, + }) +} + +func testAccCheckAWSS3ControlBucketLifecycleConfigurationDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).s3controlconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_s3control_bucket_lifecycle_configuration" { + continue + } + + parsedArn, err := arn.Parse(rs.Primary.ID) + + if err != nil { + return fmt.Errorf("error parsing S3 Control Bucket ARN (%s): %w", rs.Primary.ID, err) + } + + input := &s3control.GetBucketLifecycleConfigurationInput{ + AccountId: aws.String(parsedArn.AccountID), + Bucket: aws.String(rs.Primary.ID), + } + + _, err = conn.GetBucketLifecycleConfiguration(input) + + if tfawserr.ErrCodeEquals(err, "NoSuchBucket") { + return nil + } + + if tfawserr.ErrCodeEquals(err, "NoSuchLifecycleConfiguration") { + return nil + } + + if tfawserr.ErrCodeEquals(err, "NoSuchOutpost") { + return nil + } + + if err != nil { + return err + } + + return fmt.Errorf("S3 Control Bucket Lifecycle Configuration (%s) still exists", rs.Primary.ID) + } + + return nil +} + +func testAccCheckAWSS3ControlBucketLifecycleConfigurationExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("not found: %s", resourceName) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no resource ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).s3controlconn + + parsedArn, err := arn.Parse(rs.Primary.ID) + + if err != nil { + return fmt.Errorf("error parsing S3 Control Bucket ARN (%s): %w", rs.Primary.ID, err) + } + + input := &s3control.GetBucketLifecycleConfigurationInput{ + AccountId: aws.String(parsedArn.AccountID), + Bucket: aws.String(rs.Primary.ID), + } + + _, err = conn.GetBucketLifecycleConfiguration(input) + + if err != nil { + return err + } + + return nil + } +} + +func testAccAWSS3ControlBucketLifecycleConfigurationConfig_Rule_AbortIncompleteMultipartUpload_DaysAfterInitiation(rName string, daysAfterInitiation int) string { + return fmt.Sprintf(` +data "aws_outposts_outposts" "test" {} + +data "aws_outposts_outpost" "test" { + id = tolist(data.aws_outposts_outposts.test.ids)[0] +} + +resource "aws_s3control_bucket" "test" { + bucket = %[1]q + outpost_id = data.aws_outposts_outpost.test.id +} + +resource "aws_s3control_bucket_lifecycle_configuration" "test" { + bucket = aws_s3control_bucket.test.arn + + rule { + abort_incomplete_multipart_upload { + days_after_initiation = %[2]d + } + + expiration { + days = 365 + } + + id = "test" + } +} +`, rName, daysAfterInitiation) +} + +func testAccAWSS3ControlBucketLifecycleConfigurationConfig_Rule_Expiration_Date(rName string, date string) string { + return fmt.Sprintf(` +data "aws_outposts_outposts" "test" {} + +data "aws_outposts_outpost" "test" { + id = tolist(data.aws_outposts_outposts.test.ids)[0] +} + +resource "aws_s3control_bucket" "test" { + bucket = %[1]q + outpost_id = data.aws_outposts_outpost.test.id +} + +resource "aws_s3control_bucket_lifecycle_configuration" "test" { + bucket = aws_s3control_bucket.test.arn + + rule { + expiration { + date = %[2]q + } + + id = "test" + } +} +`, rName, date) +} + +func testAccAWSS3ControlBucketLifecycleConfigurationConfig_Rule_Expiration_Days(rName string, days int) string { + return fmt.Sprintf(` +data "aws_outposts_outposts" "test" {} + +data "aws_outposts_outpost" "test" { + id = tolist(data.aws_outposts_outposts.test.ids)[0] +} + +resource "aws_s3control_bucket" "test" { + bucket = %[1]q + outpost_id = data.aws_outposts_outpost.test.id +} + +resource "aws_s3control_bucket_lifecycle_configuration" "test" { + bucket = aws_s3control_bucket.test.arn + + rule { + expiration { + days = %[2]d + } + + id = "test" + } +} +`, rName, days) +} + +func testAccAWSS3ControlBucketLifecycleConfigurationConfig_Rule_Expiration_ExpiredObjectDeleteMarker(rName string, expiredObjectDeleteMarker bool) string { + return fmt.Sprintf(` +data "aws_outposts_outposts" "test" {} + +data "aws_outposts_outpost" "test" { + id = tolist(data.aws_outposts_outposts.test.ids)[0] +} + +resource "aws_s3control_bucket" "test" { + bucket = %[1]q + outpost_id = data.aws_outposts_outpost.test.id +} + +resource "aws_s3control_bucket_lifecycle_configuration" "test" { + bucket = aws_s3control_bucket.test.arn + + rule { + expiration { + days = %[2]t ? null : 365 + expired_object_delete_marker = %[2]t + } + + id = "test" + } +} +`, rName, expiredObjectDeleteMarker) +} + +func testAccAWSS3ControlBucketLifecycleConfigurationConfig_Rule_Filter_Prefix(rName, prefix string) string { + return fmt.Sprintf(` +data "aws_outposts_outposts" "test" {} + +data "aws_outposts_outpost" "test" { + id = tolist(data.aws_outposts_outposts.test.ids)[0] +} + +resource "aws_s3control_bucket" "test" { + bucket = %[1]q + outpost_id = data.aws_outposts_outpost.test.id +} + +resource "aws_s3control_bucket_lifecycle_configuration" "test" { + bucket = aws_s3control_bucket.test.arn + + rule { + expiration { + days = 365 + } + + filter { + prefix = %[2]q + } + + id = "test" + } +} +`, rName, prefix) +} + +func testAccAWSS3ControlBucketLifecycleConfigurationConfig_Rule_Filter_Tags1(rName, tagKey1, tagValue1 string) string { + return fmt.Sprintf(` +data "aws_outposts_outposts" "test" {} + +data "aws_outposts_outpost" "test" { + id = tolist(data.aws_outposts_outposts.test.ids)[0] +} + +resource "aws_s3control_bucket" "test" { + bucket = %[1]q + outpost_id = data.aws_outposts_outpost.test.id +} + +resource "aws_s3control_bucket_lifecycle_configuration" "test" { + bucket = aws_s3control_bucket.test.arn + + rule { + expiration { + days = 365 + } + + filter { + tags = { + %[2]q = %[3]q + } + } + + id = "test" + } +} +`, rName, tagKey1, tagValue1) +} + +// See TestAccAWSS3ControlBucketLifecycleConfiguration_Rule_Filter_Tags note about XML handling bug. +// func testAccAWSS3ControlBucketLifecycleConfigurationConfig_Rule_Filter_Tags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { +// return fmt.Sprintf(` +// data "aws_outposts_outposts" "test" {} + +// data "aws_outposts_outpost" "test" { +// id = tolist(data.aws_outposts_outposts.test.ids)[0] +// } + +// resource "aws_s3control_bucket" "test" { +// bucket = %[1]q +// outpost_id = data.aws_outposts_outpost.test.id +// } + +// resource "aws_s3control_bucket_lifecycle_configuration" "test" { +// bucket = aws_s3control_bucket.test.arn + +// rule { +// expiration { +// days = 365 +// } + +// filter { +// tags = { +// %[2]q = %[3]q +// %[4]q = %[5]q +// } +// } + +// id = "test" +// } +// } +// `, rName, tagKey1, tagValue1, tagKey2, tagValue2) +// } + +func testAccAWSS3ControlBucketLifecycleConfigurationConfig_Rule_Id(rName, id string) string { + return fmt.Sprintf(` +data "aws_outposts_outposts" "test" {} + +data "aws_outposts_outpost" "test" { + id = tolist(data.aws_outposts_outposts.test.ids)[0] +} + +resource "aws_s3control_bucket" "test" { + bucket = %[1]q + outpost_id = data.aws_outposts_outpost.test.id +} + +resource "aws_s3control_bucket_lifecycle_configuration" "test" { + bucket = aws_s3control_bucket.test.arn + + rule { + expiration { + days = 365 + } + + id = %[2]q + } +} +`, rName, id) +} + +func testAccAWSS3ControlBucketLifecycleConfigurationConfig_Rule_Status(rName, status string) string { + return fmt.Sprintf(` +data "aws_outposts_outposts" "test" {} + +data "aws_outposts_outpost" "test" { + id = tolist(data.aws_outposts_outposts.test.ids)[0] +} + +resource "aws_s3control_bucket" "test" { + bucket = %[1]q + outpost_id = data.aws_outposts_outpost.test.id +} + +resource "aws_s3control_bucket_lifecycle_configuration" "test" { + bucket = aws_s3control_bucket.test.arn + + rule { + expiration { + days = 365 + } + + id = "test" + status = %[2]q + } +} +`, rName, status) +} diff --git a/website/docs/r/s3control_bucket_lifecycle_configuration.html.markdown b/website/docs/r/s3control_bucket_lifecycle_configuration.html.markdown new file mode 100644 index 00000000000..0cc5533f109 --- /dev/null +++ b/website/docs/r/s3control_bucket_lifecycle_configuration.html.markdown @@ -0,0 +1,79 @@ +--- +subcategory: "S3 Control" +layout: "aws" +page_title: "AWS: aws_s3control_bucket_lifecycle_configuration" +description: |- + Manages an S3 Control Bucket Lifecycle Configuration. +--- + +# Resource: aws_s3control_bucket_lifecycle_configuration + +Provides a resource to manage an S3 Control Bucket Lifecycle Configuration. + +~> **NOTE:** Each S3 Control Bucket can only have one Lifecycle Configuration. Using multiple of this resource against the same S3 Control Bucket will result in perpetual differences each Terraform run. + +-> This functionality is for managing [S3 on Outposts](https://docs.aws.amazon.com/AmazonS3/latest/dev/S3onOutposts.html). To manage S3 Bucket Lifecycle Configurations in an AWS Partition, see the [`aws_s3_bucket` resource](/docs/providers/aws/r/s3_bucket.html). + +## Example Usage + +```hcl +resource "aws_s3control_bucket_lifecycle_configuration" "example" { + bucket = aws_s3control_bucket.example.arn + + rule { + expiration { + days = 365 + } + + filter { + prefix = "logs/" + } + + id = "logs" + } + + rule { + expiration { + days = 7 + } + + filter { + prefix = "temp/" + } + + id = "temp" + } +} +``` + +## Argument Reference + +The following arguments are required: + +* `bucket` - (Required) Amazon Resource Name (ARN) of the bucket. +* `rule` - (Required) Configuration block(s) containing lifecycle rules for the bucket. + * `abort_incomplete_multipart_upload` - (Optional) Configuration block containing settings for abort incomplete multipart upload. + * `days_after_initiation` - (Required) Number of days after which Amazon S3 aborts an incomplete multipart upload. + * `expiration` - (Optional) Configuration block containing settings for expiration of objects. + * `date` - (Optional) Date the object is to be deleted. Should be in `YYYY-MM-DD` date format, e.g. `2020-09-30`. + * `days` - (Optional) Number of days before the object is to be deleted. + * `expired_object_delete_marker` - (Optional) Enable to remove a delete marker with no noncurrent versions. Cannot be specified with `date` or `days`. + * `filter` - (Optional) Configuration block containing settings for filtering. + * `prefix` - (Optional) Object prefix for rule filtering. + * `tags` - (Optional) Key-value map of object tags for rule filtering. + * `id` - (Required) Unique identifier for the rule. + * `status` - (Optional) Status of the rule. Valid values: `Enabled` and `Disabled`. Defaults to `Enabled`. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - Amazon Resource Name (ARN) of the bucket. + +## Import + +S3 Control Bucket Lifecycle Configurations can be imported using the Amazon Resource Name (ARN), e.g. + +``` +$ terraform import aws_s3control_bucket_lifecycle_configuration.example arn:aws:s3-outposts:us-east-1:123456789012:outpost/op-12345678/bucket/example +```