From 34e9e313774db97de45a5e44eb67f5b4058983a2 Mon Sep 17 00:00:00 2001 From: John Ewart Date: Wed, 3 Jun 2015 17:05:02 -0700 Subject: [PATCH 01/23] Adding DynamoDB resource --- builtin/providers/aws/config.go | 31 +- builtin/providers/aws/provider.go | 1 + .../aws/resource_aws_dynamodb_table.go | 610 ++++++++++++++++++ 3 files changed, 629 insertions(+), 13 deletions(-) create mode 100644 builtin/providers/aws/resource_aws_dynamodb_table.go diff --git a/builtin/providers/aws/config.go b/builtin/providers/aws/config.go index 7908daf165..8ffd3be5e0 100644 --- a/builtin/providers/aws/config.go +++ b/builtin/providers/aws/config.go @@ -7,19 +7,20 @@ import ( "github.com/hashicorp/terraform/helper/multierror" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/service/autoscaling" - "github.com/aws/aws-sdk-go/service/ec2" - "github.com/aws/aws-sdk-go/service/elasticache" - "github.com/aws/aws-sdk-go/service/elb" - "github.com/aws/aws-sdk-go/service/iam" - "github.com/aws/aws-sdk-go/service/kinesis" - "github.com/aws/aws-sdk-go/service/rds" - "github.com/aws/aws-sdk-go/service/route53" - "github.com/aws/aws-sdk-go/service/s3" - "github.com/aws/aws-sdk-go/service/sns" - "github.com/aws/aws-sdk-go/service/sqs" + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/aws/credentials" + "github.com/awslabs/aws-sdk-go/service/autoscaling" + "github.com/awslabs/aws-sdk-go/service/ec2" + "github.com/awslabs/aws-sdk-go/service/elasticache" + "github.com/awslabs/aws-sdk-go/service/elb" + "github.com/awslabs/aws-sdk-go/service/iam" + "github.com/awslabs/aws-sdk-go/service/kinesis" + "github.com/awslabs/aws-sdk-go/service/rds" + "github.com/awslabs/aws-sdk-go/service/route53" + "github.com/awslabs/aws-sdk-go/service/s3" + "github.com/awslabs/aws-sdk-go/service/sns" + "github.com/awslabs/aws-sdk-go/service/sqs" + "github.com/awslabs/aws-sdk-go/service/dynamodb" ) type Config struct { @@ -34,6 +35,7 @@ type Config struct { } type AWSClient struct { + dynamodbconn *dynamodb.DynamoDB ec2conn *ec2.EC2 elbconn *elb.ELB autoscalingconn *autoscaling.AutoScaling @@ -84,6 +86,9 @@ func (c *Config) Client() (interface{}, error) { MaxRetries: c.MaxRetries, } + log.Println("[INFO] Initializing DynamoDB connection") + client.dynamodbconn = dynamodb.New(awsConfig) + log.Println("[INFO] Initializing ELB connection") client.elbconn = elb.New(awsConfig) diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 9e0f928a42..8851617655 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -90,6 +90,7 @@ func Provider() terraform.ResourceProvider { "aws_db_parameter_group": resourceAwsDbParameterGroup(), "aws_db_security_group": resourceAwsDbSecurityGroup(), "aws_db_subnet_group": resourceAwsDbSubnetGroup(), + "aws_dynamodb_table": resourceAwsDynamoDbTable(), "aws_ebs_volume": resourceAwsEbsVolume(), "aws_eip": resourceAwsEip(), "aws_elasticache_cluster": resourceAwsElasticacheCluster(), diff --git a/builtin/providers/aws/resource_aws_dynamodb_table.go b/builtin/providers/aws/resource_aws_dynamodb_table.go new file mode 100644 index 0000000000..a4131eb661 --- /dev/null +++ b/builtin/providers/aws/resource_aws_dynamodb_table.go @@ -0,0 +1,610 @@ +package aws + +import ( + "bytes" + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform/helper/schema" + + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/service/dynamodb" + "github.com/hashicorp/terraform/helper/hashcode" +) + +// A number of these are marked as computed because if you don't +// provide a value, DynamoDB will provide you with defaults (which are the +// default values specified below) +func resourceAwsDynamoDbTable() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsDynamoDbTableCreate, + Read: resourceAwsDynamoDbTableRead, + Update: resourceAwsDynamoDbTableUpdate, + Delete: resourceAwsDynamoDbTableDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "hash_key": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "range_key": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "write_capacity": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + "read_capacity": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + "attribute": &schema.Schema{ + Type: schema.TypeSet, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + }, + }, + Set: func(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["name"].(string))) + return hashcode.String(buf.String()) + }, + }, + "local_secondary_index": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "range_key": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "projection_type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "non_key_attributes": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + Set: func(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["name"].(string))) + return hashcode.String(buf.String()) + }, + }, + "global_secondary_index": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "write_capacity": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + "read_capacity": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + "hash_key": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "range_key": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "projection_type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "non_key_attributes": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + // GSI names are the uniqueness constraint + Set: func(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["name"].(string))) + return hashcode.String(buf.String()) + }, + }, + }, + } +} + +func resourceAwsDynamoDbTableCreate(d *schema.ResourceData, meta interface{}) error { + dynamodbconn := meta.(*AWSClient).dynamodbconn + + name := d.Get("name").(string) + + log.Printf("[DEBUG] DynamoDB table create: %s", name) + + throughput := &dynamodb.ProvisionedThroughput{ + ReadCapacityUnits: aws.Long(int64(d.Get("read_capacity").(int))), + WriteCapacityUnits: aws.Long(int64(d.Get("write_capacity").(int))), + } + + hash_key_name := d.Get("hash_key").(string) + keyschema := []*dynamodb.KeySchemaElement{ + &dynamodb.KeySchemaElement{ + AttributeName: aws.String(hash_key_name), + KeyType: aws.String("HASH"), + }, + } + + if range_key, ok := d.GetOk("range_key"); ok { + range_schema_element := &dynamodb.KeySchemaElement{ + AttributeName: aws.String(range_key.(string)), + KeyType: aws.String("RANGE"), + } + keyschema = append(keyschema, range_schema_element) + } + + req := &dynamodb.CreateTableInput{ + TableName: aws.String(name), + ProvisionedThroughput: throughput, + KeySchema: keyschema, + } + + if attributedata, ok := d.GetOk("attribute"); ok { + attributes := []*dynamodb.AttributeDefinition{} + attributeSet := attributedata.(*schema.Set) + for _, attribute := range attributeSet.List() { + attr := attribute.(map[string]interface{}) + attributes = append(attributes, &dynamodb.AttributeDefinition{ + AttributeName: aws.String(attr["name"].(string)), + AttributeType: aws.String(attr["type"].(string)), + }) + } + + req.AttributeDefinitions = attributes + } + + if lsidata, ok := d.GetOk("local_secondary_index"); ok { + fmt.Printf("[DEBUG] Adding LSI data to the table") + + lsiSet := lsidata.(*schema.Set) + localSecondaryIndexes := []*dynamodb.LocalSecondaryIndex{} + for _, lsiObject := range lsiSet.List() { + lsi := lsiObject.(map[string]interface{}) + + projection := &dynamodb.Projection{ + ProjectionType: aws.String(lsi["projection_type"].(string)), + } + + if lsi["projection_type"] != "ALL" { + non_key_attributes := []*string{} + for _, attr := range lsi["non_key_attributes"].([]interface{}) { + non_key_attributes = append(non_key_attributes, aws.String(attr.(string))) + } + projection.NonKeyAttributes = non_key_attributes + } + + localSecondaryIndexes = append(localSecondaryIndexes, &dynamodb.LocalSecondaryIndex{ + IndexName: aws.String(lsi["name"].(string)), + KeySchema: []*dynamodb.KeySchemaElement{ + &dynamodb.KeySchemaElement{ + AttributeName: aws.String(hash_key_name), + KeyType: aws.String("HASH"), + }, + &dynamodb.KeySchemaElement{ + AttributeName: aws.String(lsi["range_key"].(string)), + KeyType: aws.String("RANGE"), + }, + }, + Projection: projection, + }) + } + + req.LocalSecondaryIndexes = localSecondaryIndexes + + fmt.Printf("[DEBUG] Added %d LSI definitions", len(localSecondaryIndexes)) + } + + if gsidata, ok := d.GetOk("global_secondary_index"); ok { + globalSecondaryIndexes := []*dynamodb.GlobalSecondaryIndex{} + + gsiSet := gsidata.(*schema.Set) + for _, gsiObject := range gsiSet.List() { + gsi := gsiObject.(map[string]interface{}) + gsiObject := createGSIFromData(&gsi) + globalSecondaryIndexes = append(globalSecondaryIndexes, &gsiObject) + } + req.GlobalSecondaryIndexes = globalSecondaryIndexes + } + + output, err := dynamodbconn.CreateTable(req) + if err != nil { + return fmt.Errorf("Error creating DynamoDB table: %s", err) + } + + d.SetId(*output.TableDescription.TableName) + + // Creation complete, nothing to re-read + return nil +} + +func resourceAwsDynamoDbTableUpdate(d *schema.ResourceData, meta interface{}) error { + + log.Printf("[DEBUG] Updating DynamoDB table %s", d.Id()) + dynamodbconn := meta.(*AWSClient).dynamodbconn + + // Ensure table is active before trying to update + waitForTableToBeActive(d.Id(), meta) + + // LSI can only be done at create-time, abort if it's been changed + if d.HasChange("local_secondary_index") { + return fmt.Errorf("Local secondary indexes can only be built at creation, you cannot update them!") + } + + if d.HasChange("hash_key") { + return fmt.Errorf("Hash key can only be specified at creation, you cannot modify it.") + } + + if d.HasChange("range_key") { + return fmt.Errorf("Range key can only be specified at creation, you cannot modify it.") + } + + if d.HasChange("attribute") { + req := &dynamodb.UpdateTableInput{ + TableName: aws.String(d.Id()), + } + + newAttributes := []*dynamodb.AttributeDefinition{} + _, n := d.GetChange("attribute") + newAttributeSet := n.(*schema.Set) + for _, attribute := range newAttributeSet.List() { + attr := attribute.(map[string]interface{}) + newAttributes = append(newAttributes, &dynamodb.AttributeDefinition{ + AttributeName: aws.String(attr["name"].(string)), + AttributeType: aws.String(attr["type"].(string)), + }) + } + + req.AttributeDefinitions = newAttributes + } + + if d.HasChange("read_capacity") || d.HasChange("write_capacity") { + req := &dynamodb.UpdateTableInput{ + TableName: aws.String(d.Id()), + } + + throughput := &dynamodb.ProvisionedThroughput{ + ReadCapacityUnits: aws.Long(int64(d.Get("read_capacity").(int))), + WriteCapacityUnits: aws.Long(int64(d.Get("write_capacity").(int))), + } + req.ProvisionedThroughput = throughput + + _, err := dynamodbconn.UpdateTable(req) + + if err != nil { + return err + } + + waitForTableToBeActive(d.Id(), meta) + } + + if d.HasChange("global_secondary_index") { + req := &dynamodb.UpdateTableInput{ + TableName: aws.String(d.Id()), + } + + o, n := d.GetChange("global_secondary_index") + + oldSet := o.(*schema.Set) + newSet := n.(*schema.Set) + changedSet := newSet.Intersection(oldSet) + + // First determine what's new + for _, newgsidata := range newSet.List() { + updates := []*dynamodb.GlobalSecondaryIndexUpdate{} + if !oldSet.Contains(newgsidata) { + attributes := []*dynamodb.AttributeDefinition{} + gsidata := newgsidata.(map[string]interface{}) + gsi := createGSIFromData(&gsidata) + log.Printf("[DEBUG] Adding GSI %s", *gsi.IndexName) + update := &dynamodb.GlobalSecondaryIndexUpdate{ + Create: &dynamodb.CreateGlobalSecondaryIndexAction{ + IndexName: gsi.IndexName, + KeySchema: gsi.KeySchema, + ProvisionedThroughput: gsi.ProvisionedThroughput, + Projection: gsi.Projection, + }, + } + updates = append(updates, update) + hashkey_type, err := getAttributeType(d, *(gsi.KeySchema[0].AttributeName)) + if err != nil { + return err + } + + rangekey_type, err := getAttributeType(d, *(gsi.KeySchema[1].AttributeName)) + if err != nil { + return err + } + + attributes = append(attributes, &dynamodb.AttributeDefinition{ + AttributeName: gsi.KeySchema[0].AttributeName, + AttributeType: aws.String(hashkey_type), + }) + attributes = append(attributes, &dynamodb.AttributeDefinition{ + AttributeName: gsi.KeySchema[1].AttributeName, + AttributeType: aws.String(rangekey_type), + }) + + req.AttributeDefinitions = attributes + req.GlobalSecondaryIndexUpdates = updates + _, err = dynamodbconn.UpdateTable(req) + + if err != nil { + return err + } + + waitForTableToBeActive(d.Id(), meta) + waitForGSIToBeActive(d.Id(), *gsi.IndexName, meta) + + } + } + + for _, oldgsidata := range oldSet.List() { + updates := []*dynamodb.GlobalSecondaryIndexUpdate{} + if !newSet.Contains(oldgsidata) { + gsidata := oldgsidata.(map[string]interface{}) + log.Printf("[DEBUG] Deleting GSI %s", gsidata["name"].(string)) + update := &dynamodb.GlobalSecondaryIndexUpdate{ + Delete: &dynamodb.DeleteGlobalSecondaryIndexAction{ + IndexName: aws.String(gsidata["name"].(string)), + }, + } + updates = append(updates, update) + + req.GlobalSecondaryIndexUpdates = updates + _, err := dynamodbconn.UpdateTable(req) + + if err != nil { + return err + } + + waitForTableToBeActive(d.Id(), meta) + } + } + + for _, updatedgsidata := range changedSet.List() { + updates := []*dynamodb.GlobalSecondaryIndexUpdate{} + gsidata := updatedgsidata.(map[string]interface{}) + log.Printf("[DEBUG] Updating GSI %s", gsidata["name"].(string)) + update := &dynamodb.GlobalSecondaryIndexUpdate{ + Update: &dynamodb.UpdateGlobalSecondaryIndexAction{ + IndexName: aws.String(gsidata["name"].(string)), + ProvisionedThroughput: &dynamodb.ProvisionedThroughput{ + WriteCapacityUnits: aws.Long(int64(gsidata["write_capacity"].(int))), + ReadCapacityUnits: aws.Long(int64(gsidata["read_capacity"].(int))), + }, + }, + } + updates = append(updates, update) + + req.GlobalSecondaryIndexUpdates = updates + + _, err := dynamodbconn.UpdateTable(req) + + if err != nil { + log.Printf("[DEBUG] Error updating table: %s", err) + return err + } + } + } + + return resourceAwsDynamoDbTableRead(d, meta) +} + +func resourceAwsDynamoDbTableRead(d *schema.ResourceData, meta interface{}) error { + dynamodbconn := meta.(*AWSClient).dynamodbconn + req := &dynamodb.DescribeTableInput{ + TableName: aws.String(d.Id()), + } + + result, err := dynamodbconn.DescribeTable(req) + + if err != nil { + return err + } + + table := result.Table + + d.Set("write_capacity", table.ProvisionedThroughput.WriteCapacityUnits) + d.Set("read_capacity", table.ProvisionedThroughput.ReadCapacityUnits) + + attributes := []interface{}{} + for _, attrdef := range table.AttributeDefinitions { + attribute := make(map[string]string) + attribute["name"] = *(attrdef.AttributeName) + attribute["type"] = *(attrdef.AttributeType) + attributes = append(attributes, attribute) + } + + d.Set("attribute", attributes) + + gsiList := []interface{}{} + for _, gsiObject := range table.GlobalSecondaryIndexes { + gsi := make(map[string]interface{}) + gsi["write_capacity"] = gsiObject.ProvisionedThroughput.WriteCapacityUnits + gsi["read_capacity"] = gsiObject.ProvisionedThroughput.ReadCapacityUnits + gsi["name"] = gsiObject.IndexName + gsiList = append(gsiList, gsi) + } + + d.Set("global_secondary_index", gsiList) + + return nil +} + +func resourceAwsDynamoDbTableDelete(d *schema.ResourceData, meta interface{}) error { + dynamodbconn := meta.(*AWSClient).dynamodbconn + + waitForTableToBeActive(d.Id(), meta) + + log.Printf("[DEBUG] DynamoDB delete table: %s", d.Id()) + + _, err := dynamodbconn.DeleteTable(&dynamodb.DeleteTableInput{ + TableName: aws.String(d.Id()), + }) + if err != nil { + return err + } + return nil +} + +func createGSIFromData(data *map[string]interface{}) dynamodb.GlobalSecondaryIndex { + + projection := &dynamodb.Projection{ + ProjectionType: aws.String((*data)["projection_type"].(string)), + } + + if (*data)["projection_type"] != "ALL" { + non_key_attributes := []*string{} + for _, attr := range (*data)["non_key_attributes"].([]interface{}) { + non_key_attributes = append(non_key_attributes, aws.String(attr.(string))) + } + projection.NonKeyAttributes = non_key_attributes + } + + writeCapacity := (*data)["write_capacity"].(int) + readCapacity := (*data)["read_capacity"].(int) + + return dynamodb.GlobalSecondaryIndex{ + IndexName: aws.String((*data)["name"].(string)), + KeySchema: []*dynamodb.KeySchemaElement{ + &dynamodb.KeySchemaElement{ + AttributeName: aws.String((*data)["hash_key"].(string)), + KeyType: aws.String("HASH"), + }, + &dynamodb.KeySchemaElement{ + AttributeName: aws.String((*data)["range_key"].(string)), + KeyType: aws.String("RANGE"), + }, + }, + Projection: projection, + ProvisionedThroughput: &dynamodb.ProvisionedThroughput{ + WriteCapacityUnits: aws.Long(int64(writeCapacity)), + ReadCapacityUnits: aws.Long(int64(readCapacity)), + }, + } +} + +func getAttributeType(d *schema.ResourceData, attributeName string) (string, error) { + if attributedata, ok := d.GetOk("attribute"); ok { + attributeSet := attributedata.(*schema.Set) + for _, attribute := range attributeSet.List() { + attr := attribute.(map[string]interface{}) + if attr["name"] == attributeName { + return attr["type"].(string), nil + } + } + } + + return "", fmt.Errorf("Unable to find an attribute named %s", attributeName) +} + +func waitForGSIToBeActive(tableName string, gsiName string, meta interface{}) error { + dynamodbconn := meta.(*AWSClient).dynamodbconn + req := &dynamodb.DescribeTableInput{ + TableName: aws.String(tableName), + } + + activeIndex := false + + for activeIndex == false { + + result, err := dynamodbconn.DescribeTable(req) + + if err != nil { + return err + } + + table := result.Table + var targetGSI *dynamodb.GlobalSecondaryIndexDescription = nil + + for _, gsi := range table.GlobalSecondaryIndexes { + if *gsi.IndexName == gsiName { + targetGSI = gsi + } + } + + if targetGSI != nil { + activeIndex = *targetGSI.IndexStatus == "ACTIVE" + + if !activeIndex { + log.Printf("[DEBUG] Sleeping for 3 seconds for %s GSI to become active", gsiName) + time.Sleep(3 * time.Second) + } + } else { + log.Printf("[DEBUG] GSI %s did not exist, giving up", gsiName) + break + } + } + + return nil + +} + +func waitForTableToBeActive(tableName string, meta interface{}) error { + dynamodbconn := meta.(*AWSClient).dynamodbconn + req := &dynamodb.DescribeTableInput{ + TableName: aws.String(tableName), + } + + activeState := false + + for activeState == false { + result, err := dynamodbconn.DescribeTable(req) + + if err != nil { + return err + } + + activeState = *(result.Table.TableStatus) == "ACTIVE" + + // Wait for a few seconds + if !activeState { + log.Printf("[DEBUG] Sleeping for 3 seconds for table to become active") + time.Sleep(3 * time.Second) + } + } + + return nil + +} From 1669c35007f5c20fc156766456a5e1b593766868 Mon Sep 17 00:00:00 2001 From: John Ewart Date: Wed, 3 Jun 2015 17:05:40 -0700 Subject: [PATCH 02/23] Adding DynamoDB acceptance tests --- .../aws/resource_aws_dynamodb_table_test.go | 296 ++++++++++++++++++ 1 file changed, 296 insertions(+) create mode 100644 builtin/providers/aws/resource_aws_dynamodb_table_test.go diff --git a/builtin/providers/aws/resource_aws_dynamodb_table_test.go b/builtin/providers/aws/resource_aws_dynamodb_table_test.go new file mode 100644 index 0000000000..149f36a559 --- /dev/null +++ b/builtin/providers/aws/resource_aws_dynamodb_table_test.go @@ -0,0 +1,296 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/aws/awserr" + "github.com/awslabs/aws-sdk-go/service/dynamodb" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSDynamoDbTable(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSDynamoDbTableDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSDynamoDbConfigInitialState, + Check: resource.ComposeTestCheckFunc( + testAccCheckInitialAWSDynamoDbTableExists("aws_dynamodb_table.basic-dynamodb-table"), + ), + }, + resource.TestStep{ + Config: testAccAWSDynamoDbConfigAddSecondaryGSI, + Check: resource.ComposeTestCheckFunc( + testAccCheckDynamoDbTableWasUpdated("aws_dynamodb_table.basic-dynamodb-table"), + ), + }, + }, + }) +} + +func testAccCheckAWSDynamoDbTableDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).dynamodbconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_dynamodb_table" { + continue + } + + fmt.Printf("[DEBUG] Checking if DynamoDB table %s exists", rs.Primary.ID) + // Check if queue exists by checking for its attributes + params := &dynamodb.DescribeTableInput{ + TableName: aws.String(rs.Primary.ID), + } + _, err := conn.DescribeTable(params) + if err == nil { + return fmt.Errorf("DynamoDB table %s still exists. Failing!", rs.Primary.ID) + } + + // Verify the error is what we want + _, ok := err.(awserr.Error) + if !ok { + return err + } + } + + return nil +} + +func testAccCheckInitialAWSDynamoDbTableExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + fmt.Printf("[DEBUG] Trying to create initial table state!") + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No DynamoDB table name specified!") + } + + conn := testAccProvider.Meta().(*AWSClient).dynamodbconn + + params := &dynamodb.DescribeTableInput{ + TableName: aws.String(rs.Primary.ID), + } + + resp, err := conn.DescribeTable(params) + + if err != nil { + fmt.Printf("[ERROR] Problem describing table '%s': %s", rs.Primary.ID, err) + return err + } + + table := resp.Table + + fmt.Printf("[DEBUG] Checking on table %s", rs.Primary.ID) + + if *table.ProvisionedThroughput.WriteCapacityUnits != 20 { + return fmt.Errorf("Provisioned write capacity was %d, not 20!", table.ProvisionedThroughput.WriteCapacityUnits) + } + + if *table.ProvisionedThroughput.ReadCapacityUnits != 10 { + return fmt.Errorf("Provisioned read capacity was %d, not 10!", table.ProvisionedThroughput.ReadCapacityUnits) + } + + attrCount := len(table.AttributeDefinitions) + gsiCount := len(table.GlobalSecondaryIndexes) + lsiCount := len(table.LocalSecondaryIndexes) + + if attrCount != 4 { + return fmt.Errorf("There were %d attributes, not 4 like there should have been!", attrCount) + } + + if gsiCount != 1 { + return fmt.Errorf("There were %d GSIs, not 1 like there should have been!", gsiCount) + } + + if lsiCount != 1 { + return fmt.Errorf("There were %d LSIs, not 1 like there should have been!", lsiCount) + } + + attrmap := dynamoDbAttributesToMap(&table.AttributeDefinitions) + if attrmap["TestTableHashKey"] != "S" { + return fmt.Errorf("Test table hash key was of type %s instead of S!", attrmap["TestTableHashKey"]) + } + if attrmap["TestTableRangeKey"] != "S" { + return fmt.Errorf("Test table range key was of type %s instead of S!", attrmap["TestTableRangeKey"]) + } + if attrmap["TestLSIRangeKey"] != "N" { + return fmt.Errorf("Test table LSI range key was of type %s instead of N!", attrmap["TestLSIRangeKey"]) + } + if attrmap["TestGSIRangeKey"] != "S" { + return fmt.Errorf("Test table GSI range key was of type %s instead of S!", attrmap["TestGSIRangeKey"]) + } + + return nil + } +} + +func testAccCheckDynamoDbTableWasUpdated(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No DynamoDB table name specified!") + } + + conn := testAccProvider.Meta().(*AWSClient).dynamodbconn + + params := &dynamodb.DescribeTableInput{ + TableName: aws.String(rs.Primary.ID), + } + resp, err := conn.DescribeTable(params) + table := resp.Table + + if err != nil { + return err + } + + attrCount := len(table.AttributeDefinitions) + gsiCount := len(table.GlobalSecondaryIndexes) + lsiCount := len(table.LocalSecondaryIndexes) + + if attrCount != 4 { + return fmt.Errorf("There were %d attributes, not 4 like there should have been!", attrCount) + } + + if gsiCount != 1 { + return fmt.Errorf("There were %d GSIs, not 1 like there should have been!", gsiCount) + } + + if lsiCount != 1 { + return fmt.Errorf("There were %d LSIs, not 1 like there should have been!", lsiCount) + } + + if dynamoDbGetGSIIndex(&table.GlobalSecondaryIndexes, "ReplacementTestTableGSI") == -1 { + return fmt.Errorf("Could not find GSI named 'ReplacementTestTableGSI' in the table!") + } + + if dynamoDbGetGSIIndex(&table.GlobalSecondaryIndexes, "InitialTestTableGSI") != -1 { + return fmt.Errorf("Should have removed 'InitialTestTableGSI' but it still exists!") + } + + attrmap := dynamoDbAttributesToMap(&table.AttributeDefinitions) + if attrmap["TestTableHashKey"] != "S" { + return fmt.Errorf("Test table hash key was of type %s instead of S!", attrmap["TestTableHashKey"]) + } + if attrmap["TestTableRangeKey"] != "S" { + return fmt.Errorf("Test table range key was of type %s instead of S!", attrmap["TestTableRangeKey"]) + } + if attrmap["TestLSIRangeKey"] != "N" { + return fmt.Errorf("Test table LSI range key was of type %s instead of N!", attrmap["TestLSIRangeKey"]) + } + if attrmap["ReplacementGSIRangeKey"] != "N" { + return fmt.Errorf("Test table replacement GSI range key was of type %s instead of N!", attrmap["ReplacementGSIRangeKey"]) + } + + return nil + } +} + +func dynamoDbGetGSIIndex(gsiList *[]*dynamodb.GlobalSecondaryIndexDescription, target string) int { + for idx, gsiObject := range *gsiList { + if *gsiObject.IndexName == target { + return idx + } + } + + return -1 +} + +func dynamoDbAttributesToMap(attributes *[]*dynamodb.AttributeDefinition) map[string]string { + attrmap := make(map[string]string) + + for _, attrdef := range *attributes { + attrmap[*(attrdef.AttributeName)] = *(attrdef.AttributeType) + } + + return attrmap +} + +const testAccAWSDynamoDbConfigInitialState = ` +resource "aws_dynamodb_table" "basic-dynamodb-table" { + name = "TerraformTestTable" + read_capacity = 10 + write_capacity = 20 + hash_key = "TestTableHashKey" + range_key = "TestTableRangeKey" + attribute { + name = "TestTableHashKey" + type = "S" + } + attribute { + name = "TestTableRangeKey" + type = "S" + } + attribute { + name = "TestLSIRangeKey" + type = "N" + } + attribute { + name = "TestGSIRangeKey" + type = "S" + } + local_secondary_index { + name = "TestTableLSI" + range_key = "TestLSIRangeKey" + projection_type = "ALL" + } + global_secondary_index { + name = "InitialTestTableGSI" + hash_key = "TestTableHashKey" + range_key = "TestGSIRangeKey" + write_capacity = 10 + read_capacity = 10 + projection_type = "ALL" + } +} +` + +const testAccAWSDynamoDbConfigAddSecondaryGSI = ` +resource "aws_dynamodb_table" "basic-dynamodb-table" { + name = "TerraformTestTable" + read_capacity = 20 + write_capacity = 20 + hash_key = "TestTableHashKey" + range_key = "TestTableRangeKey" + attribute { + name = "TestTableHashKey" + type = "S" + } + attribute { + name = "TestTableRangeKey" + type = "S" + } + attribute { + name = "TestLSIRangeKey" + type = "N" + } + attribute { + name = "ReplacementGSIRangeKey" + type = "N" + } + local_secondary_index { + name = "TestTableLSI" + range_key = "TestLSIRangeKey" + projection_type = "ALL" + } + global_secondary_index { + name = "ReplacementTestTableGSI" + hash_key = "TestTableHashKey" + range_key = "ReplacementGSIRangeKey" + write_capacity = 5 + read_capacity = 5 + projection_type = "ALL" + } +} +` From bbc6f14807e826120aa27c028521540b4234b9f3 Mon Sep 17 00:00:00 2001 From: John Ewart Date: Wed, 3 Jun 2015 17:07:03 -0700 Subject: [PATCH 03/23] Adding DynamoDB documentation --- .../aws/r/dynamodb_table.html.markdown | 109 ++++++++++++++++++ website/source/layouts/aws.erb | 4 + 2 files changed, 113 insertions(+) create mode 100644 website/source/docs/providers/aws/r/dynamodb_table.html.markdown diff --git a/website/source/docs/providers/aws/r/dynamodb_table.html.markdown b/website/source/docs/providers/aws/r/dynamodb_table.html.markdown new file mode 100644 index 0000000000..e176f39a0f --- /dev/null +++ b/website/source/docs/providers/aws/r/dynamodb_table.html.markdown @@ -0,0 +1,109 @@ +--- +layout: "aws" +page_title: "AWS: dynamodb_table" +sidebar_current: "docs-aws-resource-dynamodb-table" +description: |- + Provides a DynamoDB table resource +--- + +# aws\_dynamodb\_table + +Provides a DynamoDB table resource + +## Example Usage + +The following dynamodb table description models the table and GSI shown +in the [AWS SDK example documentation](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html) + +``` +resource "aws_dynamodb_table" "basic-dynamodb-table" { + name = "GameScores" + read_capacity = 20 + write_capacity = 20 + hash_key = "UserId" + range_key = "GameTitle" + attribute { + name = "Username" + type = "S" + } + attribute { + name = "GameTitle" + type = "S" + } + attribute { + name = "TopScore" + type = "N" + } + attribute { + name = "TopScoreDateTime" + type = "S" + } + attribute { + name = "Wins" + type = "N" + } + attribute { + name = "Losses" + type = "N" + } + global_secondary_index { + name = "GameTitleIndex" + hash_key = "GameTitle" + range_key = "TopScore" + write_capacity = 10 + read_capacity = 10 + projection_type = "INCLUDE" + non_key_attributes = [ "UserId" ] + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the table, this needs to be unique + within a region. +* `read_capacity` - (Required) The number of read units for this table +* `write_capacity` - (Required) The number of write units for this table +* `hash_key` - (Required) The attribute to use as the hash key (the + attribute must also be defined as an attribute record +* `range_key` - (Optional) The attribute to use as the range key (must + also be defined) +* `attribute` - Define an attribute, has two properties: + * `name` - The name of the attribute + * `type` - One of: S, N, or B for (S)tring, (N)umber or (B)inary data +* `local_secondary_index` - (Optional) Describe an LSI on the table; + these can only be allocated *at creation* so you cannot change this +definition after you have created the resource. +* `global_secondary_index` - (Optional) Describe a GSO for the table; + subject to the normal limits on the number of GSIs, projected +attributes, etc. + +For both `local_secondary_index` and `global_secondary_index` objects, +the following properties are supported: + +* `name` - (Required) The name of the LSI or GSI +* `hash_key` - (Required) The name of the hash key in the index; must be + defined as an attribute in the resource +* `range_key` - (Required) The name of the range key; must be defined +* `projection_type` - (Required) One of "ALL", "INCLUDE" or "KEYS_ONLY" + where *ALL* projects every attribute into the index, *KEYS_ONLY* + projects just the hash and range key into the index, and *INCLUDE* + projects only the keys specified in the _non_key_attributes_ +parameter. +* `non_key_attributes` - (Optional) Only required with *INCLUDE* as a + projection type; a list of attributes to project into the index. For +each attribute listed, you need to make sure that it has been defined in +the table object. + +For `global_secondary_index` objects only, you need to specify +`write_capacity` and `read_capacity` in the same way you would for the +table as they have separate I/O capacity. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The name of the table + diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index 219b3ed3c3..2239975f3c 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -37,6 +37,10 @@ aws_db_subnet_group + > + aws_dynamodb_table + + > aws_ebs_volume From 4784eff9cad317a2aae6e445a2d86d0333622e0c Mon Sep 17 00:00:00 2001 From: John Ewart Date: Wed, 3 Jun 2015 17:12:41 -0700 Subject: [PATCH 04/23] Fix AWS SDK imports --- builtin/providers/aws/config.go | 28 +++++++++---------- .../aws/resource_aws_dynamodb_table.go | 4 +-- .../aws/resource_aws_dynamodb_table_test.go | 6 ++-- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/builtin/providers/aws/config.go b/builtin/providers/aws/config.go index 8ffd3be5e0..ee491b59f1 100644 --- a/builtin/providers/aws/config.go +++ b/builtin/providers/aws/config.go @@ -7,20 +7,20 @@ import ( "github.com/hashicorp/terraform/helper/multierror" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/credentials" - "github.com/awslabs/aws-sdk-go/service/autoscaling" - "github.com/awslabs/aws-sdk-go/service/ec2" - "github.com/awslabs/aws-sdk-go/service/elasticache" - "github.com/awslabs/aws-sdk-go/service/elb" - "github.com/awslabs/aws-sdk-go/service/iam" - "github.com/awslabs/aws-sdk-go/service/kinesis" - "github.com/awslabs/aws-sdk-go/service/rds" - "github.com/awslabs/aws-sdk-go/service/route53" - "github.com/awslabs/aws-sdk-go/service/s3" - "github.com/awslabs/aws-sdk-go/service/sns" - "github.com/awslabs/aws-sdk-go/service/sqs" - "github.com/awslabs/aws-sdk-go/service/dynamodb" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/service/autoscaling" + "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/elasticache" + "github.com/aws/aws-sdk-go/service/elb" + "github.com/aws/aws-sdk-go/service/iam" + "github.com/aws/aws-sdk-go/service/kinesis" + "github.com/aws/aws-sdk-go/service/rds" + "github.com/aws/aws-sdk-go/service/route53" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/service/sns" + "github.com/aws/aws-sdk-go/service/sqs" ) type Config struct { diff --git a/builtin/providers/aws/resource_aws_dynamodb_table.go b/builtin/providers/aws/resource_aws_dynamodb_table.go index a4131eb661..75cf664dbd 100644 --- a/builtin/providers/aws/resource_aws_dynamodb_table.go +++ b/builtin/providers/aws/resource_aws_dynamodb_table.go @@ -8,8 +8,8 @@ import ( "github.com/hashicorp/terraform/helper/schema" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/service/dynamodb" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/hashicorp/terraform/helper/hashcode" ) diff --git a/builtin/providers/aws/resource_aws_dynamodb_table_test.go b/builtin/providers/aws/resource_aws_dynamodb_table_test.go index 149f36a559..786a946b60 100644 --- a/builtin/providers/aws/resource_aws_dynamodb_table_test.go +++ b/builtin/providers/aws/resource_aws_dynamodb_table_test.go @@ -4,9 +4,9 @@ import ( "fmt" "testing" - "github.com/awslabs/aws-sdk-go/aws" - "github.com/awslabs/aws-sdk-go/aws/awserr" - "github.com/awslabs/aws-sdk-go/service/dynamodb" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) From f458521be9fd630257d17ecb9af03f53d31995dd Mon Sep 17 00:00:00 2001 From: John Ewart Date: Mon, 8 Jun 2015 15:58:42 -0700 Subject: [PATCH 05/23] Remove request for attribute changes --- .../aws/resource_aws_dynamodb_table.go | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/builtin/providers/aws/resource_aws_dynamodb_table.go b/builtin/providers/aws/resource_aws_dynamodb_table.go index 75cf664dbd..e8e8add8c0 100644 --- a/builtin/providers/aws/resource_aws_dynamodb_table.go +++ b/builtin/providers/aws/resource_aws_dynamodb_table.go @@ -279,25 +279,6 @@ func resourceAwsDynamoDbTableUpdate(d *schema.ResourceData, meta interface{}) er return fmt.Errorf("Range key can only be specified at creation, you cannot modify it.") } - if d.HasChange("attribute") { - req := &dynamodb.UpdateTableInput{ - TableName: aws.String(d.Id()), - } - - newAttributes := []*dynamodb.AttributeDefinition{} - _, n := d.GetChange("attribute") - newAttributeSet := n.(*schema.Set) - for _, attribute := range newAttributeSet.List() { - attr := attribute.(map[string]interface{}) - newAttributes = append(newAttributes, &dynamodb.AttributeDefinition{ - AttributeName: aws.String(attr["name"].(string)), - AttributeType: aws.String(attr["type"].(string)), - }) - } - - req.AttributeDefinitions = newAttributes - } - if d.HasChange("read_capacity") || d.HasChange("write_capacity") { req := &dynamodb.UpdateTableInput{ TableName: aws.String(d.Id()), From 320e4b222c4905f7550708541b489db07703ad8f Mon Sep 17 00:00:00 2001 From: John Ewart Date: Mon, 8 Jun 2015 16:04:22 -0700 Subject: [PATCH 06/23] Change sleep time for DynamoDB table waits from 3 seconds to 5 seconds --- builtin/providers/aws/resource_aws_dynamodb_table.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/builtin/providers/aws/resource_aws_dynamodb_table.go b/builtin/providers/aws/resource_aws_dynamodb_table.go index e8e8add8c0..bbc0fd861e 100644 --- a/builtin/providers/aws/resource_aws_dynamodb_table.go +++ b/builtin/providers/aws/resource_aws_dynamodb_table.go @@ -549,8 +549,8 @@ func waitForGSIToBeActive(tableName string, gsiName string, meta interface{}) er activeIndex = *targetGSI.IndexStatus == "ACTIVE" if !activeIndex { - log.Printf("[DEBUG] Sleeping for 3 seconds for %s GSI to become active", gsiName) - time.Sleep(3 * time.Second) + log.Printf("[DEBUG] Sleeping for 5 seconds for %s GSI to become active", gsiName) + time.Sleep(5 * time.Second) } } else { log.Printf("[DEBUG] GSI %s did not exist, giving up", gsiName) From 4e219b3bad3b34532b58f2f8119ff658b7fe97f1 Mon Sep 17 00:00:00 2001 From: John Ewart Date: Mon, 15 Jun 2015 17:05:50 -0700 Subject: [PATCH 07/23] Fixes support for changing just the read / write capacity of a GSI --- .../aws/resource_aws_dynamodb_table.go | 217 +++++++++++++----- 1 file changed, 165 insertions(+), 52 deletions(-) diff --git a/builtin/providers/aws/resource_aws_dynamodb_table.go b/builtin/providers/aws/resource_aws_dynamodb_table.go index bbc0fd861e..163d14d8d8 100644 --- a/builtin/providers/aws/resource_aws_dynamodb_table.go +++ b/builtin/providers/aws/resource_aws_dynamodb_table.go @@ -121,7 +121,7 @@ func resourceAwsDynamoDbTable() *schema.Resource { }, "range_key": &schema.Schema{ Type: schema.TypeString, - Required: true, + Optional: true, }, "projection_type": &schema.Schema{ Type: schema.TypeString, @@ -139,6 +139,8 @@ func resourceAwsDynamoDbTable() *schema.Resource { var buf bytes.Buffer m := v.(map[string]interface{}) buf.WriteString(fmt.Sprintf("%s-", m["name"].(string))) + buf.WriteString(fmt.Sprintf("%d-", m["write_capacity"].(int))) + buf.WriteString(fmt.Sprintf("%d-", m["read_capacity"].(int))) return hashcode.String(buf.String()) }, }, @@ -300,6 +302,7 @@ func resourceAwsDynamoDbTableUpdate(d *schema.ResourceData, meta interface{}) er } if d.HasChange("global_secondary_index") { + log.Printf("[DEBUG] Changed GSI data") req := &dynamodb.UpdateTableInput{ TableName: aws.String(d.Id()), } @@ -308,12 +311,29 @@ func resourceAwsDynamoDbTableUpdate(d *schema.ResourceData, meta interface{}) er oldSet := o.(*schema.Set) newSet := n.(*schema.Set) - changedSet := newSet.Intersection(oldSet) + + // Track old names so we can know which ones we need to just update based on + // capacity changes, terraform appears to only diff on the set hash, not the + // contents so we need to make sure we don't delete any indexes that we + // just want to update the capacity for + oldGsiNameSet := make(map[string]bool) + newGsiNameSet := make(map[string]bool) + + for _, gsidata := range oldSet.List() { + gsiName := gsidata.(map[string]interface{})["name"].(string) + oldGsiNameSet[gsiName] = true + } + + for _, gsidata := range newSet.List() { + gsiName := gsidata.(map[string]interface{})["name"].(string) + newGsiNameSet[gsiName] = true + } // First determine what's new for _, newgsidata := range newSet.List() { updates := []*dynamodb.GlobalSecondaryIndexUpdate{} - if !oldSet.Contains(newgsidata) { + newGsiName := newgsidata.(map[string]interface{})["name"].(string) + if _, exists := oldGsiNameSet[newGsiName]; !exists { attributes := []*dynamodb.AttributeDefinition{} gsidata := newgsidata.(map[string]interface{}) gsi := createGSIFromData(&gsidata) @@ -327,12 +347,9 @@ func resourceAwsDynamoDbTableUpdate(d *schema.ResourceData, meta interface{}) er }, } updates = append(updates, update) - hashkey_type, err := getAttributeType(d, *(gsi.KeySchema[0].AttributeName)) - if err != nil { - return err - } - rangekey_type, err := getAttributeType(d, *(gsi.KeySchema[1].AttributeName)) + // Hash key is required, range key isn't + hashkey_type, err := getAttributeType(d, *(gsi.KeySchema[0].AttributeName)) if err != nil { return err } @@ -341,10 +358,19 @@ func resourceAwsDynamoDbTableUpdate(d *schema.ResourceData, meta interface{}) er AttributeName: gsi.KeySchema[0].AttributeName, AttributeType: aws.String(hashkey_type), }) - attributes = append(attributes, &dynamodb.AttributeDefinition{ - AttributeName: gsi.KeySchema[1].AttributeName, - AttributeType: aws.String(rangekey_type), - }) + + // If there's a range key, there will be 2 elements in KeySchema + if len(gsi.KeySchema) == 2 { + rangekey_type, err := getAttributeType(d, *(gsi.KeySchema[1].AttributeName)) + if err != nil { + return err + } + + attributes = append(attributes, &dynamodb.AttributeDefinition{ + AttributeName: gsi.KeySchema[1].AttributeName, + AttributeType: aws.String(rangekey_type), + }) + } req.AttributeDefinitions = attributes req.GlobalSecondaryIndexUpdates = updates @@ -362,7 +388,8 @@ func resourceAwsDynamoDbTableUpdate(d *schema.ResourceData, meta interface{}) er for _, oldgsidata := range oldSet.List() { updates := []*dynamodb.GlobalSecondaryIndexUpdate{} - if !newSet.Contains(oldgsidata) { + oldGsiName := oldgsidata.(map[string]interface{})["name"].(string) + if _, exists := newGsiNameSet[oldGsiName]; !exists { gsidata := oldgsidata.(map[string]interface{}) log.Printf("[DEBUG] Deleting GSI %s", gsidata["name"].(string)) update := &dynamodb.GlobalSecondaryIndexUpdate{ @@ -382,31 +409,80 @@ func resourceAwsDynamoDbTableUpdate(d *schema.ResourceData, meta interface{}) er waitForTableToBeActive(d.Id(), meta) } } + } - for _, updatedgsidata := range changedSet.List() { - updates := []*dynamodb.GlobalSecondaryIndexUpdate{} - gsidata := updatedgsidata.(map[string]interface{}) - log.Printf("[DEBUG] Updating GSI %s", gsidata["name"].(string)) - update := &dynamodb.GlobalSecondaryIndexUpdate{ - Update: &dynamodb.UpdateGlobalSecondaryIndexAction{ - IndexName: aws.String(gsidata["name"].(string)), - ProvisionedThroughput: &dynamodb.ProvisionedThroughput{ - WriteCapacityUnits: aws.Long(int64(gsidata["write_capacity"].(int))), - ReadCapacityUnits: aws.Long(int64(gsidata["read_capacity"].(int))), - }, - }, - } - updates = append(updates, update) + // Update any out-of-date read / write capacity + if gsiObjects, ok := d.GetOk("global_secondary_index"); ok { + gsiSet := gsiObjects.(*schema.Set) + if len(gsiSet.List()) > 0 { + log.Printf("Updating capacity as needed!") - req.GlobalSecondaryIndexUpdates = updates - - _, err := dynamodbconn.UpdateTable(req) + // We can only change throughput, but we need to make sure it's actually changed + tableDescription, err := dynamodbconn.DescribeTable(&dynamodb.DescribeTableInput{ + TableName: aws.String(d.Id()), + }) if err != nil { - log.Printf("[DEBUG] Error updating table: %s", err) return err } + + table := tableDescription.Table + + updates := []*dynamodb.GlobalSecondaryIndexUpdate{} + + for _, updatedgsidata := range gsiSet.List() { + gsidata := updatedgsidata.(map[string]interface{}) + gsiName := gsidata["name"].(string) + gsiWriteCapacity := gsidata["write_capacity"].(int) + gsiReadCapacity := gsidata["read_capacity"].(int) + + log.Printf("[DEBUG] Updating GSI %s", gsiName) + gsi, err := getGlobalSecondaryIndex(gsiName, table.GlobalSecondaryIndexes) + + if err != nil { + return err + } + + capacityUpdated := false + + if int64(gsiReadCapacity) != *(gsi.ProvisionedThroughput.ReadCapacityUnits) || + int64(gsiWriteCapacity) != *(gsi.ProvisionedThroughput.WriteCapacityUnits) { + capacityUpdated = true + } + + if capacityUpdated { + update := &dynamodb.GlobalSecondaryIndexUpdate{ + Update: &dynamodb.UpdateGlobalSecondaryIndexAction{ + IndexName: aws.String(gsidata["name"].(string)), + ProvisionedThroughput: &dynamodb.ProvisionedThroughput{ + WriteCapacityUnits: aws.Long(int64(gsiWriteCapacity)), + ReadCapacityUnits: aws.Long(int64(gsiReadCapacity)), + }, + }, + } + updates = append(updates, update) + + } + + if len(updates) > 0 { + + req := &dynamodb.UpdateTableInput{ + TableName: aws.String(d.Id()), + } + + req.GlobalSecondaryIndexUpdates = updates + + log.Printf("[DEBUG] Updating GSI read / write capacity on %s", d.Id()) + _, err := dynamodbconn.UpdateTable(req) + + if err != nil { + log.Printf("[DEBUG] Error updating table: %s", err) + return err + } + } + } } + } return resourceAwsDynamoDbTableRead(d, meta) @@ -414,6 +490,7 @@ func resourceAwsDynamoDbTableUpdate(d *schema.ResourceData, meta interface{}) er func resourceAwsDynamoDbTableRead(d *schema.ResourceData, meta interface{}) error { dynamodbconn := meta.(*AWSClient).dynamodbconn + log.Printf("[DEBUG] Loading data for DynamoDB table '%s'", d.Id()) req := &dynamodb.DescribeTableInput{ TableName: aws.String(d.Id()), } @@ -431,21 +508,39 @@ func resourceAwsDynamoDbTableRead(d *schema.ResourceData, meta interface{}) erro attributes := []interface{}{} for _, attrdef := range table.AttributeDefinitions { - attribute := make(map[string]string) - attribute["name"] = *(attrdef.AttributeName) - attribute["type"] = *(attrdef.AttributeType) + attribute := map[string]string{ + "name": *(attrdef.AttributeName), + "type": *(attrdef.AttributeType), + } attributes = append(attributes, attribute) + log.Printf("[DEBUG] Added Attribute: %s", attribute["name"]) } d.Set("attribute", attributes) - gsiList := []interface{}{} + gsiList := make([]map[string]interface{}, 0, len(table.GlobalSecondaryIndexes)) for _, gsiObject := range table.GlobalSecondaryIndexes { - gsi := make(map[string]interface{}) - gsi["write_capacity"] = gsiObject.ProvisionedThroughput.WriteCapacityUnits - gsi["read_capacity"] = gsiObject.ProvisionedThroughput.ReadCapacityUnits - gsi["name"] = gsiObject.IndexName + gsi := map[string]interface{}{ + "write_capacity": *(gsiObject.ProvisionedThroughput.WriteCapacityUnits), + "read_capacity": *(gsiObject.ProvisionedThroughput.ReadCapacityUnits), + "name": *(gsiObject.IndexName), + } + + for _, attribute := range gsiObject.KeySchema { + if *attribute.KeyType == "HASH" { + gsi["hash_key"] = *attribute.AttributeName + } + + if *attribute.KeyType == "RANGE" { + gsi["range_key"] = *attribute.AttributeName + } + } + + gsi["projection_type"] = *(gsiObject.Projection.ProjectionType) + gsi["non_key_attributes"] = gsiObject.Projection.NonKeyAttributes + gsiList = append(gsiList, gsi) + log.Printf("[DEBUG] Added GSI: %s - Read: %d / Write: %d", gsi["name"], gsi["read_capacity"], gsi["write_capacity"]) } d.Set("global_secondary_index", gsiList) @@ -486,18 +581,26 @@ func createGSIFromData(data *map[string]interface{}) dynamodb.GlobalSecondaryInd writeCapacity := (*data)["write_capacity"].(int) readCapacity := (*data)["read_capacity"].(int) - return dynamodb.GlobalSecondaryIndex{ - IndexName: aws.String((*data)["name"].(string)), - KeySchema: []*dynamodb.KeySchemaElement{ - &dynamodb.KeySchemaElement{ - AttributeName: aws.String((*data)["hash_key"].(string)), - KeyType: aws.String("HASH"), - }, - &dynamodb.KeySchemaElement{ - AttributeName: aws.String((*data)["range_key"].(string)), - KeyType: aws.String("RANGE"), - }, + key_schema := []*dynamodb.KeySchemaElement{ + &dynamodb.KeySchemaElement{ + AttributeName: aws.String((*data)["hash_key"].(string)), + KeyType: aws.String("HASH"), }, + } + + range_key_name := (*data)["range_key"] + if range_key_name != "" { + range_key_element := &dynamodb.KeySchemaElement{ + AttributeName: aws.String(range_key_name.(string)), + KeyType: aws.String("RANGE"), + } + + key_schema = append(key_schema, range_key_element) + } + + return dynamodb.GlobalSecondaryIndex{ + IndexName: aws.String((*data)["name"].(string)), + KeySchema: key_schema, Projection: projection, ProvisionedThroughput: &dynamodb.ProvisionedThroughput{ WriteCapacityUnits: aws.Long(int64(writeCapacity)), @@ -506,6 +609,16 @@ func createGSIFromData(data *map[string]interface{}) dynamodb.GlobalSecondaryInd } } +func getGlobalSecondaryIndex(indexName string, indexList []*dynamodb.GlobalSecondaryIndexDescription) (*dynamodb.GlobalSecondaryIndexDescription, error) { + for _, gsi := range indexList { + if *(gsi.IndexName) == indexName { + return gsi, nil + } + } + + return &dynamodb.GlobalSecondaryIndexDescription{}, fmt.Errorf("Can't find a GSI by that name...") +} + func getAttributeType(d *schema.ResourceData, attributeName string) (string, error) { if attributedata, ok := d.GetOk("attribute"); ok { attributeSet := attributedata.(*schema.Set) @@ -581,8 +694,8 @@ func waitForTableToBeActive(tableName string, meta interface{}) error { // Wait for a few seconds if !activeState { - log.Printf("[DEBUG] Sleeping for 3 seconds for table to become active") - time.Sleep(3 * time.Second) + log.Printf("[DEBUG] Sleeping for 5 seconds for table to become active") + time.Sleep(5 * time.Second) } } From 5d0891ff52dc3a3935c4a01f65e17b864e88ca94 Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Tue, 16 Jun 2015 16:24:10 -0500 Subject: [PATCH 08/23] add v0.5.3 transitory deps refs #2356 --- deps/v0-5-3.json | 176 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) diff --git a/deps/v0-5-3.json b/deps/v0-5-3.json index 94020f6f57..09fe5c60ff 100644 --- a/deps/v0-5-3.json +++ b/deps/v0-5-3.json @@ -120,6 +120,182 @@ { "ImportPath": "golang.org/x/crypto/ssh/terminal", "Rev": "c10c31b5e94b6f7a0283272dc2bb27163dcea24b" + }, + { + "ImportPath": "github.com/Sirupsen/logrus", + "Rev": "52919f182f9c314f8a38c5afe96506f73d02b4b2" + }, + { + "ImportPath": "github.com/armon/circbuf", + "Rev": "f092b4f207b6e5cce0569056fba9e1a2735cb6cf" + }, + { + "ImportPath": "github.com/cyberdelia/heroku-go", + "Rev": "594d483b9b6a8ddc7cd2f1e3e7d1de92fa2de665" + }, + { + "ImportPath": "github.com/docker/docker", + "Rev": "42cfc95549728014811cc9aa2c5b07bdf5553a54" + }, + { + "ImportPath": "github.com/dylanmei/iso8601", + "Rev": "2075bf119b58e5576c6ed9f867b8f3d17f2e54d4" + }, + { + "ImportPath": "github.com/dylanmei/winrmtest", + "Rev": "3e9661c52c45dab9a8528966a23d421922fca9b9" + }, + { + "ImportPath": "github.com/fsouza/go-dockerclient", + "Rev": "f90594a4da6a7cbdaedd29ee5495ddd6b39fe5d3" + }, + { + "ImportPath": "github.com/hashicorp/atlas-go", + "Rev": "6a87d5f443991e9916104392cd5fc77678843e1d" + }, + { + "ImportPath": "github.com/hashicorp/aws-sdk-go", + "Rev": "e6ea0192eee4640f32ec73c0cbb71f63e4f2b65a" + }, + { + "ImportPath": "github.com/hashicorp/consul", + "Rev": "9417fd37686241d65918208874a7faa4d0cd92d2" + }, + { + "ImportPath": "github.com/hashicorp/errwrap", + "Rev": "7554cd9344cec97297fa6649b055a8c98c2a1e55" + }, + { + "ImportPath": "github.com/hashicorp/go-checkpoint", + "Rev": "88326f6851319068e7b34981032128c0b1a6524d" + }, + { + "ImportPath": "github.com/hashicorp/go-multierror", + "Rev": "fcdddc395df1ddf4247c69bd436e84cfa0733f7e" + }, + { + "ImportPath": "github.com/hashicorp/go-version", + "Rev": "999359b6b7a041ce16e695d51e92145b83f01087" + }, + { + "ImportPath": "github.com/hashicorp/hcl", + "Rev": "513e04c400ee2e81e97f5e011c08fb42c6f69b84" + }, + { + "ImportPath": "github.com/hashicorp/yamux", + "Rev": "b2e55852ddaf823a85c67f798080eb7d08acd71d" + }, + { + "ImportPath": "github.com/imdario/mergo", + "Rev": "2fcac9923693d66dc0e03988a31b21da05cdea84" + }, + { + "ImportPath": "github.com/masterzen/simplexml", + "Rev": "95ba30457eb1121fa27753627c774c7cd4e90083" + }, + { + "ImportPath": "github.com/masterzen/winrm", + "Rev": "132339029dfa67fd39ff8edeed2af78f2cca4fbb" + }, + { + "ImportPath": "github.com/masterzen/xmlpath", + "Rev": "13f4951698adc0fa9c1dda3e275d489a24201161" + }, + { + "ImportPath": "github.com/mitchellh/cli", + "Rev": "6cc8bc522243675a2882b81662b0b0d2e04b99c9" + }, + { + "ImportPath": "github.com/mitchellh/colorstring", + "Rev": "61164e49940b423ba1f12ddbdf01632ac793e5e9" + }, + { + "ImportPath": "github.com/mitchellh/copystructure", + "Rev": "6fc66267e9da7d155a9d3bd489e00dad02666dc6" + }, + { + "ImportPath": "github.com/mitchellh/go-homedir", + "Rev": "1f6da4a72e57d4e7edd4a7295a585e0a3999a2d4" + }, + { + "ImportPath": "github.com/mitchellh/gox", + "Rev": "e8e6fd4fe12510cc46893dff18c5188a6a6dc549" + }, + { + "ImportPath": "github.com/mitchellh/iochan", + "Rev": "b584a329b193e206025682ae6c10cdbe03b0cd77" + }, + { + "ImportPath": "github.com/mitchellh/go-linereader", + "Rev": "07bab5fdd9580500aea6ada0e09df4aa28e68abd" + }, + { + "ImportPath": "github.com/mitchellh/mapstructure", + "Rev": "442e588f213303bec7936deba67901f8fc8f18b1" + }, + { + "ImportPath": "github.com/mitchellh/osext", + "Rev": "0dd3f918b21bec95ace9dc86c7e70266cfc5c702" + }, + { + "ImportPath": "github.com/mitchellh/packer", + "Rev": "350a5f8cad6a0e4c2b24c3049a84c4f294416e16" + }, + { + "ImportPath": "github.com/mitchellh/panicwrap", + "Rev": "45cbfd3bae250c7676c077fb275be1a2968e066a" + }, + { + "ImportPath": "github.com/mitchellh/prefixedio", + "Rev": "89d9b535996bf0a185f85b59578f2e245f9e1724" + }, + { + "ImportPath": "github.com/mitchellh/reflectwalk", + "Rev": "242be0c275dedfba00a616563e6db75ab8f279ec" + }, + { + "ImportPath": "github.com/nu7hatch/gouuid", + "Rev": "179d4d0c4d8d407a32af483c2354df1d2c91e6c3" + }, + { + "ImportPath": "github.com/packer-community/winrmcp", + "Rev": "650a91d1da6dc3fefa8f052289ffce648924a304" + }, + { + "ImportPath": "github.com/pearkes/cloudflare", + "Rev": "19e280b056f3742e535ea12ae92a37ea7767ea82" + }, + { + "ImportPath": "github.com/pearkes/digitalocean", + "Rev": "e966f00c2d9de5743e87697ab77c7278f5998914" + }, + { + "ImportPath": "github.com/pearkes/dnsimple", + "Rev": "1e0c2b0eb33ca7b5632a130d6d34376a1ea46c84" + }, + { + "ImportPath": "github.com/pearkes/mailgun", + "Rev": "5b02e7e9ffee9869f81393e80db138f6ff726260" + }, + { + "ImportPath": "github.com/rackspace/gophercloud", + "Rev": "9ad4137a6b3e786b9c1e161b8d354b44482ab6d7" + }, + { + "ImportPath": "github.com/satori/go.uuid", + "Rev": "7c7f2020c4c9491594b85767967f4619c2fa75f9" + }, + { + "ImportPath": "github.com/soniah/dnsmadeeasy", + "Rev": "5578a8c15e33958c61cf7db720b6181af65f4a9e" + }, + { + "ImportPath": "github.com/vaughan0/go-ini", + "Rev": "a98ad7ee00ec53921f08832bc06ecf7fd600e6a1" + }, + { + "ImportPath": "github.com/xanzy/go-cloudstack", + "Rev": "f73f6ff1b843dbdac0a01da7b7f39883adfe2bdb" } ] } From 8b2e1bd273971803e83ec1a8dd6bdf5b5e60d633 Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Tue, 16 Jun 2015 16:25:27 -0500 Subject: [PATCH 09/23] docs: snapshot plugin dependencies when releasing refs #2356 --- RELEASING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASING.md b/RELEASING.md index d32bb87752..ca2d84319d 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -30,7 +30,7 @@ export VERSION="vX.Y.Z" # Edit version.go, setting VersionPrelease to empty string # Snapshot dependency information -godep save +godep save ./... mv Godeps/Godeps.json deps/$(echo $VERSION | sed 's/\./-/g').json rm -rf Godeps From 071db417fedfad2ece085b66a37a4f36078612e6 Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Tue, 16 Jun 2015 16:39:57 -0500 Subject: [PATCH 10/23] docs: add description of archive format to download page --- website/source/downloads.html.erb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/website/source/downloads.html.erb b/website/source/downloads.html.erb index 834e18af3c..1eb63cc22a 100644 --- a/website/source/downloads.html.erb +++ b/website/source/downloads.html.erb @@ -17,6 +17,12 @@ description: |- operating system and architecture. You can find SHA256 checksums for packages here.

+ +

+ Each release archive is a zip file containing the Terraform binary + executables at the top level. These executables are meant to be extracted + to a location where they can be found by your shell. +

<% product_versions.each do |os, versions| %> From d4834091761ed00d1d2c2a1c8d9fd1fed6995015 Mon Sep 17 00:00:00 2001 From: Ben Wen Date: Tue, 16 Jun 2015 17:01:22 -0700 Subject: [PATCH 11/23] Getting Started: Added a Next Step upon finishing install. --- .../source/intro/getting-started/install.html.markdown | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/website/source/intro/getting-started/install.html.markdown b/website/source/intro/getting-started/install.html.markdown index 629e97a573..2392e35491 100644 --- a/website/source/intro/getting-started/install.html.markdown +++ b/website/source/intro/getting-started/install.html.markdown @@ -57,4 +57,10 @@ If you get an error that `terraform` could not be found, then your PATH environment variable was not setup properly. Please go back and ensure that your PATH variable contains the directory where Terraform was installed. -Otherwise, Terraform is installed and ready to go! +Otherwise, Terraform is installed and ready to go! Nice! + +## Next Step + +Time to [build infrastructure](/intro/getting-started/build.html) +using a minimal Terraform configuration file. You will be able to +examine Terraform's execution plan before you deploy it to AWS. From a75095aa6988d32f346913ef18859b505d5a64d5 Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Wed, 17 Jun 2015 10:10:23 -0500 Subject: [PATCH 12/23] docs: clarify wording around destroy/apply args inspired by #2360 with clarification on args vs flags /cc @catsby --- website/source/docs/commands/apply.html.markdown | 2 +- website/source/docs/commands/destroy.html.markdown | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/website/source/docs/commands/apply.html.markdown b/website/source/docs/commands/apply.html.markdown index 9bb5acdbff..dec4ea19d6 100644 --- a/website/source/docs/commands/apply.html.markdown +++ b/website/source/docs/commands/apply.html.markdown @@ -14,7 +14,7 @@ set of actions generated by a `terraform plan` execution plan. ## Usage -Usage: `terraform apply [options] [dir]` +Usage: `terraform apply [options] [dir-or-plan]` By default, `apply` scans the current directory for the configuration and applies the changes appropriately. However, a path to another configuration diff --git a/website/source/docs/commands/destroy.html.markdown b/website/source/docs/commands/destroy.html.markdown index 0a0f3a738b..98f1b79e6f 100644 --- a/website/source/docs/commands/destroy.html.markdown +++ b/website/source/docs/commands/destroy.html.markdown @@ -18,9 +18,11 @@ Usage: `terraform destroy [options] [dir]` Infrastructure managed by Terraform will be destroyed. This will ask for confirmation before destroying. -This command accepts all the flags that the -[apply command](/docs/commands/apply.html) accepts. If `-force` is -set, then the destroy confirmation will not be shown. +This command accepts all the arguments and flags that the [apply +command](/docs/commands/apply.html) accepts, with the exception of a plan file +argument. + +If `-force` is set, then the destroy confirmation will not be shown. The `-target` flag, instead of affecting "dependencies" will instead also destroy any resources that _depend on_ the target(s) specified. From f0d8682df60b83d2c80b8d17c53dbbb234231326 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Wed, 17 Jun 2015 16:12:27 +0100 Subject: [PATCH 13/23] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d69b172999..c6d2924d67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ FEATURES: * **New provider: `azure`** [GH-2053] * **New resource: `aws_autoscaling_notification`** [GH-2197] + * **New resource: `aws_dynamodb_table`** [GH-2121] * **New resource: `aws_ecs_cluster`** [GH-1803] * **New resource: `aws_ecs_service`** [GH-1803] * **New resource: `aws_ecs_task_definition`** [GH-1803] From 357ed8e774ed7dc569c281c11649d726a72d8789 Mon Sep 17 00:00:00 2001 From: aznashwan Date: Wed, 17 Jun 2015 08:04:10 +0300 Subject: [PATCH 14/23] Added Azure SQL server and service support. --- builtin/providers/azure/config.go | 5 + builtin/providers/azure/provider.go | 2 + .../resource_azure_sql_database_server.go | 118 +++++++++ ...resource_azure_sql_database_server_test.go | 119 +++++++++ .../resource_azure_sql_database_service.go | 234 ++++++++++++++++++ ...esource_azure_sql_database_service_test.go | 189 ++++++++++++++ .../azure/r/sql_database_server.html.markdown | 50 ++++ .../r/sql_database_service.html.markdown | 53 ++++ website/source/layouts/azure.erb | 8 + 9 files changed, 778 insertions(+) create mode 100644 builtin/providers/azure/resource_azure_sql_database_server.go create mode 100644 builtin/providers/azure/resource_azure_sql_database_server_test.go create mode 100644 builtin/providers/azure/resource_azure_sql_database_service.go create mode 100644 builtin/providers/azure/resource_azure_sql_database_service_test.go create mode 100644 website/source/docs/providers/azure/r/sql_database_server.html.markdown create mode 100644 website/source/docs/providers/azure/r/sql_database_service.html.markdown diff --git a/builtin/providers/azure/config.go b/builtin/providers/azure/config.go index cf02bf5571..52c69d40cb 100644 --- a/builtin/providers/azure/config.go +++ b/builtin/providers/azure/config.go @@ -9,6 +9,7 @@ import ( "github.com/Azure/azure-sdk-for-go/management/hostedservice" "github.com/Azure/azure-sdk-for-go/management/networksecuritygroup" "github.com/Azure/azure-sdk-for-go/management/osimage" + "github.com/Azure/azure-sdk-for-go/management/sql" "github.com/Azure/azure-sdk-for-go/management/storageservice" "github.com/Azure/azure-sdk-for-go/management/virtualmachine" "github.com/Azure/azure-sdk-for-go/management/virtualmachinedisk" @@ -36,6 +37,8 @@ type Client struct { osImageClient osimage.OSImageClient + sqlClient sql.SqlDatabaseClient + storageServiceClient storageservice.StorageServiceClient vmClient virtualmachine.VirtualMachineClient @@ -107,6 +110,7 @@ func (c *Config) NewClientFromSettingsFile() (*Client, error) { hostedServiceClient: hostedservice.NewClient(mc), secGroupClient: networksecuritygroup.NewClient(mc), osImageClient: osimage.NewClient(mc), + sqlClient: sql.NewClient(mc), storageServiceClient: storageservice.NewClient(mc), vmClient: virtualmachine.NewClient(mc), vmDiskClient: virtualmachinedisk.NewClient(mc), @@ -129,6 +133,7 @@ func (c *Config) NewClient() (*Client, error) { hostedServiceClient: hostedservice.NewClient(mc), secGroupClient: networksecuritygroup.NewClient(mc), osImageClient: osimage.NewClient(mc), + sqlClient: sql.NewClient(mc), storageServiceClient: storageservice.NewClient(mc), vmClient: virtualmachine.NewClient(mc), vmDiskClient: virtualmachinedisk.NewClient(mc), diff --git a/builtin/providers/azure/provider.go b/builtin/providers/azure/provider.go index 98a7b2a097..a6be93f5ca 100644 --- a/builtin/providers/azure/provider.go +++ b/builtin/providers/azure/provider.go @@ -34,6 +34,8 @@ func Provider() terraform.ResourceProvider { ResourcesMap: map[string]*schema.Resource{ "azure_instance": resourceAzureInstance(), "azure_data_disk": resourceAzureDataDisk(), + "azure_sql_database_server": resourceAzureSqlDatabaseServer(), + "azure_sql_database_service": resourceAzureSqlDatabaseService(), "azure_hosted_service": resourceAzureHostedService(), "azure_storage_service": resourceAzureStorageService(), "azure_storage_container": resourceAzureStorageContainer(), diff --git a/builtin/providers/azure/resource_azure_sql_database_server.go b/builtin/providers/azure/resource_azure_sql_database_server.go new file mode 100644 index 0000000000..b3412b7b3e --- /dev/null +++ b/builtin/providers/azure/resource_azure_sql_database_server.go @@ -0,0 +1,118 @@ +package azure + +import ( + "fmt" + "log" + + "github.com/Azure/azure-sdk-for-go/management/sql" + "github.com/hashicorp/terraform/helper/schema" +) + +// resourceAzureDatabaseServer returns the *schema.Resource associated +// to a database server on Azure. +func resourceAzureSqlDatabaseServer() *schema.Resource { + return &schema.Resource{ + Create: resourceAzureSqlDatabaseServerCreate, + Read: resourceAzureSqlDatabaseServerRead, + Delete: resourceAzureSqlDatabaseServerDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + ForceNew: true, + }, + "location": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "username": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "password": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "version": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "12.0", + ForceNew: true, + }, + "url": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +// resourceAzureSqlDatabaseServerCreate does all the necessary API calls to +// create an SQL database server off Azure. +func resourceAzureSqlDatabaseServerCreate(d *schema.ResourceData, meta interface{}) error { + sqlClient := meta.(*Client).sqlClient + + log.Println("[INFO] Began constructing SQL Server creation request.") + params := sql.DatabaseServerCreateParams{ + Location: d.Get("location").(string), + AdministratorLogin: d.Get("username").(string), + AdministratorLoginPassword: d.Get("password").(string), + Version: d.Get("version").(string), + } + + log.Println("[INFO] Issuing SQL Server creation request to Azure.") + name, err := sqlClient.CreateServer(params) + if err != nil { + return fmt.Errorf("Error creating SQL Server on Azure: %s", err) + } + + d.Set("name", name) + + d.SetId(name) + return resourceAzureSqlDatabaseServerRead(d, meta) +} + +// resourceAzureSqlDatabaseServerRead does all the necessary API calls to +// read the state of the SQL database server off Azure. +func resourceAzureSqlDatabaseServerRead(d *schema.ResourceData, meta interface{}) error { + sqlClient := meta.(*Client).sqlClient + + log.Println("[INFO] Sending SQL Servers list query to Azure.") + srvList, err := sqlClient.ListServers() + if err != nil { + return fmt.Errorf("Error issuing SQL Servers list query to Azure: %s", err) + } + + // search for our particular server: + name := d.Get("name") + for _, srv := range srvList.DatabaseServers { + if srv.Name == name { + d.Set("url", srv.FullyQualifiedDomainName) + d.Set("state", srv.State) + return nil + } + } + + // if reached here; it means out server doesn't exist, so we must untrack it: + d.SetId("") + return nil +} + +// resourceAzureSqlDatabaseServerDelete does all the necessary API calls to +// delete the SQL database server off Azure. +func resourceAzureSqlDatabaseServerDelete(d *schema.ResourceData, meta interface{}) error { + sqlClient := meta.(*Client).sqlClient + + log.Println("[INFO] Sending SQL Server deletion request to Azure.") + name := d.Get("name").(string) + err := sqlClient.DeleteServer(name) + if err != nil { + return fmt.Errorf("Error while issuing SQL Server deletion request to Azure: %s", err) + } + + return nil +} diff --git a/builtin/providers/azure/resource_azure_sql_database_server_test.go b/builtin/providers/azure/resource_azure_sql_database_server_test.go new file mode 100644 index 0000000000..8f65b39929 --- /dev/null +++ b/builtin/providers/azure/resource_azure_sql_database_server_test.go @@ -0,0 +1,119 @@ +package azure + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +// testAccAzureSqlServerName is a helper variable in which to store +// the randomly-generated name of the SQL Server after it is created. +// The anonymous function is there because go is too good to &"" directly. +var testAccAzureSqlServerName *string = func(s string) *string { return &s }("") + +func TestAccAzureSqlDatabaseServer(t *testing.T) { + name := "azure_sql_database_server.foo" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAzureSqlDatabaseServerDeleted, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureSqlDatabaseServerConfig, + Check: resource.ComposeTestCheckFunc( + testAccAzureSqlDatabaseServerGetName, + testAccCheckAzureSqlDatabaseServerExists(name), + resource.TestCheckResourceAttrPtr(name, "name", testAccAzureSqlServerName), + resource.TestCheckResourceAttr(name, "username", "SuperUser"), + resource.TestCheckResourceAttr(name, "password", "SuperSEKR3T"), + resource.TestCheckResourceAttr(name, "version", "2.0"), + ), + }, + }, + }) +} + +func testAccCheckAzureSqlDatabaseServerExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + resource, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("SQL Server %s doesn't exist.", name) + } + + if resource.Primary.ID == "" { + return fmt.Errorf("SQL Server %s resource ID not set.", name) + } + + sqlClient := testAccProvider.Meta().(*Client).sqlClient + servers, err := sqlClient.ListServers() + if err != nil { + return fmt.Errorf("Error issuing Azure SQL Server list request: %s", err) + } + + for _, srv := range servers.DatabaseServers { + if srv.Name == resource.Primary.ID { + return nil + } + } + + return fmt.Errorf("SQL Server %s doesn't exist.", name) + } +} + +func testAccCheckAzureSqlDatabaseServerDeleted(s *terraform.State) error { + for _, resource := range s.RootModule().Resources { + if resource.Type != "azure_sql_database_server" { + continue + } + + if resource.Primary.ID == "" { + return fmt.Errorf("SQL Server resource ID not set.") + } + + sqlClient := testAccProvider.Meta().(*Client).sqlClient + servers, err := sqlClient.ListServers() + if err != nil { + return fmt.Errorf("Error issuing Azure SQL Server list request: %s", err) + } + + for _, srv := range servers.DatabaseServers { + if srv.Name == resource.Primary.ID { + fmt.Errorf("SQL Server %s still exists.", resource.Primary.ID) + } + } + } + return nil +} + +// testAccAzureSqlDatabaseServerGetName is ahelper function which reads the current +// state form Terraform and sets the testAccAzureSqlServerName variable +// to the ID (which is actually the name) of the newly created server. +// It is modeled as a resource.TestCheckFunc so as to be easily-embeddable in +// test cases and run live. +func testAccAzureSqlDatabaseServerGetName(s *terraform.State) error { + for _, resource := range s.RootModule().Resources { + if resource.Type != "azure_sql_database_server" { + continue + } + + if resource.Primary.ID == "" { + return fmt.Errorf("Azure SQL Server resource ID not set.") + } + + *testAccAzureSqlServerName = resource.Primary.ID + return nil + } + + return fmt.Errorf("No Azure SQL Servers found.") +} + +const testAccAzureSqlDatabaseServerConfig = ` +resource "azure_sql_database_server" "foo" { + location = "West US" + username = "SuperUser" + password = "SuperSEKR3T" + version = "2.0" +} +` diff --git a/builtin/providers/azure/resource_azure_sql_database_service.go b/builtin/providers/azure/resource_azure_sql_database_service.go new file mode 100644 index 0000000000..29824d7439 --- /dev/null +++ b/builtin/providers/azure/resource_azure_sql_database_service.go @@ -0,0 +1,234 @@ +package azure + +import ( + "fmt" + "log" + "strconv" + "strings" + + "github.com/Azure/azure-sdk-for-go/management/sql" + "github.com/hashicorp/terraform/helper/schema" +) + +// resourceAzureSqlDatabaseService returns the *schema.Resource +// associated to an SQL Database Service on Azure. +func resourceAzureSqlDatabaseService() *schema.Resource { + return &schema.Resource{ + Create: resourceAzureSqlDatabaseServiceCreate, + Read: resourceAzureSqlDatabaseServiceRead, + Update: resourceAzureSqlDatabaseServiceUpdate, + Exists: resourceAzureSqlDatabaseServiceExists, + Delete: resourceAzureSqlDatabaseServiceDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "database_server_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "collation": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "edition": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "max_size_bytes": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "service_level_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + } +} + +// resourceAzureSqlDatabaseServiceCreate does all the necessary API calls to +// create an SQL Database Service on Azure. +func resourceAzureSqlDatabaseServiceCreate(d *schema.ResourceData, meta interface{}) error { + sqlClient := meta.(*Client).sqlClient + + log.Println("[INFO] Creating Azure SQL Database service creation request.") + name := d.Get("name").(string) + serverName := d.Get("database_server_name").(string) + params := sql.DatabaseCreateParams{ + Name: name, + Edition: d.Get("edition").(string), + CollationName: d.Get("collation").(string), + ServiceObjectiveID: d.Get("service_level_id").(string), + } + + if maxSize, ok := d.GetOk("max_size_bytes"); ok { + val, err := strconv.ParseInt(maxSize.(string), 10, 64) + if err != nil { + return fmt.Errorf("Provided max_size_bytes is not an integer: %s", err) + } + params.MaxSizeBytes = val + } + + log.Println("[INFO] Sending SQL Database Service creation request to Azure.") + err := sqlClient.CreateDatabase(serverName, params) + if err != nil { + return fmt.Errorf("Error issuing Azure SQL Database Service creation: %s", err) + } + + log.Println("[INFO] Beginning wait for Azure SQL Database Service creation.") + err = sqlClient.WaitForDatabaseCreation(serverName, name, nil) + if err != nil { + return fmt.Errorf("Error whilst waiting for Azure SQL Database Service creation: %s", err) + } + + d.SetId(name) + + return resourceAzureSqlDatabaseServiceRead(d, meta) +} + +// resourceAzureSqlDatabaseServiceRead does all the necessary API calls to +// read the state of the SQL Database Service off Azure. +func resourceAzureSqlDatabaseServiceRead(d *schema.ResourceData, meta interface{}) error { + sqlClient := meta.(*Client).sqlClient + + log.Println("[INFO] Issuing Azure SQL Database Services list operation.") + serverName := d.Get("database_server_name").(string) + dbs, err := sqlClient.ListDatabases(serverName) + if err != nil { + return fmt.Errorf("Error whilst listing Database Services off Azure: %s", err) + } + + // search for our database: + var found bool + name := d.Get("name").(string) + for _, db := range dbs.ServiceResources { + if db.Name == name { + found = true + d.Set("edition", db.Edition) + d.Set("collation", db.CollationName) + d.Set("max_size_bytes", strconv.FormatInt(db.MaxSizeBytes, 10)) + d.Set("service_level_id", db.ServiceObjectiveID) + break + } + } + + // if not found; we must untrack the resource: + if !found { + d.SetId("") + } + + return nil +} + +// resourceAzureSqlDatabaseServiceUpdate does all the necessary API calls to +// update the state of the SQL Database Service off Azure. +func resourceAzureSqlDatabaseServiceUpdate(d *schema.ResourceData, meta interface{}) error { + azureClient := meta.(*Client) + mgmtClient := azureClient.mgmtClient + sqlClient := azureClient.sqlClient + serverName := d.Get("database_server_name").(string) + + // changes to the name must occur seperately from changes to the attributes: + if d.HasChange("name") { + oldv, newv := d.GetChange("name") + + // issue the update request: + log.Println("[INFO] Issuing Azure Database Service name change.") + reqID, err := sqlClient.UpdateDatabase(serverName, oldv.(string), + sql.ServiceResourceUpdateParams{ + Name: newv.(string), + }) + + // wait for the update to occur: + log.Println("[INFO] Waiting for Azure SQL Database Service name change.") + err = mgmtClient.WaitForOperation(reqID, nil) + if err != nil { + return fmt.Errorf("Error waiting for Azure SQL Database Service name update: %s", err) + } + + // set the new name as the ID: + d.SetId(newv.(string)) + } + + name := d.Get("name").(string) + cedition := d.HasChange("edition") + cmaxsize := d.HasChange("max_size_bytes") + clevel := d.HasChange("service_level_id") + if cedition || cmaxsize || clevel { + updateParams := sql.ServiceResourceUpdateParams{ + // we still have to stick the name in here for good measure: + Name: name, + } + + // build the update request: + if cedition { + updateParams.Edition = d.Get("edition").(string) + } + if maxSize, ok := d.GetOk("max_size_bytes"); cmaxsize && ok && maxSize.(string) != "" { + val, err := strconv.ParseInt(maxSize.(string), 10, 64) + if err != nil { + return fmt.Errorf("Provided max_size_bytes is not an integer: %s", err) + } + updateParams.MaxSizeBytes = val + } + if clevel { + updateParams.ServiceObjectiveID = d.Get("service_level_id").(string) + } + + // issue the update: + log.Println("[INFO] Issuing Azure Database Service parameter update.") + reqID, err := sqlClient.UpdateDatabase(serverName, name, updateParams) + if err != nil { + return fmt.Errorf("Failed issuing Azure SQL Service paramater update: %s", err) + } + + log.Println("[INFO] Waiting for Azure SQL Database Service parameter update.") + err = mgmtClient.WaitForOperation(reqID, nil) + if err != nil { + return fmt.Errorf("Error waiting for Azure SQL Database Service parameter update: %s", err) + } + } + + return nil +} + +// resourceAzureSqlDatabaseServiceExists does all the necessary API calls to +// check for the existence of the SQL Database Service off Azure. +func resourceAzureSqlDatabaseServiceExists(d *schema.ResourceData, meta interface{}) (bool, error) { + sqlClient := meta.(*Client).sqlClient + + log.Println("[INFO] Issuing Azure SQL Database Service get request.") + name := d.Get("name").(string) + serverName := d.Get("database_server_name").(string) + _, err := sqlClient.GetDatabase(serverName, name) + if err != nil { + if strings.Contains(err.Error(), "does not exist") { + d.SetId("") + return false, nil + } else { + return false, fmt.Errorf("Error whilst getting Azure SQL Database Service info: %s", err) + } + } + + return true, nil +} + +// resourceAzureSqlDatabaseServiceDelete does all the necessary API calls to +// delete the SQL Database Service off Azure. +func resourceAzureSqlDatabaseServiceDelete(d *schema.ResourceData, meta interface{}) error { + sqlClient := meta.(*Client).sqlClient + + log.Println("[INFO] Issuing Azure SQL Database deletion request.") + name := d.Get("name").(string) + serverName := d.Get("database_server_name").(string) + return sqlClient.DeleteDatabase(serverName, name) +} diff --git a/builtin/providers/azure/resource_azure_sql_database_service_test.go b/builtin/providers/azure/resource_azure_sql_database_service_test.go new file mode 100644 index 0000000000..bc93fdb621 --- /dev/null +++ b/builtin/providers/azure/resource_azure_sql_database_service_test.go @@ -0,0 +1,189 @@ +package azure + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAzureSqlDatabaseServiceBasic(t *testing.T) { + name := "azure_sql_database_service.foo" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAzureSqlDatabaseServiceDeleted, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureSqlDatabaseServiceConfigBasic, + Check: resource.ComposeTestCheckFunc( + testAccAzureSqlDatabaseServerGetName, + testAccCheckAzureSqlDatabaseServiceExists(name), + resource.TestCheckResourceAttr(name, "name", "terraform-testing-db"), + resource.TestCheckResourceAttrPtr(name, "database_server_name", + testAccAzureSqlServerName), + resource.TestCheckResourceAttr(name, "collation", + "SQL_Latin1_General_CP1_CI_AS"), + resource.TestCheckResourceAttr(name, "edition", "Standard"), + ), + }, + }, + }) +} + +func TestAccAzureSqlDatabaseServiceAdvanced(t *testing.T) { + name := "azure_sql_database_service.foo" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAzureSqlDatabaseServiceDeleted, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureSqlDatabaseServiceConfigAdvanced, + Check: resource.ComposeTestCheckFunc( + testAccAzureSqlDatabaseServerGetName, + testAccCheckAzureSqlDatabaseServiceExists(name), + resource.TestCheckResourceAttr(name, "name", "terraform-testing-db"), + resource.TestCheckResourceAttrPtr(name, "database_server_name", + testAccAzureSqlServerName), + resource.TestCheckResourceAttr(name, "edition", "Premium"), + resource.TestCheckResourceAttr(name, "collation", + "Arabic_BIN"), + resource.TestCheckResourceAttr(name, "max_size_bytes", "10737418240"), + resource.TestCheckResourceAttr(name, "service_level_id", + "7203483a-c4fb-4304-9e9f-17c71c904f5d"), + ), + }, + }, + }) +} + +func TestAccAzureSqlDatabaseServiceUpdate(t *testing.T) { + name := "azure_sql_database_service.foo" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAzureSqlDatabaseServiceDeleted, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureSqlDatabaseServiceConfigAdvanced, + Check: resource.ComposeTestCheckFunc( + testAccAzureSqlDatabaseServerGetName, + testAccCheckAzureSqlDatabaseServiceExists(name), + resource.TestCheckResourceAttr(name, "name", "terraform-testing-db"), + resource.TestCheckResourceAttrPtr(name, "database_server_name", + testAccAzureSqlServerName), + resource.TestCheckResourceAttr(name, "edition", "Premium"), + resource.TestCheckResourceAttr(name, "collation", + "Arabic_BIN"), + resource.TestCheckResourceAttr(name, "max_size_bytes", "10737418240"), + resource.TestCheckResourceAttr(name, "service_level_id", + "7203483a-c4fb-4304-9e9f-17c71c904f5d"), + ), + }, + resource.TestStep{ + Config: testAccAzureSqlDatabaseServiceConfigUpdate, + Check: resource.ComposeTestCheckFunc( + testAccAzureSqlDatabaseServerGetName, + testAccCheckAzureSqlDatabaseServiceExists(name), + resource.TestCheckResourceAttr(name, "name", + "terraform-testing-db-renamed"), + resource.TestCheckResourceAttrPtr(name, "database_server_name", + testAccAzureSqlServerName), + resource.TestCheckResourceAttr(name, "edition", "Standard"), + resource.TestCheckResourceAttr(name, "collation", + "SQL_Latin1_General_CP1_CI_AS"), + resource.TestCheckResourceAttr(name, "max_size_bytes", "5368709120"), + resource.TestCheckResourceAttr(name, "service_level_id", + "f1173c43-91bd-4aaa-973c-54e79e15235b"), + ), + }, + }, + }) +} + +func testAccCheckAzureSqlDatabaseServiceExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + resource, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("SQL Service %s doesn't exist.", name) + } + + if resource.Primary.ID == "" { + return fmt.Errorf("SQL Service %s resource ID not set.", name) + } + + sqlClient := testAccProvider.Meta().(*Client).sqlClient + dbs, err := sqlClient.ListDatabases(*testAccAzureSqlServerName) + if err != nil { + return fmt.Errorf("Error issuing Azure SQL Service list request: %s", err) + } + + for _, srv := range dbs.ServiceResources { + if srv.Name == resource.Primary.ID { + return nil + } + } + + return fmt.Errorf("SQL Service %s doesn't exist.", name) + } +} + +func testAccCheckAzureSqlDatabaseServiceDeleted(s *terraform.State) error { + for _, resource := range s.RootModule().Resources { + if resource.Type != "azure_sql_database_server" { + continue + } + + if resource.Primary.ID == "" { + return fmt.Errorf("SQL Service resource ID not set.") + } + + sqlClient := testAccProvider.Meta().(*Client).sqlClient + dbs, err := sqlClient.ListDatabases(*testAccAzureSqlServerName) + if err != nil { + return fmt.Errorf("Error issuing Azure SQL Service list request: %s", err) + } + + for _, srv := range dbs.ServiceResources { + if srv.Name == resource.Primary.ID { + fmt.Errorf("SQL Service %s still exists.", resource.Primary.ID) + } + } + } + return nil +} + +const testAccAzureSqlDatabaseServiceConfigBasic = testAccAzureSqlDatabaseServerConfig + ` +resource "azure_sql_database_service" "foo" { + name = "terraform-testing-db" + database_server_name = "${azure_sql_database_server.foo.name}" + edition = "Standard" +} +` + +const testAccAzureSqlDatabaseServiceConfigAdvanced = testAccAzureSqlDatabaseServerConfig + ` +resource "azure_sql_database_service" "foo" { + name = "terraform-testing-db" + database_server_name = "${azure_sql_database_server.foo.name}" + edition = "Premium" + collation = "Arabic_BIN" + max_size_bytes = "10737418240" + service_level_id = "7203483a-c4fb-4304-9e9f-17c71c904f5d" +} +` + +const testAccAzureSqlDatabaseServiceConfigUpdate = testAccAzureSqlDatabaseServerConfig + ` +resource "azure_sql_database_service" "foo" { + name = "terraform-testing-db-renamed" + database_server_name = "${azure_sql_database_server.foo.name}" + edition = "Standard" + collation = "SQL_Latin1_General_CP1_CI_AS" + max_size_bytes = "5368709120" + service_level_id = "f1173c43-91bd-4aaa-973c-54e79e15235b" +} +` diff --git a/website/source/docs/providers/azure/r/sql_database_server.html.markdown b/website/source/docs/providers/azure/r/sql_database_server.html.markdown new file mode 100644 index 0000000000..c038731813 --- /dev/null +++ b/website/source/docs/providers/azure/r/sql_database_server.html.markdown @@ -0,0 +1,50 @@ +--- +layout: "azure" +page_title: "Azure: azure_sql_database_server" +sidebar_current: "docs-azure-sql-database-server" +description: |- + Allocates a new SQL Database Server on Azure. +--- + +# azure\_sql\_database\_server + +Allocates a new SQL Database Server on Azure. + +## Example Usage + +``` +resource "azure_sql_database_server" "sql-serv" { + name = "" + location = "West US" + username = "SuperUser" + password = "SuperSEKR3T" + version = "2.0" + url = "" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Computed) The name of the database server. It is determined upon + creation as it is randomly-generated per server. + +* `location` - (Required) The location where the database server should be created. + For a list of all Azure locations, please consult [this link](http://azure.microsoft.com/en-us/regions/). + +* `username` - (Required) The username for the administrator of the database server. + +* `password` - (Required) The password for the administrator of the database server. + +* `version` - (Optional) The version of the database server to be used. Can be any + one of `2.0` or `12.0`. + +* `url` - (Computed) The fully qualified domain name of the database server. + Will be of the form `.database.windows.net`. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The database server ID. Coincides with the randomly-generated `name`. diff --git a/website/source/docs/providers/azure/r/sql_database_service.html.markdown b/website/source/docs/providers/azure/r/sql_database_service.html.markdown new file mode 100644 index 0000000000..aeabda52b0 --- /dev/null +++ b/website/source/docs/providers/azure/r/sql_database_service.html.markdown @@ -0,0 +1,53 @@ +--- +layout: "azure" +page_title: "Azure: azure_sql_database_service" +sidebar_current: "docs-azure-sql-database-service" +description: |- + Creates a new SQL Database Service on an Azure Database Server. +--- + +# azure\_sql\_database\_service + +Creates a new SQL database service on an Azure database server. + +## Example Usage + +``` +resource "azure_sql_database_service" "sql-server" { + name = "terraform-testing-db-renamed" + database_server_name = "flibberflabber" + edition = "Standard" + collation = "SQL_Latin1_General_CP1_CI_AS" + max_size_bytes = "5368709120" + service_level_id = "f1173c43-91bd-4aaa-973c-54e79e15235b" +} + +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the database service. + +* `database_server_name` - (Required) The name of the database server this service + should run on. Changes here force the creation of a new resource. + +* `edition` - (Optional) The edition of the database service. For more information + on each variant, please view [this](https://msdn.microsoft.com/library/azure/dn741340.aspx) link. + +* `collation` - (Optional) The collation to be used within the database service. + Defaults to the standard Latin charset. + +* `max_size_bytes` - (Optional) The maximum size in bytes the database service + should be allowed to expand to. Range depends on the database `edition` + selected above. + +* `service_level_id` - (Optional) The ID corresponding to the service level per + edition. Please refer to [this](https://msdn.microsoft.com/en-us/library/azure/dn505701.aspx) link for more details. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The database service ID. Coincides with the given `name`. diff --git a/website/source/layouts/azure.erb b/website/source/layouts/azure.erb index f010482850..4fc096f0e4 100644 --- a/website/source/layouts/azure.erb +++ b/website/source/layouts/azure.erb @@ -41,6 +41,14 @@ azure_security_group_rule + > + azure_sql_database_server + + + > + azure_sql_database_service + + > azure_storage_blob From e1c7990c9c77b6706e3f21e10968b5a211c290f8 Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Wed, 17 Jun 2015 13:45:00 -0500 Subject: [PATCH 15/23] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6d2924d67..ca5790a4ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ FEATURES: - * **New provider: `azure`** [GH-2053] + * **New provider: `azure`** [GH-2052, GH-2053, GH-2372, GH-2380] * **New resource: `aws_autoscaling_notification`** [GH-2197] * **New resource: `aws_dynamodb_table`** [GH-2121] * **New resource: `aws_ecs_cluster`** [GH-1803] From 385b17d6797009ff5258417ebd35eab16ce728a3 Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Wed, 17 Jun 2015 13:58:01 -0500 Subject: [PATCH 16/23] provider/template: don't error when rendering fails in Exists The Exists function can run in a context where the contents of the template have changed, but it uses the old set of variables from the state. This means that when the set of variables changes, rendering will fail in Exists. This was returning an error, but really it just needs to be treated as a scenario where the template needs re-rendering. fixes #2344 and possibly a few other template issues floating around --- builtin/providers/template/resource.go | 15 ++++- builtin/providers/template/resource_test.go | 62 ++++++++++++++++++--- helper/resource/testing.go | 8 +++ 3 files changed, 74 insertions(+), 11 deletions(-) diff --git a/builtin/providers/template/resource.go b/builtin/providers/template/resource.go index 8eb3ce9eb3..9019dcfc93 100644 --- a/builtin/providers/template/resource.go +++ b/builtin/providers/template/resource.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "fmt" "io/ioutil" + "log" "os" "path/filepath" @@ -75,7 +76,13 @@ func Delete(d *schema.ResourceData, meta interface{}) error { func Exists(d *schema.ResourceData, meta interface{}) (bool, error) { rendered, err := render(d) if err != nil { - return false, err + if _, ok := err.(templateRenderError); ok { + log.Printf("[DEBUG] Got error while rendering in Exists: %s", err) + log.Printf("[DEBUG] Returning false so the template re-renders using latest variables from config.") + return false, nil + } else { + return false, err + } } return hash(rendered) == d.Id(), nil } @@ -87,6 +94,8 @@ func Read(d *schema.ResourceData, meta interface{}) error { return nil } +type templateRenderError error + var readfile func(string) ([]byte, error) = ioutil.ReadFile // testing hook func render(d *schema.ResourceData) (string, error) { @@ -105,7 +114,9 @@ func render(d *schema.ResourceData) (string, error) { rendered, err := execute(string(buf), vars) if err != nil { - return "", fmt.Errorf("failed to render %v: %v", filename, err) + return "", templateRenderError( + fmt.Errorf("failed to render %v: %v", filename, err), + ) } return rendered, nil diff --git a/builtin/providers/template/resource_test.go b/builtin/providers/template/resource_test.go index 13b6cf0522..7f461325a2 100644 --- a/builtin/providers/template/resource_test.go +++ b/builtin/providers/template/resource_test.go @@ -34,15 +34,7 @@ func TestTemplateRendering(t *testing.T) { Providers: testProviders, Steps: []r.TestStep{ r.TestStep{ - Config: ` -resource "template_file" "t0" { - filename = "mock" - vars = ` + tt.vars + ` -} -output "rendered" { - value = "${template_file.t0.rendered}" -} -`, + Config: testTemplateConfig(tt.vars), Check: func(s *terraform.State) error { got := s.RootModule().Outputs["rendered"] if tt.want != got { @@ -55,3 +47,55 @@ output "rendered" { }) } } + +// https://github.com/hashicorp/terraform/issues/2344 +func TestTemplateVariableChange(t *testing.T) { + steps := []struct { + vars string + template string + want string + }{ + {`{a="foo"}`, `${a}`, `foo`}, + {`{b="bar"}`, `${b}`, `bar`}, + } + + var testSteps []r.TestStep + for i, step := range steps { + testSteps = append(testSteps, r.TestStep{ + PreConfig: func(template string) func() { + return func() { + readfile = func(string) ([]byte, error) { + return []byte(template), nil + } + } + }(step.template), + Config: testTemplateConfig(step.vars), + Check: func(i int, want string) r.TestCheckFunc { + return func(s *terraform.State) error { + got := s.RootModule().Outputs["rendered"] + if want != got { + return fmt.Errorf("[%d] got:\n%q\nwant:\n%q\n", i, got, want) + } + return nil + } + }(i, step.want), + }) + } + + r.Test(t, r.TestCase{ + Providers: testProviders, + Steps: testSteps, + }) +} + +func testTemplateConfig(vars string) string { + return ` +resource "template_file" "t0" { + filename = "mock" + vars = ` + vars + ` +} +output "rendered" { + value = "${template_file.t0.rendered}" +} + ` +} diff --git a/helper/resource/testing.go b/helper/resource/testing.go index 6832146b22..3835e8639c 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -60,6 +60,10 @@ type TestCase struct { // potentially complex update logic. In general, simply create/destroy // tests will only need one step. type TestStep struct { + // PreConfig is called before the Config is applied to perform any per-step + // setup that needs to happen + PreConfig func() + // Config a string of the configuration to give to Terraform. Config string @@ -160,6 +164,10 @@ func testStep( opts terraform.ContextOpts, state *terraform.State, step TestStep) (*terraform.State, error) { + if step.PreConfig != nil { + step.PreConfig() + } + cfgPath, err := ioutil.TempDir("", "tf-test") if err != nil { return state, fmt.Errorf( From 466d6bf95181633377a13900d1b4024a95c4a4c0 Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Wed, 17 Jun 2015 15:37:51 -0500 Subject: [PATCH 17/23] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca5790a4ef..f8106d6698 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,8 @@ BUG FIXES: when non-lowercase protocol strings were used [GH-2246] * provider/aws: corrected frankfurt S3 website region [GH-2259] * provider/aws: `aws_elasticache_cluster` port is required [GH-2160] + * provider/template: fix issue causing "unknown variable" rendering errors + when an existing set of template variables is changed [GH-2386] ## 0.5.3 (June 1, 2015) From 6fafff34164565efcde44fd07b7efbc9d355f4fd Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Wed, 17 Jun 2015 15:38:06 -0500 Subject: [PATCH 18/23] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8106d6698..8d71af9703 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ FEATURES: * **New remote state backend: `swift`**: You can now store remote state in a OpenStack Swift. [GH-2254] * command/output: support display of module outputs [GH-2102] - * core: keys() and values() funcs for map variables [GH-2198] + * core: `keys()` and `values()` funcs for map variables [GH-2198] IMPROVEMENTS: From ceeb94e1573443d447155fa2aaf4f9155753d995 Mon Sep 17 00:00:00 2001 From: Alex Pilon Date: Sun, 7 Jun 2015 11:23:32 +0200 Subject: [PATCH 19/23] provider/aws: Add cloudwatch_metric_alarm --- builtin/providers/aws/config.go | 5 + builtin/providers/aws/provider.go | 1 + .../resource_aws_cloudwatch_metric_alarm.go | 287 ++++++++++++++++++ ...source_aws_cloudwatch_metric_alarm_test.go | 95 ++++++ 4 files changed, 388 insertions(+) create mode 100644 builtin/providers/aws/resource_aws_cloudwatch_metric_alarm.go create mode 100644 builtin/providers/aws/resource_aws_cloudwatch_metric_alarm_test.go diff --git a/builtin/providers/aws/config.go b/builtin/providers/aws/config.go index 658e0ec914..93f7db9190 100644 --- a/builtin/providers/aws/config.go +++ b/builtin/providers/aws/config.go @@ -10,6 +10,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/service/autoscaling" + "github.com/aws/aws-sdk-go/service/cloudwatch" "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ecs" @@ -37,6 +38,7 @@ type Config struct { } type AWSClient struct { + cloudwatchconn *cloudwatch.CloudWatch dynamodbconn *dynamodb.DynamoDB ec2conn *ec2.EC2 ecsconn *ecs.ECS @@ -143,6 +145,9 @@ func (c *Config) Client() (interface{}, error) { log.Println("[INFO] Initializing Lambda Connection") client.lambdaconn = lambda.New(awsConfig) + + log.Println("[INFO] Initializing CloudWatch SDK connection") + client.cloudwatchconn = cloudwatch.New(awsConfig) } if len(errs) > 0 { diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index f28cf69ea0..44f881debe 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -86,6 +86,7 @@ func Provider() terraform.ResourceProvider { "aws_app_cookie_stickiness_policy": resourceAwsAppCookieStickinessPolicy(), "aws_autoscaling_group": resourceAwsAutoscalingGroup(), "aws_autoscaling_notification": resourceAwsAutoscalingNotification(), + "aws_cloudwatch_metric_alarm": resourceAwsCloudWatchMetricAlarm(), "aws_customer_gateway": resourceAwsCustomerGateway(), "aws_db_instance": resourceAwsDbInstance(), "aws_db_parameter_group": resourceAwsDbParameterGroup(), diff --git a/builtin/providers/aws/resource_aws_cloudwatch_metric_alarm.go b/builtin/providers/aws/resource_aws_cloudwatch_metric_alarm.go new file mode 100644 index 0000000000..9f9d60ce8a --- /dev/null +++ b/builtin/providers/aws/resource_aws_cloudwatch_metric_alarm.go @@ -0,0 +1,287 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudwatch" +) + +func resourceAwsCloudWatchMetricAlarm() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsCloudWatchMetricAlarmCreate, + Read: resourceAwsCloudWatchMetricAlarmRead, + Update: resourceAwsCloudWatchMetricAlarmUpdate, + Delete: resourceAwsCloudWatchMetricAlarmDelete, + + Schema: map[string]*schema.Schema{ + "alarm_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "comparison_operator": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "evaluation_periods": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + "metric_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "namespace": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "period": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + "statistic": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "threshold": &schema.Schema{ + Type: schema.TypeFloat, + Required: true, + }, + "actions_enabled": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + "alarm_actions": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + }, + "alarm_description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "dimensions": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + }, + "insufficient_data_actions": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + }, + "ok_actions": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + }, + "unit": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +func resourceAwsCloudWatchMetricAlarmCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).cloudwatchconn + + params := getAwsCloudWatchPutMetricAlarmInput(d) + + log.Printf("[DEBUG] Creating CloudWatch Metric Alarm: %#v", params) + _, err := conn.PutMetricAlarm(¶ms) + if err != nil { + return fmt.Errorf("Creating metric alarm failed: %s", err) + } + d.SetId(d.Get("alarm_name").(string)) + log.Println("[INFO] CloudWatch Metric Alarm created") + + return resourceAwsCloudWatchMetricAlarmRead(d, meta) +} + +func resourceAwsCloudWatchMetricAlarmRead(d *schema.ResourceData, meta interface{}) error { + a, err := getAwsCloudWatchMetricAlarm(d, meta) + if err != nil { + return err + } + if a == nil { + d.SetId("") + return nil + } + + log.Printf("[DEBUG] Reading CloudWatch Metric Alarm: %s", d.Get("alarm_name")) + + d.Set("actions_enabled", a.ActionsEnabled) + + if err := d.Set("alarm_actions", _strArrPtrToList(a.AlarmActions)); err != nil { + log.Printf("[WARN] Error setting Alarm Actions: %s", err) + } + d.Set("alarm_description", a.AlarmDescription) + d.Set("alarm_name", a.AlarmName) + d.Set("comparison_operator", a.ComparisonOperator) + d.Set("dimensions", a.Dimensions) + d.Set("evaluation_periods", a.EvaluationPeriods) + + if err := d.Set("insufficient_data_actions", _strArrPtrToList(a.InsufficientDataActions)); err != nil { + log.Printf("[WARN] Error setting Insufficient Data Actions: %s", err) + } + d.Set("metric_name", a.MetricName) + d.Set("namespace", a.Namespace) + + if err := d.Set("ok_actions", _strArrPtrToList(a.OKActions)); err != nil { + log.Printf("[WARN] Error setting OK Actions: %s", err) + } + d.Set("period", a.Period) + d.Set("statistic", a.Statistic) + d.Set("threshold", a.Threshold) + d.Set("unit", a.Unit) + + return nil +} + +func resourceAwsCloudWatchMetricAlarmUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).cloudwatchconn + params := getAwsCloudWatchPutMetricAlarmInput(d) + + log.Printf("[DEBUG] Updating CloudWatch Metric Alarm: %#v", params) + _, err := conn.PutMetricAlarm(¶ms) + if err != nil { + return fmt.Errorf("Updating metric alarm failed: %s", err) + } + log.Println("[INFO] CloudWatch Metric Alarm updated") + + return resourceAwsCloudWatchMetricAlarmRead(d, meta) +} + +func resourceAwsCloudWatchMetricAlarmDelete(d *schema.ResourceData, meta interface{}) error { + p, err := getAwsCloudWatchMetricAlarm(d, meta) + if err != nil { + return err + } + if p == nil { + log.Printf("[DEBUG] CloudWatch Metric Alarm %s is already gone", d.Id()) + return nil + } + + log.Printf("[INFO] Deleting CloudWatch Metric Alarm: %s", d.Id()) + + conn := meta.(*AWSClient).cloudwatchconn + params := cloudwatch.DeleteAlarmsInput{ + AlarmNames: []*string{aws.String(d.Id())}, + } + + if _, err := conn.DeleteAlarms(¶ms); err != nil { + return fmt.Errorf("Error deleting CloudWatch Metric Alarm: %s", err) + } + log.Println("[INFO] CloudWatch Metric Alarm deleted") + + d.SetId("") + return nil +} + +func getAwsCloudWatchPutMetricAlarmInput(d *schema.ResourceData) cloudwatch.PutMetricAlarmInput { + params := cloudwatch.PutMetricAlarmInput{ + AlarmName: aws.String(d.Get("alarm_name").(string)), + ComparisonOperator: aws.String(d.Get("comparison_operator").(string)), + EvaluationPeriods: aws.Long(int64(d.Get("evaluation_periods").(int))), + MetricName: aws.String(d.Get("metric_name").(string)), + Namespace: aws.String(d.Get("namespace").(string)), + Period: aws.Long(int64(d.Get("period").(int))), + Statistic: aws.String(d.Get("statistic").(string)), + Threshold: aws.Double(d.Get("threshold").(float64)), + } + + if v := d.Get("actions_enabled"); v != nil { + params.ActionsEnabled = aws.Boolean(v.(bool)) + } + + if v, ok := d.GetOk("alarm_description"); ok { + params.AlarmDescription = aws.String(v.(string)) + } + + if v, ok := d.GetOk("unit"); ok { + params.Unit = aws.String(v.(string)) + } + + var alarmActions []*string + if v := d.Get("alarm_actions"); v != nil { + for _, v := range v.(*schema.Set).List() { + str := v.(string) + alarmActions = append(alarmActions, aws.String(str)) + } + params.AlarmActions = alarmActions + } + + var insufficientDataActions []*string + if v := d.Get("insufficient_data_actions"); v != nil { + for _, v := range v.(*schema.Set).List() { + str := v.(string) + insufficientDataActions = append(insufficientDataActions, aws.String(str)) + } + params.InsufficientDataActions = insufficientDataActions + } + + var okActions []*string + if v := d.Get("ok_actions"); v != nil { + for _, v := range v.(*schema.Set).List() { + str := v.(string) + okActions = append(okActions, aws.String(str)) + } + params.OKActions = okActions + } + + a := d.Get("dimensions").(map[string]interface{}) + dimensions := make([]*cloudwatch.Dimension, 0, len(a)) + for k, v := range a { + dimensions = append(dimensions, &cloudwatch.Dimension{ + Name: aws.String(k), + Value: aws.String(v.(string)), + }) + } + params.Dimensions = dimensions + + return params +} + +func getAwsCloudWatchMetricAlarm(d *schema.ResourceData, meta interface{}) (*cloudwatch.MetricAlarm, error) { + conn := meta.(*AWSClient).cloudwatchconn + + params := cloudwatch.DescribeAlarmsInput{ + AlarmNames: []*string{aws.String(d.Id())}, + } + + resp, err := conn.DescribeAlarms(¶ms) + if err != nil { + return nil, nil + } + + // Find it and return it + for idx, ma := range resp.MetricAlarms { + if *ma.AlarmName == d.Id() { + return resp.MetricAlarms[idx], nil + } + } + + return nil, nil +} + +func _strArrPtrToList(strArrPtr []*string) []string { + var result []string + for _, elem := range strArrPtr { + result = append(result, *elem) + } + return result +} diff --git a/builtin/providers/aws/resource_aws_cloudwatch_metric_alarm_test.go b/builtin/providers/aws/resource_aws_cloudwatch_metric_alarm_test.go new file mode 100644 index 0000000000..8d9a53c360 --- /dev/null +++ b/builtin/providers/aws/resource_aws_cloudwatch_metric_alarm_test.go @@ -0,0 +1,95 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudwatch" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSCloudWatchMetricAlarm_basic(t *testing.T) { + var alarm cloudwatch.MetricAlarm + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCloudWatchMetricAlarmDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSCloudWatchMetricAlarmConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudWatchMetricAlarmExists("aws_cloudwatch_metric_alarm.foobar", &alarm), + resource.TestCheckResourceAttr("aws_cloudwatch_metric_alarm.foobar", "metric_name", "CPUUtilization"), + resource.TestCheckResourceAttr("aws_cloudwatch_metric_alarm.foobar", "statistic", "Average"), + ), + }, + }, + }) +} + +func testAccCheckCloudWatchMetricAlarmExists(n string, alarm *cloudwatch.MetricAlarm) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := testAccProvider.Meta().(*AWSClient).cloudwatchconn + params := cloudwatch.DescribeAlarmsInput{ + AlarmNames: []*string{aws.String(rs.Primary.ID)}, + } + resp, err := conn.DescribeAlarms(¶ms) + if err != nil { + return err + } + if len(resp.MetricAlarms) == 0 { + return fmt.Errorf("Alarm not found") + } + *alarm = *resp.MetricAlarms[0] + + return nil + } +} + +func testAccCheckAWSCloudWatchMetricAlarmDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).cloudwatchconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_cloudwatch_metric_alarm" { + continue + } + + params := cloudwatch.DescribeAlarmsInput{ + AlarmNames: []*string{aws.String(rs.Primary.ID)}, + } + + resp, err := conn.DescribeAlarms(¶ms) + + if err == nil { + if len(resp.MetricAlarms) != 0 && + *resp.MetricAlarms[0].AlarmName == rs.Primary.ID { + return fmt.Errorf("Alarm Still Exists: %s", rs.Primary.ID) + } + } + } + + return nil +} + +var testAccAWSCloudWatchMetricAlarmConfig = fmt.Sprintf(` +resource "aws_cloudwatch_metric_alarm" "foobar" { + alarm_name = "terraform-test-foobar5" + comparison_operator = "GreaterThanOrEqualToThreshold" + evaluation_periods = "2" + metric_name = "CPUUtilization" + namespace = "AWS/EC2" + period = "120" + statistic = "Average" + threshold = "80" + alarm_description = "This metric monitor ec2 cpu utilization" + insufficient_data_actions = [] +} +`) From 05f4b9bfd940d717fa3b6646a5d71c6de41541f6 Mon Sep 17 00:00:00 2001 From: Alex Pilon Date: Sun, 7 Jun 2015 11:18:34 +0200 Subject: [PATCH 20/23] provider/aws: Add autoscaling_policy --- builtin/providers/aws/provider.go | 1 + .../aws/resource_aws_autoscaling_policy.go | 181 ++++++++++++++++++ .../resource_aws_autoscaling_policy_test.go | 118 ++++++++++++ 3 files changed, 300 insertions(+) create mode 100644 builtin/providers/aws/resource_aws_autoscaling_policy.go create mode 100644 builtin/providers/aws/resource_aws_autoscaling_policy_test.go diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 44f881debe..0e54e7b501 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -86,6 +86,7 @@ func Provider() terraform.ResourceProvider { "aws_app_cookie_stickiness_policy": resourceAwsAppCookieStickinessPolicy(), "aws_autoscaling_group": resourceAwsAutoscalingGroup(), "aws_autoscaling_notification": resourceAwsAutoscalingNotification(), + "aws_autoscaling_policy": resourceAwsAutoscalingPolicy(), "aws_cloudwatch_metric_alarm": resourceAwsCloudWatchMetricAlarm(), "aws_customer_gateway": resourceAwsCustomerGateway(), "aws_db_instance": resourceAwsDbInstance(), diff --git a/builtin/providers/aws/resource_aws_autoscaling_policy.go b/builtin/providers/aws/resource_aws_autoscaling_policy.go new file mode 100644 index 0000000000..d2f6d2d47b --- /dev/null +++ b/builtin/providers/aws/resource_aws_autoscaling_policy.go @@ -0,0 +1,181 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/autoscaling" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsAutoscalingPolicy() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsAutoscalingPolicyCreate, + Read: resourceAwsAutoscalingPolicyRead, + Update: resourceAwsAutoscalingPolicyUpdate, + Delete: resourceAwsAutoscalingPolicyDelete, + + Schema: map[string]*schema.Schema{ + "arn": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "adjustment_type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "autoscaling_group_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "cooldown": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + "min_adjustment_step": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + "scaling_adjustment": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + }, + } +} + +func resourceAwsAutoscalingPolicyCreate(d *schema.ResourceData, meta interface{}) error { + autoscalingconn := meta.(*AWSClient).autoscalingconn + + params := getAwsAutoscalingPutScalingPolicyInput(d) + + log.Printf("[DEBUG] AutoScaling PutScalingPolicy: %#v", params) + resp, err := autoscalingconn.PutScalingPolicy(¶ms) + if err != nil { + return fmt.Errorf("Error putting scaling policy: %s", err) + } + + d.Set("arn", resp.PolicyARN) + d.SetId(d.Get("name").(string)) + log.Printf("[INFO] AutoScaling Scaling PolicyARN: %s", d.Get("arn").(string)) + + return resourceAwsAutoscalingPolicyRead(d, meta) +} + +func resourceAwsAutoscalingPolicyRead(d *schema.ResourceData, meta interface{}) error { + p, err := getAwsAutoscalingPolicy(d, meta) + if err != nil { + return err + } + if p == nil { + d.SetId("") + return nil + } + + log.Printf("[DEBUG] Read Scaling Policy: ASG: %s, SP: %s, Obj: %#v", d.Get("autoscaling_group_name"), d.Get("name"), p) + + d.Set("adjustment_type", p.AdjustmentType) + d.Set("autoscaling_group_name", p.AutoScalingGroupName) + d.Set("cooldown", p.Cooldown) + d.Set("min_adjustment_step", p.MinAdjustmentStep) + d.Set("arn", p.PolicyARN) + d.Set("name", p.PolicyName) + d.Set("scaling_adjustment", p.ScalingAdjustment) + + return nil +} + +func resourceAwsAutoscalingPolicyUpdate(d *schema.ResourceData, meta interface{}) error { + autoscalingconn := meta.(*AWSClient).autoscalingconn + + params := getAwsAutoscalingPutScalingPolicyInput(d) + + log.Printf("[DEBUG] Autoscaling Update Scaling Policy: %#v", params) + _, err := autoscalingconn.PutScalingPolicy(¶ms) + if err != nil { + return err + } + + return resourceAwsAutoscalingPolicyRead(d, meta) +} + +func resourceAwsAutoscalingPolicyDelete(d *schema.ResourceData, meta interface{}) error { + autoscalingconn := meta.(*AWSClient).autoscalingconn + p, err := getAwsAutoscalingPolicy(d, meta) + if err != nil { + return err + } + if p == nil { + return nil + } + + params := autoscaling.DeletePolicyInput{ + AutoScalingGroupName: aws.String(d.Get("autoscaling_group_name").(string)), + PolicyName: aws.String(d.Get("name").(string)), + } + if _, err := autoscalingconn.DeletePolicy(¶ms); err != nil { + return fmt.Errorf("Autoscaling Scaling Policy: %s ", err) + } + + d.SetId("") + return nil +} + +// PutScalingPolicy seems to require all params to be resent, so create and update can share this common function +func getAwsAutoscalingPutScalingPolicyInput(d *schema.ResourceData) autoscaling.PutScalingPolicyInput { + var params = autoscaling.PutScalingPolicyInput{ + AutoScalingGroupName: aws.String(d.Get("autoscaling_group_name").(string)), + PolicyName: aws.String(d.Get("name").(string)), + } + + if v, ok := d.GetOk("adjustment_type"); ok { + params.AdjustmentType = aws.String(v.(string)) + } + + if v, ok := d.GetOk("cooldown"); ok { + params.Cooldown = aws.Long(int64(v.(int))) + } + + if v, ok := d.GetOk("scaling_adjustment"); ok { + params.ScalingAdjustment = aws.Long(int64(v.(int))) + } + + if v, ok := d.GetOk("min_adjustment_step"); ok { + params.MinAdjustmentStep = aws.Long(int64(v.(int))) + } + + return params +} + +func getAwsAutoscalingPolicy(d *schema.ResourceData, meta interface{}) (*autoscaling.ScalingPolicy, error) { + autoscalingconn := meta.(*AWSClient).autoscalingconn + + params := autoscaling.DescribePoliciesInput{ + AutoScalingGroupName: aws.String(d.Get("autoscaling_group_name").(string)), + PolicyNames: []*string{aws.String(d.Get("name").(string))}, + } + + log.Printf("[DEBUG] AutoScaling Scaling Policy Describe Params: %#v", params) + resp, err := autoscalingconn.DescribePolicies(¶ms) + if err != nil { + return nil, fmt.Errorf("Error retrieving scaling policies: %s", err) + } + + // find scaling policy + name := d.Get("name") + for idx, sp := range resp.ScalingPolicies { + if *sp.PolicyName == name { + return resp.ScalingPolicies[idx], nil + } + } + + // policy not found + return nil, nil +} diff --git a/builtin/providers/aws/resource_aws_autoscaling_policy_test.go b/builtin/providers/aws/resource_aws_autoscaling_policy_test.go new file mode 100644 index 0000000000..2254640b08 --- /dev/null +++ b/builtin/providers/aws/resource_aws_autoscaling_policy_test.go @@ -0,0 +1,118 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/autoscaling" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSAutoscalingPolicy_basic(t *testing.T) { + var policy autoscaling.ScalingPolicy + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAutoscalingPolicyDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSAutoscalingPolicyConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckScalingPolicyExists("aws_autoscaling_policy.foobar", &policy), + resource.TestCheckResourceAttr("aws_autoscaling_policy.foobar", "adjustment_type", "ChangeInCapacity"), + resource.TestCheckResourceAttr("aws_autoscaling_policy.foobar", "cooldown", "300"), + ), + }, + }, + }) +} + +func testAccCheckScalingPolicyExists(n string, policy *autoscaling.ScalingPolicy) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + rs = rs + return fmt.Errorf("Not found: %s", n) + } + + conn := testAccProvider.Meta().(*AWSClient).autoscalingconn + params := &autoscaling.DescribePoliciesInput{ + AutoScalingGroupName: aws.String(rs.Primary.Attributes["autoscaling_group_name"]), + PolicyNames: []*string{aws.String(rs.Primary.ID)}, + } + resp, err := conn.DescribePolicies(params) + if err != nil { + return err + } + if len(resp.ScalingPolicies) == 0 { + return fmt.Errorf("ScalingPolicy not found") + } + + *policy = *resp.ScalingPolicies[0] + + return nil + } +} + +func testAccCheckAWSAutoscalingPolicyDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).autoscalingconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_autoscaling_group" { + continue + } + + params := autoscaling.DescribePoliciesInput{ + AutoScalingGroupName: aws.String(rs.Primary.Attributes["autoscaling_group_name"]), + PolicyNames: []*string{aws.String(rs.Primary.ID)}, + } + + resp, err := conn.DescribePolicies(¶ms) + + if err == nil { + if len(resp.ScalingPolicies) != 0 && + *resp.ScalingPolicies[0].PolicyName == rs.Primary.ID { + return fmt.Errorf("Scaling Policy Still Exists: %s", rs.Primary.ID) + } + } + } + + return nil +} + +var testAccAWSAutoscalingPolicyConfig = fmt.Sprintf(` +resource "aws_launch_configuration" "foobar" { + name = "terraform-test-foobar5" + image_id = "ami-21f78e11" + instance_type = "t1.micro" +} + +resource "aws_autoscaling_group" "foobar" { + availability_zones = ["us-west-2a"] + name = "terraform-test-foobar5" + max_size = 5 + min_size = 2 + health_check_grace_period = 300 + health_check_type = "ELB" + desired_capacity = 4 + force_delete = true + termination_policies = ["OldestInstance"] + launch_configuration = "${aws_launch_configuration.foobar.name}" + tag { + key = "Foo" + value = "foo-bar" + propagate_at_launch = true + } +} + +resource "aws_autoscaling_policy" "foobar" { + name = "foobar" + scaling_adjustment = 4 + adjustment_type = "ChangeInCapacity" + cooldown = 300 + autoscaling_group_name = "${aws_autoscaling_group.foobar.name}" +} +`) From 14f4e5fe54584fb6e7a2212217a9d5d9d753dd59 Mon Sep 17 00:00:00 2001 From: Alex Pilon Date: Sun, 7 Jun 2015 11:19:57 +0200 Subject: [PATCH 21/23] provider/aws: Add docs for autoscaling_policy + cloudwatch_metric_alarm --- .../resource_aws_autoscaling_policy_test.go | 3 - .../resource_aws_cloudwatch_metric_alarm.go | 1 + .../aws/r/autoscaling_policy.html.markdown | 53 +++++++++++++ .../r/cloudwatch_metric_alarm.html.markdown | 75 +++++++++++++++++++ website/source/layouts/aws.erb | 8 ++ 5 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 website/source/docs/providers/aws/r/autoscaling_policy.html.markdown create mode 100644 website/source/docs/providers/aws/r/cloudwatch_metric_alarm.html.markdown diff --git a/builtin/providers/aws/resource_aws_autoscaling_policy_test.go b/builtin/providers/aws/resource_aws_autoscaling_policy_test.go index 2254640b08..0a7aeff916 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_policy_test.go +++ b/builtin/providers/aws/resource_aws_autoscaling_policy_test.go @@ -51,8 +51,6 @@ func testAccCheckScalingPolicyExists(n string, policy *autoscaling.ScalingPolicy return fmt.Errorf("ScalingPolicy not found") } - *policy = *resp.ScalingPolicies[0] - return nil } } @@ -97,7 +95,6 @@ resource "aws_autoscaling_group" "foobar" { min_size = 2 health_check_grace_period = 300 health_check_type = "ELB" - desired_capacity = 4 force_delete = true termination_policies = ["OldestInstance"] launch_configuration = "${aws_launch_configuration.foobar.name}" diff --git a/builtin/providers/aws/resource_aws_cloudwatch_metric_alarm.go b/builtin/providers/aws/resource_aws_cloudwatch_metric_alarm.go index 9f9d60ce8a..4c8b401400 100644 --- a/builtin/providers/aws/resource_aws_cloudwatch_metric_alarm.go +++ b/builtin/providers/aws/resource_aws_cloudwatch_metric_alarm.go @@ -55,6 +55,7 @@ func resourceAwsCloudWatchMetricAlarm() *schema.Resource { "actions_enabled": &schema.Schema{ Type: schema.TypeBool, Optional: true, + Default: true, }, "alarm_actions": &schema.Schema{ Type: schema.TypeSet, diff --git a/website/source/docs/providers/aws/r/autoscaling_policy.html.markdown b/website/source/docs/providers/aws/r/autoscaling_policy.html.markdown new file mode 100644 index 0000000000..2543c0220d --- /dev/null +++ b/website/source/docs/providers/aws/r/autoscaling_policy.html.markdown @@ -0,0 +1,53 @@ +--- +layout: "aws" +page_title: "AWS: aws_autoscaling_policy" +sidebar_current: "docs-aws-resource-autoscaling-policy" +description: |- + Provides an AutoScaling Scaling Group resource. +--- + +# aws\_autoscaling\_policy + +Provides an AutoScaling Scaling Policy resource. + +~> **NOTE:** You may want to omit `desired_capacity` attribute from attached `aws_autoscaling_group` +when using autoscaling policies. It's good practice to pick either +[manual](http://docs.aws.amazon.com/AutoScaling/latest/DeveloperGuide/as-manual-scaling.html) +or [dynamic](http://docs.aws.amazon.com/AutoScaling/latest/DeveloperGuide/as-scale-based-on-demand.html) +(policy-based) scaling. + +## Example Usage +``` +resource "aws_autoscaling_policy" "bat" { + name = "foobar3-terraform-test" + scaling_adjustment = 4 + adjustment_type = "ChangeInCapacity" + cooldown = 300 + autoscaling_group_name = "${aws_autoscaling_group.bar.name}" +} + +resource "aws_autoscaling_group" "bar" { + availability_zones = ["us-east-1a"] + name = "foobar3-terraform-test" + max_size = 5 + min_size = 2 + health_check_grace_period = 300 + health_check_type = "ELB" + force_delete = true + launch_configuration = "${aws_launch_configuration.foo.name}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the policy. +* `autoscaling_group_name` - (Required) The name or ARN of the group. +* `adjustment_type` - (Required) Specifies whether the `scaling_adjustment` is an absolute number or a percentage of the current capacity. Valid values are `ChangeInCapacity`, `ExactCapacity`, and `PercentChangeInCapacity`. +* `scaling_adjustment` - (Required) The number of instances by which to scale. `adjustment_type` determines the interpretation of this number (e.g., as an absolute number or as a percentage of the existing Auto Scaling group size). A positive increment adds to the current capacity and a negative value removes from the current capacity. +* `cooldown` - (Optional) The amount of time, in seconds, after a scaling activity completes and before the next scaling activity can start. +* `min_adjustment_step` - (Optional) Used with `adjustment_type` with the value `PercentChangeInCapacity`, the scaling policy changes the `desired_capacity` of the Auto Scaling group by at least the number of instances specified in the value. + +## Attribute Reference +* `arn` - The ARN assigned by AWS to the scaling policy. diff --git a/website/source/docs/providers/aws/r/cloudwatch_metric_alarm.html.markdown b/website/source/docs/providers/aws/r/cloudwatch_metric_alarm.html.markdown new file mode 100644 index 0000000000..e6ede269fc --- /dev/null +++ b/website/source/docs/providers/aws/r/cloudwatch_metric_alarm.html.markdown @@ -0,0 +1,75 @@ +--- +layout: "aws" +page_title: "AWS: cloudwatch_metric_alarm" +sidebar_current: "docs-aws-resource-cloudwatch-metric-alarm" +description: |- + Provides an AutoScaling Scaling Group resource. +--- + +# aws\_cloudwatch\_metric\_alarm + +Provides a CloudWatch Metric Alarm resource. + +## Example Usage +``` +resource "aws_cloudwatch_metric_alarm" "foobar" { + alarm_name = "terraform-test-foobar5" + comparison_operator = "GreaterThanOrEqualToThreshold" + evaluation_periods = "2" + metric_name = "CPUUtilization" + namespace = "AWS/EC2" + period = "120" + statistic = "Average" + threshold = "80" + alarm_description = "This metric monitor ec2 cpu utilization" + insufficient_data_actions = [] +} +``` + +## Example in Conjuction with Scaling Policies +``` +resource "aws_autoscaling_policy" "bat" { + name = "foobar3-terraform-test" + scaling_adjustment = 4 + adjustment_type = "ChangeInCapacity" + cooldown = 300 + autoscaling_group_name = "${aws_autoscaling_group.bar.name}" +} + +resource "aws_cloudwatch_metric_alarm" "bat" { + alarm_name = "terraform-test-foobar5" + comparison_operator = "GreaterThanOrEqualToThreshold" + evaluation_periods = "2" + metric_name = "CPUUtilization" + namespace = "AWS/EC2" + period = "120" + statistic = "Average" + threshold = "80" + alarm_description = "This metric monitor ec2 cpu utilization" + alarm_actions = ["${aws_autoscaling_policy.bat.arn}"] +} +``` +## Argument Reference + +See [related part of AWS Docs](http://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_PutMetricAlarm.html) +for details about valid values. + +The following arguments are supported: + +* `alarm_name` - (Required) The descriptive name for the alarm. This name must be unique within the user's AWS account +* `comparison_operator` - (Required) The arithmetic operation to use when comparing the specified Statistic and Threshold. The specified Statistic value is used as the first operand. Either of the following is supported: `GreaterThanOrEqualToThreshold`, `GreaterThanThreshold`, `LessThanThreshold`, `LessThanOrEqualToThreshold`. +* `evaluation_periods` - (Required) The number of periods over which data is compared to the specified threshold. +* `metric_name` - (Required) The name for the alarm's associated metric. + See docs for [supported metrics]([valid metrics](http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/CW_Support_For_AWS.html)). +* `namespace` - (Required) The namespace for the alarm's associated metric. +* `period` - (Required) The period in seconds over which the specified `statistic` is applied. +* `statistic` - (Required) The statistic to apply to the alarm's associated metric. + Either of the following is supported: `SampleCount`, `Average`, `Sum`, `Minimum`, `Maximum` +* `threshold` - (Required) The value against which the specified statistic is compared. +* `actions_enabled` - (Optional) Indicates whether or not actions should be executed during any changes to the alarm's state. Defaults to `true`. +* `alarm_actions` - (Optional) The list of actions to execute when this alarm transitions into an ALARM state from any other state. Each action is specified as an Amazon Resource Number (ARN). +* `alarm_description` - (Optional) The description for the alarm. +* `dimensions` - (Optional) The dimensions for the alarm's associated metric. +* `insufficient_data_actions` - (Optional) The list of actions to execute when this alarm transitions into an INSUFFICIENT_DATA state from any other state. Each action is specified as an Amazon Resource Number (ARN). +* `ok_actions` - (Optional) The list of actions to execute when this alarm transitions into an OK state from any other state. Each action is specified as an Amazon Resource Number (ARN). +* `unit` - (Optional) The unit for the alarm's associated metric. diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index 7afd00060b..801945dec9 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -21,6 +21,14 @@ aws_autoscaling_notification + > + aws_autoscaling_policy + + + > + aws_cloudwatch_metric_alarm + + > aws_customer_gateway From ad4ff531209a33a6b386490930e2b105b9dcfe95 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Wed, 17 Jun 2015 23:23:21 +0100 Subject: [PATCH 22/23] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d71af9703..7bfe3876a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ FEATURES: * **New provider: `azure`** [GH-2052, GH-2053, GH-2372, GH-2380] * **New resource: `aws_autoscaling_notification`** [GH-2197] + * **New resource: `aws_autoscaling_policy`** [GH-2201] + * **New resource: `aws_cloudwatch_metric_alarm`** [GH-2201] * **New resource: `aws_dynamodb_table`** [GH-2121] * **New resource: `aws_ecs_cluster`** [GH-1803] * **New resource: `aws_ecs_service`** [GH-1803] From 611741c1088333b23a1e5238bd10846d8ad59ca0 Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Thu, 18 Jun 2015 15:47:54 -0500 Subject: [PATCH 23/23] typo --- builtin/providers/aws/resource_aws_autoscaling_group.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/providers/aws/resource_aws_autoscaling_group.go b/builtin/providers/aws/resource_aws_autoscaling_group.go index 0fc62b4b25..bf9b30c6a2 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_group.go +++ b/builtin/providers/aws/resource_aws_autoscaling_group.go @@ -408,7 +408,7 @@ func waitForASGCapacity(d *schema.ResourceData, meta interface{}) error { } wantELB := d.Get("min_elb_capacity").(int) - log.Printf("[DEBUG] Wanting for capacity: %d ASG, %d ELB", wantASG, wantELB) + log.Printf("[DEBUG] Waiting for capacity: %d ASG, %d ELB", wantASG, wantELB) return resource.Retry(waitForASGCapacityTimeout, func() error { g, err := getAwsAutoscalingGroup(d, meta)