From 17b70d7db6d3d98b3bf126b845778cf8a8d1d06b Mon Sep 17 00:00:00 2001 From: Angie Pinilla Date: Tue, 18 Jan 2022 21:28:41 -0500 Subject: [PATCH] r/s3_bucket_website_configuration: new resource --- internal/provider/provider.go | 1 + .../s3/bucket_website_configuration.go | 613 ++++++++++++++++++ .../s3/bucket_website_configuration_test.go | 594 +++++++++++++++++ internal/service/s3/errors.go | 1 + ...bucket_website_configuration.html.markdown | 112 ++++ 5 files changed, 1321 insertions(+) create mode 100644 internal/service/s3/bucket_website_configuration.go create mode 100644 internal/service/s3/bucket_website_configuration_test.go create mode 100644 website/docs/r/s3_bucket_website_configuration.html.markdown diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 494e18df435..793adc949c2 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -1567,6 +1567,7 @@ func Provider() *schema.Provider { "aws_s3_bucket_policy": s3.ResourceBucketPolicy(), "aws_s3_bucket_public_access_block": s3.ResourceBucketPublicAccessBlock(), "aws_s3_bucket_replication_configuration": s3.ResourceBucketReplicationConfiguration(), + "aws_s3_bucket_website_configuration": s3.ResourceBucketWebsiteConfiguration(), "aws_s3_object_copy": s3.ResourceObjectCopy(), "aws_s3_access_point": s3control.ResourceAccessPoint(), diff --git a/internal/service/s3/bucket_website_configuration.go b/internal/service/s3/bucket_website_configuration.go new file mode 100644 index 00000000000..26f8f6edcbf --- /dev/null +++ b/internal/service/s3/bucket_website_configuration.go @@ -0,0 +1,613 @@ +package s3 + +import ( + "context" + "fmt" + "log" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/verify" +) + +func ResourceBucketWebsiteConfiguration() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceBucketWebsiteConfigurationCreate, + ReadContext: resourceBucketWebsiteConfigurationRead, + UpdateContext: resourceBucketWebsiteConfigurationUpdate, + DeleteContext: resourceBucketWebsiteConfigurationDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "bucket": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 63), + }, + "error_document": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + "expected_bucket_owner": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: verify.ValidAccountID, + }, + "index_document": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "suffix": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + "redirect_all_requests_to": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ConflictsWith: []string{ + "error_document", + "index_document", + "routing_rule", + }, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "host_name": { + Type: schema.TypeString, + Required: true, + }, + "protocol": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(s3.Protocol_Values(), false), + }, + }, + }, + }, + "routing_rule": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "condition": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "http_error_code_returned_equals": { + Type: schema.TypeString, + Optional: true, + }, + "key_prefix_equals": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "redirect": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "host_name": { + Type: schema.TypeString, + Optional: true, + }, + "http_redirect_code": { + Type: schema.TypeString, + Optional: true, + }, + "protocol": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(s3.Protocol_Values(), false), + }, + "replace_key_prefix_with": { + Type: schema.TypeString, + Optional: true, + }, + "replace_key_with": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func resourceBucketWebsiteConfigurationCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).S3Conn + + bucket := d.Get("bucket").(string) + expectedBucketOwner := d.Get("expected_bucket_owner").(string) + + websiteConfig := &s3.WebsiteConfiguration{} + + if v, ok := d.GetOk("error_document"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + websiteConfig.ErrorDocument = expandS3BucketWebsiteConfigurationErrorDocument(v.([]interface{})) + } + + if v, ok := d.GetOk("index_document"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + websiteConfig.IndexDocument = expandS3BucketWebsiteConfigurationIndexDocument(v.([]interface{})) + } + + if v, ok := d.GetOk("redirect_all_requests_to"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + websiteConfig.RedirectAllRequestsTo = expandS3BucketWebsiteConfigurationRedirectAllRequestsTo(v.([]interface{})) + } + + if v, ok := d.GetOk("routing_rule"); ok && v.(*schema.Set).Len() > 0 { + websiteConfig.RoutingRules = expandS3BucketWebsiteConfigurationRoutingRules(v.(*schema.Set).List()) + } + + input := &s3.PutBucketWebsiteInput{ + Bucket: aws.String(bucket), + WebsiteConfiguration: websiteConfig, + } + + if expectedBucketOwner != "" { + input.ExpectedBucketOwner = aws.String(expectedBucketOwner) + } + + _, err := verify.RetryOnAWSCode(s3.ErrCodeNoSuchBucket, func() (interface{}, error) { + return conn.PutBucketWebsiteWithContext(ctx, input) + }) + + if err != nil { + return diag.FromErr(fmt.Errorf("error creating S3 bucket (%s) website configuration: %w", bucket, err)) + } + + d.SetId(resourceBucketWebsiteConfigurationCreateResourceID(bucket, expectedBucketOwner)) + + return resourceBucketWebsiteConfigurationRead(ctx, d, meta) +} + +func resourceBucketWebsiteConfigurationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).S3Conn + + bucket, expectedBucketOwner, err := resourceBucketWebsiteConfigurationParseResourceID(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + input := &s3.GetBucketWebsiteInput{ + Bucket: aws.String(bucket), + } + + if expectedBucketOwner != "" { + input.ExpectedBucketOwner = aws.String(expectedBucketOwner) + } + + output, err := conn.GetBucketWebsiteWithContext(ctx, input) + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, s3.ErrCodeNoSuchBucket, ErrCodeNoSuchWebsiteConfiguration) { + log.Printf("[WARN] S3 Bucket Website Configuration (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if output == nil { + if d.IsNewResource() { + return diag.FromErr(fmt.Errorf("error reading S3 bucket website configuration (%s): empty output", d.Id())) + } + log.Printf("[WARN] S3 Bucket Website Configuration (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + d.Set("bucket", bucket) + d.Set("expected_bucket_owner", expectedBucketOwner) + + if err := d.Set("error_document", flattenS3BucketWebsiteConfigurationErrorDocument(output.ErrorDocument)); err != nil { + return diag.FromErr(fmt.Errorf("error setting error_document: %w", err)) + } + + if err := d.Set("index_document", flattenS3BucketWebsiteConfigurationIndexDocument(output.IndexDocument)); err != nil { + return diag.FromErr(fmt.Errorf("error setting index_document: %w", err)) + } + + if err := d.Set("redirect_all_requests_to", flattenS3BucketWebsiteConfigurationRedirectAllRequestsTo(output.RedirectAllRequestsTo)); err != nil { + return diag.FromErr(fmt.Errorf("error setting redirect_all_requests_to: %w", err)) + } + + if err := d.Set("routing_rule", flattenS3BucketWebsiteConfigurationRoutingRules(output.RoutingRules)); err != nil { + return diag.FromErr(fmt.Errorf("error setting routing_rule: %w", err)) + } + + return nil +} + +func resourceBucketWebsiteConfigurationUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).S3Conn + + bucket, expectedBucketOwner, err := resourceBucketWebsiteConfigurationParseResourceID(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + websiteConfig := &s3.WebsiteConfiguration{} + + if v, ok := d.GetOk("error_document"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + websiteConfig.ErrorDocument = expandS3BucketWebsiteConfigurationErrorDocument(v.([]interface{})) + } + + if v, ok := d.GetOk("index_document"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + websiteConfig.IndexDocument = expandS3BucketWebsiteConfigurationIndexDocument(v.([]interface{})) + } + + if v, ok := d.GetOk("redirect_all_requests_to"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + websiteConfig.RedirectAllRequestsTo = expandS3BucketWebsiteConfigurationRedirectAllRequestsTo(v.([]interface{})) + } + + if v, ok := d.GetOk("routing_rule"); ok && v.(*schema.Set).Len() > 0 { + websiteConfig.RoutingRules = expandS3BucketWebsiteConfigurationRoutingRules(v.(*schema.Set).List()) + } + + input := &s3.PutBucketWebsiteInput{ + Bucket: aws.String(bucket), + WebsiteConfiguration: websiteConfig, + } + + if expectedBucketOwner != "" { + input.ExpectedBucketOwner = aws.String(expectedBucketOwner) + } + + _, err = conn.PutBucketWebsiteWithContext(ctx, input) + + if err != nil { + return diag.FromErr(fmt.Errorf("error updating S3 bucket website configuration (%s): %w", d.Id(), err)) + } + + return resourceBucketWebsiteConfigurationRead(ctx, d, meta) +} + +func resourceBucketWebsiteConfigurationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).S3Conn + + bucket, expectedBucketOwner, err := resourceBucketWebsiteConfigurationParseResourceID(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + input := &s3.DeleteBucketWebsiteInput{ + Bucket: aws.String(bucket), + } + + if expectedBucketOwner != "" { + input.ExpectedBucketOwner = aws.String(expectedBucketOwner) + } + + _, err = conn.DeleteBucketWebsiteWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, s3.ErrCodeNoSuchBucket, ErrCodeNoSuchWebsiteConfiguration) { + return nil + } + + if err != nil { + return diag.FromErr(fmt.Errorf("error deleting S3 bucket website configuration (%s): %w", d.Id(), err)) + } + + return nil +} + +func resourceBucketWebsiteConfigurationCreateResourceID(bucket, expectedBucketOwner string) string { + if bucket == "" { + return expectedBucketOwner + } + + if expectedBucketOwner == "" { + return bucket + } + + parts := []string{bucket, expectedBucketOwner} + id := strings.Join(parts, ",") + + return id +} + +func resourceBucketWebsiteConfigurationParseResourceID(id string) (string, string, error) { + parts := strings.Split(id, ",") + + if len(parts) == 1 && parts[0] != "" { + return parts[0], "", nil + } + + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + return parts[0], parts[1], nil + } + + return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected BUCKET or BUCKET,EXPECTED_BUCKET_OWNER", id) +} + +func expandS3BucketWebsiteConfigurationErrorDocument(l []interface{}) *s3.ErrorDocument { + if len(l) == 0 || l[0] == nil { + return nil + } + + tfMap, ok := l[0].(map[string]interface{}) + if !ok { + return nil + } + + result := &s3.ErrorDocument{} + + if v, ok := tfMap["key"].(string); ok && v != "" { + result.Key = aws.String(v) + } + + return result +} + +func expandS3BucketWebsiteConfigurationIndexDocument(l []interface{}) *s3.IndexDocument { + if len(l) == 0 || l[0] == nil { + return nil + } + + tfMap, ok := l[0].(map[string]interface{}) + if !ok { + return nil + } + + result := &s3.IndexDocument{} + + if v, ok := tfMap["suffix"].(string); ok && v != "" { + result.Suffix = aws.String(v) + } + + return result +} + +func expandS3BucketWebsiteConfigurationRedirectAllRequestsTo(l []interface{}) *s3.RedirectAllRequestsTo { + if len(l) == 0 || l[0] == nil { + return nil + } + + tfMap, ok := l[0].(map[string]interface{}) + if !ok { + return nil + } + + result := &s3.RedirectAllRequestsTo{} + + if v, ok := tfMap["host_name"].(string); ok && v != "" { + result.HostName = aws.String(v) + } + + if v, ok := tfMap["protocol"].(string); ok && v != "" { + result.Protocol = aws.String(v) + } + + return result +} + +func expandS3BucketWebsiteConfigurationRoutingRules(l []interface{}) []*s3.RoutingRule { + var results []*s3.RoutingRule + + for _, tfMapRaw := range l { + tfMap, ok := tfMapRaw.(map[string]interface{}) + if !ok { + continue + } + + rule := &s3.RoutingRule{} + + if v, ok := tfMap["condition"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + rule.Condition = expandS3BucketWebsiteConfigurationRoutingRuleCondition(v) + } + + if v, ok := tfMap["redirect"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + rule.Redirect = expandS3BucketWebsiteConfigurationRoutingRuleRedirect(v) + } + + results = append(results, rule) + } + + return results +} + +func expandS3BucketWebsiteConfigurationRoutingRuleCondition(l []interface{}) *s3.Condition { + if len(l) == 0 || l[0] == nil { + return nil + } + + tfMap, ok := l[0].(map[string]interface{}) + if !ok { + return nil + } + + result := &s3.Condition{} + + if v, ok := tfMap["http_error_code_returned_equals"].(string); ok && v != "" { + result.HttpErrorCodeReturnedEquals = aws.String(v) + } + + if v, ok := tfMap["key_prefix_equals"].(string); ok && v != "" { + result.KeyPrefixEquals = aws.String(v) + } + + return result +} + +func expandS3BucketWebsiteConfigurationRoutingRuleRedirect(l []interface{}) *s3.Redirect { + if len(l) == 0 || l[0] == nil { + return nil + } + + tfMap, ok := l[0].(map[string]interface{}) + if !ok { + return nil + } + + result := &s3.Redirect{} + + if v, ok := tfMap["host_name"].(string); ok && v != "" { + result.HostName = aws.String(v) + } + + if v, ok := tfMap["http_redirect_code"].(string); ok && v != "" { + result.HttpRedirectCode = aws.String(v) + } + + if v, ok := tfMap["protocol"].(string); ok && v != "" { + result.Protocol = aws.String(v) + } + + if v, ok := tfMap["replace_key_prefix_with"].(string); ok && v != "" { + result.ReplaceKeyPrefixWith = aws.String(v) + } + + if v, ok := tfMap["replace_key_with"].(string); ok && v != "" { + result.ReplaceKeyWith = aws.String(v) + } + + return result +} + +func flattenS3BucketWebsiteConfigurationIndexDocument(i *s3.IndexDocument) []interface{} { + if i == nil { + return []interface{}{} + } + + m := make(map[string]interface{}) + + if i.Suffix != nil { + m["suffix"] = aws.StringValue(i.Suffix) + } + + return []interface{}{m} +} + +func flattenS3BucketWebsiteConfigurationErrorDocument(e *s3.ErrorDocument) []interface{} { + if e == nil { + return []interface{}{} + } + + m := make(map[string]interface{}) + + if e.Key != nil { + m["key"] = aws.StringValue(e.Key) + } + + return []interface{}{m} +} + +func flattenS3BucketWebsiteConfigurationRedirectAllRequestsTo(r *s3.RedirectAllRequestsTo) []interface{} { + if r == nil { + return []interface{}{} + } + + m := make(map[string]interface{}) + + if r.HostName != nil { + m["host_name"] = aws.StringValue(r.HostName) + } + + if r.Protocol != nil { + m["protocol"] = aws.StringValue(r.Protocol) + } + + return []interface{}{m} +} + +func flattenS3BucketWebsiteConfigurationRoutingRules(rules []*s3.RoutingRule) []interface{} { + var results []interface{} + + for _, rule := range rules { + if rule == nil { + continue + } + + m := make(map[string]interface{}) + + if rule.Condition != nil { + m["condition"] = flattenS3BucketWebsiteConfigurationRoutingRuleCondition(rule.Condition) + } + + if rule.Redirect != nil { + m["redirect"] = flattenS3BucketWebsiteConfigurationRoutingRuleRedirect(rule.Redirect) + } + + results = append(results, m) + } + + return results +} + +func flattenS3BucketWebsiteConfigurationRoutingRuleCondition(c *s3.Condition) []interface{} { + if c == nil { + return []interface{}{} + } + + m := make(map[string]interface{}) + + if c.KeyPrefixEquals != nil { + m["key_prefix_equals"] = aws.StringValue(c.KeyPrefixEquals) + } + + if c.HttpErrorCodeReturnedEquals != nil { + m["http_error_code_returned_equals"] = aws.StringValue(c.HttpErrorCodeReturnedEquals) + } + + return []interface{}{m} +} + +func flattenS3BucketWebsiteConfigurationRoutingRuleRedirect(r *s3.Redirect) []interface{} { + if r == nil { + return []interface{}{} + } + + m := make(map[string]interface{}) + + if r.HostName != nil { + m["host_name"] = aws.StringValue(r.HostName) + } + + if r.HttpRedirectCode != nil { + m["http_redirect_code"] = aws.StringValue(r.HttpRedirectCode) + } + + if r.Protocol != nil { + m["protocol"] = aws.StringValue(r.Protocol) + } + + if r.ReplaceKeyWith != nil { + m["replace_key_with"] = aws.StringValue(r.ReplaceKeyWith) + } + + if r.ReplaceKeyPrefixWith != nil { + m["replace_key_prefix_with"] = aws.StringValue(r.ReplaceKeyPrefixWith) + } + + return []interface{}{m} +} diff --git a/internal/service/s3/bucket_website_configuration_test.go b/internal/service/s3/bucket_website_configuration_test.go new file mode 100644 index 00000000000..a438b19e4e5 --- /dev/null +++ b/internal/service/s3/bucket_website_configuration_test.go @@ -0,0 +1,594 @@ +package s3_test + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + sdkacctest "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/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfs3 "github.com/hashicorp/terraform-provider-aws/internal/service/s3" +) + +func TestAccS3BucketWebsiteConfiguration_basic(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_s3_bucket_website_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, s3.EndpointsID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckBucketWebsiteConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBucketWebsiteConfigurationBasicConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketWebsiteConfigurationExists(resourceName), + resource.TestCheckResourceAttrPair(resourceName, "bucket", "aws_s3_bucket.test", "id"), + resource.TestCheckResourceAttr(resourceName, "index_document.#", "1"), + resource.TestCheckResourceAttr(resourceName, "index_document.0.suffix", "index.html"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccS3BucketWebsiteConfiguration_disappears(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_s3_bucket_website_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, s3.EndpointsID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckBucketWebsiteConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBucketWebsiteConfigurationBasicConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketWebsiteConfigurationExists(resourceName), + acctest.CheckResourceDisappears(acctest.Provider, tfs3.ResourceBucketWebsiteConfiguration(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccS3BucketWebsiteConfiguration_update(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_s3_bucket_website_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, s3.EndpointsID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckBucketWebsiteConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBucketWebsiteConfigurationBasicConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketWebsiteConfigurationExists(resourceName), + ), + }, + { + Config: testAccBucketWebsiteConfigurationUpdateConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketWebsiteConfigurationExists(resourceName), + resource.TestCheckResourceAttrPair(resourceName, "bucket", "aws_s3_bucket.test", "id"), + resource.TestCheckResourceAttr(resourceName, "index_document.#", "1"), + resource.TestCheckResourceAttr(resourceName, "index_document.0.suffix", "index.html"), + resource.TestCheckResourceAttr(resourceName, "error_document.#", "1"), + resource.TestCheckResourceAttr(resourceName, "error_document.0.key", "error.html"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccS3BucketWebsiteConfiguration_Redirect(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_s3_bucket_website_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, s3.EndpointsID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckBucketWebsiteConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBucketWebsiteConfigurationConfig_Redirect(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketWebsiteConfigurationExists(resourceName), + resource.TestCheckResourceAttrPair(resourceName, "bucket", "aws_s3_bucket.test", "id"), + resource.TestCheckResourceAttr(resourceName, "redirect_all_requests_to.#", "1"), + resource.TestCheckResourceAttr(resourceName, "redirect_all_requests_to.0.host_name", "example.com"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccS3BucketWebsiteConfiguration_RoutingRules_ConditionAndRedirect(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_s3_bucket_website_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, s3.EndpointsID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckBucketWebsiteConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBucketWebsiteConfigurationConfig_RoutingRules_OptionalRedirection(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketWebsiteConfigurationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "routing_rule.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "routing_rule.*", map[string]string{ + "condition.#": "1", + "condition.0.key_prefix_equals": "docs/", + "redirect.#": "1", + "redirect.0.replace_key_prefix_with": "documents/", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccBucketWebsiteConfigurationConfig_RoutingRules_RedirectErrors(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketWebsiteConfigurationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "routing_rule.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "routing_rule.*", map[string]string{ + "condition.#": "1", + "condition.0.http_error_code_returned_equals": "404", + "redirect.#": "1", + "redirect.0.replace_key_prefix_with": "report-404", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccBucketWebsiteConfigurationConfig_RoutingRules_RedirectToPage(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketWebsiteConfigurationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "routing_rule.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "routing_rule.*", map[string]string{ + "condition.#": "1", + "condition.0.key_prefix_equals": "images/", + "redirect.#": "1", + "redirect.0.replace_key_with": "errorpage.html", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccS3BucketWebsiteConfiguration_RoutingRules_MultipleRules(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_s3_bucket_website_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, s3.EndpointsID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckBucketWebsiteConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBucketWebsiteConfigurationConfig_RoutingRules_MultipleRules(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketWebsiteConfigurationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "routing_rule.#", "2"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "routing_rule.*", map[string]string{ + "condition.#": "1", + "condition.0.key_prefix_equals": "docs/", + "redirect.#": "1", + "redirect.0.replace_key_with": "errorpage.html", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "routing_rule.*", map[string]string{ + "condition.#": "1", + "condition.0.key_prefix_equals": "images/", + "redirect.#": "1", + "redirect.0.replace_key_with": "errorpage.html", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccBucketWebsiteConfigurationBasicConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketWebsiteConfigurationExists(resourceName), + ), + }, + }, + }) +} + +func TestAccS3BucketWebsiteConfiguration_RoutingRules_RedirectOnly(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_s3_bucket_website_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, s3.EndpointsID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckBucketWebsiteConfigurationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBucketWebsiteConfigurationConfig_RoutingRules_RedirectOnly(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketWebsiteConfigurationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "routing_rule.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "routing_rule.*", map[string]string{ + "redirect.#": "1", + "redirect.0.protocol": s3.ProtocolHttps, + "redirect.0.replace_key_with": "errorpage.html", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckBucketWebsiteConfigurationDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).S3Conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_s3_bucket_website_configuration" { + continue + } + + input := &s3.GetBucketWebsiteInput{ + Bucket: aws.String(rs.Primary.ID), + } + + output, err := conn.GetBucketWebsite(input) + + if tfawserr.ErrCodeEquals(err, s3.ErrCodeNoSuchBucket, tfs3.ErrCodeNoSuchWebsiteConfiguration) { + continue + } + + if err != nil { + return fmt.Errorf("error getting S3 bucket website configuration (%s): %w", rs.Primary.ID, err) + } + + if output != nil { + return fmt.Errorf("S3 bucket website configuration (%s) still exists", rs.Primary.ID) + } + } + + return nil +} + +func testAccCheckBucketWebsiteConfigurationExists(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("Resource (%s) ID not set", resourceName) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).S3Conn + + input := &s3.GetBucketWebsiteInput{ + Bucket: aws.String(rs.Primary.ID), + } + + output, err := conn.GetBucketWebsite(input) + + if err != nil { + return fmt.Errorf("error getting S3 bucket website configuration (%s): %w", rs.Primary.ID, err) + } + + if output == nil { + return fmt.Errorf("S3 Bucket website configuration (%s) not found", rs.Primary.ID) + } + + return nil + } +} + +func testAccBucketWebsiteConfigurationBasicConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = %[1]q + acl = "public-read" + + lifecycle { + ignore_changes = [ + website + ] + } +} + +resource "aws_s3_bucket_website_configuration" "test" { + bucket = aws_s3_bucket.test.id + index_document { + suffix = "index.html" + } +} +`, rName) +} + +func testAccBucketWebsiteConfigurationUpdateConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = %[1]q + acl = "public-read" + + lifecycle { + ignore_changes = [ + website + ] + } +} + +resource "aws_s3_bucket_website_configuration" "test" { + bucket = aws_s3_bucket.test.id + + index_document { + suffix = "index.html" + } + + error_document { + key = "error.html" + } +} +`, rName) +} + +func testAccBucketWebsiteConfigurationConfig_Redirect(rName string) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = %[1]q + acl = "public-read" + + lifecycle { + ignore_changes = [ + website + ] + } +} + +resource "aws_s3_bucket_website_configuration" "test" { + bucket = aws_s3_bucket.test.id + redirect_all_requests_to { + host_name = "example.com" + } +} +`, rName) +} + +func testAccBucketWebsiteConfigurationConfig_RoutingRules_OptionalRedirection(rName string) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = %[1]q + acl = "public-read" + + lifecycle { + ignore_changes = [ + website + ] + } +} + +resource "aws_s3_bucket_website_configuration" "test" { + bucket = aws_s3_bucket.test.id + + index_document { + suffix = "index.html" + } + + error_document { + key = "error.html" + } + + routing_rule { + condition { + key_prefix_equals = "docs/" + } + redirect { + replace_key_prefix_with = "documents/" + } + } +} +`, rName) +} + +func testAccBucketWebsiteConfigurationConfig_RoutingRules_RedirectErrors(rName string) string { + return acctest.ConfigCompose( + acctest.ConfigLatestAmazonLinuxHvmEbsAmi(), + fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = %[1]q + acl = "public-read" + + lifecycle { + ignore_changes = [ + website + ] + } +} + +resource "aws_s3_bucket_website_configuration" "test" { + bucket = aws_s3_bucket.test.id + + index_document { + suffix = "index.html" + } + + error_document { + key = "error.html" + } + + routing_rule { + condition { + http_error_code_returned_equals = "404" + } + redirect { + replace_key_prefix_with = "report-404" + } + } +} +`, rName)) +} + +func testAccBucketWebsiteConfigurationConfig_RoutingRules_RedirectToPage(rName string) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = %[1]q + acl = "public-read" + + lifecycle { + ignore_changes = [ + website + ] + } +} + +resource "aws_s3_bucket_website_configuration" "test" { + bucket = aws_s3_bucket.test.id + + index_document { + suffix = "index.html" + } + + error_document { + key = "error.html" + } + + routing_rule { + condition { + key_prefix_equals = "images/" + } + redirect { + replace_key_with = "errorpage.html" + } + } +} +`, rName) +} + +func testAccBucketWebsiteConfigurationConfig_RoutingRules_RedirectOnly(rName string) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = %[1]q + acl = "public-read" + + lifecycle { + ignore_changes = [ + website + ] + } +} + +resource "aws_s3_bucket_website_configuration" "test" { + bucket = aws_s3_bucket.test.id + + index_document { + suffix = "index.html" + } + + error_document { + key = "error.html" + } + + routing_rule { + redirect { + protocol = "https" + replace_key_with = "errorpage.html" + } + } +} +`, rName) +} + +func testAccBucketWebsiteConfigurationConfig_RoutingRules_MultipleRules(rName string) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = %[1]q + acl = "public-read" + + lifecycle { + ignore_changes = [ + website + ] + } +} + +resource "aws_s3_bucket_website_configuration" "test" { + bucket = aws_s3_bucket.test.id + + index_document { + suffix = "index.html" + } + + error_document { + key = "error.html" + } + + routing_rule { + condition { + key_prefix_equals = "images/" + } + redirect { + replace_key_with = "errorpage.html" + } + } + + routing_rule { + condition { + key_prefix_equals = "docs/" + } + redirect { + replace_key_with = "errorpage.html" + } + } +} +`, rName) +} diff --git a/internal/service/s3/errors.go b/internal/service/s3/errors.go index bfbbb273ba7..6c4c01390a2 100644 --- a/internal/service/s3/errors.go +++ b/internal/service/s3/errors.go @@ -6,5 +6,6 @@ package s3 const ( ErrCodeNoSuchConfiguration = "NoSuchConfiguration" ErrCodeNoSuchPublicAccessBlockConfiguration = "NoSuchPublicAccessBlockConfiguration" + ErrCodeNoSuchWebsiteConfiguration = "NoSuchWebsiteConfiguration" ErrCodeOperationAborted = "OperationAborted" ) diff --git a/website/docs/r/s3_bucket_website_configuration.html.markdown b/website/docs/r/s3_bucket_website_configuration.html.markdown new file mode 100644 index 00000000000..6636bf9c48a --- /dev/null +++ b/website/docs/r/s3_bucket_website_configuration.html.markdown @@ -0,0 +1,112 @@ +--- +subcategory: "S3" +layout: "aws" +page_title: "AWS: aws_s3_bucket_website_configuration" +description: |- + Provides an S3 bucket website configuration resource. +--- + +# Resource: aws_s3_bucket_website_configuration + +Provides an S3 bucket website configuration resource. For more information, see [Hosting Websites on S3](https://docs.aws.amazon.com/AmazonS3/latest/dev/WebsiteHosting.html). + +## Example Usage + +```terraform +resource "aws_s3_bucket_website_configuration" "example" { + bucket = aws_s3_bucket.example.bucket + + index_document { + suffix = "index.html" + } + + error_document { + key = "error.html" + } + + routing_rule { + condition { + key_prefix_equals = "docs/" + } + redirect { + replace_key_prefix_with = "documents/" + } + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `bucket` - (Required, Forces new resource) The name of the bucket. +* `error_document` - (Optional, Conflicts with `redirect_all_requests_to`) The name of the error document for the website [detailed below](#error_document). +* `expected_bucket_owner` - (Optional, Forces new resource) The account ID of the expected bucket owner. +* `index_document` - (Optional, Required if `redirect_all_requests_to` is not specified) The name of the index document for the website [detailed below](#index_document). +* `redirect_all_requests_to` - (Optional, Required if `index_document` is not specified) The redirect behavior for every request to this bucket's website endpoint [detailed below](#redirect_all_requests_to). Conflicts with `error_document`, `index_document`, and `routing_rule`. +* `routing_rule` - (Optional, Conflicts with `redirect_all_requests_to`) Set of rules that define when a redirect is applied and the redirect behavior [detailed below](#routing_rule). + +### error_document + +The `error_document` configuration block supports the following arguments: + +* `key` - (Required) The object key name to use when a 4XX class error occurs. + +### index_document + +The `index_document` configuration block supports the following arguments: + +* `suffix` - (Required) A suffix that is appended to a request that is for a directory on the website endpoint. +For example, if the suffix is `index.html` and you make a request to `samplebucket/images/`, the data that is returned will be for the object with the key name `images/index.html`. +The suffix must not be empty and must not include a slash character. + +### redirect_all_requests_to + +The `redirect_all_requests_to` configuration block supports the following arguments: + +* `host_name` - (Required) Name of the host where requests are redirected. +* `protocol` - (Optional) Protocol to use when redirecting requests. The default is the protocol that is used in the original request. Valid values: `http`, `https`. + +### routing_rule + +The `routing_rule` configuration block supports the following arguments: + +* `condition` - (Optional) A configuration block for describing a condition that must be met for the specified redirect to apply [detailed below](#condition). +* `redirect` - (Required) A configuration block for redirect information [detailed below](#redirect). + +### condition + +The `condition` configuration block supports the following arguments: + +* `http_error_code_returned_equals` - (Optional, Required if `key_prefix_equals` is not specified) The HTTP error code when the redirect is applied. If specified with `key_prefix_equals`, then both must be true for the redirect to be applied. +* `key_prefix_equals` - (Optional, Required if `http_error_code_returned_equals` is not specified) The object key name prefix when the redirect is applied. If specified with `http_error_code_returned_equals`, then both must be true for the redirect to be applied. + +### redirect + +The `redirect` configuration block supports the following arguments: + +* `host_name` - (Optional) The host name to use in the redirect request. +* `http_redirect_code` - (Optional) The HTTP redirect code to use on the response. +* `protocol` - (Optional) Protocol to use when redirecting requests. The default is the protocol that is used in the original request. Valid values: `http`, `https`. +* `replace_key_prefix_with` - (Optional, Conflicts with `replace_key_with`) The object key prefix to use in the redirect request. For example, to redirect requests for all pages with prefix `docs/` (objects in the `docs/` folder) to `documents/`, you can set a `condition` block with `key_prefix_equals` set to `docs/` and in the `redirect` set `replace_key_prefix_with` to `/documents`. +* `replace_key_with` - (Optional, Conflicts with `replace_key_prefix_with`) The specific object key to use in the redirect request. For example, redirect request to `error.html`. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The `bucket` or `bucket` and `expected_bucket_owner` separated by a comma (`,`) if the latter is provided. + +## Import + +S3 bucket website configuration can be imported using the `bucket` e.g., + +``` +$ terraform import aws_s3_bucket_website_configuration.example bucket-name +``` + +In addition, S3 bucket website configuration can be imported using the `bucket` and `expected_bucket_owner` separated by a comma (`,`) e.g., + +``` +$ terraform import aws_s3_bucket_website_configuration.example bucket-name,123456789012 +```