diff --git a/builtin/providers/aws/config.go b/builtin/providers/aws/config.go index 672af6f07..7e84d3a27 100644 --- a/builtin/providers/aws/config.go +++ b/builtin/providers/aws/config.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/aws-sdk-go/gen/autoscaling" "github.com/hashicorp/aws-sdk-go/gen/ec2" "github.com/hashicorp/aws-sdk-go/gen/elb" + "github.com/hashicorp/aws-sdk-go/gen/iam" "github.com/hashicorp/aws-sdk-go/gen/rds" "github.com/hashicorp/aws-sdk-go/gen/route53" "github.com/hashicorp/aws-sdk-go/gen/s3" @@ -30,6 +31,7 @@ type AWSClient struct { r53conn *route53.Route53 region string rdsconn *rds.RDS + iamconn *iam.IAM } // Client configures and returns a fully initailized AWSClient @@ -70,6 +72,8 @@ func (c *Config) Client() (interface{}, error) { client.r53conn = route53.New(creds, "us-east-1", nil) log.Println("[INFO] Initializing EC2 Connection") client.ec2conn = ec2.New(creds, c.Region, nil) + + client.iamconn = iam.New(creds, c.Region, nil) } if len(errs) > 0 { diff --git a/builtin/providers/aws/resource_aws_db_instance.go b/builtin/providers/aws/resource_aws_db_instance.go index e99744a0f..b690c9a1a 100644 --- a/builtin/providers/aws/resource_aws_db_instance.go +++ b/builtin/providers/aws/resource_aws_db_instance.go @@ -6,6 +6,7 @@ import ( "time" "github.com/hashicorp/aws-sdk-go/aws" + "github.com/hashicorp/aws-sdk-go/gen/iam" "github.com/hashicorp/aws-sdk-go/gen/rds" "github.com/hashicorp/terraform/helper/hashcode" @@ -17,6 +18,7 @@ func resourceAwsDbInstance() *schema.Resource { return &schema.Resource{ Create: resourceAwsDbInstanceCreate, Read: resourceAwsDbInstanceRead, + Update: resourceAwsDbInstanceUpdate, Delete: resourceAwsDbInstanceDelete, Schema: map[string]*schema.Schema{ @@ -138,6 +140,7 @@ func resourceAwsDbInstance() *schema.Resource { "vpc_security_group_ids": &schema.Schema{ Type: schema.TypeSet, Optional: true, + Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, Set: func(v interface{}) int { return hashcode.String(v.(string)) @@ -162,6 +165,7 @@ func resourceAwsDbInstance() *schema.Resource { Type: schema.TypeString, Optional: true, ForceNew: true, + Computed: true, }, "parameter_group_name": &schema.Schema{ @@ -185,12 +189,14 @@ func resourceAwsDbInstance() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "tags": tagsSchema(), }, } } func resourceAwsDbInstanceCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).rdsconn + tags := tagsFromMapRDS(d.Get("tags").(map[string]interface{})) opts := rds.CreateDBInstanceMessage{ AllocatedStorage: aws.Integer(d.Get("allocated_storage").(int)), DBInstanceClass: aws.String(d.Get("instance_class").(string)), @@ -201,6 +207,7 @@ func resourceAwsDbInstanceCreate(d *schema.ResourceData, meta interface{}) error Engine: aws.String(d.Get("engine").(string)), EngineVersion: aws.String(d.Get("engine_version").(string)), StorageEncrypted: aws.Boolean(d.Get("storage_encrypted").(bool)), + Tags: tags, } if attr, ok := d.GetOk("storage_type"); ok { @@ -328,6 +335,28 @@ func resourceAwsDbInstanceRead(d *schema.ResourceData, meta interface{}) error { d.Set("status", *v.DBInstanceStatus) d.Set("storage_encrypted", *v.StorageEncrypted) + // list tags for resource + // set tags + conn := meta.(*AWSClient).rdsconn + arn, err := buildRDSARN(d, meta) + if err != nil { + log.Printf("[DEBUG] Error building ARN for DB Instance, not setting Tags for DB %s", *v.DBName) + } else { + resp, err := conn.ListTagsForResource(&rds.ListTagsForResourceMessage{ + ResourceName: aws.String(arn), + }) + + if err != nil { + log.Printf("[DEBUG] Error retreiving tags for ARN: %s", arn) + } + + var dt []rds.Tag + if len(resp.TagList) > 0 { + dt = resp.TagList + } + d.Set("tags", tagsToMapRDS(dt)) + } + // Create an empty schema.Set to hold all vpc security group ids ids := &schema.Set{ F: func(v interface{}) int { @@ -390,6 +419,21 @@ func resourceAwsDbInstanceDelete(d *schema.ResourceData, meta interface{}) error return nil } +func resourceAwsDbInstanceUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).rdsconn + + d.Partial(true) + if arn, err := buildRDSARN(d, meta); err == nil { + if err := setTagsRDS(conn, d, arn); err != nil { + return err + } else { + d.SetPartial("tags") + } + } + d.Partial(false) + return resourceAwsDbInstanceRead(d, meta) +} + func resourceAwsBbInstanceRetrieve( d *schema.ResourceData, meta interface{}) (*rds.DBInstance, error) { conn := meta.(*AWSClient).rdsconn @@ -439,3 +483,16 @@ func resourceAwsDbInstanceStateRefreshFunc( return v, *v.DBInstanceStatus, nil } } + +func buildRDSARN(d *schema.ResourceData, meta interface{}) (string, error) { + iamconn := meta.(*AWSClient).iamconn + region := meta.(*AWSClient).region + // An zero value GetUserRequest{} defers to the currently logged in user + resp, err := iamconn.GetUser(&iam.GetUserRequest{}) + if err != nil { + return "", err + } + user := resp.User + arn := fmt.Sprintf("arn:aws:rds:%s:%s:db:%s", region, *user.UserID, d.Id()) + return arn, nil +} diff --git a/builtin/providers/aws/tagsRDS.go b/builtin/providers/aws/tagsRDS.go new file mode 100644 index 000000000..8eb592427 --- /dev/null +++ b/builtin/providers/aws/tagsRDS.go @@ -0,0 +1,95 @@ +package aws + +import ( + "log" + + "github.com/hashicorp/aws-sdk-go/aws" + "github.com/hashicorp/aws-sdk-go/gen/rds" + "github.com/hashicorp/terraform/helper/schema" +) + +// setTags is a helper to set the tags for a resource. It expects the +// tags field to be named "tags" +func setTagsRDS(conn *rds.RDS, d *schema.ResourceData, arn string) error { + if d.HasChange("tags") { + oraw, nraw := d.GetChange("tags") + o := oraw.(map[string]interface{}) + n := nraw.(map[string]interface{}) + create, remove := diffTagsRDS(tagsFromMapRDS(o), tagsFromMapRDS(n)) + + // Set tags + if len(remove) > 0 { + log.Printf("[DEBUG] Removing tags: %#v", remove) + k := make([]string, len(remove), len(remove)) + for i, t := range remove { + k[i] = *t.Key + } + + err := conn.RemoveTagsFromResource(&rds.RemoveTagsFromResourceMessage{ + ResourceName: aws.String(arn), + TagKeys: k, + }) + if err != nil { + return err + } + } + if len(create) > 0 { + log.Printf("[DEBUG] Creating tags: %#v", create) + err := conn.AddTagsToResource(&rds.AddTagsToResourceMessage{ + ResourceName: aws.String(arn), + Tags: create, + }) + if err != nil { + return err + } + } + } + + return nil +} + +// diffTags takes our tags locally and the ones remotely and returns +// the set of tags that must be created, and the set of tags that must +// be destroyed. +func diffTagsRDS(oldTags, newTags []rds.Tag) ([]rds.Tag, []rds.Tag) { + // First, we're creating everything we have + create := make(map[string]interface{}) + for _, t := range newTags { + create[*t.Key] = *t.Value + } + + // Build the list of what to remove + var remove []rds.Tag + for _, t := range oldTags { + old, ok := create[*t.Key] + if !ok || old != *t.Value { + // Delete it! + remove = append(remove, t) + } + } + + return tagsFromMapRDS(create), remove +} + +// tagsFromMap returns the tags for the given map of data. +func tagsFromMapRDS(m map[string]interface{}) []rds.Tag { + result := make([]rds.Tag, 0, len(m)) + for k, v := range m { + result = append(result, rds.Tag{ + Key: aws.String(k), + Value: aws.String(v.(string)), + }) + } + + return result +} + +// tagsToMap turns the list of tags into a map. +func tagsToMapRDS(ts []rds.Tag) map[string]string { + result := make(map[string]string) + for _, t := range ts { + result[*t.Key] = *t.Value + } + + return result +} diff --git a/builtin/providers/aws/tagsRDS_test.go b/builtin/providers/aws/tagsRDS_test.go new file mode 100644 index 000000000..1d9da8357 --- /dev/null +++ b/builtin/providers/aws/tagsRDS_test.go @@ -0,0 +1,85 @@ +package aws + +import ( + "fmt" + "reflect" + "testing" + + "github.com/hashicorp/aws-sdk-go/gen/rds" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestDiffRDSTags(t *testing.T) { + cases := []struct { + Old, New map[string]interface{} + Create, Remove map[string]string + }{ + // Basic add/remove + { + Old: map[string]interface{}{ + "foo": "bar", + }, + New: map[string]interface{}{ + "bar": "baz", + }, + Create: map[string]string{ + "bar": "baz", + }, + Remove: map[string]string{ + "foo": "bar", + }, + }, + + // Modify + { + Old: map[string]interface{}{ + "foo": "bar", + }, + New: map[string]interface{}{ + "foo": "baz", + }, + Create: map[string]string{ + "foo": "baz", + }, + Remove: map[string]string{ + "foo": "bar", + }, + }, + } + + for i, tc := range cases { + c, r := diffTagsRDS(tagsFromMapRDS(tc.Old), tagsFromMapRDS(tc.New)) + cm := tagsToMapRDS(c) + rm := tagsToMapRDS(r) + if !reflect.DeepEqual(cm, tc.Create) { + t.Fatalf("%d: bad create: %#v", i, cm) + } + if !reflect.DeepEqual(rm, tc.Remove) { + t.Fatalf("%d: bad remove: %#v", i, rm) + } + } +} + +// testAccCheckTags can be used to check the tags on a resource. +func testAccCheckRDSTags( + ts *[]rds.Tag, key string, value string) resource.TestCheckFunc { + return func(s *terraform.State) error { + m := tagsToMapRDS(*ts) + v, ok := m[key] + if value != "" && !ok { + return fmt.Errorf("Missing tag: %s", key) + } else if value == "" && ok { + return fmt.Errorf("Extra tag: %s", key) + } + if value == "" { + return nil + } + + if v != value { + return fmt.Errorf("%s: bad value: %s", key, v) + } + + return nil + } +}