diff --git a/builtin/providers/aws/resource_aws_s3_bucket.go b/builtin/providers/aws/resource_aws_s3_bucket.go index 0e07ee733..a329d4ff6 100644 --- a/builtin/providers/aws/resource_aws_s3_bucket.go +++ b/builtin/providers/aws/resource_aws_s3_bucket.go @@ -1,6 +1,7 @@ package aws import ( + "bytes" "encoding/json" "fmt" "log" @@ -10,6 +11,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/s3" + "github.com/hashicorp/terraform/helper/hashcode" ) func resourceAwsS3Bucket() *schema.Resource { @@ -88,6 +90,27 @@ func resourceAwsS3Bucket() *schema.Resource { Computed: true, }, + "versioning": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + }, + }, + Set: func(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%t-", m["enabled"].(bool))) + + return hashcode.String(buf.String()) + }, + }, + "tags": tagsSchema(), "force_destroy": &schema.Schema{ @@ -151,6 +174,12 @@ func resourceAwsS3BucketUpdate(d *schema.ResourceData, meta interface{}) error { } } + if d.HasChange("versioning") { + if err := resourceAwsS3BucketVersioningUpdate(s3conn, d); err != nil { + return err + } + } + return resourceAwsS3BucketRead(d, meta) } @@ -218,6 +247,28 @@ func resourceAwsS3BucketRead(d *schema.ResourceData, meta interface{}) error { return err } + // Read the versioning configuration + versioning, err := s3conn.GetBucketVersioning(&s3.GetBucketVersioningInput{ + Bucket: aws.String(d.Id()), + }) + if err != nil { + return err + } + log.Printf("[DEBUG] S3 Bucket: %s, versioning: %v", d.Id(), versioning) + if versioning.Status != nil && *versioning.Status == s3.BucketVersioningStatusEnabled { + vcl := make([]map[string]interface{}, 0, 1) + vc := make(map[string]interface{}) + if *versioning.Status == s3.BucketVersioningStatusEnabled { + vc["enabled"] = true + } else { + vc["enabled"] = false + } + vcl = append(vcl, vc) + if err := d.Set("versioning", vcl); err != nil { + return err + } + } + // Add the region as an attribute location, err := s3conn.GetBucketLocation( &s3.GetBucketLocationInput{ @@ -459,6 +510,37 @@ func WebsiteDomainUrl(region string) string { return fmt.Sprintf("s3-website-%s.amazonaws.com", region) } +func resourceAwsS3BucketVersioningUpdate(s3conn *s3.S3, d *schema.ResourceData) error { + v := d.Get("versioning").(*schema.Set).List() + bucket := d.Get("bucket").(string) + vc := &s3.VersioningConfiguration{} + + if len(v) > 0 { + c := v[0].(map[string]interface{}) + + if c["enabled"].(bool) { + vc.Status = aws.String(s3.BucketVersioningStatusEnabled) + } else { + vc.Status = aws.String(s3.BucketVersioningStatusSuspended) + } + } else { + vc.Status = aws.String(s3.BucketVersioningStatusSuspended) + } + + i := &s3.PutBucketVersioningInput{ + Bucket: aws.String(bucket), + VersioningConfiguration: vc, + } + log.Printf("[DEBUG] S3 put bucket versioning: %#v", i) + + _, err := s3conn.PutBucketVersioning(i) + if err != nil { + return fmt.Errorf("Error putting S3 versioning: %s", err) + } + + return nil +} + func normalizeJson(jsonString interface{}) string { if jsonString == nil { return "" diff --git a/builtin/providers/aws/resource_aws_s3_bucket_test.go b/builtin/providers/aws/resource_aws_s3_bucket_test.go index 69b061986..e494816b3 100644 --- a/builtin/providers/aws/resource_aws_s3_bucket_test.go +++ b/builtin/providers/aws/resource_aws_s3_bucket_test.go @@ -154,6 +154,40 @@ func TestAccAWSS3Bucket_shouldFailNotFound(t *testing.T) { }) } +func TestAccAWSS3Bucket_Versioning(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"), + testAccCheckAWSS3BucketVersioning( + "aws_s3_bucket.bucket", ""), + ), + }, + resource.TestStep{ + Config: testAccAWSS3BucketConfigWithVersioning, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3BucketExists("aws_s3_bucket.bucket"), + testAccCheckAWSS3BucketVersioning( + "aws_s3_bucket.bucket", s3.BucketVersioningStatusEnabled), + ), + }, + resource.TestStep{ + Config: testAccAWSS3BucketConfigWithDisableVersioning, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3BucketExists("aws_s3_bucket.bucket"), + testAccCheckAWSS3BucketVersioning( + "aws_s3_bucket.bucket", s3.BucketVersioningStatusSuspended), + ), + }, + }, + }) +} + func testAccCheckAWSS3BucketDestroy(s *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).s3conn @@ -310,6 +344,33 @@ func testAccCheckAWSS3BucketWebsite(n string, indexDoc string, errorDoc string, } } +func testAccCheckAWSS3BucketVersioning(n string, versioningStatus string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, _ := s.RootModule().Resources[n] + conn := testAccProvider.Meta().(*AWSClient).s3conn + + out, err := conn.GetBucketVersioning(&s3.GetBucketVersioningInput{ + Bucket: aws.String(rs.Primary.ID), + }) + + if err != nil { + return fmt.Errorf("GetBucketVersioning error: %v", err) + } + + if v := out.Status; v == nil { + if versioningStatus != "" { + return fmt.Errorf("bad error versioning status, found nil, expected: %s", versioningStatus) + } + } else { + if *v != versioningStatus { + return fmt.Errorf("bad error versioning status, expected: %s, got %s", versioningStatus, *v) + } + } + + 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() @@ -372,3 +433,22 @@ resource "aws_s3_bucket" "bucket" { acl = "public-read" } `, destroyedName) +var testAccAWSS3BucketConfigWithVersioning = fmt.Sprintf(` +resource "aws_s3_bucket" "bucket" { + bucket = "tf-test-bucket-%d" + acl = "public-read" + versioning { + enabled = true + } +} +`, randInt) + +var testAccAWSS3BucketConfigWithDisableVersioning = fmt.Sprintf(` +resource "aws_s3_bucket" "bucket" { + bucket = "tf-test-bucket-%d" + acl = "public-read" + versioning { + enabled = false + } +} +`, 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 f30c153c5..011f73347 100644 --- a/website/source/docs/providers/aws/r/s3_bucket.html.markdown +++ b/website/source/docs/providers/aws/r/s3_bucket.html.markdown @@ -41,6 +41,18 @@ resource "aws_s3_bucket" "b" { } ``` +### Using versioning + +``` +resource "aws_s3_bucket" "b" { + bucket = "my_tf_test_bucket" + acl = "private" + versioning { + enabled = true + } +} +``` + ## Argument Reference The following arguments are supported: @@ -52,6 +64,7 @@ The following arguments are supported: * `tags` - (Optional) A mapping of tags to assign to the bucket. * `force_destroy` - (Optional, Default:false ) A boolean that indicates all objects should be deleted from the bucket so that the bucket can be destroyed without error. These objects are *not* recoverable. * `website` - (Optional) A website object (documented below). +* `versioning` - (Optional) A state of [versioning](http://docs.aws.amazon.com/AmazonS3/latest/dev/Versioning.html) (documented below) The website object supports the following: @@ -59,6 +72,10 @@ The website object supports the following: * `error_document` - (Optional) An absolute path to the document to return in case of a 4XX error. * `redirect_all_requests_to` - (Optional) A hostname to redirect all website requests for this bucket to. +The versioning supports the following: + +* `enabled` - (Optional) Enable versioning. Once you version-enable a bucket, it can never return to an unversioned state. You can, however, suspend versioning on that bucket. + ## Attributes Reference The following attributes are exported: