diff --git a/builtin/providers/aws/resource_aws_db_instance.go b/builtin/providers/aws/resource_aws_db_instance.go index 98c863c46..763aebc37 100644 --- a/builtin/providers/aws/resource_aws_db_instance.go +++ b/builtin/providers/aws/resource_aws_db_instance.go @@ -83,6 +83,11 @@ func resource_aws_db_instance_create( rs.Attributes, "vpc_security_group_ids").([]interface{})) } + if _, ok := rs.Attributes["security_group_names.#"]; ok { + opts.DBSecurityGroupNames = expandStringList(flatmap.Expand( + rs.Attributes, "security_group_names").([]interface{})) + } + opts.DBInstanceIdentifier = rs.Attributes["identifier"] opts.DBName = rs.Attributes["name"] opts.MasterUsername = rs.Attributes["username"] @@ -206,6 +211,7 @@ func resource_aws_db_instance_diff( "publicly_accessible": diff.AttrTypeCreate, "username": diff.AttrTypeCreate, "vpc_security_group_ids": diff.AttrTypeCreate, + "security_group_names": diff.AttrTypeCreate, }, ComputedAttrs: []string{ @@ -249,6 +255,9 @@ func resource_aws_db_instance_update_state( // Flatten our group values toFlatten := make(map[string]interface{}) + if len(v.DBSecurityGroupNames) > 0 && v.DBSecurityGroupNames[0] != "" { + toFlatten["security_group_names"] = v.DBSecurityGroupNames + } if len(v.VpcSecurityGroupIds) > 0 && v.VpcSecurityGroupIds[0] != "" { toFlatten["vpc_security_group_ids"] = v.VpcSecurityGroupIds } @@ -307,8 +316,9 @@ func resource_aws_db_instance_validation() *config.Validator { "multi_az", "port", "publicly_accessible", - "vpc_security_group_ids", + "vpc_security_group_ids.*", "skip_final_snapshot", + "security_group_names.*", }, } } diff --git a/builtin/providers/aws/resource_aws_db_instance_test.go b/builtin/providers/aws/resource_aws_db_instance_test.go index 733f80397..ebd8781e8 100644 --- a/builtin/providers/aws/resource_aws_db_instance_test.go +++ b/builtin/providers/aws/resource_aws_db_instance_test.go @@ -108,6 +108,16 @@ func testAccCheckAWSDBInstanceExists(n string, v *rds.DBInstance) resource.TestC } const testAccAWSDBInstanceConfig = ` + +resource "aws_db_security_group" "bar" { + name = "secgrouplol" + description = "just cuz" + + ingress { + cidr = "10.0.0.1/24" + } +} + resource "aws_db_instance" "bar" { identifier = "foobarbaz-test-terraform-2" @@ -120,5 +130,7 @@ resource "aws_db_instance" "bar" { username = "foo" skip_final_snapshot = true + + security_group_names = [${aws_db_security_group.bar.name}] } ` diff --git a/builtin/providers/aws/resource_aws_db_security_group.go b/builtin/providers/aws/resource_aws_db_security_group.go new file mode 100644 index 000000000..c00fdd2df --- /dev/null +++ b/builtin/providers/aws/resource_aws_db_security_group.go @@ -0,0 +1,281 @@ +package aws + +import ( + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform/flatmap" + "github.com/hashicorp/terraform/helper/config" + "github.com/hashicorp/terraform/helper/diff" + "github.com/hashicorp/terraform/helper/multierror" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/goamz/rds" +) + +func resource_aws_db_security_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 + var errs []error + + opts := rds.CreateDBSecurityGroup{ + DBSecurityGroupName: rs.Attributes["name"], + DBSecurityGroupDescription: rs.Attributes["description"], + } + + log.Printf("[DEBUG] DB Security Group create configuration: %#v", opts) + _, err = conn.CreateDBSecurityGroup(&opts) + if err != nil { + return nil, fmt.Errorf("Error creating DB Security Group: %s", err) + } + + rs.ID = rs.Attributes["name"] + + log.Printf("[INFO] DB Security Group ID: %s", rs.ID) + + v, err := resource_aws_db_security_group_retrieve(rs.ID, conn) + if err != nil { + return rs, err + } + log.Printf("%#v", rs.Attributes) + + if _, ok := rs.Attributes["ingress.#"]; ok { + ingresses := flatmap.Expand( + rs.Attributes, "ingress").([]interface{}) + + for _, ing := range ingresses { + err = authorize_ingress_rule(ing, v.Name, conn) + + if err != nil { + errs = append(errs, err) + } + } + + if len(errs) > 0 { + return rs, &multierror.Error{Errors: errs} + } + } + + log.Println( + "[INFO] Waiting for Ingress Authorizations to be authorized") + + stateConf := &resource.StateChangeConf{ + Pending: []string{"authorizing"}, + Target: "authorized", + Refresh: DBSecurityGroupStateRefreshFunc(rs.ID, conn), + Timeout: 10 * time.Minute, + } + + // Wait, catching any errors + _, err = stateConf.WaitForState() + if err != nil { + return rs, err + } + + return resource_aws_db_security_group_update_state(rs, v) +} + +func resource_aws_db_security_group_update( + s *terraform.ResourceState, + d *terraform.ResourceDiff, + meta interface{}) (*terraform.ResourceState, error) { + + panic("Cannot update DB") + + return nil, nil +} + +func resource_aws_db_security_group_destroy( + s *terraform.ResourceState, + meta interface{}) error { + p := meta.(*ResourceProvider) + conn := p.rdsconn + + log.Printf("[DEBUG] DB Security Group destroy: %v", s.ID) + + opts := rds.DeleteDBSecurityGroup{DBSecurityGroupName: s.ID} + + log.Printf("[DEBUG] DB Security Group destroy configuration: %v", opts) + _, err := conn.DeleteDBSecurityGroup(&opts) + + if err != nil { + newerr, ok := err.(*rds.Error) + if ok && newerr.Code == "InvalidDBSecurityGroup.NotFound" { + return nil + } + return err + } + + return nil +} + +func resource_aws_db_security_group_refresh( + s *terraform.ResourceState, + meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + conn := p.rdsconn + + v, err := resource_aws_db_security_group_retrieve(s.ID, conn) + + if err != nil { + return s, err + } + + return resource_aws_db_security_group_update_state(s, v) +} + +func resource_aws_db_security_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, + "ingress": diff.AttrTypeCreate, + }, + + ComputedAttrs: []string{ + "ingress_cidr", + "ingress_security_groups", + }, + } + + return b.Diff(s, c) +} + +func resource_aws_db_security_group_update_state( + s *terraform.ResourceState, + v *rds.DBSecurityGroup) (*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.EC2SecurityGroupOwnerIds) > 0 && v.EC2SecurityGroupOwnerIds[0] != "" { + toFlatten["ingress_security_groups"] = v.EC2SecurityGroupOwnerIds + } + + if len(v.CidrIps) > 0 && v.CidrIps[0] != "" { + toFlatten["ingress_cidr"] = v.CidrIps + } + + for k, v := range flatmap.Flatten(toFlatten) { + s.Attributes[k] = v + } + + return s, nil +} + +func resource_aws_db_security_group_retrieve(id string, conn *rds.Rds) (*rds.DBSecurityGroup, error) { + opts := rds.DescribeDBSecurityGroups{ + DBSecurityGroupName: id, + } + + log.Printf("[DEBUG] DB Security Group describe configuration: %#v", opts) + + resp, err := conn.DescribeDBSecurityGroups(&opts) + + if err != nil { + return nil, fmt.Errorf("Error retrieving DB Security Groups: %s", err) + } + + log.Printf("resp: %#v", resp.DBSecurityGroups) + + if len(resp.DBSecurityGroups) != 1 || + resp.DBSecurityGroups[0].Name != id { + if err != nil { + return nil, fmt.Errorf("Unable to find DB Security Group: %#v", resp.DBSecurityGroups) + } + } + + v := resp.DBSecurityGroups[0] + + return &v, nil +} + +// Authorizes the ingress rule on the db security group +func authorize_ingress_rule(ingress interface{}, dbSecurityGroupName string, conn *rds.Rds) error { + ing := ingress.(map[string]interface{}) + + opts := rds.AuthorizeDBSecurityGroupIngress{ + DBSecurityGroupName: dbSecurityGroupName, + } + + if attr, ok := ing["cidr"].(string); ok && attr != "" { + opts.Cidr = attr + } + + if attr, ok := ing["security_group_name"].(string); ok && attr != "" { + opts.EC2SecurityGroupName = attr + } + + if attr, ok := ing["security_group_id"].(string); ok && attr != "" { + opts.EC2SecurityGroupId = attr + } + + if attr, ok := ing["security_group_owner_id"].(string); ok && attr != "" { + opts.EC2SecurityGroupOwnerId = attr + } + + log.Printf("[DEBUG] Authorize ingress rule configuration: %#v", opts) + + _, err := conn.AuthorizeDBSecurityGroupIngress(&opts) + + if err != nil { + return fmt.Errorf("Error authorizing security group ingress: %s", err) + } + + return nil +} + +func resource_aws_db_security_group_validation() *config.Validator { + return &config.Validator{ + Required: []string{ + "name", + "description", + }, + Optional: []string{ + "ingress.*", + "ingress.*.cidr", + "ingress.*.security_group_name", + "ingress.*.security_group_id", + "ingress.*.security_group_owner_id", + }, + } +} + +func DBSecurityGroupStateRefreshFunc(id string, conn *rds.Rds) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + v, err := resource_aws_db_security_group_retrieve(id, conn) + + if err != nil { + log.Printf("Error on retrieving DB Security Group when waiting: %s", err) + return nil, "", err + } + + statuses := append(v.EC2SecurityGroupStatuses, v.CidrStatuses...) + + for _, stat := range statuses { + // Not done + if stat != "authorized" { + return nil, "authorizing", nil + } + } + + return v, "authorized", nil + } +} diff --git a/builtin/providers/aws/resource_aws_db_security_group_test.go b/builtin/providers/aws/resource_aws_db_security_group_test.go new file mode 100644 index 000000000..e26e0f479 --- /dev/null +++ b/builtin/providers/aws/resource_aws_db_security_group_test.go @@ -0,0 +1,142 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/goamz/rds" +) + +func TestAccAWSDBSecurityGroup(t *testing.T) { + var v rds.DBSecurityGroup + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSDBSecurityGroupDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSDBSecurityGroupConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSDBSecurityGroupExists("aws_db_security_group.bar", &v), + testAccCheckAWSDBSecurityGroupAttributes(&v), + resource.TestCheckResourceAttr( + "aws_db_security_group.bar", "name", "secgroup-terraform"), + resource.TestCheckResourceAttr( + "aws_db_security_group.bar", "description", "just cuz"), + resource.TestCheckResourceAttr( + "aws_db_security_group.bar", "ingress.0.cidr", "10.0.0.1/24"), + resource.TestCheckResourceAttr( + "aws_db_security_group.bar", "ingress.#", "1"), + ), + }, + }, + }) +} + +func testAccCheckAWSDBSecurityGroupDestroy(s *terraform.State) error { + conn := testAccProvider.rdsconn + + for _, rs := range s.Resources { + if rs.Type != "aws_db_security_group" { + continue + } + + // Try to find the Group + resp, err := conn.DescribeDBSecurityGroups( + &rds.DescribeDBSecurityGroups{ + DBSecurityGroupName: rs.ID, + }) + + if err == nil { + if len(resp.DBSecurityGroups) != 0 && + resp.DBSecurityGroups[0].Name == rs.ID { + return fmt.Errorf("DB Security Group still exists") + } + } + + // Verify the error + newerr, ok := err.(*rds.Error) + if !ok { + return err + } + if newerr.Code != "InvalidDBSecurityGroup.NotFound" { + return err + } + } + + return nil +} + +func testAccCheckAWSDBSecurityGroupAttributes(group *rds.DBSecurityGroup) resource.TestCheckFunc { + return func(s *terraform.State) error { + if len(group.CidrIps) == 0 { + return fmt.Errorf("no cidr: %#v", group.CidrIps) + } + + if group.CidrIps[0] != "10.0.0.1/24" { + return fmt.Errorf("bad cidr: %#v", group.CidrIps) + } + + if group.CidrStatuses[0] != "authorized" { + return fmt.Errorf("bad status: %#v", group.CidrStatuses) + } + + if group.Name != "secgroup-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 testAccCheckAWSDBSecurityGroupExists(n string, v *rds.DBSecurityGroup) 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 Security Group ID is set") + } + + conn := testAccProvider.rdsconn + + opts := rds.DescribeDBSecurityGroups{ + DBSecurityGroupName: rs.ID, + } + + resp, err := conn.DescribeDBSecurityGroups(&opts) + + if err != nil { + return err + } + + if len(resp.DBSecurityGroups) != 1 || + resp.DBSecurityGroups[0].Name != rs.ID { + return fmt.Errorf("DB Security Group not found") + } + + *v = resp.DBSecurityGroups[0] + + return nil + } +} + +const testAccAWSDBSecurityGroupConfig = ` +resource "aws_db_security_group" "bar" { + name = "secgroup-terraform" + description = "just cuz" + + ingress { + cidr = "10.0.0.1/24" + } +} +` diff --git a/builtin/providers/aws/resources.go b/builtin/providers/aws/resources.go index 6e2c6c220..6e80f8392 100644 --- a/builtin/providers/aws/resources.go +++ b/builtin/providers/aws/resources.go @@ -30,6 +30,15 @@ func init() { Update: resource_aws_db_instance_update, }, + "aws_db_security_group": resource.Resource{ + ConfigValidator: resource_aws_db_security_group_validation(), + Create: resource_aws_db_security_group_create, + Destroy: resource_aws_db_security_group_destroy, + Diff: resource_aws_db_security_group_diff, + Refresh: resource_aws_db_security_group_refresh, + Update: resource_aws_db_security_group_update, + }, + "aws_elb": resource.Resource{ ConfigValidator: resource_aws_elb_validation(), Create: resource_aws_elb_create,