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

providers/aws: S3 bucket website support #1738

Merged
merged 13 commits into from
May 7, 2015
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
152 changes: 152 additions & 0 deletions builtin/providers/aws/resource_aws_s3_bucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
},
}
Expand Down Expand Up @@ -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)
}

Expand All @@ -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
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to set website_endpoint to "" if you disable website support on an existing bucket. How do I trigger other resources that depend on that attribute to be updated?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd actually just add an acceptance test that includes the disable case to test this. So it'd be a test with one step that has website support and the next step that removes it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in bb63f5c


tagSet, err := getTagSetS3(s3conn, d.Id())
if err != nil {
return err
Expand All @@ -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 == "" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently S3 will return an empty string for Classic ಠ_ಠ

empty string (for the US Classic region)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added that link to the comment

region = "us-east-1"
}

endpoint := fmt.Sprintf("%s.s3-website-%s.amazonaws.com", bucket, region)

return endpoint, nil
}
113 changes: 108 additions & 5 deletions builtin/providers/aws/resource_aws_s3_bucket_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,57 @@ import (
)

func TestAccAWSS3Bucket(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSS3BucketDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSS3BucketConfig,
Check: resource.ComposeTestCheckFunc(
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.bar"),
testAccCheckAWSS3BucketExists("aws_s3_bucket.bucket"),
testAccCheckAWSS3BucketWebsite(
"aws_s3_bucket.bucket", "", ""),
resource.TestCheckResourceAttr(
"aws_s3_bucket.bucket", "website_endpoint", ""),
),
},
},
Expand Down Expand Up @@ -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)
26 changes: 24 additions & 2 deletions website/source/docs/providers/aws/r/s3_bucket.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -24,17 +26,37 @@ 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:

* `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.