diff --git a/builtin/providers/aws/resource_aws_dynamodb_table.go b/builtin/providers/aws/resource_aws_dynamodb_table.go index fff6775c16..155da08f95 100644 --- a/builtin/providers/aws/resource_aws_dynamodb_table.go +++ b/builtin/providers/aws/resource_aws_dynamodb_table.go @@ -92,6 +92,23 @@ func resourceAwsDynamoDbTable() *schema.Resource { return hashcode.String(buf.String()) }, }, + "ttl": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "attribute_name": { + Type: schema.TypeString, + Required: true, + }, + "enabled": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, "local_secondary_index": { Type: schema.TypeSet, Optional: true, @@ -296,6 +313,7 @@ func resourceAwsDynamoDbTableCreate(d *schema.ResourceData, meta interface{}) er log.Printf("[DEBUG] Adding StreamSpecifications to the table") } + _, timeToLiveOk := d.GetOk("ttl") _, tagsOk := d.GetOk("tags") attemptCount := 1 @@ -326,12 +344,28 @@ func resourceAwsDynamoDbTableCreate(d *schema.ResourceData, meta interface{}) er if err := d.Set("arn", tableArn); err != nil { return err } + + // Wait, till table is active before imitating any TimeToLive changes + if err := waitForTableToBeActive(d.Id(), meta); err != nil { + log.Printf("[DEBUG] Error waiting for table to be active: %s", err) + return err + } + + log.Printf("[DEBUG] Setting DynamoDB TimeToLive on arn: %s", tableArn) + if timeToLiveOk { + if err := updateTimeToLive(d, meta); err != nil { + log.Printf("[DEBUG] Error updating table TimeToLive: %s", err) + return err + } + } + if tagsOk { log.Printf("[DEBUG] Setting DynamoDB Tags on arn: %s", tableArn) if err := createTableTags(d, meta); err != nil { return err } } + return resourceAwsDynamoDbTableRead(d, meta) } } @@ -587,6 +621,13 @@ func resourceAwsDynamoDbTableUpdate(d *schema.ResourceData, meta interface{}) er } + if d.HasChange("ttl") { + if err := updateTimeToLive(d, meta); err != nil { + log.Printf("[DEBUG] Error updating table TimeToLive: %s", err) + return err + } + } + // Update tags if err := setTagsDynamoDb(dynamodbconn, d); err != nil { return err @@ -595,6 +636,46 @@ func resourceAwsDynamoDbTableUpdate(d *schema.ResourceData, meta interface{}) er return resourceAwsDynamoDbTableRead(d, meta) } +func updateTimeToLive(d *schema.ResourceData, meta interface{}) error { + dynamodbconn := meta.(*AWSClient).dynamodbconn + + if ttl, ok := d.GetOk("ttl"); ok { + + timeToLiveSet := ttl.(*schema.Set) + + spec := &dynamodb.TimeToLiveSpecification{} + + timeToLive := timeToLiveSet.List()[0].(map[string]interface{}) + spec.AttributeName = aws.String(timeToLive["attribute_name"].(string)) + spec.Enabled = aws.Bool(timeToLive["enabled"].(bool)) + + req := &dynamodb.UpdateTimeToLiveInput{ + TableName: aws.String(d.Id()), + TimeToLiveSpecification: spec, + } + + _, err := dynamodbconn.UpdateTimeToLive(req) + + if err != nil { + // If ttl was not set within the .tf file before and has now been added we still run this command to update + // But there has been no change so lets continue + if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "ValidationException" && awsErr.Message() == "TimeToLive is already disabled" { + return nil + } + log.Printf("[DEBUG] Error updating TimeToLive on table: %s", err) + return err + } + + log.Printf("[DEBUG] Updated TimeToLive on table") + + if err := waitForTimeToLiveUpdateToBeCompleted(d.Id(), timeToLive["enabled"].(bool), meta); err != nil { + return errwrap.Wrapf("Error waiting for Dynamo DB TimeToLive to be updated: {{err}}", err) + } + } + + return nil +} + func resourceAwsDynamoDbTableRead(d *schema.ResourceData, meta interface{}) error { dynamodbconn := meta.(*AWSClient).dynamodbconn log.Printf("[DEBUG] Loading data for DynamoDB table '%s'", d.Id()) @@ -711,6 +792,23 @@ func resourceAwsDynamoDbTableRead(d *schema.ResourceData, meta interface{}) erro d.Set("arn", table.TableArn) + timeToLiveReq := &dynamodb.DescribeTimeToLiveInput{ + TableName: aws.String(d.Id()), + } + timeToLiveOutput, err := dynamodbconn.DescribeTimeToLive(timeToLiveReq) + if err != nil { + return err + } + timeToLive := []interface{}{} + attribute := map[string]*string{ + "name": timeToLiveOutput.TimeToLiveDescription.AttributeName, + "type": timeToLiveOutput.TimeToLiveDescription.TimeToLiveStatus, + } + timeToLive = append(timeToLive, attribute) + d.Set("timeToLive", timeToLive) + + log.Printf("[DEBUG] Loaded TimeToLive data for DynamoDB table '%s'", d.Id()) + tags, err := readTableTags(d, meta) if err != nil { return err @@ -910,6 +1008,39 @@ func waitForTableToBeActive(tableName string, meta interface{}) error { } +func waitForTimeToLiveUpdateToBeCompleted(tableName string, enabled bool, meta interface{}) error { + dynamodbconn := meta.(*AWSClient).dynamodbconn + req := &dynamodb.DescribeTimeToLiveInput{ + TableName: aws.String(tableName), + } + + stateMatched := false + for stateMatched == false { + result, err := dynamodbconn.DescribeTimeToLive(req) + + if err != nil { + return err + } + + if enabled { + stateMatched = *result.TimeToLiveDescription.TimeToLiveStatus == dynamodb.TimeToLiveStatusEnabled + } else { + stateMatched = *result.TimeToLiveDescription.TimeToLiveStatus == dynamodb.TimeToLiveStatusDisabled + } + + // Wait for a few seconds, this may take a long time... + if !stateMatched { + log.Printf("[DEBUG] Sleeping for 5 seconds before checking TimeToLive state again") + time.Sleep(5 * time.Second) + } + } + + log.Printf("[DEBUG] TimeToLive update complete") + + return nil + +} + func createTableTags(d *schema.ResourceData, meta interface{}) error { // DynamoDB Table has to be in the ACTIVE state in order to tag the resource if err := waitForTableToBeActive(d.Id(), meta); err != nil { diff --git a/builtin/providers/aws/resource_aws_dynamodb_table_test.go b/builtin/providers/aws/resource_aws_dynamodb_table_test.go index fe2ce175f9..59cebc4a1b 100644 --- a/builtin/providers/aws/resource_aws_dynamodb_table_test.go +++ b/builtin/providers/aws/resource_aws_dynamodb_table_test.go @@ -110,6 +110,71 @@ func TestAccAWSDynamoDbTable_gsiUpdate(t *testing.T) { }) } +func TestAccAWSDynamoDbTable_ttl(t *testing.T) { + var conf dynamodb.DescribeTableOutput + + rName := acctest.RandomWithPrefix("TerraformTestTable-") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSDynamoDbTableDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSDynamoDbConfigInitialState(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckInitialAWSDynamoDbTableExists("aws_dynamodb_table.basic-dynamodb-table", &conf), + ), + }, + { + Config: testAccAWSDynamoDbConfigAddTimeToLive(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckDynamoDbTableTimeToLiveWasUpdated("aws_dynamodb_table.basic-dynamodb-table"), + ), + }, + }, + }) +} +func testAccCheckDynamoDbTableTimeToLiveWasUpdated(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + log.Printf("[DEBUG] Trying to create initial table state!") + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No DynamoDB table name specified!") + } + + conn := testAccProvider.Meta().(*AWSClient).dynamodbconn + + params := &dynamodb.DescribeTimeToLiveInput{ + TableName: aws.String(rs.Primary.ID), + } + + resp, err := conn.DescribeTimeToLive(params) + + if err != nil { + return fmt.Errorf("[ERROR] Problem describing time to live for table '%s': %s", rs.Primary.ID, err) + } + + ttlDescription := resp.TimeToLiveDescription + + log.Printf("[DEBUG] Checking on table %s", rs.Primary.ID) + + if *ttlDescription.TimeToLiveStatus != dynamodb.TimeToLiveStatusEnabled { + return fmt.Errorf("TimeToLiveStatus %s, not ENABLED!", *ttlDescription.TimeToLiveStatus) + } + + if *ttlDescription.AttributeName != "TestTTL" { + return fmt.Errorf("AttributeName was %s, not TestTTL!", *ttlDescription.AttributeName) + } + + return nil + } +} + func TestResourceAWSDynamoDbTableStreamViewType_validation(t *testing.T) { cases := []struct { Value string @@ -678,3 +743,55 @@ resource "aws_dynamodb_table" "test" { } `, name) } + +func testAccAWSDynamoDbConfigAddTimeToLive(rName string) string { + return fmt.Sprintf(` +resource "aws_dynamodb_table" "basic-dynamodb-table" { + name = "%s" + read_capacity = 10 + write_capacity = 20 + hash_key = "TestTableHashKey" + range_key = "TestTableRangeKey" + + attribute { + name = "TestTableHashKey" + type = "S" + } + + attribute { + name = "TestTableRangeKey" + type = "S" + } + + attribute { + name = "TestLSIRangeKey" + type = "N" + } + + attribute { + name = "TestGSIRangeKey" + type = "S" + } + + local_secondary_index { + name = "TestTableLSI" + range_key = "TestLSIRangeKey" + projection_type = "ALL" + } + + ttl { + attribute_name = "TestTTL" + enabled = true + } + + global_secondary_index { + name = "InitialTestTableGSI" + hash_key = "TestTableHashKey" + range_key = "TestGSIRangeKey" + write_capacity = 10 + read_capacity = 10 + projection_type = "KEYS_ONLY" + } +} +`, rName) +} diff --git a/website/source/docs/providers/aws/r/dynamodb_table.html.markdown b/website/source/docs/providers/aws/r/dynamodb_table.html.markdown index b3cd64cf40..3a07f70b80 100644 --- a/website/source/docs/providers/aws/r/dynamodb_table.html.markdown +++ b/website/source/docs/providers/aws/r/dynamodb_table.html.markdown @@ -38,6 +38,11 @@ resource "aws_dynamodb_table" "basic-dynamodb-table" { type = "N" } + ttl { + attribute_name = "TimeToExist" + enabled = false + } + global_secondary_index { name = "GameTitleIndex" hash_key = "GameTitle" @@ -72,6 +77,7 @@ The following arguments are supported: * `type` - One of: S, N, or B for (S)tring, (N)umber or (B)inary data * `stream_enabled` - (Optional) Indicates whether Streams are to be enabled (true) or disabled (false). * `stream_view_type` - (Optional) When an item in the table is modified, StreamViewType determines what information is written to the table's stream. Valid values are KEYS_ONLY, NEW_IMAGE, OLD_IMAGE, NEW_AND_OLD_IMAGES. +* `ttl` - (Optional) Indicates whether time to live is enabled (true) or disabled (false) and the `attribute_name` to be used. * `local_secondary_index` - (Optional, Forces new resource) Describe an LSI on the table; these can only be allocated *at creation* so you cannot change this definition after you have created the resource.