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