github.com/leeprovoost/terraform@v0.6.10-0.20160119085442-96f3f76118e7/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/terraform/helper/resource"
    11  	"github.com/hashicorp/terraform/helper/schema"
    12  
    13  	"github.com/aws/aws-sdk-go/aws"
    14  	"github.com/aws/aws-sdk-go/aws/awserr"
    15  	"github.com/aws/aws-sdk-go/service/dynamodb"
    16  	"github.com/hashicorp/terraform/helper/hashcode"
    17  )
    18  
    19  // Number of times to retry if a throttling-related exception occurs
    20  const DYNAMODB_MAX_THROTTLE_RETRIES = 5
    21  
    22  // How long to sleep when a throttle-event happens
    23  const DYNAMODB_THROTTLE_SLEEP = 5 * time.Second
    24  
    25  // How long to sleep if a limit-exceeded event happens
    26  const DYNAMODB_LIMIT_EXCEEDED_SLEEP = 10 * time.Second
    27  
    28  // A number of these are marked as computed because if you don't
    29  // provide a value, DynamoDB will provide you with defaults (which are the
    30  // default values specified below)
    31  func resourceAwsDynamoDbTable() *schema.Resource {
    32  	return &schema.Resource{
    33  		Create: resourceAwsDynamoDbTableCreate,
    34  		Read:   resourceAwsDynamoDbTableRead,
    35  		Update: resourceAwsDynamoDbTableUpdate,
    36  		Delete: resourceAwsDynamoDbTableDelete,
    37  
    38  		Schema: map[string]*schema.Schema{
    39  			"arn": &schema.Schema{
    40  				Type:     schema.TypeString,
    41  				Computed: true,
    42  			},
    43  			"name": &schema.Schema{
    44  				Type:     schema.TypeString,
    45  				Required: true,
    46  				ForceNew: true,
    47  			},
    48  			"hash_key": &schema.Schema{
    49  				Type:     schema.TypeString,
    50  				Required: true,
    51  			},
    52  			"range_key": &schema.Schema{
    53  				Type:     schema.TypeString,
    54  				Optional: true,
    55  			},
    56  			"write_capacity": &schema.Schema{
    57  				Type:     schema.TypeInt,
    58  				Required: true,
    59  			},
    60  			"read_capacity": &schema.Schema{
    61  				Type:     schema.TypeInt,
    62  				Required: true,
    63  			},
    64  			"attribute": &schema.Schema{
    65  				Type:     schema.TypeSet,
    66  				Required: true,
    67  				Elem: &schema.Resource{
    68  					Schema: map[string]*schema.Schema{
    69  						"name": &schema.Schema{
    70  							Type:     schema.TypeString,
    71  							Required: true,
    72  						},
    73  						"type": &schema.Schema{
    74  							Type:     schema.TypeString,
    75  							Required: true,
    76  						},
    77  					},
    78  				},
    79  				Set: func(v interface{}) int {
    80  					var buf bytes.Buffer
    81  					m := v.(map[string]interface{})
    82  					buf.WriteString(fmt.Sprintf("%s-", m["name"].(string)))
    83  					return hashcode.String(buf.String())
    84  				},
    85  			},
    86  			"local_secondary_index": &schema.Schema{
    87  				Type:     schema.TypeSet,
    88  				Optional: true,
    89  				Elem: &schema.Resource{
    90  					Schema: map[string]*schema.Schema{
    91  						"name": &schema.Schema{
    92  							Type:     schema.TypeString,
    93  							Required: true,
    94  						},
    95  						"range_key": &schema.Schema{
    96  							Type:     schema.TypeString,
    97  							Required: true,
    98  						},
    99  						"projection_type": &schema.Schema{
   100  							Type:     schema.TypeString,
   101  							Required: true,
   102  						},
   103  						"non_key_attributes": &schema.Schema{
   104  							Type:     schema.TypeList,
   105  							Optional: true,
   106  							Elem:     &schema.Schema{Type: schema.TypeString},
   107  						},
   108  					},
   109  				},
   110  				Set: func(v interface{}) int {
   111  					var buf bytes.Buffer
   112  					m := v.(map[string]interface{})
   113  					buf.WriteString(fmt.Sprintf("%s-", m["name"].(string)))
   114  					return hashcode.String(buf.String())
   115  				},
   116  			},
   117  			"global_secondary_index": &schema.Schema{
   118  				Type:     schema.TypeSet,
   119  				Optional: true,
   120  				Elem: &schema.Resource{
   121  					Schema: map[string]*schema.Schema{
   122  						"name": &schema.Schema{
   123  							Type:     schema.TypeString,
   124  							Required: true,
   125  						},
   126  						"write_capacity": &schema.Schema{
   127  							Type:     schema.TypeInt,
   128  							Required: true,
   129  						},
   130  						"read_capacity": &schema.Schema{
   131  							Type:     schema.TypeInt,
   132  							Required: true,
   133  						},
   134  						"hash_key": &schema.Schema{
   135  							Type:     schema.TypeString,
   136  							Required: true,
   137  						},
   138  						"range_key": &schema.Schema{
   139  							Type:     schema.TypeString,
   140  							Optional: true,
   141  						},
   142  						"projection_type": &schema.Schema{
   143  							Type:     schema.TypeString,
   144  							Required: true,
   145  						},
   146  						"non_key_attributes": &schema.Schema{
   147  							Type:     schema.TypeList,
   148  							Optional: true,
   149  							Elem:     &schema.Schema{Type: schema.TypeString},
   150  						},
   151  					},
   152  				},
   153  				// GSI names are the uniqueness constraint
   154  				Set: func(v interface{}) int {
   155  					var buf bytes.Buffer
   156  					m := v.(map[string]interface{})
   157  					buf.WriteString(fmt.Sprintf("%s-", m["name"].(string)))
   158  					buf.WriteString(fmt.Sprintf("%d-", m["write_capacity"].(int)))
   159  					buf.WriteString(fmt.Sprintf("%d-", m["read_capacity"].(int)))
   160  					return hashcode.String(buf.String())
   161  				},
   162  			},
   163  			"stream_enabled": &schema.Schema{
   164  				Type:     schema.TypeBool,
   165  				Optional: true,
   166  				Computed: true,
   167  			},
   168  			"stream_view_type": &schema.Schema{
   169  				Type:     schema.TypeString,
   170  				Optional: true,
   171  				Computed: true,
   172  				StateFunc: func(v interface{}) string {
   173  					value := v.(string)
   174  					return strings.ToUpper(value)
   175  				},
   176  				ValidateFunc: validateStreamViewType,
   177  			},
   178  		},
   179  	}
   180  }
   181  
   182  func resourceAwsDynamoDbTableCreate(d *schema.ResourceData, meta interface{}) error {
   183  	dynamodbconn := meta.(*AWSClient).dynamodbconn
   184  
   185  	name := d.Get("name").(string)
   186  
   187  	log.Printf("[DEBUG] DynamoDB table create: %s", name)
   188  
   189  	throughput := &dynamodb.ProvisionedThroughput{
   190  		ReadCapacityUnits:  aws.Int64(int64(d.Get("read_capacity").(int))),
   191  		WriteCapacityUnits: aws.Int64(int64(d.Get("write_capacity").(int))),
   192  	}
   193  
   194  	hash_key_name := d.Get("hash_key").(string)
   195  	keyschema := []*dynamodb.KeySchemaElement{
   196  		&dynamodb.KeySchemaElement{
   197  			AttributeName: aws.String(hash_key_name),
   198  			KeyType:       aws.String("HASH"),
   199  		},
   200  	}
   201  
   202  	if range_key, ok := d.GetOk("range_key"); ok {
   203  		range_schema_element := &dynamodb.KeySchemaElement{
   204  			AttributeName: aws.String(range_key.(string)),
   205  			KeyType:       aws.String("RANGE"),
   206  		}
   207  		keyschema = append(keyschema, range_schema_element)
   208  	}
   209  
   210  	req := &dynamodb.CreateTableInput{
   211  		TableName:             aws.String(name),
   212  		ProvisionedThroughput: throughput,
   213  		KeySchema:             keyschema,
   214  	}
   215  
   216  	if attributedata, ok := d.GetOk("attribute"); ok {
   217  		attributes := []*dynamodb.AttributeDefinition{}
   218  		attributeSet := attributedata.(*schema.Set)
   219  		for _, attribute := range attributeSet.List() {
   220  			attr := attribute.(map[string]interface{})
   221  			attributes = append(attributes, &dynamodb.AttributeDefinition{
   222  				AttributeName: aws.String(attr["name"].(string)),
   223  				AttributeType: aws.String(attr["type"].(string)),
   224  			})
   225  		}
   226  
   227  		req.AttributeDefinitions = attributes
   228  	}
   229  
   230  	if lsidata, ok := d.GetOk("local_secondary_index"); ok {
   231  		fmt.Printf("[DEBUG] Adding LSI data to the table")
   232  
   233  		lsiSet := lsidata.(*schema.Set)
   234  		localSecondaryIndexes := []*dynamodb.LocalSecondaryIndex{}
   235  		for _, lsiObject := range lsiSet.List() {
   236  			lsi := lsiObject.(map[string]interface{})
   237  
   238  			projection := &dynamodb.Projection{
   239  				ProjectionType: aws.String(lsi["projection_type"].(string)),
   240  			}
   241  
   242  			if lsi["projection_type"] == "INCLUDE" {
   243  				non_key_attributes := []*string{}
   244  				for _, attr := range lsi["non_key_attributes"].([]interface{}) {
   245  					non_key_attributes = append(non_key_attributes, aws.String(attr.(string)))
   246  				}
   247  				projection.NonKeyAttributes = non_key_attributes
   248  			}
   249  
   250  			localSecondaryIndexes = append(localSecondaryIndexes, &dynamodb.LocalSecondaryIndex{
   251  				IndexName: aws.String(lsi["name"].(string)),
   252  				KeySchema: []*dynamodb.KeySchemaElement{
   253  					&dynamodb.KeySchemaElement{
   254  						AttributeName: aws.String(hash_key_name),
   255  						KeyType:       aws.String("HASH"),
   256  					},
   257  					&dynamodb.KeySchemaElement{
   258  						AttributeName: aws.String(lsi["range_key"].(string)),
   259  						KeyType:       aws.String("RANGE"),
   260  					},
   261  				},
   262  				Projection: projection,
   263  			})
   264  		}
   265  
   266  		req.LocalSecondaryIndexes = localSecondaryIndexes
   267  
   268  		fmt.Printf("[DEBUG] Added %d LSI definitions", len(localSecondaryIndexes))
   269  	}
   270  
   271  	if gsidata, ok := d.GetOk("global_secondary_index"); ok {
   272  		globalSecondaryIndexes := []*dynamodb.GlobalSecondaryIndex{}
   273  
   274  		gsiSet := gsidata.(*schema.Set)
   275  		for _, gsiObject := range gsiSet.List() {
   276  			gsi := gsiObject.(map[string]interface{})
   277  			gsiObject := createGSIFromData(&gsi)
   278  			globalSecondaryIndexes = append(globalSecondaryIndexes, &gsiObject)
   279  		}
   280  		req.GlobalSecondaryIndexes = globalSecondaryIndexes
   281  	}
   282  
   283  	if _, ok := d.GetOk("stream_enabled"); ok {
   284  
   285  		req.StreamSpecification = &dynamodb.StreamSpecification{
   286  			StreamEnabled:  aws.Bool(d.Get("stream_enabled").(bool)),
   287  			StreamViewType: aws.String(d.Get("stream_view_type").(string)),
   288  		}
   289  
   290  		fmt.Printf("[DEBUG] Adding StreamSpecifications to the table")
   291  	}
   292  
   293  	attemptCount := 1
   294  	for attemptCount <= DYNAMODB_MAX_THROTTLE_RETRIES {
   295  		output, err := dynamodbconn.CreateTable(req)
   296  		if err != nil {
   297  			if awsErr, ok := err.(awserr.Error); ok {
   298  				if awsErr.Code() == "ThrottlingException" {
   299  					log.Printf("[DEBUG] Attempt %d/%d: Sleeping for a bit to throttle back create request", attemptCount, DYNAMODB_MAX_THROTTLE_RETRIES)
   300  					time.Sleep(DYNAMODB_THROTTLE_SLEEP)
   301  					attemptCount += 1
   302  				} else if awsErr.Code() == "LimitExceededException" {
   303  					log.Printf("[DEBUG] Limit on concurrent table creations hit, sleeping for a bit")
   304  					time.Sleep(DYNAMODB_LIMIT_EXCEEDED_SLEEP)
   305  					attemptCount += 1
   306  				} else {
   307  					// Some other non-retryable exception occurred
   308  					return fmt.Errorf("AWS Error creating DynamoDB table: %s", err)
   309  				}
   310  			} else {
   311  				// Non-AWS exception occurred, give up
   312  				return fmt.Errorf("Error creating DynamoDB table: %s", err)
   313  			}
   314  		} else {
   315  			// No error, set ID and return
   316  			d.SetId(*output.TableDescription.TableName)
   317  			if err := d.Set("arn", *output.TableDescription.TableArn); err != nil {
   318  				return err
   319  			}
   320  
   321  			return resourceAwsDynamoDbTableRead(d, meta)
   322  		}
   323  	}
   324  
   325  	// Too many throttling events occurred, give up
   326  	return fmt.Errorf("Unable to create DynamoDB table '%s' after %d attempts", name, attemptCount)
   327  }
   328  
   329  func resourceAwsDynamoDbTableUpdate(d *schema.ResourceData, meta interface{}) error {
   330  
   331  	log.Printf("[DEBUG] Updating DynamoDB table %s", d.Id())
   332  	dynamodbconn := meta.(*AWSClient).dynamodbconn
   333  
   334  	// Ensure table is active before trying to update
   335  	waitForTableToBeActive(d.Id(), meta)
   336  
   337  	// LSI can only be done at create-time, abort if it's been changed
   338  	if d.HasChange("local_secondary_index") {
   339  		return fmt.Errorf("Local secondary indexes can only be built at creation, you cannot update them!")
   340  	}
   341  
   342  	if d.HasChange("hash_key") {
   343  		return fmt.Errorf("Hash key can only be specified at creation, you cannot modify it.")
   344  	}
   345  
   346  	if d.HasChange("range_key") {
   347  		return fmt.Errorf("Range key can only be specified at creation, you cannot modify it.")
   348  	}
   349  
   350  	if d.HasChange("read_capacity") || d.HasChange("write_capacity") {
   351  		req := &dynamodb.UpdateTableInput{
   352  			TableName: aws.String(d.Id()),
   353  		}
   354  
   355  		throughput := &dynamodb.ProvisionedThroughput{
   356  			ReadCapacityUnits:  aws.Int64(int64(d.Get("read_capacity").(int))),
   357  			WriteCapacityUnits: aws.Int64(int64(d.Get("write_capacity").(int))),
   358  		}
   359  		req.ProvisionedThroughput = throughput
   360  
   361  		_, err := dynamodbconn.UpdateTable(req)
   362  
   363  		if err != nil {
   364  			return err
   365  		}
   366  
   367  		waitForTableToBeActive(d.Id(), meta)
   368  	}
   369  
   370  	if d.HasChange("stream_enabled") || d.HasChange("stream_view_type") {
   371  		req := &dynamodb.UpdateTableInput{
   372  			TableName: aws.String(d.Id()),
   373  		}
   374  
   375  		req.StreamSpecification = &dynamodb.StreamSpecification{
   376  			StreamEnabled:  aws.Bool(d.Get("stream_enabled").(bool)),
   377  			StreamViewType: aws.String(d.Get("stream_view_type").(string)),
   378  		}
   379  
   380  		_, err := dynamodbconn.UpdateTable(req)
   381  
   382  		if err != nil {
   383  			return err
   384  		}
   385  
   386  		waitForTableToBeActive(d.Id(), meta)
   387  	}
   388  
   389  	if d.HasChange("global_secondary_index") {
   390  		log.Printf("[DEBUG] Changed GSI data")
   391  		req := &dynamodb.UpdateTableInput{
   392  			TableName: aws.String(d.Id()),
   393  		}
   394  
   395  		o, n := d.GetChange("global_secondary_index")
   396  
   397  		oldSet := o.(*schema.Set)
   398  		newSet := n.(*schema.Set)
   399  
   400  		// Track old names so we can know which ones we need to just update based on
   401  		// capacity changes, terraform appears to only diff on the set hash, not the
   402  		// contents so we need to make sure we don't delete any indexes that we
   403  		// just want to update the capacity for
   404  		oldGsiNameSet := make(map[string]bool)
   405  		newGsiNameSet := make(map[string]bool)
   406  
   407  		for _, gsidata := range oldSet.List() {
   408  			gsiName := gsidata.(map[string]interface{})["name"].(string)
   409  			oldGsiNameSet[gsiName] = true
   410  		}
   411  
   412  		for _, gsidata := range newSet.List() {
   413  			gsiName := gsidata.(map[string]interface{})["name"].(string)
   414  			newGsiNameSet[gsiName] = true
   415  		}
   416  
   417  		// First determine what's new
   418  		for _, newgsidata := range newSet.List() {
   419  			updates := []*dynamodb.GlobalSecondaryIndexUpdate{}
   420  			newGsiName := newgsidata.(map[string]interface{})["name"].(string)
   421  			if _, exists := oldGsiNameSet[newGsiName]; !exists {
   422  				attributes := []*dynamodb.AttributeDefinition{}
   423  				gsidata := newgsidata.(map[string]interface{})
   424  				gsi := createGSIFromData(&gsidata)
   425  				log.Printf("[DEBUG] Adding GSI %s", *gsi.IndexName)
   426  				update := &dynamodb.GlobalSecondaryIndexUpdate{
   427  					Create: &dynamodb.CreateGlobalSecondaryIndexAction{
   428  						IndexName:             gsi.IndexName,
   429  						KeySchema:             gsi.KeySchema,
   430  						ProvisionedThroughput: gsi.ProvisionedThroughput,
   431  						Projection:            gsi.Projection,
   432  					},
   433  				}
   434  				updates = append(updates, update)
   435  
   436  				// Hash key is required, range key isn't
   437  				hashkey_type, err := getAttributeType(d, *gsi.KeySchema[0].AttributeName)
   438  				if err != nil {
   439  					return err
   440  				}
   441  
   442  				attributes = append(attributes, &dynamodb.AttributeDefinition{
   443  					AttributeName: gsi.KeySchema[0].AttributeName,
   444  					AttributeType: aws.String(hashkey_type),
   445  				})
   446  
   447  				// If there's a range key, there will be 2 elements in KeySchema
   448  				if len(gsi.KeySchema) == 2 {
   449  					rangekey_type, err := getAttributeType(d, *gsi.KeySchema[1].AttributeName)
   450  					if err != nil {
   451  						return err
   452  					}
   453  
   454  					attributes = append(attributes, &dynamodb.AttributeDefinition{
   455  						AttributeName: gsi.KeySchema[1].AttributeName,
   456  						AttributeType: aws.String(rangekey_type),
   457  					})
   458  				}
   459  
   460  				req.AttributeDefinitions = attributes
   461  				req.GlobalSecondaryIndexUpdates = updates
   462  				_, err = dynamodbconn.UpdateTable(req)
   463  
   464  				if err != nil {
   465  					return err
   466  				}
   467  
   468  				waitForTableToBeActive(d.Id(), meta)
   469  				waitForGSIToBeActive(d.Id(), *gsi.IndexName, meta)
   470  
   471  			}
   472  		}
   473  
   474  		for _, oldgsidata := range oldSet.List() {
   475  			updates := []*dynamodb.GlobalSecondaryIndexUpdate{}
   476  			oldGsiName := oldgsidata.(map[string]interface{})["name"].(string)
   477  			if _, exists := newGsiNameSet[oldGsiName]; !exists {
   478  				gsidata := oldgsidata.(map[string]interface{})
   479  				log.Printf("[DEBUG] Deleting GSI %s", gsidata["name"].(string))
   480  				update := &dynamodb.GlobalSecondaryIndexUpdate{
   481  					Delete: &dynamodb.DeleteGlobalSecondaryIndexAction{
   482  						IndexName: aws.String(gsidata["name"].(string)),
   483  					},
   484  				}
   485  				updates = append(updates, update)
   486  
   487  				req.GlobalSecondaryIndexUpdates = updates
   488  				_, err := dynamodbconn.UpdateTable(req)
   489  
   490  				if err != nil {
   491  					return err
   492  				}
   493  
   494  				waitForTableToBeActive(d.Id(), meta)
   495  			}
   496  		}
   497  	}
   498  
   499  	// Update any out-of-date read / write capacity
   500  	if gsiObjects, ok := d.GetOk("global_secondary_index"); ok {
   501  		gsiSet := gsiObjects.(*schema.Set)
   502  		if len(gsiSet.List()) > 0 {
   503  			log.Printf("Updating capacity as needed!")
   504  
   505  			// We can only change throughput, but we need to make sure it's actually changed
   506  			tableDescription, err := dynamodbconn.DescribeTable(&dynamodb.DescribeTableInput{
   507  				TableName: aws.String(d.Id()),
   508  			})
   509  
   510  			if err != nil {
   511  				return err
   512  			}
   513  
   514  			table := tableDescription.Table
   515  
   516  			updates := []*dynamodb.GlobalSecondaryIndexUpdate{}
   517  
   518  			for _, updatedgsidata := range gsiSet.List() {
   519  				gsidata := updatedgsidata.(map[string]interface{})
   520  				gsiName := gsidata["name"].(string)
   521  				gsiWriteCapacity := gsidata["write_capacity"].(int)
   522  				gsiReadCapacity := gsidata["read_capacity"].(int)
   523  
   524  				log.Printf("[DEBUG] Updating GSI %s", gsiName)
   525  				gsi, err := getGlobalSecondaryIndex(gsiName, table.GlobalSecondaryIndexes)
   526  
   527  				if err != nil {
   528  					return err
   529  				}
   530  
   531  				capacityUpdated := false
   532  
   533  				if int64(gsiReadCapacity) != *gsi.ProvisionedThroughput.ReadCapacityUnits ||
   534  					int64(gsiWriteCapacity) != *gsi.ProvisionedThroughput.WriteCapacityUnits {
   535  					capacityUpdated = true
   536  				}
   537  
   538  				if capacityUpdated {
   539  					update := &dynamodb.GlobalSecondaryIndexUpdate{
   540  						Update: &dynamodb.UpdateGlobalSecondaryIndexAction{
   541  							IndexName: aws.String(gsidata["name"].(string)),
   542  							ProvisionedThroughput: &dynamodb.ProvisionedThroughput{
   543  								WriteCapacityUnits: aws.Int64(int64(gsiWriteCapacity)),
   544  								ReadCapacityUnits:  aws.Int64(int64(gsiReadCapacity)),
   545  							},
   546  						},
   547  					}
   548  					updates = append(updates, update)
   549  
   550  				}
   551  
   552  				if len(updates) > 0 {
   553  
   554  					req := &dynamodb.UpdateTableInput{
   555  						TableName: aws.String(d.Id()),
   556  					}
   557  
   558  					req.GlobalSecondaryIndexUpdates = updates
   559  
   560  					log.Printf("[DEBUG] Updating GSI read / write capacity on %s", d.Id())
   561  					_, err := dynamodbconn.UpdateTable(req)
   562  
   563  					if err != nil {
   564  						log.Printf("[DEBUG] Error updating table: %s", err)
   565  						return err
   566  					}
   567  				}
   568  			}
   569  		}
   570  
   571  	}
   572  
   573  	return resourceAwsDynamoDbTableRead(d, meta)
   574  }
   575  
   576  func resourceAwsDynamoDbTableRead(d *schema.ResourceData, meta interface{}) error {
   577  	dynamodbconn := meta.(*AWSClient).dynamodbconn
   578  	log.Printf("[DEBUG] Loading data for DynamoDB table '%s'", d.Id())
   579  	req := &dynamodb.DescribeTableInput{
   580  		TableName: aws.String(d.Id()),
   581  	}
   582  
   583  	result, err := dynamodbconn.DescribeTable(req)
   584  
   585  	if err != nil {
   586  		return err
   587  	}
   588  
   589  	table := result.Table
   590  
   591  	d.Set("write_capacity", table.ProvisionedThroughput.WriteCapacityUnits)
   592  	d.Set("read_capacity", table.ProvisionedThroughput.ReadCapacityUnits)
   593  
   594  	attributes := []interface{}{}
   595  	for _, attrdef := range table.AttributeDefinitions {
   596  		attribute := map[string]string{
   597  			"name": *attrdef.AttributeName,
   598  			"type": *attrdef.AttributeType,
   599  		}
   600  		attributes = append(attributes, attribute)
   601  		log.Printf("[DEBUG] Added Attribute: %s", attribute["name"])
   602  	}
   603  
   604  	d.Set("attribute", attributes)
   605  
   606  	gsiList := make([]map[string]interface{}, 0, len(table.GlobalSecondaryIndexes))
   607  	for _, gsiObject := range table.GlobalSecondaryIndexes {
   608  		gsi := map[string]interface{}{
   609  			"write_capacity": *gsiObject.ProvisionedThroughput.WriteCapacityUnits,
   610  			"read_capacity":  *gsiObject.ProvisionedThroughput.ReadCapacityUnits,
   611  			"name":           *gsiObject.IndexName,
   612  		}
   613  
   614  		for _, attribute := range gsiObject.KeySchema {
   615  			if *attribute.KeyType == "HASH" {
   616  				gsi["hash_key"] = *attribute.AttributeName
   617  			}
   618  
   619  			if *attribute.KeyType == "RANGE" {
   620  				gsi["range_key"] = *attribute.AttributeName
   621  			}
   622  		}
   623  
   624  		gsi["projection_type"] = *(gsiObject.Projection.ProjectionType)
   625  
   626  		nonKeyAttrs := make([]string, 0, len(gsiObject.Projection.NonKeyAttributes))
   627  		for _, nonKeyAttr := range gsiObject.Projection.NonKeyAttributes {
   628  			nonKeyAttrs = append(nonKeyAttrs, *nonKeyAttr)
   629  		}
   630  		gsi["non_key_attributes"] = nonKeyAttrs
   631  
   632  		gsiList = append(gsiList, gsi)
   633  		log.Printf("[DEBUG] Added GSI: %s - Read: %d / Write: %d", gsi["name"], gsi["read_capacity"], gsi["write_capacity"])
   634  	}
   635  
   636  	if table.StreamSpecification != nil {
   637  		d.Set("stream_view_type", table.StreamSpecification.StreamViewType)
   638  		d.Set("stream_enabled", table.StreamSpecification.StreamEnabled)
   639  	}
   640  
   641  	err = d.Set("global_secondary_index", gsiList)
   642  	if err != nil {
   643  		return err
   644  	}
   645  
   646  	d.Set("arn", table.TableArn)
   647  
   648  	return nil
   649  }
   650  
   651  func resourceAwsDynamoDbTableDelete(d *schema.ResourceData, meta interface{}) error {
   652  	dynamodbconn := meta.(*AWSClient).dynamodbconn
   653  
   654  	waitForTableToBeActive(d.Id(), meta)
   655  
   656  	log.Printf("[DEBUG] DynamoDB delete table: %s", d.Id())
   657  
   658  	_, err := dynamodbconn.DeleteTable(&dynamodb.DeleteTableInput{
   659  		TableName: aws.String(d.Id()),
   660  	})
   661  	if err != nil {
   662  		return err
   663  	}
   664  
   665  	params := &dynamodb.DescribeTableInput{
   666  		TableName: aws.String(d.Id()),
   667  	}
   668  
   669  	err = resource.Retry(10*time.Minute, func() error {
   670  		t, err := dynamodbconn.DescribeTable(params)
   671  		if err != nil {
   672  			if awserr, ok := err.(awserr.Error); ok && awserr.Code() == "ResourceNotFoundException" {
   673  				return nil
   674  			}
   675  			// Didn't recognize the error, so shouldn't retry.
   676  			return resource.RetryError{Err: err}
   677  		}
   678  
   679  		if t != nil {
   680  			if t.Table.TableStatus != nil && strings.ToLower(*t.Table.TableStatus) == "deleting" {
   681  				log.Printf("[DEBUG] AWS Dynamo DB table (%s) is still deleting", d.Id())
   682  				return fmt.Errorf("still deleting")
   683  			}
   684  		}
   685  
   686  		// we should be not found or deleting, so error here
   687  		return resource.RetryError{Err: fmt.Errorf("[ERR] Error deleting Dynamo DB table, unexpected state: %s", t)}
   688  	})
   689  
   690  	// check error from retry
   691  	if err != nil {
   692  		return err
   693  	}
   694  
   695  	return nil
   696  }
   697  
   698  func createGSIFromData(data *map[string]interface{}) dynamodb.GlobalSecondaryIndex {
   699  
   700  	projection := &dynamodb.Projection{
   701  		ProjectionType: aws.String((*data)["projection_type"].(string)),
   702  	}
   703  
   704  	if (*data)["projection_type"] == "INCLUDE" {
   705  		non_key_attributes := []*string{}
   706  		for _, attr := range (*data)["non_key_attributes"].([]interface{}) {
   707  			non_key_attributes = append(non_key_attributes, aws.String(attr.(string)))
   708  		}
   709  		projection.NonKeyAttributes = non_key_attributes
   710  	}
   711  
   712  	writeCapacity := (*data)["write_capacity"].(int)
   713  	readCapacity := (*data)["read_capacity"].(int)
   714  
   715  	key_schema := []*dynamodb.KeySchemaElement{
   716  		&dynamodb.KeySchemaElement{
   717  			AttributeName: aws.String((*data)["hash_key"].(string)),
   718  			KeyType:       aws.String("HASH"),
   719  		},
   720  	}
   721  
   722  	range_key_name := (*data)["range_key"]
   723  	if range_key_name != "" {
   724  		range_key_element := &dynamodb.KeySchemaElement{
   725  			AttributeName: aws.String(range_key_name.(string)),
   726  			KeyType:       aws.String("RANGE"),
   727  		}
   728  
   729  		key_schema = append(key_schema, range_key_element)
   730  	}
   731  
   732  	return dynamodb.GlobalSecondaryIndex{
   733  		IndexName:  aws.String((*data)["name"].(string)),
   734  		KeySchema:  key_schema,
   735  		Projection: projection,
   736  		ProvisionedThroughput: &dynamodb.ProvisionedThroughput{
   737  			WriteCapacityUnits: aws.Int64(int64(writeCapacity)),
   738  			ReadCapacityUnits:  aws.Int64(int64(readCapacity)),
   739  		},
   740  	}
   741  }
   742  
   743  func getGlobalSecondaryIndex(indexName string, indexList []*dynamodb.GlobalSecondaryIndexDescription) (*dynamodb.GlobalSecondaryIndexDescription, error) {
   744  	for _, gsi := range indexList {
   745  		if *gsi.IndexName == indexName {
   746  			return gsi, nil
   747  		}
   748  	}
   749  
   750  	return &dynamodb.GlobalSecondaryIndexDescription{}, fmt.Errorf("Can't find a GSI by that name...")
   751  }
   752  
   753  func getAttributeType(d *schema.ResourceData, attributeName string) (string, error) {
   754  	if attributedata, ok := d.GetOk("attribute"); ok {
   755  		attributeSet := attributedata.(*schema.Set)
   756  		for _, attribute := range attributeSet.List() {
   757  			attr := attribute.(map[string]interface{})
   758  			if attr["name"] == attributeName {
   759  				return attr["type"].(string), nil
   760  			}
   761  		}
   762  	}
   763  
   764  	return "", fmt.Errorf("Unable to find an attribute named %s", attributeName)
   765  }
   766  
   767  func waitForGSIToBeActive(tableName string, gsiName string, meta interface{}) error {
   768  	dynamodbconn := meta.(*AWSClient).dynamodbconn
   769  	req := &dynamodb.DescribeTableInput{
   770  		TableName: aws.String(tableName),
   771  	}
   772  
   773  	activeIndex := false
   774  
   775  	for activeIndex == false {
   776  
   777  		result, err := dynamodbconn.DescribeTable(req)
   778  
   779  		if err != nil {
   780  			return err
   781  		}
   782  
   783  		table := result.Table
   784  		var targetGSI *dynamodb.GlobalSecondaryIndexDescription = nil
   785  
   786  		for _, gsi := range table.GlobalSecondaryIndexes {
   787  			if *gsi.IndexName == gsiName {
   788  				targetGSI = gsi
   789  			}
   790  		}
   791  
   792  		if targetGSI != nil {
   793  			activeIndex = *targetGSI.IndexStatus == "ACTIVE"
   794  
   795  			if !activeIndex {
   796  				log.Printf("[DEBUG] Sleeping for 5 seconds for %s GSI to become active", gsiName)
   797  				time.Sleep(5 * time.Second)
   798  			}
   799  		} else {
   800  			log.Printf("[DEBUG] GSI %s did not exist, giving up", gsiName)
   801  			break
   802  		}
   803  	}
   804  
   805  	return nil
   806  
   807  }
   808  
   809  func waitForTableToBeActive(tableName string, meta interface{}) error {
   810  	dynamodbconn := meta.(*AWSClient).dynamodbconn
   811  	req := &dynamodb.DescribeTableInput{
   812  		TableName: aws.String(tableName),
   813  	}
   814  
   815  	activeState := false
   816  
   817  	for activeState == false {
   818  		result, err := dynamodbconn.DescribeTable(req)
   819  
   820  		if err != nil {
   821  			return err
   822  		}
   823  
   824  		activeState = *result.Table.TableStatus == "ACTIVE"
   825  
   826  		// Wait for a few seconds
   827  		if !activeState {
   828  			log.Printf("[DEBUG] Sleeping for 5 seconds for table to become active")
   829  			time.Sleep(5 * time.Second)
   830  		}
   831  	}
   832  
   833  	return nil
   834  
   835  }