diff --git a/builtin/providers/aws/resource_aws_db_instance.go b/builtin/providers/aws/resource_aws_db_instance.go new file mode 100644 index 000000000..1a15c4ca1 --- /dev/null +++ b/builtin/providers/aws/resource_aws_db_instance.go @@ -0,0 +1,325 @@ +package aws + +import ( + "fmt" + "log" + "strconv" + "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_instance_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 attr string + + opts := rds.CreateDBInstance{} + + if attr = rs.Attributes["allocated_storage"]; attr != "" { + opts.AllocatedStorage, err = strconv.Atoi(attr) + opts.SetAllocatedStorage = true + } + + if attr = rs.Attributes["backup_retention_period"]; attr != "" { + opts.BackupRetentionPeriod, err = strconv.Atoi(attr) + opts.SetBackupRetentionPeriod = true + } + + if attr = rs.Attributes["iops"]; attr != "" { + opts.Iops, err = strconv.Atoi(attr) + opts.SetIops = true + } + + if attr = rs.Attributes["port"]; attr != "" { + opts.Port, err = strconv.Atoi(attr) + opts.SetPort = true + } + + if attr = rs.Attributes["availability_zone"]; attr != "" { + opts.AvailabilityZone = attr + } + + if attr = rs.Attributes["instance_class"]; attr != "" { + opts.DBInstanceClass = attr + } + + if attr = rs.Attributes["maintenance_window"]; attr != "" { + opts.PreferredMaintenanceWindow = attr + } + + if attr = rs.Attributes["backup_window"]; attr != "" { + opts.PreferredBackupWindow = attr + } + + if attr = rs.Attributes["multi_az"]; attr == "true" { + opts.MultiAZ = true + } + + if attr = rs.Attributes["publicly_accessible"]; attr == "true" { + opts.PubliclyAccessible = true + } + + if err != nil { + return nil, fmt.Errorf("Error parsing configuration: %s", err) + } + + if _, ok := rs.Attributes["vpc_security_group_ids.#"]; ok { + opts.VpcSecurityGroupIds = expandStringList(flatmap.Expand( + rs.Attributes, "vpc_security_group_ids").([]interface{})) + } + + opts.DBInstanceIdentifier = rs.Attributes["identifier"] + opts.DBName = rs.Attributes["name"] + opts.MasterUsername = rs.Attributes["username"] + opts.MasterUserPassword = rs.Attributes["password"] + opts.EngineVersion = rs.Attributes["engine_version"] + opts.EngineVersion = rs.Attributes["engine"] + + // Don't keep the password around in the state + delete(rs.Attributes, "password") + + log.Printf("[DEBUG] DB Instance create configuration: %#v", opts) + _, err = conn.CreateDBInstance(&opts) + if err != nil { + return nil, fmt.Errorf("Error creating DB Instance: %s", err) + } + + rs.ID = rs.Attributes["identifier"] + + log.Printf("[INFO] DB Instance ID: %s", rs.ID) + + log.Println( + "[INFO] Waiting for DB Instance to be available") + + stateConf := &resource.StateChangeConf{ + Pending: []string{"creating", "backing-up"}, + Target: "available", + Refresh: DBInstanceStateRefreshFunc(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_instance_retrieve(rs.ID, conn) + if err != nil { + return rs, err + } + + return resource_aws_db_instance_update_state(rs, v) +} + +func resource_aws_db_instance_update( + s *terraform.ResourceState, + d *terraform.ResourceDiff, + meta interface{}) (*terraform.ResourceState, error) { + + panic("Cannot update DB") + + return nil, nil +} + +func resource_aws_db_instance_destroy( + s *terraform.ResourceState, + meta interface{}) error { + p := meta.(*ResourceProvider) + conn := p.rdsconn + + log.Printf("[DEBUG] DB Instance destroy: %v", s.ID) + + opts := rds.DeleteDBInstance{DBInstanceIdentifier: s.ID} + + if s.Attributes["skip_final_snapshot"] == "true" { + opts.SkipFinalSnapshot = true + } + + log.Printf("[DEBUG] DB Instance destroy configuration: %v", opts) + _, err := conn.DeleteDBInstance(&opts) + + if err != nil { + newerr, ok := err.(*rds.Error) + if ok && newerr.Code == "InvalidDBInstance.NotFound" { + return nil + } + return err + } + + return nil +} + +func resource_aws_db_instance_refresh( + s *terraform.ResourceState, + meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + conn := p.rdsconn + + v, err := resource_aws_db_instance_retrieve(s.ID, conn) + + if err != nil { + return s, err + } + + return resource_aws_db_instance_update_state(s, v) +} + +func resource_aws_db_instance_diff( + s *terraform.ResourceState, + c *terraform.ResourceConfig, + meta interface{}) (*terraform.ResourceDiff, error) { + + b := &diff.ResourceBuilder{ + Attrs: map[string]diff.AttrType{ + "allocated_storage": diff.AttrTypeCreate, + "availability_zone": diff.AttrTypeCreate, + "backup_retention_period": diff.AttrTypeCreate, + "backup_window": diff.AttrTypeCreate, + "engine": diff.AttrTypeCreate, + "engine_version": diff.AttrTypeCreate, + "identifier": diff.AttrTypeCreate, + "instance_class": diff.AttrTypeCreate, + "iops": diff.AttrTypeCreate, + "maintenance_window": diff.AttrTypeCreate, + "multi_az": diff.AttrTypeCreate, + "name": diff.AttrTypeCreate, + "password": diff.AttrTypeCreate, + "port": diff.AttrTypeCreate, + "publicly_accessible": diff.AttrTypeCreate, + "username": diff.AttrTypeCreate, + "vpc_security_group_ids": diff.AttrTypeCreate, + }, + + ComputedAttrs: []string{ + "address", + "availability_zone", + "backup_retention_period", + "backup_window", + "engine_version", + "maintenance_window", + "endpoint", + "status", + "multi_az", + "port", + "address", + }, + } + + return b.Diff(s, c) +} + +func resource_aws_db_instance_update_state( + s *terraform.ResourceState, + v *rds.DBInstance) (*terraform.ResourceState, error) { + + s.Attributes["address"] = v.Address + s.Attributes["allocated_storage"] = strconv.Itoa(v.AllocatedStorage) + s.Attributes["availability_zone"] = v.AvailabilityZone + s.Attributes["backup_retention_period"] = strconv.Itoa(v.BackupRetentionPeriod) + s.Attributes["backup_window"] = v.PreferredBackupWindow + s.Attributes["endpoint"] = fmt.Sprintf("%s:%s", s.Attributes["address"], s.Attributes["port"]) + s.Attributes["engine"] = v.Engine + s.Attributes["engine_version"] = v.EngineVersion + s.Attributes["instance_class"] = v.DBInstanceClass + s.Attributes["maintenance_window"] = v.PreferredMaintenanceWindow + s.Attributes["multi_az"] = strconv.FormatBool(v.MultiAZ) + s.Attributes["name"] = v.DBName + s.Attributes["port"] = strconv.Itoa(v.Port) + s.Attributes["status"] = v.DBInstanceStatus + s.Attributes["username"] = v.MasterUsername + + // Flatten our group values + toFlatten := make(map[string]interface{}) + + if len(v.VpcSecurityGroupIds) > 0 && v.VpcSecurityGroupIds[0] != "" { + toFlatten["vpc_security_group_ids"] = v.VpcSecurityGroupIds + } + for k, v := range flatmap.Flatten(toFlatten) { + s.Attributes[k] = v + } + + return s, nil +} + +func resource_aws_db_instance_retrieve(id string, conn *rds.Rds) (*rds.DBInstance, error) { + opts := rds.DescribeDBInstances{ + DBInstanceIdentifier: id, + } + + log.Printf("[DEBUG] DB Instance describe configuration: %#v", opts) + + resp, err := conn.DescribeDBInstances(&opts) + + if err != nil { + return nil, fmt.Errorf("Error retrieving DB Instances: %s", err) + } + + log.Printf("resp: %#v", resp.DBInstances) + + if len(resp.DBInstances) != 1 || + resp.DBInstances[0].DBInstanceIdentifier != id { + if err != nil { + return nil, fmt.Errorf("Unable to find DB Instance: %#v", resp.DBInstances) + } + } + + v := resp.DBInstances[0] + + return &v, nil +} + +func resource_aws_db_instance_validation() *config.Validator { + return &config.Validator{ + Required: []string{ + "allocated_storage", + "engine", + "engine_version", + "identifier", + "instance_class", + "name", + "password", + "username", + }, + Optional: []string{ + "availability_zone", + "backup_retention_period", + "backup_window", + "iops", + "maintenance_window", + "multi_az", + "port", + "publicly_accessible", + "vpc_security_group_ids", + "skip_final_snapshot", + }, + } +} + +func DBInstanceStateRefreshFunc(id string, conn *rds.Rds) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + v, err := resource_aws_db_instance_retrieve(id, conn) + + if err != nil { + log.Printf("Error on retrieving DB Instance when waiting: %s", err) + return nil, "", err + } + + return v, v.DBInstanceStatus, nil + } +} diff --git a/builtin/providers/aws/resource_aws_db_instance_test.go b/builtin/providers/aws/resource_aws_db_instance_test.go new file mode 100644 index 000000000..733f80397 --- /dev/null +++ b/builtin/providers/aws/resource_aws_db_instance_test.go @@ -0,0 +1,124 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/goamz/rds" +) + +func TestAccAWSDBInstance(t *testing.T) { + var v rds.DBInstance + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSDBInstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSDBInstanceConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSDBInstanceExists("aws_db_instance.bar", &v), + testAccCheckAWSDBInstanceAttributes(&v), + resource.TestCheckResourceAttr( + "aws_db_instance.bar", "instance_identifier", "some_name"), + ), + }, + }, + }) +} + +func testAccCheckAWSDBInstanceDestroy(s *terraform.State) error { + conn := testAccProvider.rdsconn + + for _, rs := range s.Resources { + if rs.Type != "aws_db_instance" { + continue + } + + // Try to find the Group + resp, err := conn.DescribeDBInstances( + &rds.DescribeDBInstances{ + DBInstanceIdentifier: rs.ID, + }) + + if err == nil { + if len(resp.DBInstances) != 0 && + resp.DBInstances[0].DBInstanceIdentifier == rs.ID { + return fmt.Errorf("DB Instance still exists") + } + } + + // Verify the error + newerr, ok := err.(*rds.Error) + if !ok { + return err + } + if newerr.Code != "InvalidDBInstance.NotFound" { + return err + } + } + + return nil +} + +func testAccCheckAWSDBInstanceAttributes(group *rds.DBInstance) resource.TestCheckFunc { + return func(s *terraform.State) error { + + // check attrs + + return nil + } +} + +func testAccCheckAWSDBInstanceExists(n string, v *rds.DBInstance) 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 Instance ID is set") + } + + conn := testAccProvider.rdsconn + + opts := rds.DescribeDBInstances{ + DBInstanceIdentifier: rs.ID, + } + + resp, err := conn.DescribeDBInstances(&opts) + + if err != nil { + return err + } + + if len(resp.DBInstances) != 1 || + resp.DBInstances[0].DBInstanceIdentifier != rs.ID { + return fmt.Errorf("DB Instance not found") + } + + *v = resp.DBInstances[0] + + return nil + } +} + +const testAccAWSDBInstanceConfig = ` +resource "aws_db_instance" "bar" { + identifier = "foobarbaz-test-terraform-2" + + allocated_storage = 10 + engine = "mysql" + engine_version = "5.6.13" + instance_class = "db.t1.micro" + name = "baz" + password = "barbarbarbar" + username = "foo" + + skip_final_snapshot = true +} +` diff --git a/builtin/providers/aws/resource_provider.go b/builtin/providers/aws/resource_provider.go index 2a640a2a7..47b68f55a 100644 --- a/builtin/providers/aws/resource_provider.go +++ b/builtin/providers/aws/resource_provider.go @@ -9,6 +9,7 @@ import ( "github.com/mitchellh/goamz/autoscaling" "github.com/mitchellh/goamz/ec2" "github.com/mitchellh/goamz/elb" + "github.com/mitchellh/goamz/rds" "github.com/mitchellh/goamz/s3" ) @@ -19,6 +20,7 @@ type ResourceProvider struct { elbconn *elb.ELB autoscalingconn *autoscaling.AutoScaling s3conn *s3.S3 + rdsconn *rds.Rds } func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []error) { @@ -67,6 +69,8 @@ func (p *ResourceProvider) Configure(c *terraform.ResourceConfig) error { p.autoscalingconn = autoscaling.New(auth, region) log.Println("[INFO] Initializing S3 connection") p.s3conn = s3.New(auth, region) + log.Println("[INFO] Initializing RDS connection") + p.rdsconn = rds.New(auth, region) } if len(errs) > 0 { diff --git a/builtin/providers/aws/resources.go b/builtin/providers/aws/resources.go index 1a442b669..9e0ba2137 100644 --- a/builtin/providers/aws/resources.go +++ b/builtin/providers/aws/resources.go @@ -21,6 +21,15 @@ func init() { Update: resource_aws_autoscaling_group_update, }, + "aws_db_instance": resource.Resource{ + ConfigValidator: resource_aws_db_instance_validation(), + Create: resource_aws_db_instance_create, + Destroy: resource_aws_db_instance_destroy, + Diff: resource_aws_db_instance_diff, + Refresh: resource_aws_db_instance_refresh, + Update: resource_aws_db_instance_update, + }, + "aws_elb": resource.Resource{ ConfigValidator: resource_aws_elb_validation(), Create: resource_aws_elb_create,