diff --git a/builtin/providers/aws/resource_aws_db_instance.go b/builtin/providers/aws/resource_aws_db_instance.go index ec31db9f03a2..8c789673874e 100644 --- a/builtin/providers/aws/resource_aws_db_instance.go +++ b/builtin/providers/aws/resource_aws_db_instance.go @@ -89,6 +89,10 @@ func resource_aws_db_instance_create( rs.Attributes, "security_group_names").([]interface{})) } + if attr = rs.Attributes["db_subnet_group_name"]; attr != "" { + opts.DBSubnetGroupName = attr + } + opts.DBInstanceIdentifier = rs.Attributes["identifier"] opts.DBName = rs.Attributes["name"] opts.MasterUsername = rs.Attributes["username"] @@ -227,6 +231,7 @@ func resource_aws_db_instance_diff( "vpc_security_group_ids": diff.AttrTypeCreate, "security_group_names": diff.AttrTypeCreate, "skip_final_snapshot": diff.AttrTypeUpdate, + "db_subnet_group_name": diff.AttrTypeCreate, "final_snapshot_identifier": diff.AttrTypeUpdate, }, @@ -258,6 +263,7 @@ func resource_aws_db_instance_update_state( s.Attributes["availability_zone"] = v.AvailabilityZone s.Attributes["backup_retention_period"] = strconv.Itoa(v.BackupRetentionPeriod) s.Attributes["backup_window"] = v.PreferredBackupWindow + s.Attributes["db_subnet_group_name"] = v.DBSubnetGroup.Name s.Attributes["endpoint"] = fmt.Sprintf("%s:%s", s.Attributes["address"], strconv.Itoa(v.Port)) s.Attributes["engine"] = v.Engine s.Attributes["engine_version"] = v.EngineVersion @@ -341,6 +347,7 @@ func resource_aws_db_instance_validation() *config.Validator { "vpc_security_group_ids.*", "skip_final_snapshot", "security_group_names.*", + "db_subnet_group_name", }, } } diff --git a/builtin/providers/aws/resource_aws_db_subnet_group.go b/builtin/providers/aws/resource_aws_db_subnet_group.go new file mode 100644 index 000000000000..84bdd702c5c0 --- /dev/null +++ b/builtin/providers/aws/resource_aws_db_subnet_group.go @@ -0,0 +1,207 @@ +package aws + +import ( + "fmt" + "log" + "strings" + "time" + + "github.com/hashicorp/terraform/flatmap" + "github.com/hashicorp/terraform/helper/config" + "github.com/hashicorp/terraform/helper/diff" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/goamz/rds" +) + +func resource_aws_db_subnet_group_create( + s *terraform.ResourceState, + d *terraform.ResourceDiff, + meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + conn := p.rdsconn + + // Merge the diff into the state so that we have all the attributes + // properly. + rs := s.MergeDiff(d) + + var err error + + opts := rds.CreateDBSubnetGroup{ + DBSubnetGroupName: rs.Attributes["name"], + DBSubnetGroupDescription: rs.Attributes["description"], + SubnetIds: expandStringList(flatmap.Expand( + rs.Attributes, "subnet_ids").([]interface{})), + } + + log.Printf("[DEBUG] DB Subnet Group create configuration: %#v", opts) + _, err = conn.CreateDBSubnetGroup(&opts) + if err != nil { + return nil, fmt.Errorf("Error creating DB Subnet Group: %s", err) + } + + rs.ID = rs.Attributes["name"] + + log.Printf("[INFO] DB Subnet Group ID: %s", rs.ID) + + log.Println( + "[INFO] Waiting for DB Subnet Group creation to be complete") + + stateConf := &resource.StateChangeConf{ + // TODO are there any other states? + Pending: []string{}, + Target: "Complete", + Refresh: DBSubnetGroupStateRefreshFunc(rs.ID, conn), + Timeout: 10 * time.Minute, + } + + // Wait, catching any errors + _, err = stateConf.WaitForState() + if err != nil { + return rs, err + } + + v, err := resource_aws_db_subnet_group_retrieve(rs.ID, conn) + if err != nil { + return rs, err + } + + return resource_aws_db_subnet_group_update_state(rs, v) +} + +func resource_aws_db_subnet_group_update( + s *terraform.ResourceState, + d *terraform.ResourceDiff, + meta interface{}) (*terraform.ResourceState, error) { + + panic("Cannot update DB Subnet Group") + + return nil, nil +} + +func resource_aws_db_subnet_group_destroy( + s *terraform.ResourceState, + meta interface{}) error { + p := meta.(*ResourceProvider) + conn := p.rdsconn + + log.Printf("[DEBUG] DB Subnet Group destroy: %v", s.ID) + + opts := rds.DeleteDBSubnetGroup{DBSubnetGroupName: s.ID} + + log.Printf("[DEBUG] DB Subnet Group destroy configuration: %v", opts) + _, err := conn.DeleteDBSubnetGroup(&opts) + + if err != nil { + newerr, ok := err.(*rds.Error) + if ok && newerr.Code == "DBSubnetGroupNotFoundFault" { + return nil + } + return err + } + + return nil +} + +func resource_aws_db_subnet_group_refresh( + s *terraform.ResourceState, + meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + conn := p.rdsconn + + v, err := resource_aws_db_subnet_group_retrieve(s.ID, conn) + + if err != nil || v == nil { + return s, err + } + + return resource_aws_db_subnet_group_update_state(s, v) +} + +func resource_aws_db_subnet_group_diff( + s *terraform.ResourceState, + c *terraform.ResourceConfig, + meta interface{}) (*terraform.ResourceDiff, error) { + + b := &diff.ResourceBuilder{ + Attrs: map[string]diff.AttrType{ + "name": diff.AttrTypeCreate, + "description": diff.AttrTypeCreate, + "subnet_ids": diff.AttrTypeCreate, + }, + } + + return b.Diff(s, c) +} + +func resource_aws_db_subnet_group_update_state( + s *terraform.ResourceState, + v *rds.DBSubnetGroup) (*terraform.ResourceState, error) { + + s.Attributes["name"] = v.Name + s.Attributes["description"] = v.Description + + // Flatten our group values + toFlatten := make(map[string]interface{}) + + if len(v.SubnetIds) > 0 && v.SubnetIds[0] != "" { + toFlatten["subnet_ids"] = v.SubnetIds + } + + for k, v := range flatmap.Flatten(toFlatten) { + s.Attributes[k] = v + } + + return s, nil +} + +func resource_aws_db_subnet_group_retrieve(name string, conn *rds.Rds) (*rds.DBSubnetGroup, error) { + opts := rds.DescribeDBSubnetGroups{ + DBSubnetGroupName: name, + } + + log.Printf("[DEBUG] DB Subnet Group describe configuration: %#v", opts) + + resp, err := conn.DescribeDBSubnetGroups(&opts) + + if err != nil { + if strings.Contains(err.Error(), "DBSubnetGroupNotFound") { + return nil, nil + } + return nil, fmt.Errorf("Error retrieving DB Subnet Groups: %s", err) + } + + if len(resp.DBSubnetGroups) != 1 || + resp.DBSubnetGroups[0].Name != name { + if err != nil { + return nil, nil + } + } + + v := resp.DBSubnetGroups[0] + + return &v, nil +} + +func resource_aws_db_subnet_group_validation() *config.Validator { + return &config.Validator{ + Required: []string{ + "name", + "description", + "subnet_ids.*", + }, + } +} + +func DBSubnetGroupStateRefreshFunc(name string, conn *rds.Rds) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + v, err := resource_aws_db_subnet_group_retrieve(name, conn) + + if err != nil { + log.Printf("Error on retrieving DB Subnet Group when waiting: %s", err) + return nil, "", err + } + + return v, v.Status, nil + } +} diff --git a/builtin/providers/aws/resource_aws_db_subnet_group_test.go b/builtin/providers/aws/resource_aws_db_subnet_group_test.go new file mode 100644 index 000000000000..32d43a3f36ca --- /dev/null +++ b/builtin/providers/aws/resource_aws_db_subnet_group_test.go @@ -0,0 +1,143 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/goamz/rds" +) + +func TestAccAWSDBSubnetGroup(t *testing.T) { + var v rds.DBSubnetGroup + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSDBSubnetGroupDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSDBSubnetGroupConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSDBSubnetGroupExists("aws_db_subnet_group.foo", &v), + testAccCheckAWSDBSubnetGroupAttributes(&v), + resource.TestCheckResourceAttr( + "aws_db_subnet_group.foo", "name", "subgroup-terraform"), + resource.TestCheckResourceAttr( + "aws_db_subnet_group.foo", "description", "just cuz"), + // TODO check subnet ID contents + resource.TestCheckResourceAttr( + "aws_db_subnet_group.foo", "subnet_ids.#", "2"), + ), + }, + }, + }) +} + +func testAccCheckAWSDBSubnetGroupDestroy(s *terraform.State) error { + conn := testAccProvider.rdsconn + + for _, rs := range s.Resources { + if rs.Type != "aws_db_subnet_group" { + continue + } + + // Try to find the Group + resp, err := conn.DescribeDBSubnetGroups( + &rds.DescribeDBSubnetGroups{ + DBSubnetGroupName: rs.ID, + }) + + if err == nil { + if len(resp.DBSubnetGroups) != 0 && + resp.DBSubnetGroups[0].Name == rs.ID { + return fmt.Errorf("DB Subnet Group still exists") + } + } + + // Verify the error + _, ok := err.(*rds.Error) + if !ok { + return err + } + } + + return nil +} + +func testAccCheckAWSDBSubnetGroupAttributes(group *rds.DBSubnetGroup) resource.TestCheckFunc { + return func(s *terraform.State) error { + if len(group.SubnetIds) == 0 { + return fmt.Errorf("no subnets: %#v", group.SubnetIds) + } + + if group.Name != "subgroup-terraform" { + return fmt.Errorf("bad name: %#v", group.Name) + } + + if group.Description != "just cuz" { + return fmt.Errorf("bad description: %#v", group.Description) + } + + return nil + } +} + +func testAccCheckAWSDBSubnetGroupExists(n string, v *rds.DBSubnetGroup) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.ID == "" { + return fmt.Errorf("No DB Subnet Group ID is set") + } + + conn := testAccProvider.rdsconn + + opts := rds.DescribeDBSubnetGroups{ + DBSubnetGroupName: rs.ID, + } + + resp, err := conn.DescribeDBSubnetGroups(&opts) + + if err != nil { + return err + } + + if len(resp.DBSubnetGroups) != 1 || + resp.DBSubnetGroups[0].Name != rs.ID { + return fmt.Errorf("DB Subnet Group not found") + } + + *v = resp.DBSubnetGroups[0] + + return nil + } +} + +const testAccAWSDBSubnetGroupConfig = ` +resource "aws_vpc" "foo" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_subnet" "foo" { + cidr_block = "10.0.0.0/24" + vpc_id = "${aws_vpc.foo.id}" + availability_zone = "us-west-2a" +} + +resource "aws_subnet" "bar" { + cidr_block = "10.0.1.0/24" + vpc_id = "${aws_vpc.foo.id}" + availability_zone = "us-west-2b" +} + +resource "aws_db_subnet_group" "foo" { + name = "subgroup-terraform" + description = "just cuz" + subnet_ids = ["${aws_subnet.foo.id}", "${aws_subnet.bar.id}"] +} +` diff --git a/builtin/providers/aws/resources.go b/builtin/providers/aws/resources.go index c43a4ce941bd..93d2b075d894 100644 --- a/builtin/providers/aws/resources.go +++ b/builtin/providers/aws/resources.go @@ -38,6 +38,14 @@ func init() { Refresh: resource_aws_db_security_group_refresh, }, + "aws_db_subnet_group": resource.Resource{ + ConfigValidator: resource_aws_db_subnet_group_validation(), + Create: resource_aws_db_subnet_group_create, + Destroy: resource_aws_db_subnet_group_destroy, + Diff: resource_aws_db_subnet_group_diff, + Refresh: resource_aws_db_subnet_group_refresh, + }, + "aws_elb": resource.Resource{ ConfigValidator: resource_aws_elb_validation(), Create: resource_aws_elb_create, diff --git a/website/source/docs/providers/aws/r/db_instance.html.markdown b/website/source/docs/providers/aws/r/db_instance.html.markdown index 2195a8a15f1a..fac10971481d 100644 --- a/website/source/docs/providers/aws/r/db_instance.html.markdown +++ b/website/source/docs/providers/aws/r/db_instance.html.markdown @@ -29,6 +29,7 @@ resource "aws_db_instance" "default" { The following arguments are supported: * `allocated_storage` - (Required) The allocated storage in gigabytes. +* `db_subnet_group_name` - (Optional) The name of the DB subnet group. * `engine` - (Required) The database engine to use. * `engine_version` - (Required) The engine version to use. * `identifier` - (Required) The name of the RDS instance @@ -59,6 +60,7 @@ The following attributes are exported: * `availability_zone` - The availability zone of the instance * `backup_retention_period` - The backup retention period * `backup_window` - The backup window +* `db_subnet_group_name` - The name of the DB subnet group * `endpoint` - The connection endpoint * `engine` - The database engine * `engine_version` - The database engine version diff --git a/website/source/docs/providers/aws/r/db_subnet_group.html.markdown b/website/source/docs/providers/aws/r/db_subnet_group.html.markdown new file mode 100644 index 000000000000..891029bd7662 --- /dev/null +++ b/website/source/docs/providers/aws/r/db_subnet_group.html.markdown @@ -0,0 +1,34 @@ +--- +layout: "aws" +page_title: "AWS: aws_db_subnet_group" +sidebar_current: "docs-aws-resource-db-subnet-group" +--- + +# aws\_db\_subnet\_group + +Provides an RDS DB subnet group resource. + +## Example Usage + +``` +resource "aws_db_subnet_group" "default" { + name = "default" + description = "RDS default subnet group" + subnet_ids = ["${aws_subnet.foo.id}", "${aws_subnet.bar.id}"] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the DB subnet group. +* `description` - (Required) The description of the DB subnet group. +* `subnet_ids` - (Required) A list of subnet IDs. + +## Attributes Reference + +The following attributes are exported: + +* `name` - The DB subnet group name. +* `description` - The DB subnet group description. diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index c0c913ee2ae1..8485dd75eaa7 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -25,6 +25,10 @@ aws_db_security_group + > + aws_db_subnet_group + + > aws_eip