diff --git a/.changelog/18373.txt b/.changelog/18373.txt new file mode 100644 index 00000000000..499d30ce431 --- /dev/null +++ b/.changelog/18373.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_dynamodb_table: Add `kms_key_arn` argument to `replica` configuration block +``` diff --git a/aws/data_source_aws_dynamodb_table.go b/aws/data_source_aws_dynamodb_table.go index 60dae0aaac6..e4318a7cc8e 100644 --- a/aws/data_source_aws_dynamodb_table.go +++ b/aws/data_source_aws_dynamodb_table.go @@ -226,7 +226,7 @@ func dataSourceAwsDynamoDbTableRead(d *schema.ResourceData, meta interface{}) er d.SetId(aws.StringValue(result.Table.TableName)) - err = flattenAwsDynamoDbTableResource(d, result.Table) + err = flattenDynamoDbTableResource(d, result.Table) if err != nil { return err } diff --git a/aws/internal/service/dynamodb/finder/finder.go b/aws/internal/service/dynamodb/finder/finder.go new file mode 100644 index 00000000000..d48b99eecb3 --- /dev/null +++ b/aws/internal/service/dynamodb/finder/finder.go @@ -0,0 +1,88 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/dynamodb" +) + +func DynamoDBTableByName(conn *dynamodb.DynamoDB, tableName string) (*dynamodb.TableDescription, error) { + input := &dynamodb.DescribeTableInput{ + TableName: aws.String(tableName), + } + + output, err := conn.DescribeTable(input) + + if err != nil { + return nil, err + } + + if output == nil || output.Table == nil { + return nil, nil + } + + return output.Table, nil +} + +func DynamoDBGSIByTableNameIndexName(conn *dynamodb.DynamoDB, tableName, indexName string) (*dynamodb.GlobalSecondaryIndexDescription, error) { + table, err := DynamoDBTableByName(conn, tableName) + + if err != nil { + return nil, err + } + + if table == nil { + return nil, nil + } + + for _, gsi := range table.GlobalSecondaryIndexes { + if aws.StringValue(gsi.IndexName) == indexName { + return gsi, nil + } + } + + return nil, nil +} + +func DynamoDBPITRDescriptionByTableName(conn *dynamodb.DynamoDB, tableName string) (*dynamodb.PointInTimeRecoveryDescription, error) { + input := &dynamodb.DescribeContinuousBackupsInput{ + TableName: aws.String(tableName), + } + + output, err := conn.DescribeContinuousBackups(input) + + if err != nil { + return nil, err + } + + if output == nil { + return nil, nil + } + + if output.ContinuousBackupsDescription == nil || output.ContinuousBackupsDescription.PointInTimeRecoveryDescription == nil { + return nil, nil + } + + return output.ContinuousBackupsDescription.PointInTimeRecoveryDescription, nil +} + +func DynamoDBTTLRDescriptionByTableName(conn *dynamodb.DynamoDB, tableName string) (*dynamodb.TimeToLiveDescription, error) { + input := &dynamodb.DescribeTimeToLiveInput{ + TableName: aws.String(tableName), + } + + output, err := conn.DescribeTimeToLive(input) + + if err != nil { + return nil, err + } + + if output == nil { + return nil, nil + } + + if output.TimeToLiveDescription == nil { + return nil, nil + } + + return output.TimeToLiveDescription, nil +} diff --git a/aws/internal/service/dynamodb/waiter/status.go b/aws/internal/service/dynamodb/waiter/status.go new file mode 100644 index 00000000000..1b7b379631b --- /dev/null +++ b/aws/internal/service/dynamodb/waiter/status.go @@ -0,0 +1,170 @@ +package waiter + +import ( + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/dynamodb/finder" +) + +func DynamoDBTableStatus(conn *dynamodb.DynamoDB, tableName string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + table, err := finder.DynamoDBTableByName(conn, tableName) + + if tfawserr.ErrCodeEquals(err, dynamodb.ErrCodeResourceNotFoundException) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + if table == nil { + return nil, "", nil + } + + return table, aws.StringValue(table.TableStatus), nil + } +} + +func DynamoDBReplicaUpdate(conn *dynamodb.DynamoDB, tableName, region string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + result, err := conn.DescribeTable(&dynamodb.DescribeTableInput{ + TableName: aws.String(tableName), + }) + if err != nil { + return 42, "", err + } + log.Printf("[DEBUG] DynamoDB replicas: %s", result.Table.Replicas) + + var targetReplica *dynamodb.ReplicaDescription + + for _, replica := range result.Table.Replicas { + if aws.StringValue(replica.RegionName) == region { + targetReplica = replica + break + } + } + + if targetReplica == nil { + return result, dynamodb.ReplicaStatusCreating, nil + } + + return result, aws.StringValue(targetReplica.ReplicaStatus), nil + } +} + +func DynamoDBReplicaDelete(conn *dynamodb.DynamoDB, tableName, region string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + result, err := conn.DescribeTable(&dynamodb.DescribeTableInput{ + TableName: aws.String(tableName), + }) + if err != nil { + return 42, "", err + } + + log.Printf("[DEBUG] all replicas for waiting: %s", result.Table.Replicas) + var targetReplica *dynamodb.ReplicaDescription + + for _, replica := range result.Table.Replicas { + if aws.StringValue(replica.RegionName) == region { + targetReplica = replica + break + } + } + + if targetReplica == nil { + return result, "", nil + } + + return result, aws.StringValue(targetReplica.ReplicaStatus), nil + } +} + +func DynamoDBGSIStatus(conn *dynamodb.DynamoDB, tableName, indexName string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + gsi, err := finder.DynamoDBGSIByTableNameIndexName(conn, tableName, indexName) + + if tfawserr.ErrCodeEquals(err, dynamodb.ErrCodeResourceNotFoundException) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + if gsi == nil { + return nil, "", nil + } + + return gsi, aws.StringValue(gsi.IndexStatus), nil + } +} + +func DynamoDBPITRStatus(conn *dynamodb.DynamoDB, tableName string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + pitr, err := finder.DynamoDBPITRDescriptionByTableName(conn, tableName) + + if tfawserr.ErrCodeEquals(err, dynamodb.ErrCodeResourceNotFoundException) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + if pitr == nil { + return nil, "", nil + } + + return pitr, aws.StringValue(pitr.PointInTimeRecoveryStatus), nil + } +} + +func DynamoDBTTLStatus(conn *dynamodb.DynamoDB, tableName string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + ttl, err := finder.DynamoDBTTLRDescriptionByTableName(conn, tableName) + + if tfawserr.ErrCodeEquals(err, dynamodb.ErrCodeResourceNotFoundException) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + if ttl == nil { + return nil, "", nil + } + + return ttl, aws.StringValue(ttl.TimeToLiveStatus), nil + } +} + +func DynamoDBTableSESStatus(conn *dynamodb.DynamoDB, tableName string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + table, err := finder.DynamoDBTableByName(conn, tableName) + + if tfawserr.ErrCodeEquals(err, dynamodb.ErrCodeResourceNotFoundException) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + if table == nil { + return nil, "", nil + } + + // Disabling SSE returns null SSEDescription + if table.SSEDescription == nil { + return table, dynamodb.SSEStatusDisabled, nil + } + + return table, aws.StringValue(table.SSEDescription.Status), nil + } +} diff --git a/aws/internal/service/dynamodb/waiter/waiter.go b/aws/internal/service/dynamodb/waiter/waiter.go new file mode 100644 index 00000000000..c9df0e62fcd --- /dev/null +++ b/aws/internal/service/dynamodb/waiter/waiter.go @@ -0,0 +1,231 @@ +package waiter + +import ( + "time" + + "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const ( + CreateTableTimeout = 20 * time.Minute + UpdateTableTimeoutTotal = 60 * time.Minute + ReplicaUpdateTimeout = 30 * time.Minute + UpdateTableTimeout = 20 * time.Minute + UpdateTableContinuousBackupsTimeout = 20 * time.Minute + DeleteTableTimeout = 10 * time.Minute + PITRUpdateTimeout = 30 * time.Second + TTLUpdateTimeout = 30 * time.Second +) + +func DynamoDBTableActive(conn *dynamodb.DynamoDB, tableName string) (*dynamodb.TableDescription, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + dynamodb.TableStatusCreating, + dynamodb.TableStatusUpdating, + }, + Target: []string{ + dynamodb.TableStatusActive, + }, + Timeout: CreateTableTimeout, + Refresh: DynamoDBTableStatus(conn, tableName), + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*dynamodb.TableDescription); ok { + return output, err + } + + return nil, err +} + +func DynamoDBTableDeleted(conn *dynamodb.DynamoDB, tableName string) (*dynamodb.TableDescription, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + dynamodb.TableStatusActive, + dynamodb.TableStatusDeleting, + }, + Target: []string{}, + Timeout: DeleteTableTimeout, + Refresh: DynamoDBTableStatus(conn, tableName), + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*dynamodb.TableDescription); ok { + return output, err + } + + return nil, err +} + +func DynamoDBReplicaActive(conn *dynamodb.DynamoDB, tableName, region string) (*dynamodb.DescribeTableOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + dynamodb.ReplicaStatusCreating, + dynamodb.ReplicaStatusUpdating, + dynamodb.ReplicaStatusDeleting, + }, + Target: []string{ + dynamodb.ReplicaStatusActive, + }, + Timeout: ReplicaUpdateTimeout, + Refresh: DynamoDBReplicaUpdate(conn, tableName, region), + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*dynamodb.DescribeTableOutput); ok { + return output, err + } + + return nil, err +} + +func DynamoDBReplicaDeleted(conn *dynamodb.DynamoDB, tableName, region string) (*dynamodb.DescribeTableOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + dynamodb.ReplicaStatusCreating, + dynamodb.ReplicaStatusUpdating, + dynamodb.ReplicaStatusDeleting, + dynamodb.ReplicaStatusActive, + }, + Target: []string{""}, + Timeout: ReplicaUpdateTimeout, + Refresh: DynamoDBReplicaDelete(conn, tableName, region), + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*dynamodb.DescribeTableOutput); ok { + return output, err + } + + return nil, err +} + +func DynamoDBGSIActive(conn *dynamodb.DynamoDB, tableName, indexName string) (*dynamodb.GlobalSecondaryIndexDescription, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + dynamodb.IndexStatusCreating, + dynamodb.IndexStatusUpdating, + }, + Target: []string{ + dynamodb.IndexStatusActive, + }, + Timeout: UpdateTableTimeout, + Refresh: DynamoDBGSIStatus(conn, tableName, indexName), + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*dynamodb.GlobalSecondaryIndexDescription); ok { + return output, err + } + + return nil, err +} + +func DynamoDBGSIDeleted(conn *dynamodb.DynamoDB, tableName, indexName string) (*dynamodb.GlobalSecondaryIndexDescription, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + dynamodb.IndexStatusActive, + dynamodb.IndexStatusDeleting, + dynamodb.IndexStatusUpdating, + }, + Target: []string{}, + Timeout: UpdateTableTimeout, + Refresh: DynamoDBGSIStatus(conn, tableName, indexName), + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*dynamodb.GlobalSecondaryIndexDescription); ok { + return output, err + } + + return nil, err +} + +func DynamoDBPITRUpdated(conn *dynamodb.DynamoDB, tableName string, toEnable bool) (*dynamodb.PointInTimeRecoveryDescription, error) { + var pending []string + target := []string{dynamodb.TimeToLiveStatusDisabled} + + if toEnable { + pending = []string{ + "ENABLING", + } + target = []string{dynamodb.PointInTimeRecoveryStatusEnabled} + } + + stateConf := &resource.StateChangeConf{ + Pending: pending, + Target: target, + Timeout: PITRUpdateTimeout, + Refresh: DynamoDBPITRStatus(conn, tableName), + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*dynamodb.PointInTimeRecoveryDescription); ok { + return output, err + } + + return nil, err +} + +func DynamoDBTTLUpdated(conn *dynamodb.DynamoDB, tableName string, toEnable bool) (*dynamodb.TimeToLiveDescription, error) { + pending := []string{ + dynamodb.TimeToLiveStatusEnabled, + dynamodb.TimeToLiveStatusDisabling, + } + target := []string{dynamodb.TimeToLiveStatusDisabled} + + if toEnable { + pending = []string{ + dynamodb.TimeToLiveStatusDisabled, + dynamodb.TimeToLiveStatusEnabling, + } + target = []string{dynamodb.TimeToLiveStatusEnabled} + } + + stateConf := &resource.StateChangeConf{ + Pending: pending, + Target: target, + Timeout: TTLUpdateTimeout, + Refresh: DynamoDBTTLStatus(conn, tableName), + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*dynamodb.TimeToLiveDescription); ok { + return output, err + } + + return nil, err +} + +func DynamoDBSSEUpdated(conn *dynamodb.DynamoDB, tableName string) (*dynamodb.TableDescription, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + dynamodb.SSEStatusDisabling, + dynamodb.SSEStatusEnabling, + dynamodb.SSEStatusUpdating, + }, + Target: []string{ + dynamodb.SSEStatusDisabled, + dynamodb.SSEStatusEnabled, + }, + Timeout: UpdateTableTimeout, + Refresh: DynamoDBTableSESStatus(conn, tableName), + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*dynamodb.TableDescription); ok { + return output, err + } + + return nil, err +} diff --git a/aws/resource_aws_dynamodb_table.go b/aws/resource_aws_dynamodb_table.go index 658c044ce58..a2566a21aaf 100644 --- a/aws/resource_aws_dynamodb_table.go +++ b/aws/resource_aws_dynamodb_table.go @@ -3,11 +3,10 @@ package aws import ( "bytes" "context" - "errors" "fmt" "log" + "reflect" "strings" - "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/dynamodb" @@ -18,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/terraform-providers/terraform-provider-aws/aws/internal/hashcode" "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/dynamodb/waiter" ) func resourceAwsDynamoDbTable() *schema.Resource { @@ -32,9 +32,9 @@ func resourceAwsDynamoDbTable() *schema.Resource { }, Timeouts: &schema.ResourceTimeout{ - Create: schema.DefaultTimeout(10 * time.Minute), - Delete: schema.DefaultTimeout(10 * time.Minute), - Update: schema.DefaultTimeout(60 * time.Minute), + Create: schema.DefaultTimeout(waiter.CreateTableTimeout), + Delete: schema.DefaultTimeout(waiter.DeleteTableTimeout), + Update: schema.DefaultTimeout(waiter.UpdateTableTimeoutTotal), }, CustomizeDiff: customdiff.Sequence( @@ -72,38 +72,6 @@ func resourceAwsDynamoDbTable() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "name": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - }, - "hash_key": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - }, - "range_key": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - }, - "billing_mode": { - Type: schema.TypeString, - Optional: true, - Default: dynamodb.BillingModeProvisioned, - ValidateFunc: validation.StringInSlice([]string{ - dynamodb.BillingModePayPerRequest, - dynamodb.BillingModeProvisioned, - }, false), - }, - "write_capacity": { - Type: schema.TypeInt, - Optional: true, - }, - "read_capacity": { - Type: schema.TypeInt, - Optional: true, - }, "attribute": { Type: schema.TypeSet, Required: true, @@ -131,24 +99,57 @@ func resourceAwsDynamoDbTable() *schema.Resource { return hashcode.String(buf.String()) }, }, - "ttl": { - Type: schema.TypeList, + "billing_mode": { + Type: schema.TypeString, + Optional: true, + Default: dynamodb.BillingModeProvisioned, + ValidateFunc: validation.StringInSlice([]string{ + dynamodb.BillingModePayPerRequest, + dynamodb.BillingModeProvisioned, + }, false), + }, + "global_secondary_index": { + Type: schema.TypeSet, Optional: true, - MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "attribute_name": { + "hash_key": { Type: schema.TypeString, Required: true, }, - "enabled": { - Type: schema.TypeBool, + "name": { + Type: schema.TypeString, + Required: true, + }, + "non_key_attributes": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "projection_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(dynamodb.ProjectionType_Values(), false), + }, + "range_key": { + Type: schema.TypeString, + Optional: true, + }, + "read_capacity": { + Type: schema.TypeInt, + Optional: true, + }, + "write_capacity": { + Type: schema.TypeInt, Optional: true, - Default: false, }, }, }, - DiffSuppressFunc: suppressMissingOptionalConfigurationBlock, + }, + "hash_key": { + Type: schema.TypeString, + Required: true, + ForceNew: true, }, "local_secondary_index": { Type: schema.TypeSet, @@ -161,10 +162,11 @@ func resourceAwsDynamoDbTable() *schema.Resource { Required: true, ForceNew: true, }, - "range_key": { - Type: schema.TypeString, - Required: true, + "non_key_attributes": { + Type: schema.TypeList, + Optional: true, ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, }, "projection_type": { Type: schema.TypeString, @@ -172,11 +174,10 @@ func resourceAwsDynamoDbTable() *schema.Resource { ForceNew: true, ValidateFunc: validation.StringInSlice(dynamodb.ProjectionType_Values(), false), }, - "non_key_attributes": { - Type: schema.TypeList, - Optional: true, + "range_key": { + Type: schema.TypeString, + Required: true, ForceNew: true, - Elem: &schema.Schema{Type: schema.TypeString}, }, }, }, @@ -187,48 +188,84 @@ func resourceAwsDynamoDbTable() *schema.Resource { return hashcode.String(buf.String()) }, }, - "global_secondary_index": { - Type: schema.TypeSet, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "point_in_time_recovery": { + Type: schema.TypeList, Optional: true, + Computed: true, + MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, + "enabled": { + Type: schema.TypeBool, Required: true, }, - "write_capacity": { - Type: schema.TypeInt, - Optional: true, - }, - "read_capacity": { - Type: schema.TypeInt, - Optional: true, + }, + }, + }, + "range_key": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "read_capacity": { + Type: schema.TypeInt, + Optional: true, + }, + "replica": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "kms_key_arn": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validateArn, }, - "hash_key": { + "region_name": { Type: schema.TypeString, Required: true, }, - "range_key": { - Type: schema.TypeString, - Optional: true, + }, + }, + }, + "server_side_encryption": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Required: true, }, - "projection_type": { + "kms_key_arn": { Type: schema.TypeString, - Required: true, - ValidateFunc: validation.StringInSlice(dynamodb.ProjectionType_Values(), false), - }, - "non_key_attributes": { - Type: schema.TypeSet, - Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + Computed: true, + ValidateFunc: validateArn, }, }, }, }, + "stream_arn": { + Type: schema.TypeString, + Computed: true, + }, "stream_enabled": { Type: schema.TypeBool, Optional: true, }, + "stream_label": { + Type: schema.TypeString, + Computed: true, + }, "stream_view_type": { Type: schema.TypeString, Optional: true, @@ -245,60 +282,29 @@ func resourceAwsDynamoDbTable() *schema.Resource { dynamodb.StreamViewTypeKeysOnly, }, false), }, - "stream_arn": { - Type: schema.TypeString, - Computed: true, - }, - "stream_label": { - Type: schema.TypeString, - Computed: true, - }, - "server_side_encryption": { + "tags": tagsSchema(), + "ttl": { Type: schema.TypeList, Optional: true, - Computed: true, MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "enabled": { - Type: schema.TypeBool, + "attribute_name": { + Type: schema.TypeString, Required: true, }, - "kms_key_arn": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ValidateFunc: validateArn, - }, - }, - }, - }, - "tags": tagsSchema(), - "point_in_time_recovery": { - Type: schema.TypeList, - Optional: true, - Computed: true, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ "enabled": { Type: schema.TypeBool, - Required: true, + Optional: true, + Default: false, }, }, }, + DiffSuppressFunc: suppressMissingOptionalConfigurationBlock, }, - "replica": { - Type: schema.TypeSet, + "write_capacity": { + Type: schema.TypeInt, Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "region_name": { - Type: schema.TypeString, - Required: true, - }, - }, - }, }, }, } @@ -354,7 +360,7 @@ func resourceAwsDynamoDbTableCreate(d *schema.ResourceData, meta interface{}) er for _, gsiObject := range gsiSet.List() { gsi := gsiObject.(map[string]interface{}) if err := validateDynamoDbProvisionedThroughput(gsi, billingMode); err != nil { - return fmt.Errorf("Failed to create GSI: %v", err) + return fmt.Errorf("failed to create GSI: %v", err) } gsiObject := expandDynamoDbGlobalSecondaryIndex(gsi, billingMode) @@ -376,7 +382,7 @@ func resourceAwsDynamoDbTableCreate(d *schema.ResourceData, meta interface{}) er var output *dynamodb.CreateTableOutput var requiresTagging bool - err := resource.Retry(2*time.Minute, func() *resource.RetryError { + err := resource.Retry(waiter.CreateTableTimeout, func() *resource.RetryError { var err error output, err = conn.CreateTable(req) if err != nil { @@ -413,7 +419,7 @@ func resourceAwsDynamoDbTableCreate(d *schema.ResourceData, meta interface{}) er } if err != nil { - return fmt.Errorf("error creating DynamoDB Table: %s", err) + return fmt.Errorf("error creating DynamoDB Table: %w", err) } if output == nil || output.TableDescription == nil { @@ -423,61 +429,114 @@ func resourceAwsDynamoDbTableCreate(d *schema.ResourceData, meta interface{}) er d.SetId(aws.StringValue(output.TableDescription.TableName)) d.Set("arn", output.TableDescription.TableArn) - if err := waitForDynamoDbTableToBeActive(d.Id(), d.Timeout(schema.TimeoutCreate), conn); err != nil { - return err + if _, err := waiter.DynamoDBTableActive(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for creation of DynamoDB table (%s): %w", d.Id(), err) } if requiresTagging { if err := keyvaluetags.DynamodbUpdateTags(conn, d.Get("arn").(string), nil, tags); err != nil { - return fmt.Errorf("error adding DynamoDB Table (%s) tags: %s", d.Id(), err) + return fmt.Errorf("error adding DynamoDB Table (%s) tags: %w", d.Id(), err) } } if d.Get("ttl.0.enabled").(bool) { if err := updateDynamoDbTimeToLive(d.Id(), d.Get("ttl").([]interface{}), conn); err != nil { - return fmt.Errorf("error enabling DynamoDB Table (%s) Time to Live: %s", d.Id(), err) + return fmt.Errorf("error enabling DynamoDB Table (%s) Time to Live: %w", d.Id(), err) } } if d.Get("point_in_time_recovery.0.enabled").(bool) { if err := updateDynamoDbPITR(d, conn); err != nil { - return fmt.Errorf("error enabling DynamoDB Table (%s) point in time recovery: %s", d.Id(), err) + return fmt.Errorf("error enabling DynamoDB Table (%s) point in time recovery: %w", d.Id(), err) } } if v := d.Get("replica").(*schema.Set); v.Len() > 0 { - if err := createDynamoDbReplicas(d.Id(), v.List(), d.Timeout(schema.TimeoutCreate), conn); err != nil { - return fmt.Errorf("error creating DynamoDB Table (%s) replicas: %s", d.Id(), err) + if err := createDynamoDbReplicas(d.Id(), v.List(), conn); err != nil { + return fmt.Errorf("error initially creating DynamoDB Table (%s) replicas: %w", d.Id(), err) } } return resourceAwsDynamoDbTableRead(d, meta) } -func resourceAwsDynamoDbTableUpdate(d *schema.ResourceData, meta interface{}) error { +func resourceAwsDynamoDbTableRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).dynamodbconn - billingMode := d.Get("billing_mode").(string) - - // Global Secondary Index operations must occur in multiple phases - // to prevent various error scenarios. If there are no detected required - // updates in the Terraform configuration, later validation or API errors - // will signal the problems. - var gsiUpdates []*dynamodb.GlobalSecondaryIndexUpdate + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig - if d.HasChange("global_secondary_index") { - var err error - o, n := d.GetChange("global_secondary_index") - gsiUpdates, err = diffDynamoDbGSI(o.(*schema.Set).List(), n.(*schema.Set).List(), billingMode) + result, err := conn.DescribeTable(&dynamodb.DescribeTableInput{ + TableName: aws.String(d.Id()), + }) - if err != nil { - return fmt.Errorf("computing difference for DynamoDB Table (%s) Global Secondary Index updates failed: %s", d.Id(), err) + if err != nil { + if isAWSErr(err, dynamodb.ErrCodeResourceNotFoundException, "") { + log.Printf("[WARN] Dynamodb Table (%s) not found, error code (404)", d.Id()) + d.SetId("") + return nil } + return err + } - log.Printf("[DEBUG] Computed DynamoDB Table (%s) Global Secondary Index updates: %s", d.Id(), gsiUpdates) + err = flattenDynamoDbTableResource(d, result.Table) + if err != nil { + return err } - // Phase 1 of Global Secondary Index Operations: Delete Only - // * Delete indexes first to prevent error when simultaneously updating + ttlOut, err := conn.DescribeTimeToLive(&dynamodb.DescribeTimeToLiveInput{ + TableName: aws.String(d.Id()), + }) + if err != nil { + return fmt.Errorf("error describing DynamoDB Table (%s) Time to Live: %w", d.Id(), err) + } + if err := d.Set("ttl", flattenDynamoDbTtl(ttlOut)); err != nil { + return fmt.Errorf("error setting ttl: %w", err) + } + + tags, err := keyvaluetags.DynamodbListTags(conn, d.Get("arn").(string)) + + if err != nil && !isAWSErr(err, "UnknownOperationException", "Tagging is not currently supported in DynamoDB Local.") { + return fmt.Errorf("error listing tags for DynamoDB Table (%s): %w", d.Get("arn").(string), err) + } + + if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + pitrOut, err := conn.DescribeContinuousBackups(&dynamodb.DescribeContinuousBackupsInput{ + TableName: aws.String(d.Id()), + }) + if err != nil && !isAWSErr(err, "UnknownOperationException", "") { + return err + } + d.Set("point_in_time_recovery", flattenDynamoDbPitr(pitrOut)) + + return nil +} + +func resourceAwsDynamoDbTableUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).dynamodbconn + billingMode := d.Get("billing_mode").(string) + + // Global Secondary Index operations must occur in multiple phases + // to prevent various error scenarios. If there are no detected required + // updates in the Terraform configuration, later validation or API errors + // will signal the problems. + var gsiUpdates []*dynamodb.GlobalSecondaryIndexUpdate + + if d.HasChange("global_secondary_index") { + var err error + o, n := d.GetChange("global_secondary_index") + gsiUpdates, err = updateDynamoDbDiffGSI(o.(*schema.Set).List(), n.(*schema.Set).List(), billingMode) + + if err != nil { + return fmt.Errorf("computing difference for DynamoDB Table (%s) Global Secondary Index updates failed: %w", d.Id(), err) + } + + log.Printf("[DEBUG] Computed DynamoDB Table (%s) Global Secondary Index updates: %s", d.Id(), gsiUpdates) + } + + // Phase 1 of Global Secondary Index Operations: Delete Only + // * Delete indexes first to prevent error when simultaneously updating // BillingMode to PROVISIONED, which requires updating index // ProvisionedThroughput first, but we have no definition // * Only 1 online index can be deleted simultaneously per table @@ -493,11 +552,11 @@ func resourceAwsDynamoDbTableUpdate(d *schema.ResourceData, meta interface{}) er } if _, err := conn.UpdateTable(input); err != nil { - return fmt.Errorf("error deleting DynamoDB Table (%s) Global Secondary Index (%s): %s", d.Id(), idxName, err) + return fmt.Errorf("error deleting DynamoDB Table (%s) Global Secondary Index (%s): %w", d.Id(), idxName, err) } - if err := waitForDynamoDbGSIToBeDeleted(d.Id(), idxName, d.Timeout(schema.TimeoutUpdate), conn); err != nil { - return fmt.Errorf("error waiting for DynamoDB Table (%s) Global Secondary Index (%s) deletion: %s", d.Id(), idxName, err) + if _, err := waiter.DynamoDBGSIDeleted(conn, d.Id(), idxName); err != nil { + return fmt.Errorf("error waiting for DynamoDB Table (%s) Global Secondary Index (%s) deletion: %w", d.Id(), idxName, err) } } @@ -553,11 +612,11 @@ func resourceAwsDynamoDbTableUpdate(d *schema.ResourceData, meta interface{}) er _, err := conn.UpdateTable(input) if err != nil { - return fmt.Errorf("error updating DynamoDB Table (%s): %s", d.Id(), err) + return fmt.Errorf("error updating DynamoDB Table (%s): %w", d.Id(), err) } - if err := waitForDynamoDbTableToBeActive(d.Id(), d.Timeout(schema.TimeoutUpdate), conn); err != nil { - return fmt.Errorf("error waiting for DynamoDB Table (%s) update: %s", d.Id(), err) + if _, err := waiter.DynamoDBTableActive(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for DynamoDB Table (%s) update: %w", d.Id(), err) } for _, gsiUpdate := range gsiUpdates { @@ -566,8 +625,9 @@ func resourceAwsDynamoDbTableUpdate(d *schema.ResourceData, meta interface{}) er } idxName := aws.StringValue(gsiUpdate.Update.IndexName) - if err := waitForDynamoDbGSIToBeActive(d.Id(), idxName, d.Timeout(schema.TimeoutUpdate), conn); err != nil { - return fmt.Errorf("error waiting for DynamoDB Table (%s) Global Secondary Index (%s) update: %s", d.Id(), idxName, err) + + if _, err := waiter.DynamoDBGSIActive(conn, d.Id(), idxName); err != nil { + return fmt.Errorf("error waiting for DynamoDB Table (%s) Global Secondary Index (%s) update: %w", d.Id(), idxName, err) } } } @@ -587,11 +647,11 @@ func resourceAwsDynamoDbTableUpdate(d *schema.ResourceData, meta interface{}) er } if _, err := conn.UpdateTable(input); err != nil { - return fmt.Errorf("error creating DynamoDB Table (%s) Global Secondary Index (%s): %s", d.Id(), idxName, err) + return fmt.Errorf("error creating DynamoDB Table (%s) Global Secondary Index (%s): %w", d.Id(), idxName, err) } - if err := waitForDynamoDbGSIToBeActive(d.Id(), idxName, d.Timeout(schema.TimeoutUpdate), conn); err != nil { - return fmt.Errorf("error waiting for DynamoDB Table (%s) Global Secondary Index (%s) creation: %s", d.Id(), idxName, err) + if _, err := waiter.DynamoDBGSIActive(conn, d.Id(), idxName); err != nil { + return fmt.Errorf("error waiting for DynamoDB Table (%s) Global Secondary Index (%s) creation: %w", d.Id(), idxName, err) } } @@ -602,180 +662,82 @@ func resourceAwsDynamoDbTableUpdate(d *schema.ResourceData, meta interface{}) er SSESpecification: expandDynamoDbEncryptAtRestOptions(d.Get("server_side_encryption").([]interface{})), }) if err != nil { - return fmt.Errorf("error updating DynamoDB Table (%s) SSE: %s", d.Id(), err) + return fmt.Errorf("error updating DynamoDB Table (%s) SSE: %w", d.Id(), err) } - if err := waitForDynamoDbSSEUpdateToBeCompleted(d.Id(), d.Timeout(schema.TimeoutUpdate), conn); err != nil { - return fmt.Errorf("error waiting for DynamoDB Table (%s) SSE update: %s", d.Id(), err) + if _, err := waiter.DynamoDBSSEUpdated(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for DynamoDB Table (%s) SSE update: %w", d.Id(), err) } } if d.HasChange("ttl") { if err := updateDynamoDbTimeToLive(d.Id(), d.Get("ttl").([]interface{}), conn); err != nil { - return fmt.Errorf("error updating DynamoDB Table (%s) time to live: %s", d.Id(), err) + return fmt.Errorf("error updating DynamoDB Table (%s) time to live: %w", d.Id(), err) } } if d.HasChange("tags") { o, n := d.GetChange("tags") if err := keyvaluetags.DynamodbUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { - return fmt.Errorf("error updating DynamoDB Table (%s) tags: %s", d.Id(), err) + return fmt.Errorf("error updating DynamoDB Table (%s) tags: %w", d.Id(), err) } } if d.HasChange("point_in_time_recovery") { if err := updateDynamoDbPITR(d, conn); err != nil { - return fmt.Errorf("error updating DynamoDB Table (%s) point in time recovery: %s", d.Id(), err) + return fmt.Errorf("error updating DynamoDB Table (%s) point in time recovery: %w", d.Id(), err) } } if d.HasChange("replica") { if err := updateDynamoDbReplica(d, conn); err != nil { - return fmt.Errorf("error updating DynamoDB Table (%s) replica: %s", d.Id(), err) + return fmt.Errorf("error updating DynamoDB Table (%s) replica: %w", d.Id(), err) } } return resourceAwsDynamoDbTableRead(d, meta) } -func updateDynamoDbReplica(d *schema.ResourceData, conn *dynamodb.DynamoDB) error { - oRaw, nRaw := d.GetChange("replica") - o := oRaw.(*schema.Set) - n := nRaw.(*schema.Set) - - removed := o.Difference(n).List() - added := n.Difference(o).List() - - if len(added) > 0 { - if err := createDynamoDbReplicas(d.Id(), added, d.Timeout(schema.TimeoutUpdate), conn); err != nil { - return err - } - } - - if len(removed) > 0 { - if err := deleteDynamoDbReplicas(d.Id(), removed, d.Timeout(schema.TimeoutUpdate), conn); err != nil { - return err - } - } - - return nil -} - -func resourceAwsDynamoDbTableRead(d *schema.ResourceData, meta interface{}) error { - conn := meta.(*AWSClient).dynamodbconn - ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig - - result, err := conn.DescribeTable(&dynamodb.DescribeTableInput{ - TableName: aws.String(d.Id()), - }) - - if err != nil { - if isAWSErr(err, dynamodb.ErrCodeResourceNotFoundException, "") { - log.Printf("[WARN] Dynamodb Table (%s) not found, error code (404)", d.Id()) - d.SetId("") - return nil - } - return err - } - - err = flattenAwsDynamoDbTableResource(d, result.Table) - if err != nil { - return err - } - - ttlOut, err := conn.DescribeTimeToLive(&dynamodb.DescribeTimeToLiveInput{ - TableName: aws.String(d.Id()), - }) - if err != nil { - return fmt.Errorf("error describing DynamoDB Table (%s) Time to Live: %s", d.Id(), err) - } - if err := d.Set("ttl", flattenDynamoDbTtl(ttlOut)); err != nil { - return fmt.Errorf("error setting ttl: %s", err) - } - - tags, err := keyvaluetags.DynamodbListTags(conn, d.Get("arn").(string)) - - if err != nil && !isAWSErr(err, "UnknownOperationException", "Tagging is not currently supported in DynamoDB Local.") { - return fmt.Errorf("error listing tags for DynamoDB Table (%s): %s", d.Get("arn").(string), err) - } - - if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %s", err) - } - - pitrOut, err := conn.DescribeContinuousBackups(&dynamodb.DescribeContinuousBackupsInput{ - TableName: aws.String(d.Id()), - }) - if err != nil && !isAWSErr(err, "UnknownOperationException", "") { - return err - } - d.Set("point_in_time_recovery", flattenDynamoDbPitr(pitrOut)) - - return nil -} - func resourceAwsDynamoDbTableDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).dynamodbconn log.Printf("[DEBUG] DynamoDB delete table: %s", d.Id()) if replicas := d.Get("replica").(*schema.Set).List(); len(replicas) > 0 { - if err := deleteDynamoDbReplicas(d.Id(), replicas, d.Timeout(schema.TimeoutDelete), conn); err != nil { - return fmt.Errorf("error deleting DynamoDB Table (%s) replicas: %s", d.Id(), err) + if err := deleteDynamoDbReplicas(d.Id(), replicas, conn); err != nil { + return fmt.Errorf("error deleting DynamoDB Table (%s) replicas: %w", d.Id(), err) } } - err := deleteAwsDynamoDbTable(d.Id(), conn) + err := deleteDynamoDbTable(d.Id(), conn) if err != nil { if isAWSErr(err, dynamodb.ErrCodeResourceNotFoundException, "Requested resource not found: Table: ") { return nil } - return fmt.Errorf("error deleting DynamoDB Table (%s): %s", d.Id(), err) + return fmt.Errorf("error deleting DynamoDB Table (%s): %w", d.Id(), err) } - if err := waitForDynamodbTableDeletion(conn, d.Id(), d.Timeout(schema.TimeoutDelete)); err != nil { - return fmt.Errorf("error waiting for DynamoDB Table (%s) deletion: %s", d.Id(), err) + if _, err := waiter.DynamoDBTableDeleted(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for DynamoDB Table (%s) deletion: %w", d.Id(), err) } return nil } -func deleteAwsDynamoDbTable(tableName string, conn *dynamodb.DynamoDB) error { - input := &dynamodb.DeleteTableInput{ - TableName: aws.String(tableName), - } - - err := resource.Retry(5*time.Minute, func() *resource.RetryError { - _, err := conn.DeleteTable(input) - if err != nil { - // Subscriber limit exceeded: Only 10 tables can be created, updated, or deleted simultaneously - if isAWSErr(err, dynamodb.ErrCodeLimitExceededException, "simultaneously") { - return resource.RetryableError(err) - } - // This handles multiple scenarios in the DynamoDB API: - // 1. Updating a table immediately before deletion may return: - // ResourceInUseException: Attempt to change a resource which is still in use: Table is being updated: - // 2. Removing a table from a DynamoDB global table may return: - // ResourceInUseException: Attempt to change a resource which is still in use: Table is being deleted: - if isAWSErr(err, dynamodb.ErrCodeResourceInUseException, "") { - return resource.RetryableError(err) - } - if isAWSErr(err, dynamodb.ErrCodeResourceNotFoundException, "Requested resource not found: Table: ") { - return resource.NonRetryableError(err) - } - return resource.NonRetryableError(err) - } - return nil - }) +// custom diff - if isResourceTimeoutError(err) { - _, err = conn.DeleteTable(input) +func isDynamoDbTableOptionDisabled(v interface{}) bool { + options := v.([]interface{}) + if len(options) == 0 { + return true } - - return err + e := options[0].(map[string]interface{})["enabled"] + return !e.(bool) } -func deleteDynamoDbReplicas(tableName string, tfList []interface{}, timeout time.Duration, conn *dynamodb.DynamoDB) error { +// CRUD helpers + +func createDynamoDbReplicas(tableName string, tfList []interface{}, conn *dynamodb.DynamoDB) error { for _, tfMapRaw := range tfList { tfMap, ok := tfMapRaw.(map[string]interface{}) @@ -783,28 +745,26 @@ func deleteDynamoDbReplicas(tableName string, tfList []interface{}, timeout time continue } - var regionName string + var replicaInput = &dynamodb.CreateReplicationGroupMemberAction{} - if v, ok := tfMap["region_name"].(string); ok { - regionName = v + if v, ok := tfMap["region_name"].(string); ok && v != "" { + replicaInput.RegionName = aws.String(v) } - if regionName == "" { - continue + if v, ok := tfMap["kms_key_arn"].(string); ok && v != "" { + replicaInput.KMSMasterKeyId = aws.String(v) } input := &dynamodb.UpdateTableInput{ TableName: aws.String(tableName), ReplicaUpdates: []*dynamodb.ReplicationGroupUpdate{ { - Delete: &dynamodb.DeleteReplicationGroupMemberAction{ - RegionName: aws.String(regionName), - }, + Create: replicaInput, }, }, } - err := resource.Retry(20*time.Minute, func() *resource.RetryError { + err := resource.Retry(waiter.ReplicaUpdateTimeout, func() *resource.RetryError { _, err := conn.UpdateTable(input) if err != nil { if isAWSErr(err, "ThrottlingException", "") { @@ -827,135 +787,248 @@ func deleteDynamoDbReplicas(tableName string, tfList []interface{}, timeout time } if err != nil { - return fmt.Errorf("error deleting DynamoDB Table (%s) replica (%s): %s", tableName, regionName, err) + return fmt.Errorf("error creating DynamoDB Table (%s) replica (%s): %w", tableName, tfMap["region_name"].(string), err) } - if err := waitForDynamoDbReplicaDeleteToBeCompleted(tableName, regionName, timeout, conn); err != nil { - return fmt.Errorf("error waiting for DynamoDB Table (%s) replica (%s) deletion: %s", tableName, regionName, err) + if _, err := waiter.DynamoDBReplicaActive(conn, tableName, tfMap["region_name"].(string)); err != nil { + return fmt.Errorf("error waiting for DynamoDB Table (%s) replica (%s) creation: %w", tableName, tfMap["region_name"].(string), err) } } return nil } -func waitForDynamodbTableDeletion(conn *dynamodb.DynamoDB, tableName string, timeout time.Duration) error { - stateConf := &resource.StateChangeConf{ - Pending: []string{ - dynamodb.TableStatusActive, - dynamodb.TableStatusDeleting, +func updateDynamoDbTimeToLive(tableName string, ttlList []interface{}, conn *dynamodb.DynamoDB) error { + ttlMap := ttlList[0].(map[string]interface{}) + + input := &dynamodb.UpdateTimeToLiveInput{ + TableName: aws.String(tableName), + TimeToLiveSpecification: &dynamodb.TimeToLiveSpecification{ + AttributeName: aws.String(ttlMap["attribute_name"].(string)), + Enabled: aws.Bool(ttlMap["enabled"].(bool)), }, - Target: []string{}, - Timeout: timeout, - Refresh: func() (interface{}, string, error) { - input := &dynamodb.DescribeTableInput{ - TableName: aws.String(tableName), - } + } + + log.Printf("[DEBUG] Updating DynamoDB Table (%s) Time To Live: %s", tableName, input) + if _, err := conn.UpdateTimeToLive(input); err != nil { + return fmt.Errorf("error updating DynamoDB Table (%s) Time To Live: %w", tableName, err) + } - output, err := conn.DescribeTable(input) + log.Printf("[DEBUG] Waiting for DynamoDB Table (%s) Time to Live update to complete", tableName) - if isAWSErr(err, dynamodb.ErrCodeResourceNotFoundException, "") { - return nil, "", nil - } + if _, err := waiter.DynamoDBTTLUpdated(conn, tableName, ttlMap["enabled"].(bool)); err != nil { + return fmt.Errorf("error waiting for DynamoDB Table (%s) Time To Live update: %w", tableName, err) + } - if err != nil { - return 42, "", err - } + return nil +} - if output == nil { - return nil, "", nil - } +func updateDynamoDbPITR(d *schema.ResourceData, conn *dynamodb.DynamoDB) error { + toEnable := d.Get("point_in_time_recovery.0.enabled").(bool) - return output.Table, aws.StringValue(output.Table.TableStatus), nil + input := &dynamodb.UpdateContinuousBackupsInput{ + TableName: aws.String(d.Id()), + PointInTimeRecoverySpecification: &dynamodb.PointInTimeRecoverySpecification{ + PointInTimeRecoveryEnabled: aws.Bool(toEnable), }, } - _, err := stateConf.WaitForState() - - return err -} + log.Printf("[DEBUG] Updating DynamoDB point in time recovery status to %v", toEnable) -func waitForDynamoDbReplicaUpdateToBeCompleted(tableName string, region string, timeout time.Duration, conn *dynamodb.DynamoDB) error { - stateConf := &resource.StateChangeConf{ - Pending: []string{ - dynamodb.ReplicaStatusCreating, - dynamodb.ReplicaStatusUpdating, - dynamodb.ReplicaStatusDeleting, - }, - Target: []string{ - dynamodb.ReplicaStatusActive, - }, - Timeout: timeout, - Refresh: func() (interface{}, string, error) { - result, err := conn.DescribeTable(&dynamodb.DescribeTableInput{ - TableName: aws.String(tableName), - }) - if err != nil { - return 42, "", err + err := resource.Retry(waiter.UpdateTableContinuousBackupsTimeout, func() *resource.RetryError { + _, err := conn.UpdateContinuousBackups(input) + if err != nil { + // Backups are still being enabled for this newly created table + if isAWSErr(err, dynamodb.ErrCodeContinuousBackupsUnavailableException, "Backups are being enabled") { + return resource.RetryableError(err) } - log.Printf("[DEBUG] DynamoDB replicas: %s", result.Table.Replicas) + return resource.NonRetryableError(err) + } + return nil + }) + if isResourceTimeoutError(err) { + _, err = conn.UpdateContinuousBackups(input) + } + if err != nil { + return fmt.Errorf("error updating DynamoDB PITR status: %w", err) + } - var targetReplica *dynamodb.ReplicaDescription + if _, err := waiter.DynamoDBPITRUpdated(conn, d.Id(), toEnable); err != nil { + return fmt.Errorf("error waiting for DynamoDB PITR update: %w", err) + } - for _, replica := range result.Table.Replicas { - if aws.StringValue(replica.RegionName) == region { - targetReplica = replica - break - } - } + return nil +} - if targetReplica == nil { - return result, dynamodb.ReplicaStatusCreating, nil - } +func updateDynamoDbReplica(d *schema.ResourceData, conn *dynamodb.DynamoDB) error { + oRaw, nRaw := d.GetChange("replica") + o := oRaw.(*schema.Set) + n := nRaw.(*schema.Set) - return result, aws.StringValue(targetReplica.ReplicaStatus), nil - }, + removed := o.Difference(n).List() + added := n.Difference(o).List() + + if len(added) > 0 { + if err := createDynamoDbReplicas(d.Id(), added, conn); err != nil { + return fmt.Errorf("error updating DynamoDB replicas for table (%s), while creating: %w", d.Id(), err) + } } - _, err := stateConf.WaitForState() - return err + if len(removed) > 0 { + if err := deleteDynamoDbReplicas(d.Id(), removed, conn); err != nil { + return fmt.Errorf("error updating DynamoDB replicas for table (%s), while deleting: %w", d.Id(), err) + } + } + + return nil } -func waitForDynamoDbReplicaDeleteToBeCompleted(tableName string, region string, timeout time.Duration, conn *dynamodb.DynamoDB) error { - stateConf := &resource.StateChangeConf{ - Pending: []string{ - dynamodb.ReplicaStatusCreating, - dynamodb.ReplicaStatusUpdating, - dynamodb.ReplicaStatusDeleting, - dynamodb.ReplicaStatusActive, - }, - Target: []string{""}, - Timeout: timeout, - Refresh: func() (interface{}, string, error) { - result, err := conn.DescribeTable(&dynamodb.DescribeTableInput{ - TableName: aws.String(tableName), +func updateDynamoDbDiffGSI(oldGsi, newGsi []interface{}, billingMode string) (ops []*dynamodb.GlobalSecondaryIndexUpdate, e error) { + // Transform slices into maps + oldGsis := make(map[string]interface{}) + for _, gsidata := range oldGsi { + m := gsidata.(map[string]interface{}) + oldGsis[m["name"].(string)] = m + } + newGsis := make(map[string]interface{}) + for _, gsidata := range newGsi { + m := gsidata.(map[string]interface{}) + // validate throughput input early, to avoid unnecessary processing + if e = validateDynamoDbProvisionedThroughput(m, billingMode); e != nil { + return + } + newGsis[m["name"].(string)] = m + } + + for _, data := range newGsi { + newMap := data.(map[string]interface{}) + newName := newMap["name"].(string) + + if _, exists := oldGsis[newName]; !exists { + m := data.(map[string]interface{}) + idxName := m["name"].(string) + + ops = append(ops, &dynamodb.GlobalSecondaryIndexUpdate{ + Create: &dynamodb.CreateGlobalSecondaryIndexAction{ + IndexName: aws.String(idxName), + KeySchema: expandDynamoDbKeySchema(m), + ProvisionedThroughput: expandDynamoDbProvisionedThroughput(m, billingMode), + Projection: expandDynamoDbProjection(m), + }, }) + } + } + + for _, data := range oldGsi { + oldMap := data.(map[string]interface{}) + oldName := oldMap["name"].(string) + + newData, exists := newGsis[oldName] + if exists { + newMap := newData.(map[string]interface{}) + idxName := newMap["name"].(string) + + oldWriteCapacity, oldReadCapacity := oldMap["write_capacity"].(int), oldMap["read_capacity"].(int) + newWriteCapacity, newReadCapacity := newMap["write_capacity"].(int), newMap["read_capacity"].(int) + capacityChanged := (oldWriteCapacity != newWriteCapacity || oldReadCapacity != newReadCapacity) + + // pluck non_key_attributes from oldAttributes and newAttributes as reflect.DeepEquals will compare + // ordinal of elements in its equality (which we actually don't care about) + nonKeyAttributesChanged := checkIfNonKeyAttributesChanged(oldMap, newMap) + + oldAttributes, err := stripCapacityAttributes(oldMap) if err != nil { - return 42, "", err + return ops, err } + oldAttributes, err = stripNonKeyAttributes(oldAttributes) + if err != nil { + return ops, err + } + newAttributes, err := stripCapacityAttributes(newMap) + if err != nil { + return ops, err + } + newAttributes, err = stripNonKeyAttributes(newAttributes) + if err != nil { + return ops, err + } + otherAttributesChanged := nonKeyAttributesChanged || !reflect.DeepEqual(oldAttributes, newAttributes) - log.Printf("[DEBUG] all replicas for waiting: %s", result.Table.Replicas) - var targetReplica *dynamodb.ReplicaDescription - - for _, replica := range result.Table.Replicas { - if aws.StringValue(replica.RegionName) == region { - targetReplica = replica - break + if capacityChanged && !otherAttributesChanged { + update := &dynamodb.GlobalSecondaryIndexUpdate{ + Update: &dynamodb.UpdateGlobalSecondaryIndexAction{ + IndexName: aws.String(idxName), + ProvisionedThroughput: expandDynamoDbProvisionedThroughput(newMap, billingMode), + }, } + ops = append(ops, update) + } else if otherAttributesChanged { + // Other attributes cannot be updated + ops = append(ops, &dynamodb.GlobalSecondaryIndexUpdate{ + Delete: &dynamodb.DeleteGlobalSecondaryIndexAction{ + IndexName: aws.String(idxName), + }, + }) + + ops = append(ops, &dynamodb.GlobalSecondaryIndexUpdate{ + Create: &dynamodb.CreateGlobalSecondaryIndexAction{ + IndexName: aws.String(idxName), + KeySchema: expandDynamoDbKeySchema(newMap), + ProvisionedThroughput: expandDynamoDbProvisionedThroughput(newMap, billingMode), + Projection: expandDynamoDbProjection(newMap), + }, + }) } + } else { + idxName := oldName + ops = append(ops, &dynamodb.GlobalSecondaryIndexUpdate{ + Delete: &dynamodb.DeleteGlobalSecondaryIndexAction{ + IndexName: aws.String(idxName), + }, + }) + } + } + return ops, nil +} + +func deleteDynamoDbTable(tableName string, conn *dynamodb.DynamoDB) error { + input := &dynamodb.DeleteTableInput{ + TableName: aws.String(tableName), + } - if targetReplica == nil { - return result, "", nil + err := resource.Retry(waiter.DeleteTableTimeout, func() *resource.RetryError { + _, err := conn.DeleteTable(input) + if err != nil { + // Subscriber limit exceeded: Only 10 tables can be created, updated, or deleted simultaneously + if isAWSErr(err, dynamodb.ErrCodeLimitExceededException, "simultaneously") { + return resource.RetryableError(err) + } + // This handles multiple scenarios in the DynamoDB API: + // 1. Updating a table immediately before deletion may return: + // ResourceInUseException: Attempt to change a resource which is still in use: Table is being updated: + // 2. Removing a table from a DynamoDB global table may return: + // ResourceInUseException: Attempt to change a resource which is still in use: Table is being deleted: + if isAWSErr(err, dynamodb.ErrCodeResourceInUseException, "") { + return resource.RetryableError(err) + } + if isAWSErr(err, dynamodb.ErrCodeResourceNotFoundException, "Requested resource not found: Table: ") { + return resource.NonRetryableError(err) } + return resource.NonRetryableError(err) + } + return nil + }) - return result, aws.StringValue(targetReplica.ReplicaStatus), nil - }, + if isResourceTimeoutError(err) { + _, err = conn.DeleteTable(input) } - _, err := stateConf.WaitForState() return err } -func createDynamoDbReplicas(tableName string, tfList []interface{}, timeout time.Duration, conn *dynamodb.DynamoDB) error { +func deleteDynamoDbReplicas(tableName string, tfList []interface{}, conn *dynamodb.DynamoDB) error { + var g multierror.Group + for _, tfMapRaw := range tfList { tfMap, ok := tfMapRaw.(map[string]interface{}) @@ -973,322 +1046,367 @@ func createDynamoDbReplicas(tableName string, tfList []interface{}, timeout time continue } - input := &dynamodb.UpdateTableInput{ - TableName: aws.String(tableName), - ReplicaUpdates: []*dynamodb.ReplicationGroupUpdate{ - { - Create: &dynamodb.CreateReplicationGroupMemberAction{ - RegionName: aws.String(regionName), + g.Go(func() error { + input := &dynamodb.UpdateTableInput{ + TableName: aws.String(tableName), + ReplicaUpdates: []*dynamodb.ReplicationGroupUpdate{ + { + Delete: &dynamodb.DeleteReplicationGroupMemberAction{ + RegionName: aws.String(regionName), + }, }, }, - }, - } + } - err := resource.Retry(20*time.Minute, func() *resource.RetryError { - _, err := conn.UpdateTable(input) - if err != nil { - if isAWSErr(err, "ThrottlingException", "") { - return resource.RetryableError(err) - } - if isAWSErr(err, dynamodb.ErrCodeLimitExceededException, "can be created, updated, or deleted simultaneously") { - return resource.RetryableError(err) - } - if isAWSErr(err, dynamodb.ErrCodeResourceInUseException, "") { - return resource.RetryableError(err) + err := resource.Retry(waiter.UpdateTableTimeout, func() *resource.RetryError { + _, err := conn.UpdateTable(input) + if err != nil { + if isAWSErr(err, "ThrottlingException", "") { + return resource.RetryableError(err) + } + if isAWSErr(err, dynamodb.ErrCodeLimitExceededException, "can be created, updated, or deleted simultaneously") { + return resource.RetryableError(err) + } + if isAWSErr(err, dynamodb.ErrCodeResourceInUseException, "") { + return resource.RetryableError(err) + } + + return resource.NonRetryableError(err) } + return nil + }) - return resource.NonRetryableError(err) + if isResourceTimeoutError(err) { + _, err = conn.UpdateTable(input) + } + + if err != nil { + return fmt.Errorf("error deleting DynamoDB Table (%s) replica (%s): %w", tableName, regionName, err) + } + + if _, err := waiter.DynamoDBReplicaDeleted(conn, tableName, regionName); err != nil { + return fmt.Errorf("error waiting for DynamoDB Table (%s) replica (%s) deletion: %w", tableName, regionName, err) } + return nil }) + } - if isResourceTimeoutError(err) { - _, err = conn.UpdateTable(input) + return g.Wait().ErrorOrNil() +} + +// flatteners, expanders + +func flattenDynamoDbTableResource(d *schema.ResourceData, table *dynamodb.TableDescription) error { + d.Set("billing_mode", dynamodb.BillingModeProvisioned) + if table.BillingModeSummary != nil { + d.Set("billing_mode", table.BillingModeSummary.BillingMode) + } + + d.Set("write_capacity", table.ProvisionedThroughput.WriteCapacityUnits) + d.Set("read_capacity", table.ProvisionedThroughput.ReadCapacityUnits) + + attributes := make([]interface{}, len(table.AttributeDefinitions)) + for i, attrdef := range table.AttributeDefinitions { + attributes[i] = map[string]string{ + "name": aws.StringValue(attrdef.AttributeName), + "type": aws.StringValue(attrdef.AttributeType), } + } - if err != nil { - return fmt.Errorf("error creating DynamoDB Table (%s) replica (%s): %s", tableName, regionName, err) + d.Set("attribute", attributes) + d.Set("name", table.TableName) + + for _, attribute := range table.KeySchema { + if aws.StringValue(attribute.KeyType) == dynamodb.KeyTypeHash { + d.Set("hash_key", attribute.AttributeName) } - if err := waitForDynamoDbReplicaUpdateToBeCompleted(tableName, regionName, timeout, conn); err != nil { - return fmt.Errorf("error waiting for DynamoDB Table (%s) replica (%s) creation: %s", tableName, regionName, err) + if aws.StringValue(attribute.KeyType) == dynamodb.KeyTypeRange { + d.Set("range_key", attribute.AttributeName) } } - return nil -} + lsiList := make([]map[string]interface{}, 0, len(table.LocalSecondaryIndexes)) + for _, lsiObject := range table.LocalSecondaryIndexes { + lsi := map[string]interface{}{ + "name": aws.StringValue(lsiObject.IndexName), + "projection_type": aws.StringValue(lsiObject.Projection.ProjectionType), + } -func updateDynamoDbTimeToLive(tableName string, ttlList []interface{}, conn *dynamodb.DynamoDB) error { - ttlMap := ttlList[0].(map[string]interface{}) + for _, attribute := range lsiObject.KeySchema { + if aws.StringValue(attribute.KeyType) == dynamodb.KeyTypeRange { + lsi["range_key"] = aws.StringValue(attribute.AttributeName) + } + } + nkaList := make([]string, len(lsiObject.Projection.NonKeyAttributes)) + for i, nka := range lsiObject.Projection.NonKeyAttributes { + nkaList[i] = aws.StringValue(nka) + } + lsi["non_key_attributes"] = nkaList - input := &dynamodb.UpdateTimeToLiveInput{ - TableName: aws.String(tableName), - TimeToLiveSpecification: &dynamodb.TimeToLiveSpecification{ - AttributeName: aws.String(ttlMap["attribute_name"].(string)), - Enabled: aws.Bool(ttlMap["enabled"].(bool)), - }, + lsiList = append(lsiList, lsi) } - log.Printf("[DEBUG] Updating DynamoDB Table (%s) Time To Live: %s", tableName, input) - if _, err := conn.UpdateTimeToLive(input); err != nil { - return fmt.Errorf("error updating DynamoDB Table (%s) Time To Live: %s", tableName, err) + err := d.Set("local_secondary_index", lsiList) + if err != nil { + return err } - log.Printf("[DEBUG] Waiting for DynamoDB Table (%s) Time to Live update to complete", tableName) - if err := waitForDynamoDbTtlUpdateToBeCompleted(tableName, ttlMap["enabled"].(bool), conn); err != nil { - return fmt.Errorf("error waiting for DynamoDB Table (%s) Time To Live update: %s", tableName, err) - } + gsiList := make([]map[string]interface{}, len(table.GlobalSecondaryIndexes)) + for i, gsiObject := range table.GlobalSecondaryIndexes { + gsi := map[string]interface{}{ + "write_capacity": aws.Int64Value(gsiObject.ProvisionedThroughput.WriteCapacityUnits), + "read_capacity": aws.Int64Value(gsiObject.ProvisionedThroughput.ReadCapacityUnits), + "name": aws.StringValue(gsiObject.IndexName), + } - return nil -} + for _, attribute := range gsiObject.KeySchema { + if aws.StringValue(attribute.KeyType) == dynamodb.KeyTypeHash { + gsi["hash_key"] = aws.StringValue(attribute.AttributeName) + } -func updateDynamoDbPITR(d *schema.ResourceData, conn *dynamodb.DynamoDB) error { - toEnable := d.Get("point_in_time_recovery.0.enabled").(bool) + if aws.StringValue(attribute.KeyType) == dynamodb.KeyTypeRange { + gsi["range_key"] = aws.StringValue(attribute.AttributeName) + } + } - input := &dynamodb.UpdateContinuousBackupsInput{ - TableName: aws.String(d.Id()), - PointInTimeRecoverySpecification: &dynamodb.PointInTimeRecoverySpecification{ - PointInTimeRecoveryEnabled: aws.Bool(toEnable), - }, + gsi["projection_type"] = aws.StringValue(gsiObject.Projection.ProjectionType) + + nonKeyAttrs := make([]string, len(gsiObject.Projection.NonKeyAttributes)) + for i, nonKeyAttr := range gsiObject.Projection.NonKeyAttributes { + nonKeyAttrs[i] = aws.StringValue(nonKeyAttr) + } + gsi["non_key_attributes"] = nonKeyAttrs + + gsiList[i] = gsi } - log.Printf("[DEBUG] Updating DynamoDB point in time recovery status to %v", toEnable) + if table.StreamSpecification != nil { + d.Set("stream_view_type", table.StreamSpecification.StreamViewType) + d.Set("stream_enabled", table.StreamSpecification.StreamEnabled) + } else { + d.Set("stream_view_type", "") + d.Set("stream_enabled", false) + } - err := resource.Retry(20*time.Minute, func() *resource.RetryError { - _, err := conn.UpdateContinuousBackups(input) - if err != nil { - // Backups are still being enabled for this newly created table - if isAWSErr(err, dynamodb.ErrCodeContinuousBackupsUnavailableException, "Backups are being enabled") { - return resource.RetryableError(err) - } - return resource.NonRetryableError(err) - } - return nil - }) - if isResourceTimeoutError(err) { - _, err = conn.UpdateContinuousBackups(input) + d.Set("stream_arn", table.LatestStreamArn) + d.Set("stream_label", table.LatestStreamLabel) + + err = d.Set("global_secondary_index", gsiList) + if err != nil { + return err + } + + sseOptions := []map[string]interface{}{} + if sseDescription := table.SSEDescription; sseDescription != nil { + sseOptions = []map[string]interface{}{{ + "enabled": aws.StringValue(sseDescription.Status) == dynamodb.SSEStatusEnabled, + "kms_key_arn": aws.StringValue(sseDescription.KMSMasterKeyArn), + }} } + err = d.Set("server_side_encryption", sseOptions) if err != nil { - return fmt.Errorf("Error updating DynamoDB PITR status: %s", err) + return err } - if err := waitForDynamoDbBackupUpdateToBeCompleted(d.Id(), toEnable, conn); err != nil { - return fmt.Errorf("Error waiting for DynamoDB PITR update: %s", err) + err = d.Set("replica", flattenDynamoDbReplicaDescriptions(table.Replicas)) + if err != nil { + return err } + d.Set("arn", table.TableArn) + return nil } -// Waiters - -func waitForDynamoDbGSIToBeActive(tableName string, gsiName string, timeout time.Duration, conn *dynamodb.DynamoDB) error { - stateConf := &resource.StateChangeConf{ - Pending: []string{ - dynamodb.IndexStatusCreating, - dynamodb.IndexStatusUpdating, - }, - Target: []string{dynamodb.IndexStatusActive}, - Timeout: timeout, - Refresh: func() (interface{}, string, error) { - result, err := conn.DescribeTable(&dynamodb.DescribeTableInput{ - TableName: aws.String(tableName), - }) - if err != nil { - return 42, "", err - } +func expandDynamoDbAttributes(cfg []interface{}) []*dynamodb.AttributeDefinition { + attributes := make([]*dynamodb.AttributeDefinition, len(cfg)) + for i, attribute := range cfg { + attr := attribute.(map[string]interface{}) + attributes[i] = &dynamodb.AttributeDefinition{ + AttributeName: aws.String(attr["name"].(string)), + AttributeType: aws.String(attr["type"].(string)), + } + } + return attributes +} - table := result.Table +func flattenDynamoDbReplicaDescription(apiObject *dynamodb.ReplicaDescription) map[string]interface{} { + if apiObject == nil { + return nil + } - // Find index - var targetGSI *dynamodb.GlobalSecondaryIndexDescription - for _, gsi := range table.GlobalSecondaryIndexes { - if *gsi.IndexName == gsiName { - targetGSI = gsi - } - } + tfMap := map[string]interface{}{} - if targetGSI != nil { - return table, *targetGSI.IndexStatus, nil - } + if apiObject.KMSMasterKeyId != nil { + tfMap["kms_key_arn"] = aws.StringValue(apiObject.KMSMasterKeyId) + } - return nil, "", nil - }, + if apiObject.RegionName != nil { + tfMap["region_name"] = aws.StringValue(apiObject.RegionName) } - _, err := stateConf.WaitForState() - return err -} -func waitForDynamoDbGSIToBeDeleted(tableName string, gsiName string, timeout time.Duration, conn *dynamodb.DynamoDB) error { - stateConf := &resource.StateChangeConf{ - Pending: []string{ - dynamodb.IndexStatusActive, - dynamodb.IndexStatusDeleting, - }, - Target: []string{}, - Timeout: timeout, - Refresh: func() (interface{}, string, error) { - result, err := conn.DescribeTable(&dynamodb.DescribeTableInput{ - TableName: aws.String(tableName), - }) - if err != nil { - return 42, "", err - } + return tfMap - table := result.Table +} - // Find index - var targetGSI *dynamodb.GlobalSecondaryIndexDescription - for _, gsi := range table.GlobalSecondaryIndexes { - if *gsi.IndexName == gsiName { - targetGSI = gsi - } - } +func flattenDynamoDbReplicaDescriptions(apiObjects []*dynamodb.ReplicaDescription) []interface{} { + if len(apiObjects) == 0 { + return nil + } - if targetGSI == nil { - return nil, "", nil - } + var tfList []interface{} - return targetGSI, *targetGSI.IndexStatus, nil - }, + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + tfList = append(tfList, flattenDynamoDbReplicaDescription(apiObject)) } - _, err := stateConf.WaitForState() - return err + + return tfList } -func waitForDynamoDbTableToBeActive(tableName string, timeout time.Duration, conn *dynamodb.DynamoDB) error { - stateConf := &resource.StateChangeConf{ - Pending: []string{dynamodb.TableStatusCreating, dynamodb.TableStatusUpdating}, - Target: []string{dynamodb.TableStatusActive}, - Timeout: timeout, - Refresh: func() (interface{}, string, error) { - result, err := conn.DescribeTable(&dynamodb.DescribeTableInput{ - TableName: aws.String(tableName), - }) - if err != nil { - return 42, "", err - } +func flattenDynamoDbTtl(ttlOutput *dynamodb.DescribeTimeToLiveOutput) []interface{} { + m := map[string]interface{}{ + "enabled": false, + } - return result, *result.Table.TableStatus, nil - }, + if ttlOutput == nil || ttlOutput.TimeToLiveDescription == nil { + return []interface{}{m} } - _, err := stateConf.WaitForState() - return err + ttlDesc := ttlOutput.TimeToLiveDescription + + m["attribute_name"] = aws.StringValue(ttlDesc.AttributeName) + m["enabled"] = (aws.StringValue(ttlDesc.TimeToLiveStatus) == dynamodb.TimeToLiveStatusEnabled) + + return []interface{}{m} } -func waitForDynamoDbBackupUpdateToBeCompleted(tableName string, toEnable bool, conn *dynamodb.DynamoDB) error { - var pending []string - target := []string{dynamodb.TimeToLiveStatusDisabled} +func flattenDynamoDbPitr(pitrDesc *dynamodb.DescribeContinuousBackupsOutput) []interface{} { + m := map[string]interface{}{ + "enabled": false, + } + + if pitrDesc == nil { + return []interface{}{m} + } - if toEnable { - pending = []string{ - "ENABLING", + if pitrDesc.ContinuousBackupsDescription != nil { + pitr := pitrDesc.ContinuousBackupsDescription.PointInTimeRecoveryDescription + if pitr != nil { + m["enabled"] = (*pitr.PointInTimeRecoveryStatus == dynamodb.PointInTimeRecoveryStatusEnabled) } - target = []string{dynamodb.PointInTimeRecoveryStatusEnabled} } - stateConf := &resource.StateChangeConf{ - Pending: pending, - Target: target, - Timeout: 10 * time.Second, - Refresh: func() (interface{}, string, error) { - result, err := conn.DescribeContinuousBackups(&dynamodb.DescribeContinuousBackupsInput{ - TableName: aws.String(tableName), - }) - if err != nil { - return 42, "", err - } + return []interface{}{m} +} - if result.ContinuousBackupsDescription == nil || result.ContinuousBackupsDescription.PointInTimeRecoveryDescription == nil { - return 42, "", errors.New("Error reading backup status from dynamodb resource: empty description") - } - pitr := result.ContinuousBackupsDescription.PointInTimeRecoveryDescription +// TODO: Get rid of keySchemaM - the user should just explicitly define +// this in the config, we shouldn't magically be setting it like this. +// Removal will however require config change, hence BC. :/ +func expandDynamoDbLocalSecondaryIndexes(cfg []interface{}, keySchemaM map[string]interface{}) []*dynamodb.LocalSecondaryIndex { + indexes := make([]*dynamodb.LocalSecondaryIndex, len(cfg)) + for i, lsi := range cfg { + m := lsi.(map[string]interface{}) + idxName := m["name"].(string) + + // TODO: See https://github.com/hashicorp/terraform-provider-aws/issues/3176 + if _, ok := m["hash_key"]; !ok { + m["hash_key"] = keySchemaM["hash_key"] + } - return result, *pitr.PointInTimeRecoveryStatus, nil - }, + indexes[i] = &dynamodb.LocalSecondaryIndex{ + IndexName: aws.String(idxName), + KeySchema: expandDynamoDbKeySchema(m), + Projection: expandDynamoDbProjection(m), + } } - _, err := stateConf.WaitForState() - return err + return indexes } -func waitForDynamoDbTtlUpdateToBeCompleted(tableName string, toEnable bool, conn *dynamodb.DynamoDB) error { - pending := []string{ - dynamodb.TimeToLiveStatusEnabled, - dynamodb.TimeToLiveStatusDisabling, +func expandDynamoDbGlobalSecondaryIndex(data map[string]interface{}, billingMode string) *dynamodb.GlobalSecondaryIndex { + return &dynamodb.GlobalSecondaryIndex{ + IndexName: aws.String(data["name"].(string)), + KeySchema: expandDynamoDbKeySchema(data), + Projection: expandDynamoDbProjection(data), + ProvisionedThroughput: expandDynamoDbProvisionedThroughput(data, billingMode), } - target := []string{dynamodb.TimeToLiveStatusDisabled} +} - if toEnable { - pending = []string{ - dynamodb.TimeToLiveStatusDisabled, - dynamodb.TimeToLiveStatusEnabling, - } - target = []string{dynamodb.TimeToLiveStatusEnabled} +func expandDynamoDbProvisionedThroughput(data map[string]interface{}, billingMode string) *dynamodb.ProvisionedThroughput { + + if billingMode == dynamodb.BillingModePayPerRequest { + return nil } - stateConf := &resource.StateChangeConf{ - Pending: pending, - Target: target, - Timeout: 10 * time.Second, - Refresh: func() (interface{}, string, error) { - result, err := conn.DescribeTimeToLive(&dynamodb.DescribeTimeToLiveInput{ - TableName: aws.String(tableName), - }) - if err != nil { - return 42, "", err - } + return &dynamodb.ProvisionedThroughput{ + WriteCapacityUnits: aws.Int64(int64(data["write_capacity"].(int))), + ReadCapacityUnits: aws.Int64(int64(data["read_capacity"].(int))), + } +} - ttlDesc := result.TimeToLiveDescription +func expandDynamoDbProjection(data map[string]interface{}) *dynamodb.Projection { + projection := &dynamodb.Projection{ + ProjectionType: aws.String(data["projection_type"].(string)), + } - return result, *ttlDesc.TimeToLiveStatus, nil - }, + if v, ok := data["non_key_attributes"].([]interface{}); ok && len(v) > 0 { + projection.NonKeyAttributes = expandStringList(v) } - _, err := stateConf.WaitForState() - return err + if v, ok := data["non_key_attributes"].(*schema.Set); ok && v.Len() > 0 { + projection.NonKeyAttributes = expandStringSet(v) + } + + return projection } -func waitForDynamoDbSSEUpdateToBeCompleted(tableName string, timeout time.Duration, conn *dynamodb.DynamoDB) error { - stateConf := &resource.StateChangeConf{ - Pending: []string{ - dynamodb.SSEStatusDisabling, - dynamodb.SSEStatusEnabling, - dynamodb.SSEStatusUpdating, - }, - Target: []string{ - dynamodb.SSEStatusDisabled, - dynamodb.SSEStatusEnabled, - }, - Timeout: timeout, - Refresh: func() (interface{}, string, error) { - result, err := conn.DescribeTable(&dynamodb.DescribeTableInput{ - TableName: aws.String(tableName), - }) - if err != nil { - return 42, "", err - } +func expandDynamoDbKeySchema(data map[string]interface{}) []*dynamodb.KeySchemaElement { + keySchema := []*dynamodb.KeySchemaElement{} - // Disabling SSE returns null SSEDescription. - if result.Table.SSEDescription == nil { - return result, dynamodb.SSEStatusDisabled, nil - } - return result, aws.StringValue(result.Table.SSEDescription.Status), nil - }, + if v, ok := data["hash_key"]; ok && v != nil && v != "" { + keySchema = append(keySchema, &dynamodb.KeySchemaElement{ + AttributeName: aws.String(v.(string)), + KeyType: aws.String(dynamodb.KeyTypeHash), + }) } - _, err := stateConf.WaitForState() - return err + if v, ok := data["range_key"]; ok && v != nil && v != "" { + keySchema = append(keySchema, &dynamodb.KeySchemaElement{ + AttributeName: aws.String(v.(string)), + KeyType: aws.String(dynamodb.KeyTypeRange), + }) + } + + return keySchema } -func isDynamoDbTableOptionDisabled(v interface{}) bool { - options := v.([]interface{}) - if len(options) == 0 { - return true +func expandDynamoDbEncryptAtRestOptions(vOptions []interface{}) *dynamodb.SSESpecification { + options := &dynamodb.SSESpecification{} + + enabled := false + if len(vOptions) > 0 { + mOptions := vOptions[0].(map[string]interface{}) + + enabled = mOptions["enabled"].(bool) + if enabled { + if vKmsKeyArn, ok := mOptions["kms_key_arn"].(string); ok && vKmsKeyArn != "" { + options.KMSMasterKeyId = aws.String(vKmsKeyArn) + options.SSEType = aws.String(dynamodb.SSETypeKms) + } + } } - e := options[0].(map[string]interface{})["enabled"] - return !e.(bool) + options.Enabled = aws.Bool(enabled) + + return options } +// validators + func validateDynamoDbTableAttributes(d *schema.ResourceDiff) error { // Collect all indexed attributes primaryHashKey := d.Get("hash_key").(string) @@ -1337,7 +1455,7 @@ func validateDynamoDbTableAttributes(d *schema.ResourceDiff) error { var err *multierror.Error if len(unindexedAttributes) > 0 { - err = multierror.Append(err, fmt.Errorf("All attributes must be indexed. Unused attributes: %q", unindexedAttributes)) + err = multierror.Append(err, fmt.Errorf("all attributes must be indexed. Unused attributes: %q", unindexedAttributes)) } if len(indexedAttributes) > 0 { @@ -1346,8 +1464,32 @@ func validateDynamoDbTableAttributes(d *schema.ResourceDiff) error { missingIndexes = append(missingIndexes, index) } - err = multierror.Append(err, fmt.Errorf("All indexes must match a defined attribute. Unmatched indexes: %q", missingIndexes)) + err = multierror.Append(err, fmt.Errorf("all indexes must match a defined attribute. Unmatched indexes: %q", missingIndexes)) } return err.ErrorOrNil() } + +func validateDynamoDbProvisionedThroughput(data map[string]interface{}, billingMode string) error { + // if billing mode is PAY_PER_REQUEST, don't need to validate the throughput settings + if billingMode == dynamodb.BillingModePayPerRequest { + return nil + } + + writeCapacity, writeCapacitySet := data["write_capacity"].(int) + readCapacity, readCapacitySet := data["read_capacity"].(int) + + if !writeCapacitySet || !readCapacitySet { + return fmt.Errorf("read and write capacity should be set when billing mode is %s", dynamodb.BillingModeProvisioned) + } + + if writeCapacity < 1 { + return fmt.Errorf("write capacity must be > 0 when billing mode is %s", dynamodb.BillingModeProvisioned) + } + + if readCapacity < 1 { + return fmt.Errorf("read capacity must be > 0 when billing mode is %s", dynamodb.BillingModeProvisioned) + } + + return nil +} diff --git a/aws/resource_aws_dynamodb_table_test.go b/aws/resource_aws_dynamodb_table_test.go index cbe5f335255..7a9c3277d48 100644 --- a/aws/resource_aws_dynamodb_table_test.go +++ b/aws/resource_aws_dynamodb_table_test.go @@ -4,11 +4,13 @@ import ( "fmt" "log" "regexp" + "sync" "testing" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/dynamodb" + multierror "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -24,35 +26,77 @@ func init() { func testSweepDynamoDbTables(region string) error { client, err := sharedClientForRegion(region) + if err != nil { return fmt.Errorf("error getting client: %s", err) } + conn := client.(*AWSClient).dynamodbconn + sweepResources := make([]*testSweepResource, 0) + var errs *multierror.Error + var g multierror.Group + var mutex = &sync.Mutex{} + + err = conn.ListTablesPages(&dynamodb.ListTablesInput{}, func(page *dynamodb.ListTablesOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, tableName := range page.TableNames { + r := resourceAwsDynamoDbTable() + d := r.Data(nil) + + id := aws.StringValue(tableName) + d.SetId(id) - err = conn.ListTablesPages(&dynamodb.ListTablesInput{}, func(out *dynamodb.ListTablesOutput, lastPage bool) bool { - for _, tableName := range out.TableNames { - log.Printf("[INFO] Deleting DynamoDB Table: %s", *tableName) + // read concurrently and gather errors + g.Go(func() error { + // Need to Read first to fill in byte_match_tuples attribute + err := r.Read(d, client) - err := deleteAwsDynamoDbTable(*tableName, conn) - if err != nil { - log.Printf("[ERROR] Failed to delete DynamoDB Table %s: %s", *tableName, err) - continue - } + if err != nil { + sweeperErr := fmt.Errorf("error reading DynamoDB Table (%s): %w", id, err) + log.Printf("[ERROR] %s", sweeperErr) + return sweeperErr + } + + // In case it was already deleted + if d.Id() == "" { + return nil + } + + mutex.Lock() + defer mutex.Unlock() + sweepResources = append(sweepResources, NewTestSweepResource(r, d, client)) + + return nil + }) } + return !lastPage }) + if err != nil { - if testSweepSkipSweepError(err) { - log.Printf("[WARN] Skipping DynamoDB Table sweep for %s: %s", region, err) - return nil - } - return fmt.Errorf("Error retrieving DynamoDB Tables: %s", err) + errs = multierror.Append(errs, fmt.Errorf("error listing DynamoDB Tables for %s: %w", region, err)) } - return nil + if err = g.Wait().ErrorOrNil(); err != nil { + errs = multierror.Append(errs, fmt.Errorf("error concurrently reading DynamoDB Tables: %w", err)) + } + + if err = testSweepResourceOrchestrator(sweepResources); err != nil { + errs = multierror.Append(errs, fmt.Errorf("error sweeping DynamoDB Tables for %s: %w", region, err)) + } + + if testSweepSkipSweepError(errs.ErrorOrNil()) { + log.Printf("[WARN] Skipping DynamoDB Tables sweep for %s: %s", region, errs) + return nil + } + + return errs.ErrorOrNil() } -func TestDiffDynamoDbGSI(t *testing.T) { +func TestUpdateDynamoDbDiffGSI(t *testing.T) { testCases := []struct { Old []interface{} New []interface{} @@ -326,7 +370,7 @@ func TestDiffDynamoDbGSI(t *testing.T) { } for i, tc := range testCases { - ops, err := diffDynamoDbGSI(tc.Old, tc.New, dynamodb.BillingModeProvisioned) + ops, err := updateDynamoDbDiffGSI(tc.Old, tc.New, dynamodb.BillingModeProvisioned) if err != nil { t.Fatal(err) } @@ -345,7 +389,7 @@ func TestDiffDynamoDbGSI(t *testing.T) { func TestAccAWSDynamoDbTable_basic(t *testing.T) { var conf dynamodb.DescribeTableOutput resourceName := "aws_dynamodb_table.test" - rName := acctest.RandomWithPrefix("TerraformTestTable-") + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -360,9 +404,9 @@ func TestAccAWSDynamoDbTable_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "name", rName), resource.TestCheckResourceAttr(resourceName, "read_capacity", "1"), resource.TestCheckResourceAttr(resourceName, "write_capacity", "1"), - resource.TestCheckResourceAttr(resourceName, "hash_key", "TestTableHashKey"), + resource.TestCheckResourceAttr(resourceName, "hash_key", rName), resource.TestCheckTypeSetElemNestedAttrs(resourceName, "attribute.*", map[string]string{ - "name": "TestTableHashKey", + "name": rName, "type": "S", }), resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), @@ -380,7 +424,7 @@ func TestAccAWSDynamoDbTable_basic(t *testing.T) { func TestAccAWSDynamoDbTable_disappears(t *testing.T) { var table1 dynamodb.DescribeTableOutput resourceName := "aws_dynamodb_table.test" - rName := acctest.RandomWithPrefix("TerraformTestTable-") + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -400,10 +444,10 @@ func TestAccAWSDynamoDbTable_disappears(t *testing.T) { }) } -func TestAccAWSDynamoDbTable_disappears_PayPerRequestWithGSI(t *testing.T) { +func TestAccAWSDynamoDbTable_disappears_payPerRequestWithGSI(t *testing.T) { var table1, table2 dynamodb.DescribeTableOutput resourceName := "aws_dynamodb_table.test" - rName := acctest.RandomWithPrefix("TerraformTestTable-") + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -412,7 +456,7 @@ func TestAccAWSDynamoDbTable_disappears_PayPerRequestWithGSI(t *testing.T) { CheckDestroy: testAccCheckAWSDynamoDbTableDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSDynamoDbBilling_PayPerRequestWithGSI(rName), + Config: testAccAWSDynamoDbBilling_payPerRequestWithGSI(rName), Check: resource.ComposeTestCheckFunc( testAccCheckInitialAWSDynamoDbTableExists(resourceName, &table1), testAccCheckResourceDisappears(testAccProvider, resourceAwsDynamoDbTable(), resourceName), @@ -420,7 +464,7 @@ func TestAccAWSDynamoDbTable_disappears_PayPerRequestWithGSI(t *testing.T) { ExpectNonEmptyPlan: true, }, { - Config: testAccAWSDynamoDbBilling_PayPerRequestWithGSI(rName), + Config: testAccAWSDynamoDbBilling_payPerRequestWithGSI(rName), Check: resource.ComposeTestCheckFunc( testAccCheckInitialAWSDynamoDbTableExists(resourceName, &table2), ), @@ -437,7 +481,7 @@ func TestAccAWSDynamoDbTable_disappears_PayPerRequestWithGSI(t *testing.T) { func TestAccAWSDynamoDbTable_extended(t *testing.T) { var conf dynamodb.DescribeTableOutput resourceName := "aws_dynamodb_table.test" - rName := acctest.RandomWithPrefix("TerraformTestTable-") + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -511,7 +555,7 @@ func TestAccAWSDynamoDbTable_extended(t *testing.T) { func TestAccAWSDynamoDbTable_enablePitr(t *testing.T) { var conf dynamodb.DescribeTableOutput resourceName := "aws_dynamodb_table.test" - rName := acctest.RandomWithPrefix("TerraformTestTable-") + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -543,10 +587,10 @@ func TestAccAWSDynamoDbTable_enablePitr(t *testing.T) { }) } -func TestAccAWSDynamoDbTable_BillingMode_PayPerRequestToProvisioned(t *testing.T) { +func TestAccAWSDynamoDbTable_BillingMode_payPerRequestToProvisioned(t *testing.T) { var conf dynamodb.DescribeTableOutput resourceName := "aws_dynamodb_table.test" - rName := acctest.RandomWithPrefix("TerraformTestTable-") + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -555,7 +599,7 @@ func TestAccAWSDynamoDbTable_BillingMode_PayPerRequestToProvisioned(t *testing.T CheckDestroy: testAccCheckAWSDynamoDbTableDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSDynamoDbBilling_PayPerRequest(rName), + Config: testAccAWSDynamoDbBilling_payPerRequest(rName), Check: resource.ComposeTestCheckFunc( testAccCheckInitialAWSDynamoDbTableExists(resourceName, &conf), resource.TestCheckResourceAttr(resourceName, "billing_mode", dynamodb.BillingModePayPerRequest), @@ -567,7 +611,7 @@ func TestAccAWSDynamoDbTable_BillingMode_PayPerRequestToProvisioned(t *testing.T ImportStateVerify: true, }, { - Config: testAccAWSDynamoDbBilling_Provisioned(rName), + Config: testAccAWSDynamoDbBilling_provisioned(rName), Check: resource.ComposeTestCheckFunc( testAccCheckInitialAWSDynamoDbTableExists(resourceName, &conf), resource.TestCheckResourceAttr(resourceName, "billing_mode", dynamodb.BillingModeProvisioned), @@ -577,10 +621,10 @@ func TestAccAWSDynamoDbTable_BillingMode_PayPerRequestToProvisioned(t *testing.T }) } -func TestAccAWSDynamoDbTable_BillingMode_ProvisionedToPayPerRequest(t *testing.T) { +func TestAccAWSDynamoDbTable_BillingMode_provisionedToPayPerRequest(t *testing.T) { var conf dynamodb.DescribeTableOutput resourceName := "aws_dynamodb_table.test" - rName := acctest.RandomWithPrefix("TerraformTestTable-") + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -589,7 +633,7 @@ func TestAccAWSDynamoDbTable_BillingMode_ProvisionedToPayPerRequest(t *testing.T CheckDestroy: testAccCheckAWSDynamoDbTableDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSDynamoDbBilling_Provisioned(rName), + Config: testAccAWSDynamoDbBilling_provisioned(rName), Check: resource.ComposeTestCheckFunc( testAccCheckInitialAWSDynamoDbTableExists(resourceName, &conf), resource.TestCheckResourceAttr(resourceName, "billing_mode", dynamodb.BillingModeProvisioned), @@ -601,7 +645,7 @@ func TestAccAWSDynamoDbTable_BillingMode_ProvisionedToPayPerRequest(t *testing.T ImportStateVerify: true, }, { - Config: testAccAWSDynamoDbBilling_PayPerRequest(rName), + Config: testAccAWSDynamoDbBilling_payPerRequest(rName), Check: resource.ComposeTestCheckFunc( testAccCheckInitialAWSDynamoDbTableExists(resourceName, &conf), resource.TestCheckResourceAttr(resourceName, "billing_mode", dynamodb.BillingModePayPerRequest), @@ -611,10 +655,10 @@ func TestAccAWSDynamoDbTable_BillingMode_ProvisionedToPayPerRequest(t *testing.T }) } -func TestAccAWSDynamoDbTable_BillingMode_GSI_PayPerRequestToProvisioned(t *testing.T) { +func TestAccAWSDynamoDbTable_BillingMode_GSI_payPerRequestToProvisioned(t *testing.T) { var conf dynamodb.DescribeTableOutput resourceName := "aws_dynamodb_table.test" - rName := acctest.RandomWithPrefix("TerraformTestTable-") + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -623,7 +667,7 @@ func TestAccAWSDynamoDbTable_BillingMode_GSI_PayPerRequestToProvisioned(t *testi CheckDestroy: testAccCheckAWSDynamoDbTableDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSDynamoDbBilling_PayPerRequestWithGSI(rName), + Config: testAccAWSDynamoDbBilling_payPerRequestWithGSI(rName), Check: resource.ComposeTestCheckFunc( testAccCheckInitialAWSDynamoDbTableExists(resourceName, &conf), resource.TestCheckResourceAttr(resourceName, "billing_mode", dynamodb.BillingModePayPerRequest), @@ -635,7 +679,7 @@ func TestAccAWSDynamoDbTable_BillingMode_GSI_PayPerRequestToProvisioned(t *testi ImportStateVerify: true, }, { - Config: testAccAWSDynamoDbBilling_ProvisionedWithGSI(rName), + Config: testAccAWSDynamoDbBilling_provisionedWithGSI(rName), Check: resource.ComposeTestCheckFunc( testAccCheckInitialAWSDynamoDbTableExists(resourceName, &conf), resource.TestCheckResourceAttr(resourceName, "billing_mode", dynamodb.BillingModeProvisioned), @@ -645,10 +689,10 @@ func TestAccAWSDynamoDbTable_BillingMode_GSI_PayPerRequestToProvisioned(t *testi }) } -func TestAccAWSDynamoDbTable_BillingMode_GSI_ProvisionedToPayPerRequest(t *testing.T) { +func TestAccAWSDynamoDbTable_BillingMode_GSI_provisionedToPayPerRequest(t *testing.T) { var conf dynamodb.DescribeTableOutput resourceName := "aws_dynamodb_table.test" - rName := acctest.RandomWithPrefix("TerraformTestTable-") + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -657,7 +701,7 @@ func TestAccAWSDynamoDbTable_BillingMode_GSI_ProvisionedToPayPerRequest(t *testi CheckDestroy: testAccCheckAWSDynamoDbTableDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSDynamoDbBilling_ProvisionedWithGSI(rName), + Config: testAccAWSDynamoDbBilling_provisionedWithGSI(rName), Check: resource.ComposeTestCheckFunc( testAccCheckInitialAWSDynamoDbTableExists(resourceName, &conf), resource.TestCheckResourceAttr(resourceName, "billing_mode", dynamodb.BillingModeProvisioned), @@ -669,7 +713,7 @@ func TestAccAWSDynamoDbTable_BillingMode_GSI_ProvisionedToPayPerRequest(t *testi ImportStateVerify: true, }, { - Config: testAccAWSDynamoDbBilling_PayPerRequestWithGSI(rName), + Config: testAccAWSDynamoDbBilling_payPerRequestWithGSI(rName), Check: resource.ComposeTestCheckFunc( testAccCheckInitialAWSDynamoDbTableExists(resourceName, &conf), resource.TestCheckResourceAttr(resourceName, "billing_mode", dynamodb.BillingModePayPerRequest), @@ -682,7 +726,7 @@ func TestAccAWSDynamoDbTable_BillingMode_GSI_ProvisionedToPayPerRequest(t *testi func TestAccAWSDynamoDbTable_streamSpecification(t *testing.T) { var conf dynamodb.DescribeTableOutput resourceName := "aws_dynamodb_table.test" - tableName := fmt.Sprintf("TerraformTestStreamTable-%s", acctest.RandString(8)) + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -691,12 +735,12 @@ func TestAccAWSDynamoDbTable_streamSpecification(t *testing.T) { CheckDestroy: testAccCheckAWSDynamoDbTableDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSDynamoDbConfigStreamSpecification(tableName, true, "KEYS_ONLY"), + Config: testAccAWSDynamoDbConfigStreamSpecification(rName, true, "KEYS_ONLY"), Check: resource.ComposeTestCheckFunc( testAccCheckInitialAWSDynamoDbTableExists(resourceName, &conf), resource.TestCheckResourceAttr(resourceName, "stream_enabled", "true"), resource.TestCheckResourceAttr(resourceName, "stream_view_type", "KEYS_ONLY"), - testAccMatchResourceAttrRegionalARN(resourceName, "stream_arn", "dynamodb", regexp.MustCompile(fmt.Sprintf("table/%s/stream", tableName))), + testAccMatchResourceAttrRegionalARN(resourceName, "stream_arn", "dynamodb", regexp.MustCompile(fmt.Sprintf("table/%s/stream", rName))), resource.TestCheckResourceAttrSet(resourceName, "stream_label"), ), }, @@ -706,12 +750,12 @@ func TestAccAWSDynamoDbTable_streamSpecification(t *testing.T) { ImportStateVerify: true, }, { - Config: testAccAWSDynamoDbConfigStreamSpecification(tableName, false, ""), + Config: testAccAWSDynamoDbConfigStreamSpecification(rName, false, ""), Check: resource.ComposeTestCheckFunc( testAccCheckInitialAWSDynamoDbTableExists(resourceName, &conf), resource.TestCheckResourceAttr(resourceName, "stream_enabled", "false"), resource.TestCheckResourceAttr(resourceName, "stream_view_type", ""), - testAccMatchResourceAttrRegionalARN(resourceName, "stream_arn", "dynamodb", regexp.MustCompile(fmt.Sprintf("table/%s/stream", tableName))), + testAccMatchResourceAttrRegionalARN(resourceName, "stream_arn", "dynamodb", regexp.MustCompile(fmt.Sprintf("table/%s/stream", rName))), resource.TestCheckResourceAttrSet(resourceName, "stream_label"), ), }, @@ -737,6 +781,7 @@ func TestAccAWSDynamoDbTable_streamSpecificationValidation(t *testing.T) { func TestAccAWSDynamoDbTable_tags(t *testing.T) { var conf dynamodb.DescribeTableOutput resourceName := "aws_dynamodb_table.test" + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -745,7 +790,7 @@ func TestAccAWSDynamoDbTable_tags(t *testing.T) { CheckDestroy: testAccCheckAWSDynamoDbTableDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSDynamoDbConfigTags(), + Config: testAccAWSDynamoDbConfigTags(rName), Check: resource.ComposeTestCheckFunc( testAccCheckInitialAWSDynamoDbTableExists(resourceName, &conf), testAccCheckInitialAWSDynamoDbTableConf(resourceName), @@ -765,7 +810,7 @@ func TestAccAWSDynamoDbTable_tags(t *testing.T) { func TestAccAWSDynamoDbTable_gsiUpdateCapacity(t *testing.T) { var conf dynamodb.DescribeTableOutput resourceName := "aws_dynamodb_table.test" - name := acctest.RandString(10) + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -774,7 +819,7 @@ func TestAccAWSDynamoDbTable_gsiUpdateCapacity(t *testing.T) { CheckDestroy: testAccCheckAWSDynamoDbTableDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSDynamoDbConfigGsiUpdate(name), + Config: testAccAWSDynamoDbConfigGsiUpdate(rName), Check: resource.ComposeTestCheckFunc( testAccCheckInitialAWSDynamoDbTableExists(resourceName, &conf), resource.TestCheckResourceAttr(resourceName, "global_secondary_index.#", "3"), @@ -801,7 +846,7 @@ func TestAccAWSDynamoDbTable_gsiUpdateCapacity(t *testing.T) { ImportStateVerify: true, }, { - Config: testAccAWSDynamoDbConfigGsiUpdatedCapacity(name), + Config: testAccAWSDynamoDbConfigGsiUpdatedCapacity(rName), Check: resource.ComposeTestCheckFunc( testAccCheckInitialAWSDynamoDbTableExists(resourceName, &conf), resource.TestCheckResourceAttr(resourceName, "global_secondary_index.#", "3"), @@ -829,7 +874,7 @@ func TestAccAWSDynamoDbTable_gsiUpdateCapacity(t *testing.T) { func TestAccAWSDynamoDbTable_gsiUpdateOtherAttributes(t *testing.T) { var conf dynamodb.DescribeTableOutput resourceName := "aws_dynamodb_table.test" - name := acctest.RandString(10) + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -838,7 +883,7 @@ func TestAccAWSDynamoDbTable_gsiUpdateOtherAttributes(t *testing.T) { CheckDestroy: testAccCheckAWSDynamoDbTableDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSDynamoDbConfigGsiUpdate(name), + Config: testAccAWSDynamoDbConfigGsiUpdate(rName), Check: resource.ComposeTestCheckFunc( testAccCheckInitialAWSDynamoDbTableExists(resourceName, &conf), resource.TestCheckResourceAttr(resourceName, "global_secondary_index.#", "3"), @@ -877,7 +922,7 @@ func TestAccAWSDynamoDbTable_gsiUpdateOtherAttributes(t *testing.T) { ImportStateVerify: true, }, { - Config: testAccAWSDynamoDbConfigGsiUpdatedOtherAttributes(name), + Config: testAccAWSDynamoDbConfigGsiUpdatedOtherAttributes(rName), Check: resource.ComposeTestCheckFunc( testAccCheckInitialAWSDynamoDbTableExists(resourceName, &conf), resource.TestCheckResourceAttr(resourceName, "global_secondary_index.#", "3"), @@ -954,7 +999,7 @@ func TestAccAWSDynamoDbTable_lsiNonKeyAttributes(t *testing.T) { func TestAccAWSDynamoDbTable_gsiUpdateNonKeyAttributes(t *testing.T) { var conf dynamodb.DescribeTableOutput resourceName := "aws_dynamodb_table.test" - name := acctest.RandString(10) + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -963,7 +1008,7 @@ func TestAccAWSDynamoDbTable_gsiUpdateNonKeyAttributes(t *testing.T) { CheckDestroy: testAccCheckAWSDynamoDbTableDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSDynamoDbConfigGsiUpdatedOtherAttributes(name), + Config: testAccAWSDynamoDbConfigGsiUpdatedOtherAttributes(rName), Check: resource.ComposeTestCheckFunc( testAccCheckInitialAWSDynamoDbTableExists(resourceName, &conf), resource.TestCheckResourceAttr(resourceName, "global_secondary_index.#", "3"), @@ -1003,7 +1048,7 @@ func TestAccAWSDynamoDbTable_gsiUpdateNonKeyAttributes(t *testing.T) { ImportStateVerify: true, }, { - Config: testAccAWSDynamoDbConfigGsiUpdatedNonKeyAttributes(name), + Config: testAccAWSDynamoDbConfigGsiUpdatedNonKeyAttributes(rName), Check: resource.ComposeTestCheckFunc( testAccCheckInitialAWSDynamoDbTableExists(resourceName, &conf), resource.TestCheckTypeSetElemNestedAttrs(resourceName, "global_secondary_index.*", map[string]string{ @@ -1045,7 +1090,7 @@ func TestAccAWSDynamoDbTable_gsiUpdateNonKeyAttributes(t *testing.T) { func TestAccAWSDynamoDbTable_gsiUpdateNonKeyAttributes_emptyPlan(t *testing.T) { var conf dynamodb.DescribeTableOutput resourceName := "aws_dynamodb_table.test" - name := acctest.RandString(10) + rName := acctest.RandomWithPrefix("tf-acc-test") attributes := fmt.Sprintf("%q, %q", "AnotherAttribute", "RandomAttribute") reorderedAttributes := fmt.Sprintf("%q, %q", "RandomAttribute", "AnotherAttribute") @@ -1056,7 +1101,7 @@ func TestAccAWSDynamoDbTable_gsiUpdateNonKeyAttributes_emptyPlan(t *testing.T) { CheckDestroy: testAccCheckAWSDynamoDbTableDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSDynamoDbConfigGsiMultipleNonKeyAttributes(name, attributes), + Config: testAccAWSDynamoDbConfigGsiMultipleNonKeyAttributes(rName, attributes), Check: resource.ComposeTestCheckFunc( testAccCheckInitialAWSDynamoDbTableExists(resourceName, &conf), resource.TestCheckTypeSetElemNestedAttrs(resourceName, "global_secondary_index.*", map[string]string{ @@ -1078,7 +1123,7 @@ func TestAccAWSDynamoDbTable_gsiUpdateNonKeyAttributes_emptyPlan(t *testing.T) { ImportStateVerify: true, }, { - Config: testAccAWSDynamoDbConfigGsiMultipleNonKeyAttributes(name, reorderedAttributes), + Config: testAccAWSDynamoDbConfigGsiMultipleNonKeyAttributes(rName, reorderedAttributes), PlanOnly: true, ExpectNonEmptyPlan: false, }, @@ -1088,10 +1133,10 @@ func TestAccAWSDynamoDbTable_gsiUpdateNonKeyAttributes_emptyPlan(t *testing.T) { // TTL tests must be split since it can only be updated once per hour // ValidationException: Time to live has been modified multiple times within a fixed interval -func TestAccAWSDynamoDbTable_Ttl_Enabled(t *testing.T) { +func TestAccAWSDynamoDbTable_Ttl_enabled(t *testing.T) { var table dynamodb.DescribeTableOutput resourceName := "aws_dynamodb_table.test" - rName := acctest.RandomWithPrefix("TerraformTestTable-") + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -1118,10 +1163,10 @@ func TestAccAWSDynamoDbTable_Ttl_Enabled(t *testing.T) { // TTL tests must be split since it can only be updated once per hour // ValidationException: Time to live has been modified multiple times within a fixed interval -func TestAccAWSDynamoDbTable_Ttl_Disabled(t *testing.T) { +func TestAccAWSDynamoDbTable_Ttl_disabled(t *testing.T) { var table dynamodb.DescribeTableOutput resourceName := "aws_dynamodb_table.test" - rName := acctest.RandomWithPrefix("TerraformTestTable-") + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -1157,7 +1202,7 @@ func TestAccAWSDynamoDbTable_Ttl_Disabled(t *testing.T) { func TestAccAWSDynamoDbTable_attributeUpdate(t *testing.T) { var conf dynamodb.DescribeTableOutput resourceName := "aws_dynamodb_table.test" - rName := acctest.RandomWithPrefix("TerraformTestTable-") + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -1201,7 +1246,7 @@ func TestAccAWSDynamoDbTable_attributeUpdate(t *testing.T) { func TestAccAWSDynamoDbTable_lsiUpdate(t *testing.T) { var conf dynamodb.DescribeTableOutput resourceName := "aws_dynamodb_table.test" - rName := acctest.RandomWithPrefix("TerraformTestTable-") + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -1231,7 +1276,7 @@ func TestAccAWSDynamoDbTable_lsiUpdate(t *testing.T) { } func TestAccAWSDynamoDbTable_attributeUpdateValidation(t *testing.T) { - rName := acctest.RandomWithPrefix("TerraformTestTable-") + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -1241,15 +1286,15 @@ func TestAccAWSDynamoDbTable_attributeUpdateValidation(t *testing.T) { Steps: []resource.TestStep{ { Config: testAccAWSDynamoDbConfigOneAttribute(rName, "firstKey", "unusedKey", "S"), - ExpectError: regexp.MustCompile(`All attributes must be indexed. Unused attributes: \["unusedKey"\]`), + ExpectError: regexp.MustCompile(`attributes must be indexed. Unused attributes: \["unusedKey"\]`), }, { Config: testAccAWSDynamoDbConfigTwoAttributes(rName, "firstKey", "secondKey", "firstUnused", "N", "secondUnused", "S"), - ExpectError: regexp.MustCompile(`All attributes must be indexed. Unused attributes: \["firstUnused"\ \"secondUnused\"]`), + ExpectError: regexp.MustCompile(`attributes must be indexed. Unused attributes: \["firstUnused"\ \"secondUnused\"]`), }, { Config: testAccAWSDynamoDbConfigUnmatchedIndexes(rName, "firstUnused", "secondUnused"), - ExpectError: regexp.MustCompile(`All indexes must match a defined attribute. Unmatched indexes: \["firstUnused"\ \"secondUnused\"]`), + ExpectError: regexp.MustCompile(`indexes must match a defined attribute. Unmatched indexes:`), }, }, }) @@ -1258,7 +1303,7 @@ func TestAccAWSDynamoDbTable_attributeUpdateValidation(t *testing.T) { func TestAccAWSDynamoDbTable_encryption(t *testing.T) { var confBYOK, confEncEnabled, confEncDisabled dynamodb.DescribeTableOutput resourceName := "aws_dynamodb_table.test" - rName := acctest.RandomWithPrefix("TerraformTestTable-") + rName := acctest.RandomWithPrefix("tf-acc-test") kmsKeyResourceName := "aws_kms_key.test" kmsAliasDatasourceName := "data.aws_kms_alias.dynamodb" @@ -1314,6 +1359,131 @@ func TestAccAWSDynamoDbTable_encryption(t *testing.T) { }) } +func TestAccAWSDynamoDbTable_Replica_multiple(t *testing.T) { + var table dynamodb.DescribeTableOutput + var providers []*schema.Provider + resourceName := "aws_dynamodb_table.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccMultipleRegionPreCheck(t, 3) + }, + ErrorCheck: testAccErrorCheck(t, dynamodb.EndpointsID), + ProviderFactories: testAccProviderFactoriesMultipleRegion(&providers, 3), + CheckDestroy: testAccCheckAWSDynamoDbTableDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSDynamoDbTableConfigReplica2(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckInitialAWSDynamoDbTableExists(resourceName, &table), + resource.TestCheckResourceAttr(resourceName, "replica.#", "2"), + ), + }, + { + Config: testAccAWSDynamoDbTableConfigReplica2(rName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSDynamoDbTableConfigReplica0(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckInitialAWSDynamoDbTableExists(resourceName, &table), + resource.TestCheckResourceAttr(resourceName, "replica.#", "0"), + ), + }, + { + Config: testAccAWSDynamoDbTableConfigReplica2(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckInitialAWSDynamoDbTableExists(resourceName, &table), + resource.TestCheckResourceAttr(resourceName, "replica.#", "2"), + ), + }, + }, + }) +} + +func TestAccAWSDynamoDbTable_Replica_single(t *testing.T) { + var conf dynamodb.DescribeTableOutput + var providers []*schema.Provider + resourceName := "aws_dynamodb_table.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccMultipleRegionPreCheck(t, 2) + }, + ErrorCheck: testAccErrorCheck(t, dynamodb.EndpointsID), + ProviderFactories: testAccProviderFactoriesMultipleRegion(&providers, 3), // 3 due to shared test configuration + CheckDestroy: testAccCheckAWSDynamoDbTableDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSDynamoDbTableConfigReplica1(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckInitialAWSDynamoDbTableExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "replica.#", "1"), + ), + }, + { + Config: testAccAWSDynamoDbTableConfigReplica1(rName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSDynamoDbTableConfigReplica0(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckInitialAWSDynamoDbTableExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "replica.#", "0"), + ), + }, + { + Config: testAccAWSDynamoDbTableConfigReplica1(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckInitialAWSDynamoDbTableExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "replica.#", "1"), + ), + }, + }, + }) +} + +func TestAccAWSDynamoDbTable_Replica_singleWithCMK(t *testing.T) { + var conf dynamodb.DescribeTableOutput + var providers []*schema.Provider + resourceName := "aws_dynamodb_table.test" + kmsKeyResourceName := "aws_kms_key.test" + // kmsAliasDatasourceName := "data.aws_kms_alias.master" + kmsKeyReplicaResourceName := "aws_kms_key.alt_test" + // kmsAliasReplicaDatasourceName := "data.aws_kms_alias.replica" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccMultipleRegionPreCheck(t, 2) + }, + ErrorCheck: testAccErrorCheck(t, dynamodb.EndpointsID), + ProviderFactories: testAccProviderFactoriesMultipleRegion(&providers, 3), // 3 due to shared test configuration + CheckDestroy: testAccCheckAWSDynamoDbTableDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSDynamoDbTableConfigReplicaWithCMK(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckInitialAWSDynamoDbTableExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "replica.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "replica.0.kms_key_arn", kmsKeyReplicaResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "server_side_encryption.0.enabled", "true"), + resource.TestCheckResourceAttrPair(resourceName, "server_side_encryption.0.kms_key_arn", kmsKeyResourceName, "arn"), + ), + }, + }, + }) +} + func testAccCheckAWSDynamoDbTableDestroy(s *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).dynamodbconn @@ -1417,108 +1587,16 @@ func testAccCheckInitialAWSDynamoDbTableConf(resourceName string) resource.TestC ) } -func TestAccAWSDynamoDbTable_Replica_Multiple(t *testing.T) { - var table dynamodb.DescribeTableOutput - var providers []*schema.Provider - resourceName := "aws_dynamodb_table.test" - tableName := acctest.RandomWithPrefix("TerraformTestTable-") - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { - testAccPreCheck(t) - testAccMultipleRegionPreCheck(t, 3) - }, - ErrorCheck: testAccErrorCheck(t, dynamodb.EndpointsID), - ProviderFactories: testAccProviderFactoriesMultipleRegion(&providers, 3), - CheckDestroy: testAccCheckAWSDynamoDbTableDestroy, - Steps: []resource.TestStep{ - { - Config: testAccAWSDynamoDbTableConfigReplica2(tableName), - Check: resource.ComposeTestCheckFunc( - testAccCheckInitialAWSDynamoDbTableExists(resourceName, &table), - resource.TestCheckResourceAttr(resourceName, "replica.#", "2"), - ), - }, - { - Config: testAccAWSDynamoDbTableConfigReplica2(tableName), - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - }, - { - Config: testAccAWSDynamoDbTableConfigReplica0(tableName), - Check: resource.ComposeTestCheckFunc( - testAccCheckInitialAWSDynamoDbTableExists(resourceName, &table), - resource.TestCheckResourceAttr(resourceName, "replica.#", "0"), - ), - }, - { - Config: testAccAWSDynamoDbTableConfigReplica2(tableName), - Check: resource.ComposeTestCheckFunc( - testAccCheckInitialAWSDynamoDbTableExists(resourceName, &table), - resource.TestCheckResourceAttr(resourceName, "replica.#", "2"), - ), - }, - }, - }) -} - -func TestAccAWSDynamoDbTable_Replica_Single(t *testing.T) { - var conf dynamodb.DescribeTableOutput - var providers []*schema.Provider - resourceName := "aws_dynamodb_table.test" - tableName := acctest.RandomWithPrefix("TerraformTestTable-") - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { - testAccPreCheck(t) - testAccMultipleRegionPreCheck(t, 2) - }, - ErrorCheck: testAccErrorCheck(t, dynamodb.EndpointsID), - ProviderFactories: testAccProviderFactoriesMultipleRegion(&providers, 3), // 3 due to shared test configuration - CheckDestroy: testAccCheckAWSDynamoDbTableDestroy, - Steps: []resource.TestStep{ - { - Config: testAccAWSDynamoDbTableConfigReplica1(tableName), - Check: resource.ComposeTestCheckFunc( - testAccCheckInitialAWSDynamoDbTableExists(resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "replica.#", "1"), - ), - }, - { - Config: testAccAWSDynamoDbTableConfigReplica1(tableName), - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - }, - { - Config: testAccAWSDynamoDbTableConfigReplica0(tableName), - Check: resource.ComposeTestCheckFunc( - testAccCheckInitialAWSDynamoDbTableExists(resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "replica.#", "0"), - ), - }, - { - Config: testAccAWSDynamoDbTableConfigReplica1(tableName), - Check: resource.ComposeTestCheckFunc( - testAccCheckInitialAWSDynamoDbTableExists(resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "replica.#", "1"), - ), - }, - }, - }) -} - func testAccAWSDynamoDbConfig_basic(rName string) string { return fmt.Sprintf(` resource "aws_dynamodb_table" "test" { - name = "%s" + name = %[1]q read_capacity = 1 write_capacity = 1 - hash_key = "TestTableHashKey" + hash_key = %[1]q attribute { - name = "TestTableHashKey" + name = %[1]q type = "S" } } @@ -1528,7 +1606,7 @@ resource "aws_dynamodb_table" "test" { func testAccAWSDynamoDbConfig_backup(rName string) string { return fmt.Sprintf(` resource "aws_dynamodb_table" "test" { - name = "%s" + name = %[1]q read_capacity = 1 write_capacity = 1 hash_key = "TestTableHashKey" @@ -1545,10 +1623,10 @@ resource "aws_dynamodb_table" "test" { `, rName) } -func testAccAWSDynamoDbBilling_PayPerRequest(rName string) string { +func testAccAWSDynamoDbBilling_payPerRequest(rName string) string { return fmt.Sprintf(` resource "aws_dynamodb_table" "test" { - name = "%s" + name = %[1]q billing_mode = "PAY_PER_REQUEST" hash_key = "TestTableHashKey" @@ -1560,10 +1638,10 @@ resource "aws_dynamodb_table" "test" { `, rName) } -func testAccAWSDynamoDbBilling_Provisioned(rName string) string { +func testAccAWSDynamoDbBilling_provisioned(rName string) string { return fmt.Sprintf(` resource "aws_dynamodb_table" "test" { - name = "%s" + name = %[1]q billing_mode = "PROVISIONED" hash_key = "TestTableHashKey" @@ -1578,10 +1656,10 @@ resource "aws_dynamodb_table" "test" { `, rName) } -func testAccAWSDynamoDbBilling_PayPerRequestWithGSI(rName string) string { +func testAccAWSDynamoDbBilling_payPerRequestWithGSI(rName string) string { return fmt.Sprintf(` resource "aws_dynamodb_table" "test" { - name = "%s" + name = %[1]q billing_mode = "PAY_PER_REQUEST" hash_key = "TestTableHashKey" @@ -1604,12 +1682,12 @@ resource "aws_dynamodb_table" "test" { `, rName) } -func testAccAWSDynamoDbBilling_ProvisionedWithGSI(rName string) string { +func testAccAWSDynamoDbBilling_provisionedWithGSI(rName string) string { return fmt.Sprintf(` resource "aws_dynamodb_table" "test" { billing_mode = "PROVISIONED" hash_key = "TestTableHashKey" - name = %q + name = %[1]q read_capacity = 1 write_capacity = 1 @@ -1637,7 +1715,7 @@ resource "aws_dynamodb_table" "test" { func testAccAWSDynamoDbConfigInitialState(rName string) string { return fmt.Sprintf(` resource "aws_dynamodb_table" "test" { - name = "%s" + name = %[1]q read_capacity = 1 write_capacity = 2 hash_key = "TestTableHashKey" @@ -1688,11 +1766,11 @@ data "aws_kms_alias" "dynamodb" { } resource "aws_kms_key" "test" { - description = "DynamoDbTest" + description = %[1]q } resource "aws_dynamodb_table" "test" { - name = "%s" + name = %[1]q read_capacity = 1 write_capacity = 1 hash_key = "TestTableHashKey" @@ -1703,7 +1781,7 @@ resource "aws_dynamodb_table" "test" { } server_side_encryption { - enabled = %t + enabled = %[2]t } } `, rName, enabled) @@ -1712,11 +1790,11 @@ resource "aws_dynamodb_table" "test" { func testAccAWSDynamoDbConfigInitialStateWithEncryptionBYOK(rName string) string { return fmt.Sprintf(` resource "aws_kms_key" "test" { - description = "DynamoDbTest" + description = %[1]q } resource "aws_dynamodb_table" "test" { - name = "%s" + name = %[1]q read_capacity = 2 write_capacity = 2 hash_key = "TestTableHashKey" @@ -1737,7 +1815,7 @@ resource "aws_dynamodb_table" "test" { func testAccAWSDynamoDbConfigAddSecondaryGSI(rName string) string { return fmt.Sprintf(` resource "aws_dynamodb_table" "test" { - name = "%s" + name = %[1]q read_capacity = 2 write_capacity = 2 hash_key = "TestTableHashKey" @@ -1782,10 +1860,10 @@ resource "aws_dynamodb_table" "test" { `, rName) } -func testAccAWSDynamoDbConfigStreamSpecification(tableName string, enabled bool, viewType string) string { +func testAccAWSDynamoDbConfigStreamSpecification(rName string, enabled bool, viewType string) string { return fmt.Sprintf(` resource "aws_dynamodb_table" "test" { - name = "%s" + name = %[1]q read_capacity = 1 write_capacity = 2 hash_key = "TestTableHashKey" @@ -1795,16 +1873,16 @@ resource "aws_dynamodb_table" "test" { type = "S" } - stream_enabled = %t - stream_view_type = "%s" + stream_enabled = %[2]t + stream_view_type = %[3]q } -`, tableName, enabled, viewType) +`, rName, enabled, viewType) } -func testAccAWSDynamoDbConfigTags() string { +func testAccAWSDynamoDbConfigTags(rName string) string { return fmt.Sprintf(` resource "aws_dynamodb_table" "test" { - name = "TerraformTestTable-%d" + name = %[1]q read_capacity = 1 write_capacity = 2 hash_key = "TestTableHashKey" @@ -1846,22 +1924,22 @@ resource "aws_dynamodb_table" "test" { } tags = { - Name = "terraform-test-table-%d" + Name = %[1]q AccTest = "yes" Testing = "absolutely" } } -`, acctest.RandInt(), acctest.RandInt()) +`, rName) } -func testAccAWSDynamoDbConfigGsiUpdate(name string) string { +func testAccAWSDynamoDbConfigGsiUpdate(rName string) string { return fmt.Sprintf(` variable "capacity" { default = 1 } resource "aws_dynamodb_table" "test" { - name = "tf-acc-test-%s" + name = %[1]q read_capacity = var.capacity write_capacity = var.capacity hash_key = "id" @@ -1910,17 +1988,17 @@ resource "aws_dynamodb_table" "test" { projection_type = "ALL" } } -`, name) +`, rName) } -func testAccAWSDynamoDbConfigGsiUpdatedCapacity(name string) string { +func testAccAWSDynamoDbConfigGsiUpdatedCapacity(rName string) string { return fmt.Sprintf(` variable "capacity" { default = 2 } resource "aws_dynamodb_table" "test" { - name = "tf-acc-test-%s" + name = %[1]q read_capacity = var.capacity write_capacity = var.capacity hash_key = "id" @@ -1969,17 +2047,17 @@ resource "aws_dynamodb_table" "test" { projection_type = "ALL" } } -`, name) +`, rName) } -func testAccAWSDynamoDbConfigGsiUpdatedOtherAttributes(name string) string { +func testAccAWSDynamoDbConfigGsiUpdatedOtherAttributes(rName string) string { return fmt.Sprintf(` variable "capacity" { default = 1 } resource "aws_dynamodb_table" "test" { - name = "tf-acc-test-%s" + name = %[1]q read_capacity = var.capacity write_capacity = var.capacity hash_key = "id" @@ -2036,17 +2114,17 @@ resource "aws_dynamodb_table" "test" { non_key_attributes = ["RandomAttribute"] } } -`, name) +`, rName) } -func testAccAWSDynamoDbConfigGsiUpdatedNonKeyAttributes(name string) string { +func testAccAWSDynamoDbConfigGsiUpdatedNonKeyAttributes(rName string) string { return fmt.Sprintf(` variable "capacity" { default = 1 } resource "aws_dynamodb_table" "test" { - name = "tf-acc-test-%s" + name = %[1]q read_capacity = var.capacity write_capacity = var.capacity hash_key = "id" @@ -2103,17 +2181,17 @@ resource "aws_dynamodb_table" "test" { non_key_attributes = ["RandomAttribute", "AnotherAttribute"] } } -`, name) +`, rName) } -func testAccAWSDynamoDbConfigGsiMultipleNonKeyAttributes(name, attributes string) string { +func testAccAWSDynamoDbConfigGsiMultipleNonKeyAttributes(rName, attributes string) string { return fmt.Sprintf(` variable "capacity" { default = 1 } resource "aws_dynamodb_table" "test" { - name = "tf-acc-test-%s" + name = %[1]q read_capacity = var.capacity write_capacity = var.capacity hash_key = "id" @@ -2143,13 +2221,13 @@ resource "aws_dynamodb_table" "test" { non_key_attributes = [%s] } } -`, name, attributes) +`, rName, attributes) } -func testAccAWSDynamoDbConfigLsiNonKeyAttributes(name string) string { +func testAccAWSDynamoDbConfigLsiNonKeyAttributes(rName string) string { return fmt.Sprintf(` resource "aws_dynamodb_table" "test" { - name = "%s" + name = %[1]q hash_key = "TestTableHashKey" range_key = "TestTableRangeKey" write_capacity = 1 @@ -2177,7 +2255,7 @@ resource "aws_dynamodb_table" "test" { non_key_attributes = ["TestNonKeyAttribute"] } } -`, name) +`, rName) } func testAccAWSDynamoDbConfigTimeToLive(rName string, ttlEnabled bool) string { @@ -2204,7 +2282,7 @@ resource "aws_dynamodb_table" "test" { func testAccAWSDynamoDbConfigOneAttribute(rName, hashKey, attrName, attrType string) string { return fmt.Sprintf(` resource "aws_dynamodb_table" "test" { - name = "%[1]s" + name = %[1]q read_capacity = 10 write_capacity = 10 hash_key = "staticHashKey" @@ -2215,13 +2293,13 @@ resource "aws_dynamodb_table" "test" { } attribute { - name = "%[3]s" - type = "%[4]s" + name = %[3]q + type = %[4]q } global_secondary_index { name = "gsiName" - hash_key = "%[2]s" + hash_key = %[2]q write_capacity = 10 read_capacity = 10 projection_type = "KEYS_ONLY" @@ -2233,7 +2311,7 @@ resource "aws_dynamodb_table" "test" { func testAccAWSDynamoDbConfigTwoAttributes(rName, hashKey, rangeKey, attrName1, attrType1, attrName2, attrType2 string) string { return fmt.Sprintf(` resource "aws_dynamodb_table" "test" { - name = "%[1]s" + name = %[1]q read_capacity = 10 write_capacity = 10 hash_key = "staticHashKey" @@ -2244,19 +2322,19 @@ resource "aws_dynamodb_table" "test" { } attribute { - name = "%[4]s" - type = "%[5]s" + name = %[4]q + type = %[5]q } attribute { - name = "%[6]s" - type = "%[7]s" + name = %[6]q + type = %[7]q } global_secondary_index { name = "gsiName" - hash_key = "%[2]s" - range_key = "%[3]s" + hash_key = %[2]q + range_key = %[3]q write_capacity = 10 read_capacity = 10 projection_type = "KEYS_ONLY" @@ -2334,6 +2412,54 @@ resource "aws_dynamodb_table" "test" { `, rName)) } +func testAccAWSDynamoDbTableConfigReplicaWithCMK(rName string) string { + return composeConfig( + testAccMultipleRegionProviderConfig(3), // Prevent "Provider configuration not present" errors + fmt.Sprintf(` +data "aws_region" "alternate" { + provider = "awsalternate" +} + +resource "aws_kms_key" "test" { + description = %[1]q +} + +resource "aws_kms_key" "alt_test" { + provider = "awsalternate" + description = "%[1]s-2" +} + +resource "aws_dynamodb_table" "test" { + name = %[1]q + hash_key = "TestTableHashKey" + billing_mode = "PAY_PER_REQUEST" + stream_enabled = true + stream_view_type = "NEW_AND_OLD_IMAGES" + + attribute { + name = "TestTableHashKey" + type = "S" + } + + replica { + region_name = data.aws_region.alternate.name + kms_key_arn = aws_kms_key.alt_test.arn + } + + server_side_encryption { + enabled = true + kms_key_arn = aws_kms_key.test.arn + } + + timeouts { + create = "20m" + update = "20m" + delete = "20m" + } +} +`, rName)) +} + func testAccAWSDynamoDbTableConfigReplica2(rName string) string { return composeConfig( testAccMultipleRegionProviderConfig(3), @@ -2372,7 +2498,7 @@ resource "aws_dynamodb_table" "test" { func testAccAWSDynamoDbConfigLSI(rName, lsiName string) string { return fmt.Sprintf(` resource "aws_dynamodb_table" "test" { - name = "%s" + name = %[1]q read_capacity = 10 write_capacity = 10 hash_key = "staticHashKey" @@ -2394,7 +2520,7 @@ resource "aws_dynamodb_table" "test" { } local_secondary_index { - name = "%s" + name = %[2]q range_key = "staticLSIRangeKey" projection_type = "KEYS_ONLY" } diff --git a/aws/structure.go b/aws/structure.go index 29d650334a3..a605d9d3c77 100644 --- a/aws/structure.go +++ b/aws/structure.go @@ -3647,114 +3647,6 @@ func flattenResourceLifecycleConfig(rlc *elasticbeanstalk.ApplicationResourceLif return result } -func diffDynamoDbGSI(oldGsi, newGsi []interface{}, billingMode string) (ops []*dynamodb.GlobalSecondaryIndexUpdate, e error) { - // Transform slices into maps - oldGsis := make(map[string]interface{}) - for _, gsidata := range oldGsi { - m := gsidata.(map[string]interface{}) - oldGsis[m["name"].(string)] = m - } - newGsis := make(map[string]interface{}) - for _, gsidata := range newGsi { - m := gsidata.(map[string]interface{}) - // validate throughput input early, to avoid unnecessary processing - if e = validateDynamoDbProvisionedThroughput(m, billingMode); e != nil { - return - } - newGsis[m["name"].(string)] = m - } - - for _, data := range newGsi { - newMap := data.(map[string]interface{}) - newName := newMap["name"].(string) - - if _, exists := oldGsis[newName]; !exists { - m := data.(map[string]interface{}) - idxName := m["name"].(string) - - ops = append(ops, &dynamodb.GlobalSecondaryIndexUpdate{ - Create: &dynamodb.CreateGlobalSecondaryIndexAction{ - IndexName: aws.String(idxName), - KeySchema: expandDynamoDbKeySchema(m), - ProvisionedThroughput: expandDynamoDbProvisionedThroughput(m, billingMode), - Projection: expandDynamoDbProjection(m), - }, - }) - } - } - - for _, data := range oldGsi { - oldMap := data.(map[string]interface{}) - oldName := oldMap["name"].(string) - - newData, exists := newGsis[oldName] - if exists { - newMap := newData.(map[string]interface{}) - idxName := newMap["name"].(string) - - oldWriteCapacity, oldReadCapacity := oldMap["write_capacity"].(int), oldMap["read_capacity"].(int) - newWriteCapacity, newReadCapacity := newMap["write_capacity"].(int), newMap["read_capacity"].(int) - capacityChanged := (oldWriteCapacity != newWriteCapacity || oldReadCapacity != newReadCapacity) - - // pluck non_key_attributes from oldAttributes and newAttributes as reflect.DeepEquals will compare - // ordinal of elements in its equality (which we actually don't care about) - nonKeyAttributesChanged := checkIfNonKeyAttributesChanged(oldMap, newMap) - - oldAttributes, err := stripCapacityAttributes(oldMap) - if err != nil { - return ops, err - } - oldAttributes, err = stripNonKeyAttributes(oldAttributes) - if err != nil { - return ops, err - } - newAttributes, err := stripCapacityAttributes(newMap) - if err != nil { - return ops, err - } - newAttributes, err = stripNonKeyAttributes(newAttributes) - if err != nil { - return ops, err - } - otherAttributesChanged := nonKeyAttributesChanged || !reflect.DeepEqual(oldAttributes, newAttributes) - - if capacityChanged && !otherAttributesChanged { - update := &dynamodb.GlobalSecondaryIndexUpdate{ - Update: &dynamodb.UpdateGlobalSecondaryIndexAction{ - IndexName: aws.String(idxName), - ProvisionedThroughput: expandDynamoDbProvisionedThroughput(newMap, billingMode), - }, - } - ops = append(ops, update) - } else if otherAttributesChanged { - // Other attributes cannot be updated - ops = append(ops, &dynamodb.GlobalSecondaryIndexUpdate{ - Delete: &dynamodb.DeleteGlobalSecondaryIndexAction{ - IndexName: aws.String(idxName), - }, - }) - - ops = append(ops, &dynamodb.GlobalSecondaryIndexUpdate{ - Create: &dynamodb.CreateGlobalSecondaryIndexAction{ - IndexName: aws.String(idxName), - KeySchema: expandDynamoDbKeySchema(newMap), - ProvisionedThroughput: expandDynamoDbProvisionedThroughput(newMap, billingMode), - Projection: expandDynamoDbProjection(newMap), - }, - }) - } - } else { - idxName := oldName - ops = append(ops, &dynamodb.GlobalSecondaryIndexUpdate{ - Delete: &dynamodb.DeleteGlobalSecondaryIndexAction{ - IndexName: aws.String(idxName), - }, - }) - } - } - return ops, nil -} - func stripNonKeyAttributes(in map[string]interface{}) (map[string]interface{}, error) { mapCopy, err := copystructure.Copy(in) if err != nil { @@ -3796,319 +3688,6 @@ func stripCapacityAttributes(in map[string]interface{}) (map[string]interface{}, // Expanders + flatteners -func flattenDynamoDbTtl(ttlOutput *dynamodb.DescribeTimeToLiveOutput) []interface{} { - m := map[string]interface{}{ - "enabled": false, - } - - if ttlOutput == nil || ttlOutput.TimeToLiveDescription == nil { - return []interface{}{m} - } - - ttlDesc := ttlOutput.TimeToLiveDescription - - m["attribute_name"] = aws.StringValue(ttlDesc.AttributeName) - m["enabled"] = (aws.StringValue(ttlDesc.TimeToLiveStatus) == dynamodb.TimeToLiveStatusEnabled) - - return []interface{}{m} -} - -func flattenDynamoDbPitr(pitrDesc *dynamodb.DescribeContinuousBackupsOutput) []interface{} { - m := map[string]interface{}{ - "enabled": false, - } - - if pitrDesc == nil { - return []interface{}{m} - } - - if pitrDesc.ContinuousBackupsDescription != nil { - pitr := pitrDesc.ContinuousBackupsDescription.PointInTimeRecoveryDescription - if pitr != nil { - m["enabled"] = (*pitr.PointInTimeRecoveryStatus == dynamodb.PointInTimeRecoveryStatusEnabled) - } - } - - return []interface{}{m} -} - -func flattenAwsDynamoDbReplicaDescriptions(apiObjects []*dynamodb.ReplicaDescription) []interface{} { - if len(apiObjects) == 0 { - return nil - } - - var tfList []interface{} - - for _, apiObject := range apiObjects { - tfMap := map[string]interface{}{ - "region_name": aws.StringValue(apiObject.RegionName), - } - - tfList = append(tfList, tfMap) - } - - return tfList -} - -func flattenAwsDynamoDbTableResource(d *schema.ResourceData, table *dynamodb.TableDescription) error { - d.Set("billing_mode", dynamodb.BillingModeProvisioned) - if table.BillingModeSummary != nil { - d.Set("billing_mode", table.BillingModeSummary.BillingMode) - } - - d.Set("write_capacity", table.ProvisionedThroughput.WriteCapacityUnits) - d.Set("read_capacity", table.ProvisionedThroughput.ReadCapacityUnits) - - attributes := make([]interface{}, len(table.AttributeDefinitions)) - for i, attrdef := range table.AttributeDefinitions { - attributes[i] = map[string]string{ - "name": aws.StringValue(attrdef.AttributeName), - "type": aws.StringValue(attrdef.AttributeType), - } - } - - d.Set("attribute", attributes) - d.Set("name", table.TableName) - - for _, attribute := range table.KeySchema { - if aws.StringValue(attribute.KeyType) == dynamodb.KeyTypeHash { - d.Set("hash_key", attribute.AttributeName) - } - - if aws.StringValue(attribute.KeyType) == dynamodb.KeyTypeRange { - d.Set("range_key", attribute.AttributeName) - } - } - - lsiList := make([]map[string]interface{}, 0, len(table.LocalSecondaryIndexes)) - for _, lsiObject := range table.LocalSecondaryIndexes { - lsi := map[string]interface{}{ - "name": aws.StringValue(lsiObject.IndexName), - "projection_type": aws.StringValue(lsiObject.Projection.ProjectionType), - } - - for _, attribute := range lsiObject.KeySchema { - if aws.StringValue(attribute.KeyType) == dynamodb.KeyTypeRange { - lsi["range_key"] = aws.StringValue(attribute.AttributeName) - } - } - nkaList := make([]string, len(lsiObject.Projection.NonKeyAttributes)) - for i, nka := range lsiObject.Projection.NonKeyAttributes { - nkaList[i] = aws.StringValue(nka) - } - lsi["non_key_attributes"] = nkaList - - lsiList = append(lsiList, lsi) - } - - err := d.Set("local_secondary_index", lsiList) - if err != nil { - return err - } - - gsiList := make([]map[string]interface{}, len(table.GlobalSecondaryIndexes)) - for i, gsiObject := range table.GlobalSecondaryIndexes { - gsi := map[string]interface{}{ - "write_capacity": aws.Int64Value(gsiObject.ProvisionedThroughput.WriteCapacityUnits), - "read_capacity": aws.Int64Value(gsiObject.ProvisionedThroughput.ReadCapacityUnits), - "name": aws.StringValue(gsiObject.IndexName), - } - - for _, attribute := range gsiObject.KeySchema { - if aws.StringValue(attribute.KeyType) == dynamodb.KeyTypeHash { - gsi["hash_key"] = aws.StringValue(attribute.AttributeName) - } - - if aws.StringValue(attribute.KeyType) == dynamodb.KeyTypeRange { - gsi["range_key"] = aws.StringValue(attribute.AttributeName) - } - } - - gsi["projection_type"] = aws.StringValue(gsiObject.Projection.ProjectionType) - - nonKeyAttrs := make([]string, len(gsiObject.Projection.NonKeyAttributes)) - for i, nonKeyAttr := range gsiObject.Projection.NonKeyAttributes { - nonKeyAttrs[i] = aws.StringValue(nonKeyAttr) - } - gsi["non_key_attributes"] = nonKeyAttrs - - gsiList[i] = gsi - } - - if table.StreamSpecification != nil { - d.Set("stream_view_type", table.StreamSpecification.StreamViewType) - d.Set("stream_enabled", table.StreamSpecification.StreamEnabled) - } else { - d.Set("stream_view_type", "") - d.Set("stream_enabled", false) - } - - d.Set("stream_arn", table.LatestStreamArn) - d.Set("stream_label", table.LatestStreamLabel) - - err = d.Set("global_secondary_index", gsiList) - if err != nil { - return err - } - - sseOptions := []map[string]interface{}{} - if sseDescription := table.SSEDescription; sseDescription != nil { - sseOptions = []map[string]interface{}{{ - "enabled": aws.StringValue(sseDescription.Status) == dynamodb.SSEStatusEnabled, - "kms_key_arn": aws.StringValue(sseDescription.KMSMasterKeyArn), - }} - } - err = d.Set("server_side_encryption", sseOptions) - if err != nil { - return err - } - - err = d.Set("replica", flattenAwsDynamoDbReplicaDescriptions(table.Replicas)) - if err != nil { - return err - } - - d.Set("arn", table.TableArn) - - return nil -} - -func expandDynamoDbAttributes(cfg []interface{}) []*dynamodb.AttributeDefinition { - attributes := make([]*dynamodb.AttributeDefinition, len(cfg)) - for i, attribute := range cfg { - attr := attribute.(map[string]interface{}) - attributes[i] = &dynamodb.AttributeDefinition{ - AttributeName: aws.String(attr["name"].(string)), - AttributeType: aws.String(attr["type"].(string)), - } - } - return attributes -} - -// TODO: Get rid of keySchemaM - the user should just explicitly define -// this in the config, we shouldn't magically be setting it like this. -// Removal will however require config change, hence BC. :/ -func expandDynamoDbLocalSecondaryIndexes(cfg []interface{}, keySchemaM map[string]interface{}) []*dynamodb.LocalSecondaryIndex { - indexes := make([]*dynamodb.LocalSecondaryIndex, len(cfg)) - for i, lsi := range cfg { - m := lsi.(map[string]interface{}) - idxName := m["name"].(string) - - // TODO: See https://github.com/hashicorp/terraform-provider-aws/issues/3176 - if _, ok := m["hash_key"]; !ok { - m["hash_key"] = keySchemaM["hash_key"] - } - - indexes[i] = &dynamodb.LocalSecondaryIndex{ - IndexName: aws.String(idxName), - KeySchema: expandDynamoDbKeySchema(m), - Projection: expandDynamoDbProjection(m), - } - } - return indexes -} - -func expandDynamoDbGlobalSecondaryIndex(data map[string]interface{}, billingMode string) *dynamodb.GlobalSecondaryIndex { - return &dynamodb.GlobalSecondaryIndex{ - IndexName: aws.String(data["name"].(string)), - KeySchema: expandDynamoDbKeySchema(data), - Projection: expandDynamoDbProjection(data), - ProvisionedThroughput: expandDynamoDbProvisionedThroughput(data, billingMode), - } -} - -func validateDynamoDbProvisionedThroughput(data map[string]interface{}, billingMode string) error { - // if billing mode is PAY_PER_REQUEST, don't need to validate the throughput settings - if billingMode == dynamodb.BillingModePayPerRequest { - return nil - } - - writeCapacity, writeCapacitySet := data["write_capacity"].(int) - readCapacity, readCapacitySet := data["read_capacity"].(int) - - if !writeCapacitySet || !readCapacitySet { - return fmt.Errorf("Read and Write capacity should be set when billing mode is %s", dynamodb.BillingModeProvisioned) - } - - if writeCapacity < 1 { - return fmt.Errorf("Write capacity must be > 0 when billing mode is %s", dynamodb.BillingModeProvisioned) - } - - if readCapacity < 1 { - return fmt.Errorf("Read capacity must be > 0 when billing mode is %s", dynamodb.BillingModeProvisioned) - } - - return nil -} - -func expandDynamoDbProvisionedThroughput(data map[string]interface{}, billingMode string) *dynamodb.ProvisionedThroughput { - - if billingMode == dynamodb.BillingModePayPerRequest { - return nil - } - - return &dynamodb.ProvisionedThroughput{ - WriteCapacityUnits: aws.Int64(int64(data["write_capacity"].(int))), - ReadCapacityUnits: aws.Int64(int64(data["read_capacity"].(int))), - } -} - -func expandDynamoDbProjection(data map[string]interface{}) *dynamodb.Projection { - projection := &dynamodb.Projection{ - ProjectionType: aws.String(data["projection_type"].(string)), - } - - if v, ok := data["non_key_attributes"].([]interface{}); ok && len(v) > 0 { - projection.NonKeyAttributes = expandStringList(v) - } - - if v, ok := data["non_key_attributes"].(*schema.Set); ok && v.Len() > 0 { - projection.NonKeyAttributes = expandStringSet(v) - } - - return projection -} - -func expandDynamoDbKeySchema(data map[string]interface{}) []*dynamodb.KeySchemaElement { - keySchema := []*dynamodb.KeySchemaElement{} - - if v, ok := data["hash_key"]; ok && v != nil && v != "" { - keySchema = append(keySchema, &dynamodb.KeySchemaElement{ - AttributeName: aws.String(v.(string)), - KeyType: aws.String(dynamodb.KeyTypeHash), - }) - } - - if v, ok := data["range_key"]; ok && v != nil && v != "" { - keySchema = append(keySchema, &dynamodb.KeySchemaElement{ - AttributeName: aws.String(v.(string)), - KeyType: aws.String(dynamodb.KeyTypeRange), - }) - } - - return keySchema -} - -func expandDynamoDbEncryptAtRestOptions(vOptions []interface{}) *dynamodb.SSESpecification { - options := &dynamodb.SSESpecification{} - - enabled := false - if len(vOptions) > 0 { - mOptions := vOptions[0].(map[string]interface{}) - - enabled = mOptions["enabled"].(bool) - if enabled { - if vKmsKeyArn, ok := mOptions["kms_key_arn"].(string); ok && vKmsKeyArn != "" { - options.KMSMasterKeyId = aws.String(vKmsKeyArn) - options.SSEType = aws.String(dynamodb.SSETypeKms) - } - } - } - options.Enabled = aws.Bool(enabled) - - return options -} - func expandDynamoDbTableItemAttributes(input string) (map[string]*dynamodb.AttributeValue, error) { var attributes map[string]*dynamodb.AttributeValue diff --git a/website/docs/r/dynamodb_table.html.markdown b/website/docs/r/dynamodb_table.html.markdown index cf88fc894e2..6aa1b9d39f7 100644 --- a/website/docs/r/dynamodb_table.html.markdown +++ b/website/docs/r/dynamodb_table.html.markdown @@ -165,6 +165,7 @@ The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/d The `replica` configuration block supports the following arguments: * `region_name` - (Required) Region name of the replica. +* `kms_key_arn` - (Optional) The ARN of the CMK that should be used for the AWS KMS encryption. #### `server_side_encryption`