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