github.com/nathanielks/terraform@v0.6.1-0.20170509030759-13e1a62319dc/builtin/providers/aws/resource_aws_dynamodb_table.go (about)

     1  package aws
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"log"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/hashicorp/errwrap"
    11  	"github.com/hashicorp/terraform/helper/resource"
    12  	"github.com/hashicorp/terraform/helper/schema"
    13  
    14  	"github.com/aws/aws-sdk-go/aws"
    15  	"github.com/aws/aws-sdk-go/aws/awserr"
    16  	"github.com/aws/aws-sdk-go/service/dynamodb"
    17  	"github.com/hashicorp/terraform/helper/hashcode"
    18  )
    19  
    20  // Number of times to retry if a throttling-related exception occurs
    21  const DYNAMODB_MAX_THROTTLE_RETRIES = 5
    22  
    23  // How long to sleep when a throttle-event happens
    24  const DYNAMODB_THROTTLE_SLEEP = 5 * time.Second
    25  
    26  // How long to sleep if a limit-exceeded event happens
    27  const DYNAMODB_LIMIT_EXCEEDED_SLEEP = 10 * time.Second
    28  
    29  // A number of these are marked as computed because if you don't
    30  // provide a value, DynamoDB will provide you with defaults (which are the
    31  // default values specified below)
    32  func resourceAwsDynamoDbTable() *schema.Resource {
    33  	return &schema.Resource{
    34  		Create: resourceAwsDynamoDbTableCreate,
    35  		Read:   resourceAwsDynamoDbTableRead,
    36  		Update: resourceAwsDynamoDbTableUpdate,
    37  		Delete: resourceAwsDynamoDbTableDelete,
    38  		Importer: &schema.ResourceImporter{
    39  			State: schema.ImportStatePassthrough,
    40  		},
    41  
    42  		SchemaVersion: 1,
    43  		MigrateState:  resourceAwsDynamoDbTableMigrateState,
    44  
    45  		Schema: map[string]*schema.Schema{
    46  			"arn": {
    47  				Type:     schema.TypeString,
    48  				Computed: true,
    49  			},
    50  			"name": {
    51  				Type:     schema.TypeString,
    52  				Required: true,
    53  				ForceNew: true,
    54  			},
    55  			"hash_key": {
    56  				Type:     schema.TypeString,
    57  				Required: true,
    58  				ForceNew: true,
    59  			},
    60  			"range_key": {
    61  				Type:     schema.TypeString,
    62  				Optional: true,
    63  				ForceNew: true,
    64  			},
    65  			"write_capacity": {
    66  				Type:     schema.TypeInt,
    67  				Required: true,
    68  			},
    69  			"read_capacity": {
    70  				Type:     schema.TypeInt,
    71  				Required: true,
    72  			},
    73  			"attribute": {
    74  				Type:     schema.TypeSet,
    75  				Required: true,
    76  				Elem: &schema.Resource{
    77  					Schema: map[string]*schema.Schema{
    78  						"name": {
    79  							Type:     schema.TypeString,
    80  							Required: true,
    81  						},
    82  						"type": {
    83  							Type:     schema.TypeString,
    84  							Required: true,
    85  						},
    86  					},
    87  				},
    88  				Set: func(v interface{}) int {
    89  					var buf bytes.Buffer
    90  					m := v.(map[string]interface{})
    91  					buf.WriteString(fmt.Sprintf("%s-", m["name"].(string)))
    92  					return hashcode.String(buf.String())
    93  				},
    94  			},
    95  			"ttl": {
    96  				Type:     schema.TypeSet,
    97  				Optional: true,
    98  				MaxItems: 1,
    99  				Elem: &schema.Resource{
   100  					Schema: map[string]*schema.Schema{
   101  						"attribute_name": {
   102  							Type:     schema.TypeString,
   103  							Required: true,
   104  						},
   105  						"enabled": {
   106  							Type:     schema.TypeBool,
   107  							Required: true,
   108  						},
   109  					},
   110  				},
   111  			},
   112  			"local_secondary_index": {
   113  				Type:     schema.TypeSet,
   114  				Optional: true,
   115  				ForceNew: true,
   116  				Elem: &schema.Resource{
   117  					Schema: map[string]*schema.Schema{
   118  						"name": {
   119  							Type:     schema.TypeString,
   120  							Required: true,
   121  						},
   122  						"range_key": {
   123  							Type:     schema.TypeString,
   124  							Required: true,
   125  						},
   126  						"projection_type": {
   127  							Type:     schema.TypeString,
   128  							Required: true,
   129  						},
   130  						"non_key_attributes": {
   131  							Type:     schema.TypeList,
   132  							Optional: true,
   133  							Elem:     &schema.Schema{Type: schema.TypeString},
   134  						},
   135  					},
   136  				},
   137  				Set: func(v interface{}) int {
   138  					var buf bytes.Buffer
   139  					m := v.(map[string]interface{})
   140  					buf.WriteString(fmt.Sprintf("%s-", m["name"].(string)))
   141  					return hashcode.String(buf.String())
   142  				},
   143  			},
   144  			"global_secondary_index": {
   145  				Type:     schema.TypeSet,
   146  				Optional: true,
   147  				Elem: &schema.Resource{
   148  					Schema: map[string]*schema.Schema{
   149  						"name": {
   150  							Type:     schema.TypeString,
   151  							Required: true,
   152  						},
   153  						"write_capacity": {
   154  							Type:     schema.TypeInt,
   155  							Required: true,
   156  						},
   157  						"read_capacity": {
   158  							Type:     schema.TypeInt,
   159  							Required: true,
   160  						},
   161  						"hash_key": {
   162  							Type:     schema.TypeString,
   163  							Required: true,
   164  						},
   165  						"range_key": {
   166  							Type:     schema.TypeString,
   167  							Optional: true,
   168  						},
   169  						"projection_type": {
   170  							Type:     schema.TypeString,
   171  							Required: true,
   172  						},
   173  						"non_key_attributes": {
   174  							Type:     schema.TypeList,
   175  							Optional: true,
   176  							Elem:     &schema.Schema{Type: schema.TypeString},
   177  						},
   178  					},
   179  				},
   180  			},
   181  			"stream_enabled": {
   182  				Type:     schema.TypeBool,
   183  				Optional: true,
   184  				Computed: true,
   185  			},
   186  			"stream_view_type": {
   187  				Type:     schema.TypeString,
   188  				Optional: true,
   189  				Computed: true,
   190  				StateFunc: func(v interface{}) string {
   191  					value := v.(string)
   192  					return strings.ToUpper(value)
   193  				},
   194  				ValidateFunc: validateStreamViewType,
   195  			},
   196  			"stream_arn": {
   197  				Type:     schema.TypeString,
   198  				Computed: true,
   199  			},
   200  			"tags": tagsSchema(),
   201  		},
   202  	}
   203  }
   204  
   205  func resourceAwsDynamoDbTableCreate(d *schema.ResourceData, meta interface{}) error {
   206  	dynamodbconn := meta.(*AWSClient).dynamodbconn
   207  
   208  	name := d.Get("name").(string)
   209  
   210  	log.Printf("[DEBUG] DynamoDB table create: %s", name)
   211  
   212  	throughput := &dynamodb.ProvisionedThroughput{
   213  		ReadCapacityUnits:  aws.Int64(int64(d.Get("read_capacity").(int))),
   214  		WriteCapacityUnits: aws.Int64(int64(d.Get("write_capacity").(int))),
   215  	}
   216  
   217  	hash_key_name := d.Get("hash_key").(string)
   218  	keyschema := []*dynamodb.KeySchemaElement{
   219  		{
   220  			AttributeName: aws.String(hash_key_name),
   221  			KeyType:       aws.String("HASH"),
   222  		},
   223  	}
   224  
   225  	if range_key, ok := d.GetOk("range_key"); ok {
   226  		range_schema_element := &dynamodb.KeySchemaElement{
   227  			AttributeName: aws.String(range_key.(string)),
   228  			KeyType:       aws.String("RANGE"),
   229  		}
   230  		keyschema = append(keyschema, range_schema_element)
   231  	}
   232  
   233  	req := &dynamodb.CreateTableInput{
   234  		TableName:             aws.String(name),
   235  		ProvisionedThroughput: throughput,
   236  		KeySchema:             keyschema,
   237  	}
   238  
   239  	if attributedata, ok := d.GetOk("attribute"); ok {
   240  		attributes := []*dynamodb.AttributeDefinition{}
   241  		attributeSet := attributedata.(*schema.Set)
   242  		for _, attribute := range attributeSet.List() {
   243  			attr := attribute.(map[string]interface{})
   244  			attributes = append(attributes, &dynamodb.AttributeDefinition{
   245  				AttributeName: aws.String(attr["name"].(string)),
   246  				AttributeType: aws.String(attr["type"].(string)),
   247  			})
   248  		}
   249  
   250  		req.AttributeDefinitions = attributes
   251  	}
   252  
   253  	if lsidata, ok := d.GetOk("local_secondary_index"); ok {
   254  		log.Printf("[DEBUG] Adding LSI data to the table")
   255  
   256  		lsiSet := lsidata.(*schema.Set)
   257  		localSecondaryIndexes := []*dynamodb.LocalSecondaryIndex{}
   258  		for _, lsiObject := range lsiSet.List() {
   259  			lsi := lsiObject.(map[string]interface{})
   260  
   261  			projection := &dynamodb.Projection{
   262  				ProjectionType: aws.String(lsi["projection_type"].(string)),
   263  			}
   264  
   265  			if lsi["projection_type"] == "INCLUDE" {
   266  				non_key_attributes := []*string{}
   267  				for _, attr := range lsi["non_key_attributes"].([]interface{}) {
   268  					non_key_attributes = append(non_key_attributes, aws.String(attr.(string)))
   269  				}
   270  				projection.NonKeyAttributes = non_key_attributes
   271  			}
   272  
   273  			localSecondaryIndexes = append(localSecondaryIndexes, &dynamodb.LocalSecondaryIndex{
   274  				IndexName: aws.String(lsi["name"].(string)),
   275  				KeySchema: []*dynamodb.KeySchemaElement{
   276  					{
   277  						AttributeName: aws.String(hash_key_name),
   278  						KeyType:       aws.String("HASH"),
   279  					},
   280  					{
   281  						AttributeName: aws.String(lsi["range_key"].(string)),
   282  						KeyType:       aws.String("RANGE"),
   283  					},
   284  				},
   285  				Projection: projection,
   286  			})
   287  		}
   288  
   289  		req.LocalSecondaryIndexes = localSecondaryIndexes
   290  
   291  		log.Printf("[DEBUG] Added %d LSI definitions", len(localSecondaryIndexes))
   292  	}
   293  
   294  	if gsidata, ok := d.GetOk("global_secondary_index"); ok {
   295  		globalSecondaryIndexes := []*dynamodb.GlobalSecondaryIndex{}
   296  
   297  		gsiSet := gsidata.(*schema.Set)
   298  		for _, gsiObject := range gsiSet.List() {
   299  			gsi := gsiObject.(map[string]interface{})
   300  			gsiObject := createGSIFromData(&gsi)
   301  			globalSecondaryIndexes = append(globalSecondaryIndexes, &gsiObject)
   302  		}
   303  		req.GlobalSecondaryIndexes = globalSecondaryIndexes
   304  	}
   305  
   306  	if _, ok := d.GetOk("stream_enabled"); ok {
   307  
   308  		req.StreamSpecification = &dynamodb.StreamSpecification{
   309  			StreamEnabled:  aws.Bool(d.Get("stream_enabled").(bool)),
   310  			StreamViewType: aws.String(d.Get("stream_view_type").(string)),
   311  		}
   312  
   313  		log.Printf("[DEBUG] Adding StreamSpecifications to the table")
   314  	}
   315  
   316  	_, timeToLiveOk := d.GetOk("ttl")
   317  	_, tagsOk := d.GetOk("tags")
   318  
   319  	attemptCount := 1
   320  	for attemptCount <= DYNAMODB_MAX_THROTTLE_RETRIES {
   321  		output, err := dynamodbconn.CreateTable(req)
   322  		if err != nil {
   323  			if awsErr, ok := err.(awserr.Error); ok {
   324  				if awsErr.Code() == "ThrottlingException" {
   325  					log.Printf("[DEBUG] Attempt %d/%d: Sleeping for a bit to throttle back create request", attemptCount, DYNAMODB_MAX_THROTTLE_RETRIES)
   326  					time.Sleep(DYNAMODB_THROTTLE_SLEEP)
   327  					attemptCount += 1
   328  				} else if awsErr.Code() == "LimitExceededException" {
   329  					log.Printf("[DEBUG] Limit on concurrent table creations hit, sleeping for a bit")
   330  					time.Sleep(DYNAMODB_LIMIT_EXCEEDED_SLEEP)
   331  					attemptCount += 1
   332  				} else {
   333  					// Some other non-retryable exception occurred
   334  					return fmt.Errorf("AWS Error creating DynamoDB table: %s", err)
   335  				}
   336  			} else {
   337  				// Non-AWS exception occurred, give up
   338  				return fmt.Errorf("Error creating DynamoDB table: %s", err)
   339  			}
   340  		} else {
   341  			// No error, set ID and return
   342  			d.SetId(*output.TableDescription.TableName)
   343  			tableArn := *output.TableDescription.TableArn
   344  			if err := d.Set("arn", tableArn); err != nil {
   345  				return err
   346  			}
   347  
   348  			// Wait, till table is active before imitating any TimeToLive changes
   349  			if err := waitForTableToBeActive(d.Id(), meta); err != nil {
   350  				log.Printf("[DEBUG] Error waiting for table to be active: %s", err)
   351  				return err
   352  			}
   353  
   354  			log.Printf("[DEBUG] Setting DynamoDB TimeToLive on arn: %s", tableArn)
   355  			if timeToLiveOk {
   356  				if err := updateTimeToLive(d, meta); err != nil {
   357  					log.Printf("[DEBUG] Error updating table TimeToLive: %s", err)
   358  					return err
   359  				}
   360  			}
   361  
   362  			if tagsOk {
   363  				log.Printf("[DEBUG] Setting DynamoDB Tags on arn: %s", tableArn)
   364  				if err := createTableTags(d, meta); err != nil {
   365  					return err
   366  				}
   367  			}
   368  
   369  			return resourceAwsDynamoDbTableRead(d, meta)
   370  		}
   371  	}
   372  
   373  	// Too many throttling events occurred, give up
   374  	return fmt.Errorf("Unable to create DynamoDB table '%s' after %d attempts", name, attemptCount)
   375  }
   376  
   377  func resourceAwsDynamoDbTableUpdate(d *schema.ResourceData, meta interface{}) error {
   378  
   379  	log.Printf("[DEBUG] Updating DynamoDB table %s", d.Id())
   380  	dynamodbconn := meta.(*AWSClient).dynamodbconn
   381  
   382  	// Ensure table is active before trying to update
   383  	if err := waitForTableToBeActive(d.Id(), meta); err != nil {
   384  		return errwrap.Wrapf("Error waiting for Dynamo DB Table update: {{err}}", err)
   385  	}
   386  
   387  	if d.HasChange("read_capacity") || d.HasChange("write_capacity") {
   388  		req := &dynamodb.UpdateTableInput{
   389  			TableName: aws.String(d.Id()),
   390  		}
   391  
   392  		throughput := &dynamodb.ProvisionedThroughput{
   393  			ReadCapacityUnits:  aws.Int64(int64(d.Get("read_capacity").(int))),
   394  			WriteCapacityUnits: aws.Int64(int64(d.Get("write_capacity").(int))),
   395  		}
   396  		req.ProvisionedThroughput = throughput
   397  
   398  		_, err := dynamodbconn.UpdateTable(req)
   399  
   400  		if err != nil {
   401  			return err
   402  		}
   403  
   404  		if err := waitForTableToBeActive(d.Id(), meta); err != nil {
   405  			return errwrap.Wrapf("Error waiting for Dynamo DB Table update: {{err}}", err)
   406  		}
   407  	}
   408  
   409  	if d.HasChange("stream_enabled") || d.HasChange("stream_view_type") {
   410  		req := &dynamodb.UpdateTableInput{
   411  			TableName: aws.String(d.Id()),
   412  		}
   413  
   414  		req.StreamSpecification = &dynamodb.StreamSpecification{
   415  			StreamEnabled:  aws.Bool(d.Get("stream_enabled").(bool)),
   416  			StreamViewType: aws.String(d.Get("stream_view_type").(string)),
   417  		}
   418  
   419  		_, err := dynamodbconn.UpdateTable(req)
   420  
   421  		if err != nil {
   422  			return err
   423  		}
   424  
   425  		if err := waitForTableToBeActive(d.Id(), meta); err != nil {
   426  			return errwrap.Wrapf("Error waiting for Dynamo DB Table update: {{err}}", err)
   427  		}
   428  	}
   429  
   430  	if d.HasChange("global_secondary_index") {
   431  		log.Printf("[DEBUG] Changed GSI data")
   432  		req := &dynamodb.UpdateTableInput{
   433  			TableName: aws.String(d.Id()),
   434  		}
   435  
   436  		o, n := d.GetChange("global_secondary_index")
   437  
   438  		oldSet := o.(*schema.Set)
   439  		newSet := n.(*schema.Set)
   440  
   441  		// Track old names so we can know which ones we need to just update based on
   442  		// capacity changes, terraform appears to only diff on the set hash, not the
   443  		// contents so we need to make sure we don't delete any indexes that we
   444  		// just want to update the capacity for
   445  		oldGsiNameSet := make(map[string]bool)
   446  		newGsiNameSet := make(map[string]bool)
   447  
   448  		for _, gsidata := range oldSet.List() {
   449  			gsiName := gsidata.(map[string]interface{})["name"].(string)
   450  			oldGsiNameSet[gsiName] = true
   451  		}
   452  
   453  		for _, gsidata := range newSet.List() {
   454  			gsiName := gsidata.(map[string]interface{})["name"].(string)
   455  			newGsiNameSet[gsiName] = true
   456  		}
   457  
   458  		// First determine what's new
   459  		for _, newgsidata := range newSet.List() {
   460  			updates := []*dynamodb.GlobalSecondaryIndexUpdate{}
   461  			newGsiName := newgsidata.(map[string]interface{})["name"].(string)
   462  			if _, exists := oldGsiNameSet[newGsiName]; !exists {
   463  				attributes := []*dynamodb.AttributeDefinition{}
   464  				gsidata := newgsidata.(map[string]interface{})
   465  				gsi := createGSIFromData(&gsidata)
   466  				log.Printf("[DEBUG] Adding GSI %s", *gsi.IndexName)
   467  				update := &dynamodb.GlobalSecondaryIndexUpdate{
   468  					Create: &dynamodb.CreateGlobalSecondaryIndexAction{
   469  						IndexName:             gsi.IndexName,
   470  						KeySchema:             gsi.KeySchema,
   471  						ProvisionedThroughput: gsi.ProvisionedThroughput,
   472  						Projection:            gsi.Projection,
   473  					},
   474  				}
   475  				updates = append(updates, update)
   476  
   477  				// Hash key is required, range key isn't
   478  				hashkey_type, err := getAttributeType(d, *gsi.KeySchema[0].AttributeName)
   479  				if err != nil {
   480  					return err
   481  				}
   482  
   483  				attributes = append(attributes, &dynamodb.AttributeDefinition{
   484  					AttributeName: gsi.KeySchema[0].AttributeName,
   485  					AttributeType: aws.String(hashkey_type),
   486  				})
   487  
   488  				// If there's a range key, there will be 2 elements in KeySchema
   489  				if len(gsi.KeySchema) == 2 {
   490  					rangekey_type, err := getAttributeType(d, *gsi.KeySchema[1].AttributeName)
   491  					if err != nil {
   492  						return err
   493  					}
   494  
   495  					attributes = append(attributes, &dynamodb.AttributeDefinition{
   496  						AttributeName: gsi.KeySchema[1].AttributeName,
   497  						AttributeType: aws.String(rangekey_type),
   498  					})
   499  				}
   500  
   501  				req.AttributeDefinitions = attributes
   502  				req.GlobalSecondaryIndexUpdates = updates
   503  				_, err = dynamodbconn.UpdateTable(req)
   504  
   505  				if err != nil {
   506  					return err
   507  				}
   508  
   509  				if err := waitForTableToBeActive(d.Id(), meta); err != nil {
   510  					return errwrap.Wrapf("Error waiting for Dynamo DB Table update: {{err}}", err)
   511  				}
   512  
   513  				if err := waitForGSIToBeActive(d.Id(), *gsi.IndexName, meta); err != nil {
   514  					return errwrap.Wrapf("Error waiting for Dynamo DB GSIT to be active: {{err}}", err)
   515  				}
   516  
   517  			}
   518  		}
   519  
   520  		for _, oldgsidata := range oldSet.List() {
   521  			updates := []*dynamodb.GlobalSecondaryIndexUpdate{}
   522  			oldGsiName := oldgsidata.(map[string]interface{})["name"].(string)
   523  			if _, exists := newGsiNameSet[oldGsiName]; !exists {
   524  				gsidata := oldgsidata.(map[string]interface{})
   525  				log.Printf("[DEBUG] Deleting GSI %s", gsidata["name"].(string))
   526  				update := &dynamodb.GlobalSecondaryIndexUpdate{
   527  					Delete: &dynamodb.DeleteGlobalSecondaryIndexAction{
   528  						IndexName: aws.String(gsidata["name"].(string)),
   529  					},
   530  				}
   531  				updates = append(updates, update)
   532  
   533  				req.GlobalSecondaryIndexUpdates = updates
   534  				_, err := dynamodbconn.UpdateTable(req)
   535  
   536  				if err != nil {
   537  					return err
   538  				}
   539  
   540  				if err := waitForTableToBeActive(d.Id(), meta); err != nil {
   541  					return errwrap.Wrapf("Error waiting for Dynamo DB Table update: {{err}}", err)
   542  				}
   543  			}
   544  		}
   545  	}
   546  
   547  	// Update any out-of-date read / write capacity
   548  	if gsiObjects, ok := d.GetOk("global_secondary_index"); ok {
   549  		gsiSet := gsiObjects.(*schema.Set)
   550  		if len(gsiSet.List()) > 0 {
   551  			log.Printf("Updating capacity as needed!")
   552  
   553  			// We can only change throughput, but we need to make sure it's actually changed
   554  			tableDescription, err := dynamodbconn.DescribeTable(&dynamodb.DescribeTableInput{
   555  				TableName: aws.String(d.Id()),
   556  			})
   557  
   558  			if err != nil {
   559  				return err
   560  			}
   561  
   562  			table := tableDescription.Table
   563  
   564  			for _, updatedgsidata := range gsiSet.List() {
   565  				updates := []*dynamodb.GlobalSecondaryIndexUpdate{}
   566  				gsidata := updatedgsidata.(map[string]interface{})
   567  				gsiName := gsidata["name"].(string)
   568  				gsiWriteCapacity := gsidata["write_capacity"].(int)
   569  				gsiReadCapacity := gsidata["read_capacity"].(int)
   570  
   571  				log.Printf("[DEBUG] Updating GSI %s", gsiName)
   572  				gsi, err := getGlobalSecondaryIndex(gsiName, table.GlobalSecondaryIndexes)
   573  
   574  				if err != nil {
   575  					return err
   576  				}
   577  
   578  				capacityUpdated := false
   579  
   580  				if int64(gsiReadCapacity) != *gsi.ProvisionedThroughput.ReadCapacityUnits ||
   581  					int64(gsiWriteCapacity) != *gsi.ProvisionedThroughput.WriteCapacityUnits {
   582  					capacityUpdated = true
   583  				}
   584  
   585  				if capacityUpdated {
   586  					update := &dynamodb.GlobalSecondaryIndexUpdate{
   587  						Update: &dynamodb.UpdateGlobalSecondaryIndexAction{
   588  							IndexName: aws.String(gsidata["name"].(string)),
   589  							ProvisionedThroughput: &dynamodb.ProvisionedThroughput{
   590  								WriteCapacityUnits: aws.Int64(int64(gsiWriteCapacity)),
   591  								ReadCapacityUnits:  aws.Int64(int64(gsiReadCapacity)),
   592  							},
   593  						},
   594  					}
   595  					updates = append(updates, update)
   596  
   597  				}
   598  
   599  				if len(updates) > 0 {
   600  
   601  					req := &dynamodb.UpdateTableInput{
   602  						TableName: aws.String(d.Id()),
   603  					}
   604  
   605  					req.GlobalSecondaryIndexUpdates = updates
   606  
   607  					log.Printf("[DEBUG] Updating GSI read / write capacity on %s", d.Id())
   608  					_, err := dynamodbconn.UpdateTable(req)
   609  
   610  					if err != nil {
   611  						log.Printf("[DEBUG] Error updating table: %s", err)
   612  						return err
   613  					}
   614  
   615  					if err := waitForGSIToBeActive(d.Id(), gsiName, meta); err != nil {
   616  						return errwrap.Wrapf("Error waiting for Dynamo DB GSI to be active: {{err}}", err)
   617  					}
   618  				}
   619  			}
   620  		}
   621  
   622  	}
   623  
   624  	if d.HasChange("ttl") {
   625  		if err := updateTimeToLive(d, meta); err != nil {
   626  			log.Printf("[DEBUG] Error updating table TimeToLive: %s", err)
   627  			return err
   628  		}
   629  	}
   630  
   631  	// Update tags
   632  	if err := setTagsDynamoDb(dynamodbconn, d); err != nil {
   633  		return err
   634  	}
   635  
   636  	return resourceAwsDynamoDbTableRead(d, meta)
   637  }
   638  
   639  func updateTimeToLive(d *schema.ResourceData, meta interface{}) error {
   640  	dynamodbconn := meta.(*AWSClient).dynamodbconn
   641  
   642  	if ttl, ok := d.GetOk("ttl"); ok {
   643  
   644  		timeToLiveSet := ttl.(*schema.Set)
   645  
   646  		spec := &dynamodb.TimeToLiveSpecification{}
   647  
   648  		timeToLive := timeToLiveSet.List()[0].(map[string]interface{})
   649  		spec.AttributeName = aws.String(timeToLive["attribute_name"].(string))
   650  		spec.Enabled = aws.Bool(timeToLive["enabled"].(bool))
   651  
   652  		req := &dynamodb.UpdateTimeToLiveInput{
   653  			TableName:               aws.String(d.Id()),
   654  			TimeToLiveSpecification: spec,
   655  		}
   656  
   657  		_, err := dynamodbconn.UpdateTimeToLive(req)
   658  
   659  		if err != nil {
   660  			// If ttl was not set within the .tf file before and has now been added we still run this command to update
   661  			// But there has been no change so lets continue
   662  			if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "ValidationException" && awsErr.Message() == "TimeToLive is already disabled" {
   663  				return nil
   664  			}
   665  			log.Printf("[DEBUG] Error updating TimeToLive on table: %s", err)
   666  			return err
   667  		}
   668  
   669  		log.Printf("[DEBUG] Updated TimeToLive on table")
   670  
   671  		if err := waitForTimeToLiveUpdateToBeCompleted(d.Id(), timeToLive["enabled"].(bool), meta); err != nil {
   672  			return errwrap.Wrapf("Error waiting for Dynamo DB TimeToLive to be updated: {{err}}", err)
   673  		}
   674  	}
   675  
   676  	return nil
   677  }
   678  
   679  func resourceAwsDynamoDbTableRead(d *schema.ResourceData, meta interface{}) error {
   680  	dynamodbconn := meta.(*AWSClient).dynamodbconn
   681  	log.Printf("[DEBUG] Loading data for DynamoDB table '%s'", d.Id())
   682  	req := &dynamodb.DescribeTableInput{
   683  		TableName: aws.String(d.Id()),
   684  	}
   685  
   686  	result, err := dynamodbconn.DescribeTable(req)
   687  
   688  	if err != nil {
   689  		if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "ResourceNotFoundException" {
   690  			log.Printf("[WARN] Dynamodb Table (%s) not found, error code (404)", d.Id())
   691  			d.SetId("")
   692  			return nil
   693  		}
   694  		return err
   695  	}
   696  
   697  	table := result.Table
   698  
   699  	d.Set("write_capacity", table.ProvisionedThroughput.WriteCapacityUnits)
   700  	d.Set("read_capacity", table.ProvisionedThroughput.ReadCapacityUnits)
   701  
   702  	attributes := []interface{}{}
   703  	for _, attrdef := range table.AttributeDefinitions {
   704  		attribute := map[string]string{
   705  			"name": *attrdef.AttributeName,
   706  			"type": *attrdef.AttributeType,
   707  		}
   708  		attributes = append(attributes, attribute)
   709  		log.Printf("[DEBUG] Added Attribute: %s", attribute["name"])
   710  	}
   711  
   712  	d.Set("attribute", attributes)
   713  	d.Set("name", table.TableName)
   714  
   715  	for _, attribute := range table.KeySchema {
   716  		if *attribute.KeyType == "HASH" {
   717  			d.Set("hash_key", attribute.AttributeName)
   718  		}
   719  
   720  		if *attribute.KeyType == "RANGE" {
   721  			d.Set("range_key", attribute.AttributeName)
   722  		}
   723  	}
   724  
   725  	lsiList := make([]map[string]interface{}, 0, len(table.LocalSecondaryIndexes))
   726  	for _, lsiObject := range table.LocalSecondaryIndexes {
   727  		lsi := map[string]interface{}{
   728  			"name":            *lsiObject.IndexName,
   729  			"projection_type": *lsiObject.Projection.ProjectionType,
   730  		}
   731  
   732  		for _, attribute := range lsiObject.KeySchema {
   733  
   734  			if *attribute.KeyType == "RANGE" {
   735  				lsi["range_key"] = *attribute.AttributeName
   736  			}
   737  		}
   738  		nkaList := make([]string, len(lsiObject.Projection.NonKeyAttributes))
   739  		for _, nka := range lsiObject.Projection.NonKeyAttributes {
   740  			nkaList = append(nkaList, *nka)
   741  		}
   742  		lsi["non_key_attributes"] = nkaList
   743  
   744  		lsiList = append(lsiList, lsi)
   745  	}
   746  
   747  	err = d.Set("local_secondary_index", lsiList)
   748  	if err != nil {
   749  		return err
   750  	}
   751  
   752  	gsiList := make([]map[string]interface{}, 0, len(table.GlobalSecondaryIndexes))
   753  	for _, gsiObject := range table.GlobalSecondaryIndexes {
   754  		gsi := map[string]interface{}{
   755  			"write_capacity": *gsiObject.ProvisionedThroughput.WriteCapacityUnits,
   756  			"read_capacity":  *gsiObject.ProvisionedThroughput.ReadCapacityUnits,
   757  			"name":           *gsiObject.IndexName,
   758  		}
   759  
   760  		for _, attribute := range gsiObject.KeySchema {
   761  			if *attribute.KeyType == "HASH" {
   762  				gsi["hash_key"] = *attribute.AttributeName
   763  			}
   764  
   765  			if *attribute.KeyType == "RANGE" {
   766  				gsi["range_key"] = *attribute.AttributeName
   767  			}
   768  		}
   769  
   770  		gsi["projection_type"] = *(gsiObject.Projection.ProjectionType)
   771  
   772  		nonKeyAttrs := make([]string, 0, len(gsiObject.Projection.NonKeyAttributes))
   773  		for _, nonKeyAttr := range gsiObject.Projection.NonKeyAttributes {
   774  			nonKeyAttrs = append(nonKeyAttrs, *nonKeyAttr)
   775  		}
   776  		gsi["non_key_attributes"] = nonKeyAttrs
   777  
   778  		gsiList = append(gsiList, gsi)
   779  		log.Printf("[DEBUG] Added GSI: %s - Read: %d / Write: %d", gsi["name"], gsi["read_capacity"], gsi["write_capacity"])
   780  	}
   781  
   782  	if table.StreamSpecification != nil {
   783  		d.Set("stream_view_type", table.StreamSpecification.StreamViewType)
   784  		d.Set("stream_enabled", table.StreamSpecification.StreamEnabled)
   785  		d.Set("stream_arn", table.LatestStreamArn)
   786  	}
   787  
   788  	err = d.Set("global_secondary_index", gsiList)
   789  	if err != nil {
   790  		return err
   791  	}
   792  
   793  	d.Set("arn", table.TableArn)
   794  
   795  	timeToLiveReq := &dynamodb.DescribeTimeToLiveInput{
   796  		TableName: aws.String(d.Id()),
   797  	}
   798  	timeToLiveOutput, err := dynamodbconn.DescribeTimeToLive(timeToLiveReq)
   799  	if err != nil {
   800  		return err
   801  	}
   802  	timeToLive := []interface{}{}
   803  	attribute := map[string]*string{
   804  		"name": timeToLiveOutput.TimeToLiveDescription.AttributeName,
   805  		"type": timeToLiveOutput.TimeToLiveDescription.TimeToLiveStatus,
   806  	}
   807  	timeToLive = append(timeToLive, attribute)
   808  	d.Set("timeToLive", timeToLive)
   809  
   810  	log.Printf("[DEBUG] Loaded TimeToLive data for DynamoDB table '%s'", d.Id())
   811  
   812  	tags, err := readTableTags(d, meta)
   813  	if err != nil {
   814  		return err
   815  	}
   816  	if len(tags) != 0 {
   817  		d.Set("tags", tags)
   818  	}
   819  
   820  	return nil
   821  }
   822  
   823  func resourceAwsDynamoDbTableDelete(d *schema.ResourceData, meta interface{}) error {
   824  	dynamodbconn := meta.(*AWSClient).dynamodbconn
   825  
   826  	if err := waitForTableToBeActive(d.Id(), meta); err != nil {
   827  		return errwrap.Wrapf("Error waiting for Dynamo DB Table update: {{err}}", err)
   828  	}
   829  
   830  	log.Printf("[DEBUG] DynamoDB delete table: %s", d.Id())
   831  
   832  	_, err := dynamodbconn.DeleteTable(&dynamodb.DeleteTableInput{
   833  		TableName: aws.String(d.Id()),
   834  	})
   835  	if err != nil {
   836  		return err
   837  	}
   838  
   839  	params := &dynamodb.DescribeTableInput{
   840  		TableName: aws.String(d.Id()),
   841  	}
   842  
   843  	err = resource.Retry(10*time.Minute, func() *resource.RetryError {
   844  		t, err := dynamodbconn.DescribeTable(params)
   845  		if err != nil {
   846  			if awserr, ok := err.(awserr.Error); ok && awserr.Code() == "ResourceNotFoundException" {
   847  				return nil
   848  			}
   849  			// Didn't recognize the error, so shouldn't retry.
   850  			return resource.NonRetryableError(err)
   851  		}
   852  
   853  		if t != nil {
   854  			if t.Table.TableStatus != nil && strings.ToLower(*t.Table.TableStatus) == "deleting" {
   855  				log.Printf("[DEBUG] AWS Dynamo DB table (%s) is still deleting", d.Id())
   856  				return resource.RetryableError(fmt.Errorf("still deleting"))
   857  			}
   858  		}
   859  
   860  		// we should be not found or deleting, so error here
   861  		return resource.NonRetryableError(err)
   862  	})
   863  
   864  	// check error from retry
   865  	if err != nil {
   866  		return err
   867  	}
   868  
   869  	return nil
   870  }
   871  
   872  func createGSIFromData(data *map[string]interface{}) dynamodb.GlobalSecondaryIndex {
   873  
   874  	projection := &dynamodb.Projection{
   875  		ProjectionType: aws.String((*data)["projection_type"].(string)),
   876  	}
   877  
   878  	if (*data)["projection_type"] == "INCLUDE" {
   879  		non_key_attributes := []*string{}
   880  		for _, attr := range (*data)["non_key_attributes"].([]interface{}) {
   881  			non_key_attributes = append(non_key_attributes, aws.String(attr.(string)))
   882  		}
   883  		projection.NonKeyAttributes = non_key_attributes
   884  	}
   885  
   886  	writeCapacity := (*data)["write_capacity"].(int)
   887  	readCapacity := (*data)["read_capacity"].(int)
   888  
   889  	key_schema := []*dynamodb.KeySchemaElement{
   890  		{
   891  			AttributeName: aws.String((*data)["hash_key"].(string)),
   892  			KeyType:       aws.String("HASH"),
   893  		},
   894  	}
   895  
   896  	range_key_name := (*data)["range_key"]
   897  	if range_key_name != "" {
   898  		range_key_element := &dynamodb.KeySchemaElement{
   899  			AttributeName: aws.String(range_key_name.(string)),
   900  			KeyType:       aws.String("RANGE"),
   901  		}
   902  
   903  		key_schema = append(key_schema, range_key_element)
   904  	}
   905  
   906  	return dynamodb.GlobalSecondaryIndex{
   907  		IndexName:  aws.String((*data)["name"].(string)),
   908  		KeySchema:  key_schema,
   909  		Projection: projection,
   910  		ProvisionedThroughput: &dynamodb.ProvisionedThroughput{
   911  			WriteCapacityUnits: aws.Int64(int64(writeCapacity)),
   912  			ReadCapacityUnits:  aws.Int64(int64(readCapacity)),
   913  		},
   914  	}
   915  }
   916  
   917  func getGlobalSecondaryIndex(indexName string, indexList []*dynamodb.GlobalSecondaryIndexDescription) (*dynamodb.GlobalSecondaryIndexDescription, error) {
   918  	for _, gsi := range indexList {
   919  		if *gsi.IndexName == indexName {
   920  			return gsi, nil
   921  		}
   922  	}
   923  
   924  	return &dynamodb.GlobalSecondaryIndexDescription{}, fmt.Errorf("Can't find a GSI by that name...")
   925  }
   926  
   927  func getAttributeType(d *schema.ResourceData, attributeName string) (string, error) {
   928  	if attributedata, ok := d.GetOk("attribute"); ok {
   929  		attributeSet := attributedata.(*schema.Set)
   930  		for _, attribute := range attributeSet.List() {
   931  			attr := attribute.(map[string]interface{})
   932  			if attr["name"] == attributeName {
   933  				return attr["type"].(string), nil
   934  			}
   935  		}
   936  	}
   937  
   938  	return "", fmt.Errorf("Unable to find an attribute named %s", attributeName)
   939  }
   940  
   941  func waitForGSIToBeActive(tableName string, gsiName string, meta interface{}) error {
   942  	dynamodbconn := meta.(*AWSClient).dynamodbconn
   943  	req := &dynamodb.DescribeTableInput{
   944  		TableName: aws.String(tableName),
   945  	}
   946  
   947  	activeIndex := false
   948  
   949  	for activeIndex == false {
   950  
   951  		result, err := dynamodbconn.DescribeTable(req)
   952  
   953  		if err != nil {
   954  			return err
   955  		}
   956  
   957  		table := result.Table
   958  		var targetGSI *dynamodb.GlobalSecondaryIndexDescription = nil
   959  
   960  		for _, gsi := range table.GlobalSecondaryIndexes {
   961  			if *gsi.IndexName == gsiName {
   962  				targetGSI = gsi
   963  			}
   964  		}
   965  
   966  		if targetGSI != nil {
   967  			activeIndex = *targetGSI.IndexStatus == "ACTIVE"
   968  
   969  			if !activeIndex {
   970  				log.Printf("[DEBUG] Sleeping for 5 seconds for %s GSI to become active", gsiName)
   971  				time.Sleep(5 * time.Second)
   972  			}
   973  		} else {
   974  			log.Printf("[DEBUG] GSI %s did not exist, giving up", gsiName)
   975  			break
   976  		}
   977  	}
   978  
   979  	return nil
   980  
   981  }
   982  
   983  func waitForTableToBeActive(tableName string, meta interface{}) error {
   984  	dynamodbconn := meta.(*AWSClient).dynamodbconn
   985  	req := &dynamodb.DescribeTableInput{
   986  		TableName: aws.String(tableName),
   987  	}
   988  
   989  	activeState := false
   990  
   991  	for activeState == false {
   992  		result, err := dynamodbconn.DescribeTable(req)
   993  
   994  		if err != nil {
   995  			return err
   996  		}
   997  
   998  		activeState = *result.Table.TableStatus == "ACTIVE"
   999  
  1000  		// Wait for a few seconds
  1001  		if !activeState {
  1002  			log.Printf("[DEBUG] Sleeping for 5 seconds for table to become active")
  1003  			time.Sleep(5 * time.Second)
  1004  		}
  1005  	}
  1006  
  1007  	return nil
  1008  
  1009  }
  1010  
  1011  func waitForTimeToLiveUpdateToBeCompleted(tableName string, enabled bool, meta interface{}) error {
  1012  	dynamodbconn := meta.(*AWSClient).dynamodbconn
  1013  	req := &dynamodb.DescribeTimeToLiveInput{
  1014  		TableName: aws.String(tableName),
  1015  	}
  1016  
  1017  	stateMatched := false
  1018  	for stateMatched == false {
  1019  		result, err := dynamodbconn.DescribeTimeToLive(req)
  1020  
  1021  		if err != nil {
  1022  			return err
  1023  		}
  1024  
  1025  		if enabled {
  1026  			stateMatched = *result.TimeToLiveDescription.TimeToLiveStatus == dynamodb.TimeToLiveStatusEnabled
  1027  		} else {
  1028  			stateMatched = *result.TimeToLiveDescription.TimeToLiveStatus == dynamodb.TimeToLiveStatusDisabled
  1029  		}
  1030  
  1031  		// Wait for a few seconds, this may take a long time...
  1032  		if !stateMatched {
  1033  			log.Printf("[DEBUG] Sleeping for 5 seconds before checking TimeToLive state again")
  1034  			time.Sleep(5 * time.Second)
  1035  		}
  1036  	}
  1037  
  1038  	log.Printf("[DEBUG] TimeToLive update complete")
  1039  
  1040  	return nil
  1041  
  1042  }
  1043  
  1044  func createTableTags(d *schema.ResourceData, meta interface{}) error {
  1045  	// DynamoDB Table has to be in the ACTIVE state in order to tag the resource
  1046  	if err := waitForTableToBeActive(d.Id(), meta); err != nil {
  1047  		return err
  1048  	}
  1049  	tags := d.Get("tags").(map[string]interface{})
  1050  	arn := d.Get("arn").(string)
  1051  	dynamodbconn := meta.(*AWSClient).dynamodbconn
  1052  	req := &dynamodb.TagResourceInput{
  1053  		ResourceArn: aws.String(arn),
  1054  		Tags:        tagsFromMapDynamoDb(tags),
  1055  	}
  1056  	_, err := dynamodbconn.TagResource(req)
  1057  	if err != nil {
  1058  		return fmt.Errorf("Error tagging dynamodb resource: %s", err)
  1059  	}
  1060  	return nil
  1061  }
  1062  
  1063  func readTableTags(d *schema.ResourceData, meta interface{}) (map[string]string, error) {
  1064  	if err := waitForTableToBeActive(d.Id(), meta); err != nil {
  1065  		return nil, err
  1066  	}
  1067  	arn := d.Get("arn").(string)
  1068  	//result := make(map[string]string)
  1069  
  1070  	dynamodbconn := meta.(*AWSClient).dynamodbconn
  1071  	req := &dynamodb.ListTagsOfResourceInput{
  1072  		ResourceArn: aws.String(arn),
  1073  	}
  1074  
  1075  	output, err := dynamodbconn.ListTagsOfResource(req)
  1076  	if err != nil {
  1077  		return nil, fmt.Errorf("Error reading tags from dynamodb resource: %s", err)
  1078  	}
  1079  	result := tagsToMapDynamoDb(output.Tags)
  1080  	// TODO Read NextToken if avail
  1081  	return result, nil
  1082  }