diff --git a/builtin/providers/aws/resource_aws_s3_bucket.go b/builtin/providers/aws/resource_aws_s3_bucket.go index 712bdb5f2..fa802d6cf 100644 --- a/builtin/providers/aws/resource_aws_s3_bucket.go +++ b/builtin/providers/aws/resource_aws_s3_bucket.go @@ -31,6 +31,30 @@ func resourceAwsS3Bucket() *schema.Resource { ForceNew: true, }, + "website": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "index_document": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "error_document": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + + "website_endpoint": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "tags": tagsSchema(), }, } @@ -75,6 +99,11 @@ func resourceAwsS3BucketUpdate(d *schema.ResourceData, meta interface{}) error { if err := setTagsS3(s3conn, d); err != nil { return err } + + if err := resourceAwsS3BucketWebsiteUpdate(s3conn, d); err != nil { + return err + } + return resourceAwsS3BucketRead(d, meta) } @@ -94,6 +123,35 @@ func resourceAwsS3BucketRead(d *schema.ResourceData, meta interface{}) error { } } + // Read the website configuration + ws, err := s3conn.GetBucketWebsite(&s3.GetBucketWebsiteInput{ + Bucket: aws.String(d.Id()), + }) + var websites []map[string]interface{} + if err == nil { + w := make(map[string]interface{}) + + w["index_document"] = *ws.IndexDocument.Suffix + + if v := ws.ErrorDocument; v != nil { + w["error_document"] = *v.Key + } + + websites = append(websites, w) + } + if err := d.Set("website", websites); err != nil { + return err + } + + // Add website_endpoint as an output + endpoint, err := websiteEndpoint(s3conn, d) + if err != nil { + return err + } + if err := d.Set("website_endpoint", endpoint); err != nil { + return err + } + tagSet, err := getTagSetS3(s3conn, d.Id()) if err != nil { return err @@ -118,3 +176,97 @@ func resourceAwsS3BucketDelete(d *schema.ResourceData, meta interface{}) error { } return nil } + +func resourceAwsS3BucketWebsiteUpdate(s3conn *s3.S3, d *schema.ResourceData) error { + if !d.HasChange("website") { + return nil + } + + ws := d.Get("website").([]interface{}) + + if len(ws) == 1 { + w := ws[0].(map[string]interface{}) + return resourceAwsS3BucketWebsitePut(s3conn, d, w) + } else if len(ws) == 0 { + return resourceAwsS3BucketWebsiteDelete(s3conn, d) + } else { + return fmt.Errorf("Cannot specify more than one website.") + } +} + +func resourceAwsS3BucketWebsitePut(s3conn *s3.S3, d *schema.ResourceData, website map[string]interface{}) error { + bucket := d.Get("bucket").(string) + + indexDocument := website["index_document"].(string) + errorDocument := website["error_document"].(string) + + websiteConfiguration := &s3.WebsiteConfiguration{} + + websiteConfiguration.IndexDocument = &s3.IndexDocument{Suffix: aws.String(indexDocument)} + + if errorDocument != "" { + websiteConfiguration.ErrorDocument = &s3.ErrorDocument{Key: aws.String(errorDocument)} + } + + putInput := &s3.PutBucketWebsiteInput{ + Bucket: aws.String(bucket), + WebsiteConfiguration: websiteConfiguration, + } + + log.Printf("[DEBUG] S3 put bucket website: %s", putInput) + + _, err := s3conn.PutBucketWebsite(putInput) + if err != nil { + return fmt.Errorf("Error putting S3 website: %s", err) + } + + return nil +} + +func resourceAwsS3BucketWebsiteDelete(s3conn *s3.S3, d *schema.ResourceData) error { + bucket := d.Get("bucket").(string) + deleteInput := &s3.DeleteBucketWebsiteInput{Bucket: aws.String(bucket)} + + log.Printf("[DEBUG] S3 delete bucket website: %s", deleteInput) + + _, err := s3conn.DeleteBucketWebsite(deleteInput) + if err != nil { + return fmt.Errorf("Error deleting S3 website: %s", err) + } + + return nil +} + +func websiteEndpoint(s3conn *s3.S3, d *schema.ResourceData) (string, error) { + // If the bucket doesn't have a website configuration, return an empty + // endpoint + if _, ok := d.GetOk("website"); !ok { + return "", nil + } + + bucket := d.Get("bucket").(string) + + // Lookup the region for this bucket + location, err := s3conn.GetBucketLocation( + &s3.GetBucketLocationInput{ + Bucket: aws.String(bucket), + }, + ) + if err != nil { + return "", err + } + var region string + if location.LocationConstraint != nil { + region = *location.LocationConstraint + } + + // Default to us-east-1 if the bucket doesn't have a region: + // http://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketGETlocation.html + if region == "" { + region = "us-east-1" + } + + endpoint := fmt.Sprintf("%s.s3-website-%s.amazonaws.com", bucket, region) + + return endpoint, nil +} diff --git a/builtin/providers/aws/resource_aws_s3_bucket_test.go b/builtin/providers/aws/resource_aws_s3_bucket_test.go index 4e396801d..419c530cf 100644 --- a/builtin/providers/aws/resource_aws_s3_bucket_test.go +++ b/builtin/providers/aws/resource_aws_s3_bucket_test.go @@ -14,7 +14,6 @@ import ( ) func TestAccAWSS3Bucket(t *testing.T) { - resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, @@ -23,7 +22,49 @@ func TestAccAWSS3Bucket(t *testing.T) { resource.TestStep{ Config: testAccAWSS3BucketConfig, Check: resource.ComposeTestCheckFunc( - testAccCheckAWSS3BucketExists("aws_s3_bucket.bar"), + testAccCheckAWSS3BucketExists("aws_s3_bucket.bucket"), + resource.TestCheckResourceAttr( + "aws_s3_bucket.bucket", "website_endpoint", ""), + ), + }, + }, + }) +} + +func TestAccAWSS3BucketWebsite(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSS3BucketDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSS3BucketWebsiteConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3BucketExists("aws_s3_bucket.bucket"), + testAccCheckAWSS3BucketWebsite( + "aws_s3_bucket.bucket", "index.html", ""), + resource.TestCheckResourceAttr( + "aws_s3_bucket.bucket", "website_endpoint", testAccWebsiteEndpoint), + ), + }, + resource.TestStep{ + Config: testAccAWSS3BucketWebsiteConfigWithError, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3BucketExists("aws_s3_bucket.bucket"), + testAccCheckAWSS3BucketWebsite( + "aws_s3_bucket.bucket", "index.html", "error.html"), + resource.TestCheckResourceAttr( + "aws_s3_bucket.bucket", "website_endpoint", testAccWebsiteEndpoint), + ), + }, + resource.TestStep{ + Config: testAccAWSS3BucketConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3BucketExists("aws_s3_bucket.bucket"), + testAccCheckAWSS3BucketWebsite( + "aws_s3_bucket.bucket", "", ""), + resource.TestCheckResourceAttr( + "aws_s3_bucket.bucket", "website_endpoint", ""), ), }, }, @@ -70,11 +111,73 @@ func testAccCheckAWSS3BucketExists(n string) resource.TestCheckFunc { } } -// This needs a bit of randoness as the name can only be -// used once globally within AWS +func testAccCheckAWSS3BucketWebsite(n string, indexDoc string, errorDoc string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, _ := s.RootModule().Resources[n] + conn := testAccProvider.Meta().(*AWSClient).s3conn + + out, err := conn.GetBucketWebsite(&s3.GetBucketWebsiteInput{ + Bucket: aws.String(rs.Primary.ID), + }) + + if err != nil { + if indexDoc == "" { + // If we want to assert that the website is not there, than + // this error is expected + return nil + } else { + return fmt.Errorf("S3BucketWebsite error: %v", err) + } + } + + if *out.IndexDocument.Suffix != indexDoc { + return fmt.Errorf("bad: %s", out.IndexDocument) + } + + if v := out.ErrorDocument; v == nil { + if errorDoc != "" { + return fmt.Errorf("bad: %s", out.ErrorDocument) + } + } else { + if *v.Key != errorDoc { + return fmt.Errorf("bad: %s", out.ErrorDocument) + } + } + + return nil + } +} + +// These need a bit of randomness as the name can only be used once globally +// within AWS +var randInt = rand.New(rand.NewSource(time.Now().UnixNano())).Int() +var testAccWebsiteEndpoint = fmt.Sprintf("tf-test-bucket-%d.s3-website-us-east-1.amazonaws.com", randInt) var testAccAWSS3BucketConfig = fmt.Sprintf(` -resource "aws_s3_bucket" "bar" { +resource "aws_s3_bucket" "bucket" { bucket = "tf-test-bucket-%d" acl = "public-read" } -`, rand.New(rand.NewSource(time.Now().UnixNano())).Int()) +`, randInt) + +var testAccAWSS3BucketWebsiteConfig = fmt.Sprintf(` +resource "aws_s3_bucket" "bucket" { + bucket = "tf-test-bucket-%d" + acl = "public-read" + + website { + index_document = "index.html" + } +} +`, randInt) + +var testAccAWSS3BucketWebsiteConfigWithError = fmt.Sprintf(` +resource "aws_s3_bucket" "bucket" { + bucket = "tf-test-bucket-%d" + acl = "public-read" + + website { + index_document = "index.html" + error_document = "error.html" + } +} +`, randInt) diff --git a/website/source/docs/providers/aws/r/s3_bucket.html.markdown b/website/source/docs/providers/aws/r/s3_bucket.html.markdown index 49cee01ec..821918804 100644 --- a/website/source/docs/providers/aws/r/s3_bucket.html.markdown +++ b/website/source/docs/providers/aws/r/s3_bucket.html.markdown @@ -12,6 +12,8 @@ Provides a S3 bucket resource. ## Example Usage +### Private Bucket w/ Tags + ``` resource "aws_s3_bucket" "b" { bucket = "my_tf_test_bucket" @@ -24,6 +26,20 @@ resource "aws_s3_bucket" "b" { } ``` +### Static Website Hosting + +``` +resource "aws_s3_bucket" "b" { + bucket = "s3-website-test.hashicorp.com" + acl = "public-read" + + website { + index_document = "index.html" + error_document = "error.html" + } +} +``` + ## Argument Reference The following arguments are supported: @@ -31,10 +47,16 @@ The following arguments are supported: * `bucket` - (Required) The name of the bucket. * `acl` - (Optional) The [canned ACL](http://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl) to apply. Defaults to "private". * `tags` - (Optional) A mapping of tags to assign to the bucket. +* `website` - (Optional) A website object (documented below). + +The website object supports the following: + +* `index_document` - (Required) Amazon S3 returns this index document when requests are made to the root domain or any of the subfolders. +* `error_document` - (Optional) An absolute path to the document to return in case of a 4XX error. ## Attributes Reference The following attributes are exported: -* `id` - The name of the bucket - +* `id` - The name of the bucket. +* `website_endpoint` - The website endpoint, if the bucket is configured with a website. If not, this will be an empty string.