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