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

provider/aws: aws_dynamodb_table Add support for TimeToLive #14104

Merged
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
131 changes: 131 additions & 0 deletions builtin/providers/aws/resource_aws_dynamodb_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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
Expand All @@ -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())
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
117 changes: 117 additions & 0 deletions builtin/providers/aws/resource_aws_dynamodb_table_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ resource "aws_dynamodb_table" "basic-dynamodb-table" {
type = "N"
}

ttl {
attribute_name = "TimeToExist"
enabled = false
}

Choose a reason for hiding this comment

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

I was confused by the example attribute name and the way Amazon calls this a Time To Live. In fact the data stored here should be a unix timestamp of when the record expires, not an amount of time (seconds? ms?) for which the data should "live" or "exist". This might be more clear if the attribute_name was something along the lines of "ExpirationTimestamp".


global_secondary_index {
name = "GameTitleIndex"
hash_key = "GameTitle"
Expand Down Expand Up @@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's more typical for the docs to more explicitly list the sub-attributes, eg in a nested list?

Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed, opened #14492 to address.

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