Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

r/aws_dynamodb_table_gsi #22513

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changelog/22513.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```release-note:enhancement
resource/aws_dynamodb_table: Add `manage_index_as_own_resource` arguments.
```

```release-note:new-resource
aws_dynamodb_table_gsi
```
1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -1080,6 +1080,7 @@ func Provider() *schema.Provider {
"aws_dynamodb_table": dynamodb.ResourceTable(),
"aws_dynamodb_table_item": dynamodb.ResourceTableItem(),
"aws_dynamodb_tag": dynamodb.ResourceTag(),
"aws_dynamodb_table_gsi": dynamodb.ResourceAwsDynamoDbTableGsi(),

"aws_ami": ec2.ResourceAMI(),
"aws_ami_copy": ec2.ResourceAMICopy(),
Expand Down
356 changes: 356 additions & 0 deletions internal/service/dynamodb/gsi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,356 @@
package dynamodb

import (
"bytes"
"fmt"
"log"
"strings"
"time"

"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/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/hashicorp/terraform-provider-aws/internal/conns"
"github.com/hashicorp/terraform-provider-aws/internal/create"
"github.com/hashicorp/terraform-provider-aws/internal/flex"
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
)

func ResourceAwsDynamoDbTableGsi() *schema.Resource {
return &schema.Resource{
Create: resourceAwsDynamoDbTableGsiCreate,
Read: resourceAwsDynamoDbTableGsiRead,
Update: resourceAwsDynamoDbTableGsiUpdate,
Delete: resourceAwsDynamoDbTableGsiDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},

Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(10 * time.Minute),
Update: schema.DefaultTimeout(5 * time.Minute), // provisioned throughput changes only
Delete: schema.DefaultTimeout(10 * time.Minute),
},

Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"table_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,
},
"write_capacity": {
Type: schema.TypeInt,
Required: true,
},
"read_capacity": {
Type: schema.TypeInt,
Required: true,
},
"projection_type": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.StringInSlice(dynamodb.ProjectionType_Values(), false),
},
"non_key_attributes": {
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"billing_mode": {
Type: schema.TypeString,
Optional: true,
Default: dynamodb.BillingModeProvisioned,
ValidateFunc: validation.StringInSlice(dynamodb.BillingMode_Values(), false),
},
"attribute": {
Type: schema.TypeSet,
Optional: true,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
},
"type": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice([]string{
dynamodb.ScalarAttributeTypeB,
dynamodb.ScalarAttributeTypeN,
dynamodb.ScalarAttributeTypeS,
}, false),
},
},
},
Set: func(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%s-", m["name"].(string)))
return create.StringHashcode(buf.String())
},
},
},
}
}

func resourceAwsDynamoDbTableGsiCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*conns.AWSClient).DynamoDBConn

tableName := d.Get("table_name").(string)
indexName := d.Get("name").(string)
keySchemaMap := map[string]interface{}{
"hash_key": d.Get("hash_key").(string),
}
if v, ok := d.GetOk("range_key"); ok {
keySchemaMap["range_key"] = v.(string)
}

log.Printf("[DEBUG] Creating DynamoDB table index with key schema: %#v", keySchemaMap)
req := &dynamodb.UpdateTableInput{
TableName: aws.String(tableName),
}

projection := &dynamodb.Projection{
ProjectionType: aws.String(d.Get("projection_type").(string)),
}

if v, ok := d.Get("non_key_attributes").(*schema.Set); ok && len(v.List()) > 0 {
projection.NonKeyAttributes = flex.ExpandStringList(v.List())
}

capacityMap := map[string]interface{}{
"write_capacity": d.Get("write_capacity"),
"read_capacity": d.Get("read_capacity"),
}
billingMode := d.Get("billing_mode").(string)

createOp := &dynamodb.GlobalSecondaryIndexUpdate{
Create: &dynamodb.CreateGlobalSecondaryIndexAction{
IndexName: aws.String(indexName),
KeySchema: expandDynamoDbKeySchema(keySchemaMap),
Projection: projection,
ProvisionedThroughput: expandDynamoDbProvisionedThroughput(capacityMap, billingMode),
},
}

req.GlobalSecondaryIndexUpdates = []*dynamodb.GlobalSecondaryIndexUpdate{createOp}
if v, ok := d.GetOk("attribute"); ok {
aSet := v.(*schema.Set)
req.AttributeDefinitions = expandDynamoDbAttributes(aSet.List())
}

var output *dynamodb.UpdateTableOutput
err := resource.Retry(d.Timeout(schema.TimeoutCreate), func() *resource.RetryError {
var err error
output, err = conn.UpdateTable(req)
if err != nil {
if tfawserr.ErrMessageContains(err, "ThrottlingException", "") {
return resource.RetryableError(err)
}
if tfawserr.ErrMessageContains(err, dynamodb.ErrCodeResourceInUseException, "") {
return resource.RetryableError(err)
}
// Subscriber limit exceeded: Only 1 online index can be created or deleted simultaneously per table
if tfawserr.ErrMessageContains(err, dynamodb.ErrCodeLimitExceededException, "simultaneously") {
return resource.RetryableError(err)
}

return resource.NonRetryableError(err)
}
return nil
})
if tfresource.TimedOut(err) {
return fmt.Errorf(`Updating table timed out: %s`, err)
}
if err != nil {
return err
}

gsiDescription := findDynamoDbGsi(&output.TableDescription.GlobalSecondaryIndexes, indexName)
d.SetId(
combineDynamoGSIId(tableName, aws.StringValue(gsiDescription.IndexName)),
)

_, err = waitDynamoDBGSIActive(conn, tableName, aws.StringValue(gsiDescription.IndexName))
return err
}

func resourceAwsDynamoDbTableGsiUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*conns.AWSClient).DynamoDBConn

capacityMap := map[string]interface{}{
"write_capacity": d.Get("write_capacity"),
"read_capacity": d.Get("read_capacity"),
}
billingMode := d.Get("billing_mode").(string)
tableName, index := parseDynamoGSIId(d.Id())
req := &dynamodb.UpdateTableInput{
TableName: aws.String(tableName),
GlobalSecondaryIndexUpdates: []*dynamodb.GlobalSecondaryIndexUpdate{
{
Update: &dynamodb.UpdateGlobalSecondaryIndexAction{
IndexName: aws.String(index),
ProvisionedThroughput: expandDynamoDbProvisionedThroughput(capacityMap, billingMode),
},
},
},
}

err := resource.Retry(d.Timeout(schema.TimeoutUpdate), func() *resource.RetryError {
var err error
_, err = conn.UpdateTable(req)
if err != nil {
if tfawserr.ErrMessageContains(err, "ThrottlingException", "") {
return resource.RetryableError(err)
}
if tfawserr.ErrMessageContains(err, dynamodb.ErrCodeResourceInUseException, "") {
return resource.RetryableError(err)
}
// Subscriber limit exceeded: Only 1 online index can be created or deleted simultaneously per table
if tfawserr.ErrMessageContains(err, dynamodb.ErrCodeLimitExceededException, "simultaneously") {
return resource.RetryableError(err)
}
return resource.NonRetryableError(err)
}
return nil
})
if tfresource.TimedOut(err) {
return fmt.Errorf(`Updating table timed out: %s`, err)
}
if err != nil {
return err
}

_, err = waitDynamoDBGSIActive(conn, d.Get("table_name").(string), index)
return err
}

func resourceAwsDynamoDbTableGsiRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*conns.AWSClient).DynamoDBConn
tableName, index := parseDynamoGSIId(d.Id())

result, err := conn.DescribeTable(&dynamodb.DescribeTableInput{
TableName: aws.String(tableName),
})
if err != nil {
return err
}

gsi := findDynamoDbGsi(&result.Table.GlobalSecondaryIndexes, index)
d.Set("name", index)
d.Set("table_name", tableName)
d.Set("write_capacity", gsi.ProvisionedThroughput.WriteCapacityUnits)
d.Set("read_capacity", gsi.ProvisionedThroughput.ReadCapacityUnits)
d.Set("projection_type", gsi.Projection.ProjectionType)
d.Set("non_key_attributes", gsi.Projection.NonKeyAttributes)
if result.Table.BillingModeSummary != nil {
d.Set("billing_mode", result.Table.BillingModeSummary.BillingMode)
} else {
d.Set("billing_mode", dynamodb.BillingModeProvisioned)
}

gsiAttributeNames := make(map[string]struct{}, len(gsi.KeySchema))
for _, attribute := range gsi.KeySchema {
if aws.StringValue(attribute.KeyType) == dynamodb.KeyTypeHash {
d.Set("hash_key", attribute.AttributeName)
gsiAttributeNames[*attribute.AttributeName] = struct{}{}
}

if aws.StringValue(attribute.KeyType) == dynamodb.KeyTypeRange {
d.Set("range_key", attribute.AttributeName)
gsiAttributeNames[*attribute.AttributeName] = struct{}{}
}
}
attributes := []interface{}{}
for _, attrdef := range result.Table.AttributeDefinitions {
if _, ok := gsiAttributeNames[*attrdef.AttributeName]; ok {
attribute := map[string]string{
"name": *attrdef.AttributeName,
"type": *attrdef.AttributeType,
}
attributes = append(attributes, attribute)
}
}
d.Set("attribute", attributes)

return err
}

func resourceAwsDynamoDbTableGsiDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*conns.AWSClient).DynamoDBConn
tableName, index := parseDynamoGSIId(d.Id())

req := &dynamodb.UpdateTableInput{
TableName: &tableName,
GlobalSecondaryIndexUpdates: []*dynamodb.GlobalSecondaryIndexUpdate{
{
Delete: &dynamodb.DeleteGlobalSecondaryIndexAction{
IndexName: aws.String(index),
},
},
},
}

err := resource.Retry(d.Timeout(schema.TimeoutDelete), func() *resource.RetryError {
_, err := conn.UpdateTable(req)
if err != nil {
// Subscriber limit exceeded: Only 1 online index can be created or deleted simultaneously per table
if tfawserr.ErrMessageContains(err, dynamodb.ErrCodeLimitExceededException, "simultaneously") {
return resource.RetryableError(err)
}
if tfawserr.ErrMessageContains(err, dynamodb.ErrCodeResourceInUseException, "") {
return resource.RetryableError(err)
}
return resource.NonRetryableError(err)
}
return nil
})
if tfresource.TimedOut(err) {
return fmt.Errorf(`Updating table timed out: %s`, err)
}
if err != nil {
return err
}

err = waitDynamoDBGSIDeleted(conn, tableName, index)
return err
}

func findDynamoDbGsi(gsiList *[]*dynamodb.GlobalSecondaryIndexDescription, target string) *dynamodb.GlobalSecondaryIndexDescription {
for _, gsiObject := range *gsiList {
if aws.StringValue(gsiObject.IndexName) == target {
return gsiObject
}
}
return nil
}

func parseDynamoGSIId(id string) (string, string) {
parts := strings.Split(id, ":")
return parts[0], parts[1]
}

func combineDynamoGSIId(tableName string, index string) string {
return fmt.Sprintf("%s:%s", tableName, index)
}
Loading