From 27d3d2a871fb20545a79bbc608f8b21c765fc7f6 Mon Sep 17 00:00:00 2001 From: Rhys Laval Date: Sun, 30 Apr 2017 23:20:38 +0100 Subject: [PATCH 1/3] provider/aws: aws_dynamodb_table Add support for TimeToLive --- .../aws/resource_aws_dynamodb_table.go | 128 ++++++++++++++++++ .../aws/resource_aws_dynamodb_table_test.go | 117 ++++++++++++++++ .../aws/r/dynamodb_table.html.markdown | 6 + 3 files changed, 251 insertions(+) diff --git a/builtin/providers/aws/resource_aws_dynamodb_table.go b/builtin/providers/aws/resource_aws_dynamodb_table.go index fff6775c1..0bf434562 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,25 @@ 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 + waitForTableToBeActive(d.Id(), meta) + + 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 +618,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 +633,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 +789,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 +1005,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 fe2ce175f..33e5d7410 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 b3cd64cf4..3a07f70b8 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. From 8230a5ded87ff3ba1d93a713813a08c148319cb4 Mon Sep 17 00:00:00 2001 From: Rhys Laval Date: Sun, 30 Apr 2017 23:34:19 +0100 Subject: [PATCH 2/3] provider/aws: aws_dynamodb_table Add support for TimeToLive. Fix errcheck --- builtin/providers/aws/resource_aws_dynamodb_table.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/builtin/providers/aws/resource_aws_dynamodb_table.go b/builtin/providers/aws/resource_aws_dynamodb_table.go index 0bf434562..155da08f9 100644 --- a/builtin/providers/aws/resource_aws_dynamodb_table.go +++ b/builtin/providers/aws/resource_aws_dynamodb_table.go @@ -346,7 +346,10 @@ func resourceAwsDynamoDbTableCreate(d *schema.ResourceData, meta interface{}) er } // Wait, till table is active before imitating any TimeToLive changes - waitForTableToBeActive(d.Id(), meta) + 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 { From 64b5ff54ebf867d79f12ae78c3d515d5ef6d6943 Mon Sep 17 00:00:00 2001 From: Rhys Laval Date: Mon, 1 May 2017 19:56:21 +0100 Subject: [PATCH 3/3] Fix vet issue --- builtin/providers/aws/resource_aws_dynamodb_table_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/builtin/providers/aws/resource_aws_dynamodb_table_test.go b/builtin/providers/aws/resource_aws_dynamodb_table_test.go index 33e5d7410..59cebc4a1 100644 --- a/builtin/providers/aws/resource_aws_dynamodb_table_test.go +++ b/builtin/providers/aws/resource_aws_dynamodb_table_test.go @@ -164,11 +164,11 @@ func testAccCheckDynamoDbTableTimeToLiveWasUpdated(n string) resource.TestCheckF log.Printf("[DEBUG] Checking on table %s", rs.Primary.ID) if *ttlDescription.TimeToLiveStatus != dynamodb.TimeToLiveStatusEnabled { - return fmt.Errorf("TimeToLiveStatus %s, not ENABLED!", ttlDescription.TimeToLiveStatus) + return fmt.Errorf("TimeToLiveStatus %s, not ENABLED!", *ttlDescription.TimeToLiveStatus) } if *ttlDescription.AttributeName != "TestTTL" { - return fmt.Errorf("AttributeName was %s, not TestTTL!", ttlDescription.AttributeName) + return fmt.Errorf("AttributeName was %s, not TestTTL!", *ttlDescription.AttributeName) } return nil