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