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

New Resource: aws_dynamodb_global_table #2517

Merged
merged 4 commits into from
Jan 25, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ func Provider() terraform.ResourceProvider {
"aws_dx_lag": resourceAwsDxLag(),
"aws_dx_connection": resourceAwsDxConnection(),
"aws_dynamodb_table": resourceAwsDynamoDbTable(),
"aws_dynamodb_global_table": resourceAwsDynamoDbGlobalTable(),
"aws_ebs_snapshot": resourceAwsEbsSnapshot(),
"aws_ebs_volume": resourceAwsEbsVolume(),
"aws_ecr_lifecycle_policy": resourceAwsEcrLifecyclePolicy(),
Expand Down
321 changes: 321 additions & 0 deletions aws/resource_aws_dynamodb_global_table.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
package aws

import (
"fmt"
"log"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
)

func resourceAwsDynamoDbGlobalTable() *schema.Resource {
return &schema.Resource{
Create: resourceAwsDynamoDbGlobalTableCreate,
Read: resourceAwsDynamoDbGlobalTableRead,
Update: resourceAwsDynamoDbGlobalTableUpdate,
Delete: resourceAwsDynamoDbGlobalTableDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},

Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(1 * time.Minute),
Update: schema.DefaultTimeout(1 * time.Minute),
Delete: schema.DefaultTimeout(1 * time.Minute),
},

Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validateAwsDynamoDbGlobalTableName,
},

"replica": {
Type: schema.TypeSet,
Required: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"region_name": {
Type: schema.TypeString,
Required: true,
},
},
},
},

"arn": {
Type: schema.TypeString,
Computed: true,
},
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you think it is worth having ReplicationGroup as an attribute also?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I felt the extra attribute nesting to strictly follow the API would've made the resource needlessly complex here and opted for the Set of replica. If you feel like it should be nested under a replication_group, please let me know and I'll add that in.

},
}
}

func resourceAwsDynamoDbGlobalTableCreate(d *schema.ResourceData, meta interface{}) error {
dynamodbconn := meta.(*AWSClient).dynamodbconn

globalTableName := d.Get("name").(string)

input := &dynamodb.CreateGlobalTableInput{
GlobalTableName: aws.String(globalTableName),
ReplicationGroup: expandAwsDynamoDbReplicas(d.Get("replica").(*schema.Set).List()),
}

log.Printf("[DEBUG] Creating DynamoDB Global Table: %#v", input)
_, err := dynamodbconn.CreateGlobalTable(input)
if err != nil {
return err
}

d.SetId(globalTableName)

log.Println("[INFO] Waiting for DynamoDB Global Table to be created")
stateConf := &resource.StateChangeConf{
Pending: []string{
dynamodb.GlobalTableStatusCreating,
dynamodb.GlobalTableStatusDeleting,
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to handle this status when creating a table (and the updating one also)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added this as edge-case protection if we miss ACTIVE between check intervals for an immediate update or something like deletion happens outside Terraform during the creation.

When StateChangeConf sees an unexpected state, it will complain with an error that seems less friendly than our refresh function's error: Error on retrieving DynamoDB Global Table when waiting or hitting a timeout.

dynamodb.GlobalTableStatusUpdating,
Copy link
Contributor

Choose a reason for hiding this comment

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

👍 for constants usage

},
Target: []string{
dynamodb.GlobalTableStatusActive,
},
Refresh: resourceAwsDynamoDbGlobalTableStateRefreshFunc(d, meta),
Timeout: d.Timeout(schema.TimeoutCreate),
MinTimeout: 10 * time.Second,
}
_, err = stateConf.WaitForState()
if err != nil {
return err
}

return resourceAwsDynamoDbGlobalTableRead(d, meta)
}

func resourceAwsDynamoDbGlobalTableRead(d *schema.ResourceData, meta interface{}) error {
globalTableDescription, err := resourceAwsDynamoDbGlobalTableRetrieve(d, meta)

if err != nil {
return err
}
if globalTableDescription == nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

We should log log.Printf("[WARN] DynamoDB Global Table %q not found, removing from state", dashboardName)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will add this, thanks!

log.Printf("[WARN] DynamoDB Global Table %q not found, removing from state", d.Id())
d.SetId("")
return nil
}

return flattenAwsDynamoDbGlobalTable(d, globalTableDescription)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason to have this as a separated function? I think it would be more consistent with other resources to keep setting attributes in the read function. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is so we can reuse this code support a aws_dynamodb_global_table data source in the future, if we would like.

}

func resourceAwsDynamoDbGlobalTableUpdate(d *schema.ResourceData, meta interface{}) error {
dynamodbconn := meta.(*AWSClient).dynamodbconn

if d.HasChange("replica") {
o, n := d.GetChange("replica")
if o == nil {
o = new(schema.Set)
}
if n == nil {
n = new(schema.Set)
}

os := o.(*schema.Set)
ns := n.(*schema.Set)
replicaUpdateCreateReplicas := expandAwsDynamoDbReplicaUpdateCreateReplicas(ns.Difference(os).List())
replicaUpdateDeleteReplicas := expandAwsDynamoDbReplicaUpdateDeleteReplicas(os.Difference(ns).List())

replicaUpdates := make([]*dynamodb.ReplicaUpdate, 0, (len(replicaUpdateCreateReplicas) + len(replicaUpdateDeleteReplicas)))
for _, replicaUpdate := range replicaUpdateCreateReplicas {
replicaUpdates = append(replicaUpdates, replicaUpdate)
}
for _, replicaUpdate := range replicaUpdateDeleteReplicas {
replicaUpdates = append(replicaUpdates, replicaUpdate)
}

input := &dynamodb.UpdateGlobalTableInput{
GlobalTableName: aws.String(d.Id()),
ReplicaUpdates: replicaUpdates,
}
log.Printf("[DEBUG] Updating DynamoDB Global Table: %#v", input)
if _, err := dynamodbconn.UpdateGlobalTable(input); err != nil {
return err
}

log.Println("[INFO] Waiting for DynamoDB Global Table to be updated")
stateConf := &resource.StateChangeConf{
Pending: []string{
dynamodb.GlobalTableStatusCreating,
dynamodb.GlobalTableStatusDeleting,
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to handle this status or the one above when updating?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same logic as mentioned before, I added this purely for friendlier edge-case messaging. Maybe we could drop these, but leaving them could still handle the extremely weird case someone manages to delete and recreate a global table while updating (e.g. tainting one of two overlapping resources then Terraform running both in parallel).

dynamodb.GlobalTableStatusUpdating,
},
Target: []string{
dynamodb.GlobalTableStatusActive,
},
Refresh: resourceAwsDynamoDbGlobalTableStateRefreshFunc(d, meta),
Timeout: d.Timeout(schema.TimeoutUpdate),
MinTimeout: 10 * time.Second,
}
_, err := stateConf.WaitForState()
if err != nil {
return err
}
}

return nil
}

// Deleting a DynamoDB Global Table is represented by removing all replicas.
func resourceAwsDynamoDbGlobalTableDelete(d *schema.ResourceData, meta interface{}) error {
dynamodbconn := meta.(*AWSClient).dynamodbconn

input := &dynamodb.UpdateGlobalTableInput{
GlobalTableName: aws.String(d.Id()),
ReplicaUpdates: expandAwsDynamoDbReplicaUpdateDeleteReplicas(d.Get("replica").(*schema.Set).List()),
}
log.Printf("[DEBUG] Deleting DynamoDB Global Table: %#v", input)
if _, err := dynamodbconn.UpdateGlobalTable(input); err != nil {
return err
}

log.Println("[INFO] Waiting for DynamoDB Global Table to be destroyed")
stateConf := &resource.StateChangeConf{
Pending: []string{
dynamodb.GlobalTableStatusActive,
dynamodb.GlobalTableStatusCreating,
dynamodb.GlobalTableStatusDeleting,
dynamodb.GlobalTableStatusUpdating,
},
Target: []string{},
Refresh: resourceAwsDynamoDbGlobalTableStateRefreshFunc(d, meta),
Timeout: d.Timeout(schema.TimeoutDelete),
MinTimeout: 10 * time.Second,
}
_, err := stateConf.WaitForState()
return err
}

func resourceAwsDynamoDbGlobalTableRetrieve(d *schema.ResourceData, meta interface{}) (*dynamodb.GlobalTableDescription, error) {
dynamodbconn := meta.(*AWSClient).dynamodbconn

input := &dynamodb.DescribeGlobalTableInput{
GlobalTableName: aws.String(d.Id()),
}

log.Printf("[DEBUG] Retrieving DynamoDB Global Table: %#v", input)

output, err := dynamodbconn.DescribeGlobalTable(input)
if err != nil {
if isAWSErr(err, dynamodb.ErrCodeGlobalTableNotFoundException, "") {
return nil, nil
}
return nil, fmt.Errorf("Error retrieving DynamoDB Global Table: %s", err)
}

return output.GlobalTableDescription, nil
}

func resourceAwsDynamoDbGlobalTableStateRefreshFunc(
d *schema.ResourceData, meta interface{}) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
gtd, err := resourceAwsDynamoDbGlobalTableRetrieve(d, meta)

if err != nil {
log.Printf("Error on retrieving DynamoDB Global Table when waiting: %s", err)
return nil, "", err
}

if gtd == nil {
return nil, "", nil
}

if gtd.GlobalTableStatus != nil {
log.Printf("[DEBUG] Status for DynamoDB Global Table %s: %s", d.Id(), *gtd.GlobalTableStatus)
}

return gtd, *gtd.GlobalTableStatus, nil
}
}

func flattenAwsDynamoDbGlobalTable(d *schema.ResourceData, globalTableDescription *dynamodb.GlobalTableDescription) error {
var err error

d.Set("arn", globalTableDescription.GlobalTableArn)
d.Set("name", globalTableDescription.GlobalTableName)

err = d.Set("replica", flattenAwsDynamoDbReplicas(globalTableDescription.ReplicationGroup))
if err != nil {
return err
}

return nil
}

func expandAwsDynamoDbReplicaUpdateCreateReplicas(configuredReplicas []interface{}) []*dynamodb.ReplicaUpdate {
replicaUpdates := make([]*dynamodb.ReplicaUpdate, 0, len(configuredReplicas))
for _, replicaRaw := range configuredReplicas {
replica := replicaRaw.(map[string]interface{})
replicaUpdates = append(replicaUpdates, expandAwsDynamoDbReplicaUpdateCreateReplica(replica))
}
return replicaUpdates
}

func expandAwsDynamoDbReplicaUpdateCreateReplica(configuredReplica map[string]interface{}) *dynamodb.ReplicaUpdate {
replicaUpdate := &dynamodb.ReplicaUpdate{
Create: &dynamodb.CreateReplicaAction{
RegionName: aws.String(configuredReplica["region_name"].(string)),
},
}
return replicaUpdate
}

func expandAwsDynamoDbReplicaUpdateDeleteReplicas(configuredReplicas []interface{}) []*dynamodb.ReplicaUpdate {
replicaUpdates := make([]*dynamodb.ReplicaUpdate, 0, len(configuredReplicas))
for _, replicaRaw := range configuredReplicas {
replica := replicaRaw.(map[string]interface{})
replicaUpdates = append(replicaUpdates, expandAwsDynamoDbReplicaUpdateDeleteReplica(replica))
}
return replicaUpdates
}

func expandAwsDynamoDbReplicaUpdateDeleteReplica(configuredReplica map[string]interface{}) *dynamodb.ReplicaUpdate {
replicaUpdate := &dynamodb.ReplicaUpdate{
Delete: &dynamodb.DeleteReplicaAction{
RegionName: aws.String(configuredReplica["region_name"].(string)),
},
}
return replicaUpdate
}

func expandAwsDynamoDbReplicas(configuredReplicas []interface{}) []*dynamodb.Replica {
replicas := make([]*dynamodb.Replica, 0, len(configuredReplicas))
for _, replicaRaw := range configuredReplicas {
replica := replicaRaw.(map[string]interface{})
replicas = append(replicas, expandAwsDynamoDbReplica(replica))
}
return replicas
}

func expandAwsDynamoDbReplica(configuredReplica map[string]interface{}) *dynamodb.Replica {
replica := &dynamodb.Replica{
RegionName: aws.String(configuredReplica["region_name"].(string)),
}
return replica
}

func flattenAwsDynamoDbReplicas(replicaDescriptions []*dynamodb.ReplicaDescription) []interface{} {
replicas := []interface{}{}
for _, replicaDescription := range replicaDescriptions {
replicas = append(replicas, flattenAwsDynamoDbReplica(replicaDescription))
}
return replicas
}

func flattenAwsDynamoDbReplica(replicaDescription *dynamodb.ReplicaDescription) map[string]interface{} {
replica := make(map[string]interface{})
replica["region_name"] = *replicaDescription.RegionName
return replica
}
Loading