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