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