From 6f3ce6bf3c28279efea49f209d765f8a75d214ad Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Tue, 12 May 2015 17:05:03 -0500 Subject: [PATCH 1/7] WIP export cache nodes Needs to wait for len(cluster.CacheNodes) == cluster.NumCacheNodes, since apparently that takes a bit of time and the initial response always has an empty collection of nodes --- .../aws/resource_aws_elasticache_cluster.go | 56 +++++++++++++++++++ .../resource_aws_elasticache_cluster_test.go | 2 + 2 files changed, 58 insertions(+) diff --git a/builtin/providers/aws/resource_aws_elasticache_cluster.go b/builtin/providers/aws/resource_aws_elasticache_cluster.go index 5037fd01f..bd06c16d5 100644 --- a/builtin/providers/aws/resource_aws_elasticache_cluster.go +++ b/builtin/providers/aws/resource_aws_elasticache_cluster.go @@ -3,6 +3,7 @@ package aws import ( "fmt" "log" + "sort" "time" "github.com/awslabs/aws-sdk-go/aws" @@ -82,6 +83,27 @@ func resourceAwsElasticacheCluster() *schema.Resource { return hashcode.String(v.(string)) }, }, + // Exported Attributes + "cache_nodes": &schema.Schema{ + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "address": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "port": &schema.Schema{ + Type: schema.TypeInt, + Computed: true, + }, + }, + }, + }, }, } } @@ -167,11 +189,45 @@ func resourceAwsElasticacheClusterRead(d *schema.ResourceData, meta interface{}) d.Set("security_group_names", c.CacheSecurityGroups) d.Set("security_group_ids", c.SecurityGroups) d.Set("parameter_group_name", c.CacheParameterGroup) + + if err := setCacheNodeData(d, c); err != nil { + return err + } } return nil } +func setCacheNodeData(d *schema.ResourceData, c *elasticache.CacheCluster) error { + sortedCacheNodes := make([]*elasticache.CacheNode, len(c.CacheNodes)) + copy(sortedCacheNodes, c.CacheNodes) + sort.Sort(byCacheNodeId(sortedCacheNodes)) + + cacheNodeData := make([]map[string]interface{}, 0, len(sortedCacheNodes)) + + for _, node := range sortedCacheNodes { + if node.CacheNodeID == nil || node.Endpoint == nil || node.Endpoint.Address == nil || node.Endpoint.Port == nil { + return fmt.Errorf("Unexpected nil pointer in: %#v", node) + } + cacheNodeData = append(cacheNodeData, map[string]interface{}{ + "id": node.CacheNodeID, + "address": node.Endpoint.Address, + "port": node.Endpoint.Port, + }) + } + + return d.Set("cache_nodes", cacheNodeData) +} + +type byCacheNodeId []*elasticache.CacheNode + +func (b byCacheNodeId) Len() int { return len(b) } +func (b byCacheNodeId) Swap(i, j int) { b[i], b[j] = b[j], b[i] } +func (b byCacheNodeId) Less(i, j int) bool { + return b[i].CacheNodeID != nil && b[j].CacheNodeID != nil && + *b[i].CacheNodeID < *b[j].CacheNodeID +} + func resourceAwsElasticacheClusterDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).elasticacheconn diff --git a/builtin/providers/aws/resource_aws_elasticache_cluster_test.go b/builtin/providers/aws/resource_aws_elasticache_cluster_test.go index 1899e0f24..2fa38b140 100644 --- a/builtin/providers/aws/resource_aws_elasticache_cluster_test.go +++ b/builtin/providers/aws/resource_aws_elasticache_cluster_test.go @@ -23,6 +23,8 @@ func TestAccAWSElasticacheCluster(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckAWSElasticacheSecurityGroupExists("aws_elasticache_security_group.bar"), testAccCheckAWSElasticacheClusterExists("aws_elasticache_cluster.bar"), + resource.TestCheckResourceAttr( + "aws_elasticache_cluster.bar", "cache_nodes.0.id", "001"), ), }, }, From a552db0c8cb51f3840d0e18aec05020dddcb3af9 Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Thu, 14 May 2015 11:10:21 -0500 Subject: [PATCH 2/7] provider/aws: ElastiCache enhancements - request cache node info - read after create, to populate nodes --- .../providers/aws/resource_aws_elasticache_cluster.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/builtin/providers/aws/resource_aws_elasticache_cluster.go b/builtin/providers/aws/resource_aws_elasticache_cluster.go index bd06c16d5..04f0f3364 100644 --- a/builtin/providers/aws/resource_aws_elasticache_cluster.go +++ b/builtin/providers/aws/resource_aws_elasticache_cluster.go @@ -161,13 +161,14 @@ func resourceAwsElasticacheClusterCreate(d *schema.ResourceData, meta interface{ d.SetId(clusterId) - return nil + return resourceAwsElasticacheClusterRead(d, meta) } func resourceAwsElasticacheClusterRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).elasticacheconn req := &elasticache.DescribeCacheClustersInput{ - CacheClusterID: aws.String(d.Id()), + CacheClusterID: aws.String(d.Id()), + ShowCacheNodeInfo: aws.Boolean(true), } res, err := conn.DescribeCacheClusters(req) @@ -210,9 +211,9 @@ func setCacheNodeData(d *schema.ResourceData, c *elasticache.CacheCluster) error return fmt.Errorf("Unexpected nil pointer in: %#v", node) } cacheNodeData = append(cacheNodeData, map[string]interface{}{ - "id": node.CacheNodeID, - "address": node.Endpoint.Address, - "port": node.Endpoint.Port, + "id": *node.CacheNodeID, + "address": *node.Endpoint.Address, + "port": int(*node.Endpoint.Port), }) } From aad0808cc53ef642df1be9a1a788bd9a5233e737 Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Thu, 14 May 2015 11:12:07 -0500 Subject: [PATCH 3/7] make parameter group optional --- .../aws/resource_aws_elasticache_cluster.go | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/builtin/providers/aws/resource_aws_elasticache_cluster.go b/builtin/providers/aws/resource_aws_elasticache_cluster.go index 04f0f3364..db27edba7 100644 --- a/builtin/providers/aws/resource_aws_elasticache_cluster.go +++ b/builtin/providers/aws/resource_aws_elasticache_cluster.go @@ -43,6 +43,7 @@ func resourceAwsElasticacheCluster() *schema.Resource { "parameter_group_name": &schema.Schema{ Type: schema.TypeString, Optional: true, + Computed: true, ForceNew: true, }, "port": &schema.Schema{ @@ -120,7 +121,6 @@ func resourceAwsElasticacheClusterCreate(d *schema.ResourceData, meta interface{ subnetGroupName := d.Get("subnet_group_name").(string) securityNameSet := d.Get("security_group_names").(*schema.Set) securityIdSet := d.Get("security_group_ids").(*schema.Set) - paramGroupName := d.Get("parameter_group_name").(string) // default.memcached1.4 securityNames := expandStringList(securityNameSet.List()) securityIds := expandStringList(securityIdSet.List()) @@ -135,7 +135,11 @@ func resourceAwsElasticacheClusterCreate(d *schema.ResourceData, meta interface{ CacheSubnetGroupName: aws.String(subnetGroupName), CacheSecurityGroupNames: securityNames, SecurityGroupIDs: securityIds, - CacheParameterGroupName: aws.String(paramGroupName), + } + + // parameter groups are optional and can be defaulted by AWS + if v, ok := d.GetOk("parameter_group_name"); ok { + req.CacheParameterGroupName = aws.String(v.(string)) } _, err := conn.CreateCacheCluster(req) @@ -161,14 +165,14 @@ func resourceAwsElasticacheClusterCreate(d *schema.ResourceData, meta interface{ d.SetId(clusterId) - return resourceAwsElasticacheClusterRead(d, meta) + return resourceAwsElasticacheClusterRead(d, meta) } func resourceAwsElasticacheClusterRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).elasticacheconn req := &elasticache.DescribeCacheClustersInput{ - CacheClusterID: aws.String(d.Id()), - ShowCacheNodeInfo: aws.Boolean(true), + CacheClusterID: aws.String(d.Id()), + ShowCacheNodeInfo: aws.Boolean(true), } res, err := conn.DescribeCacheClusters(req) @@ -211,9 +215,9 @@ func setCacheNodeData(d *schema.ResourceData, c *elasticache.CacheCluster) error return fmt.Errorf("Unexpected nil pointer in: %#v", node) } cacheNodeData = append(cacheNodeData, map[string]interface{}{ - "id": *node.CacheNodeID, - "address": *node.Endpoint.Address, - "port": int(*node.Endpoint.Port), + "id": *node.CacheNodeID, + "address": *node.Endpoint.Address, + "port": int(*node.Endpoint.Port), }) } From d8f3783d09f54fa3ce2fb1089b4b62fcc0e87807 Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Thu, 14 May 2015 11:44:24 -0500 Subject: [PATCH 4/7] provider/aws: Add tag support to ElastiCache --- .../aws/resource_aws_elasticache_cluster.go | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/builtin/providers/aws/resource_aws_elasticache_cluster.go b/builtin/providers/aws/resource_aws_elasticache_cluster.go index db27edba7..5f5467150 100644 --- a/builtin/providers/aws/resource_aws_elasticache_cluster.go +++ b/builtin/providers/aws/resource_aws_elasticache_cluster.go @@ -4,10 +4,12 @@ import ( "fmt" "log" "sort" + "strings" "time" "github.com/awslabs/aws-sdk-go/aws" "github.com/awslabs/aws-sdk-go/service/elasticache" + "github.com/awslabs/aws-sdk-go/service/iam" "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" @@ -17,6 +19,7 @@ func resourceAwsElasticacheCluster() *schema.Resource { return &schema.Resource{ Create: resourceAwsElasticacheClusterCreate, Read: resourceAwsElasticacheClusterRead, + Update: resourceAwsElasticacheClusterUpdate, Delete: resourceAwsElasticacheClusterDelete, Schema: map[string]*schema.Schema{ @@ -105,6 +108,8 @@ func resourceAwsElasticacheCluster() *schema.Resource { }, }, }, + + "tags": tagsSchema(), }, } } @@ -125,6 +130,7 @@ func resourceAwsElasticacheClusterCreate(d *schema.ResourceData, meta interface{ securityNames := expandStringList(securityNameSet.List()) securityIds := expandStringList(securityIdSet.List()) + tags := tagsFromMapEC(d.Get("tags").(map[string]interface{})) req := &elasticache.CreateCacheClusterInput{ CacheClusterID: aws.String(clusterId), CacheNodeType: aws.String(nodeType), @@ -135,6 +141,7 @@ func resourceAwsElasticacheClusterCreate(d *schema.ResourceData, meta interface{ CacheSubnetGroupName: aws.String(subnetGroupName), CacheSecurityGroupNames: securityNames, SecurityGroupIDs: securityIds, + Tags: tags, } // parameter groups are optional and can be defaulted by AWS @@ -198,11 +205,44 @@ func resourceAwsElasticacheClusterRead(d *schema.ResourceData, meta interface{}) if err := setCacheNodeData(d, c); err != nil { return err } + // list tags for resource + // set tags + arn, err := buildECARN(d, meta) + if err != nil { + log.Printf("[DEBUG] Error building ARN for ElastiCache Cluster, not setting Tags for cluster %s", *c.CacheClusterID) + } else { + resp, err := conn.ListTagsForResource(&elasticache.ListTagsForResourceInput{ + ResourceName: aws.String(arn), + }) + + if err != nil { + log.Printf("[DEBUG] Error retreiving tags for ARN: %s", arn) + } + + var et []*elasticache.Tag + if len(resp.TagList) > 0 { + et = resp.TagList + } + d.Set("tags", tagsToMapEC(et)) + } } return nil } +func resourceAwsElasticacheClusterUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).elasticacheconn + arn, err := buildECARN(d, meta) + if err != nil { + log.Printf("[DEBUG] Error building ARN for ElastiCache Cluster, not updating Tags for cluster %s", *c.CacheClusterID) + } else { + if err := setTagsEC(conn, d, arn); err != nil { + return err + } + } + return resourceAwsElasticacheClusterRead(d, meta) +} + func setCacheNodeData(d *schema.ResourceData, c *elasticache.CacheCluster) error { sortedCacheNodes := make([]*elasticache.CacheNode, len(c.CacheNodes)) copy(sortedCacheNodes, c.CacheNodes) @@ -301,3 +341,17 @@ func CacheClusterStateRefreshFunc(conn *elasticache.ElastiCache, clusterID, give return c, *c.CacheClusterStatus, nil } } + +func buildECARN(d *schema.ResourceData, meta interface{}) (string, error) { + iamconn := meta.(*AWSClient).iamconn + region := meta.(*AWSClient).region + // An zero value GetUserInput{} defers to the currently logged in user + resp, err := iamconn.GetUser(&iam.GetUserInput{}) + if err != nil { + return "", err + } + userARN := *resp.User.ARN + accountID := strings.Split(userARN, ":")[4] + arn := fmt.Sprintf("arn:aws:elasticache:%s:%s:cluster:%s", region, accountID, d.Id()) + return arn, nil +} From 2809280e98054f4ace8312a830362b742e69abe3 Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Thu, 14 May 2015 11:51:08 -0500 Subject: [PATCH 5/7] cleanup --- builtin/providers/aws/resource_aws_elasticache_cluster.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/providers/aws/resource_aws_elasticache_cluster.go b/builtin/providers/aws/resource_aws_elasticache_cluster.go index 5f5467150..9cb6a86b0 100644 --- a/builtin/providers/aws/resource_aws_elasticache_cluster.go +++ b/builtin/providers/aws/resource_aws_elasticache_cluster.go @@ -234,7 +234,7 @@ func resourceAwsElasticacheClusterUpdate(d *schema.ResourceData, meta interface{ conn := meta.(*AWSClient).elasticacheconn arn, err := buildECARN(d, meta) if err != nil { - log.Printf("[DEBUG] Error building ARN for ElastiCache Cluster, not updating Tags for cluster %s", *c.CacheClusterID) + log.Printf("[DEBUG] Error building ARN for ElastiCache Cluster, not updating Tags for cluster %s", d.Id()) } else { if err := setTagsEC(conn, d, arn); err != nil { return err From 10fc184c9701721dac2eef7f51a939b0f2b0dcd6 Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Thu, 14 May 2015 12:32:40 -0500 Subject: [PATCH 6/7] add tags helper library for ElastiCache --- builtin/providers/aws/tagsEC.go | 95 ++++++++++++++++++++++++++++ builtin/providers/aws/tagsEC_test.go | 85 +++++++++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 builtin/providers/aws/tagsEC.go create mode 100644 builtin/providers/aws/tagsEC_test.go diff --git a/builtin/providers/aws/tagsEC.go b/builtin/providers/aws/tagsEC.go new file mode 100644 index 000000000..55aa2c4e5 --- /dev/null +++ b/builtin/providers/aws/tagsEC.go @@ -0,0 +1,95 @@ +package aws + +import ( + "log" + + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/service/elasticache" + "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 setTagsEC(conn *elasticache.ElastiCache, 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 := diffTagsEC(tagsFromMapEC(o), tagsFromMapEC(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(&elasticache.RemoveTagsFromResourceInput{ + 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(&elasticache.AddTagsToResourceInput{ + 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 diffTagsEC(oldTags, newTags []*elasticache.Tag) ([]*elasticache.Tag, []*elasticache.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 []*elasticache.Tag + for _, t := range oldTags { + old, ok := create[*t.Key] + if !ok || old != *t.Value { + // Delete it! + remove = append(remove, t) + } + } + + return tagsFromMapEC(create), remove +} + +// tagsFromMap returns the tags for the given map of data. +func tagsFromMapEC(m map[string]interface{}) []*elasticache.Tag { + result := make([]*elasticache.Tag, 0, len(m)) + for k, v := range m { + result = append(result, &elasticache.Tag{ + Key: aws.String(k), + Value: aws.String(v.(string)), + }) + } + + return result +} + +// tagsToMap turns the list of tags into a map. +func tagsToMapEC(ts []*elasticache.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/tagsEC_test.go b/builtin/providers/aws/tagsEC_test.go new file mode 100644 index 000000000..dc61bd0cc --- /dev/null +++ b/builtin/providers/aws/tagsEC_test.go @@ -0,0 +1,85 @@ +package aws + +import ( + "fmt" + "reflect" + "testing" + + "github.com/awslabs/aws-sdk-go/service/elasticache" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestDiffelasticacheTags(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 := diffTagsEC(tagsFromMapEC(tc.Old), tagsFromMapEC(tc.New)) + cm := tagsToMapEC(c) + rm := tagsToMapEC(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 testAccCheckelasticacheTags( + ts []*elasticache.Tag, key string, value string) resource.TestCheckFunc { + return func(s *terraform.State) error { + m := tagsToMapEC(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 + } +} From d81e63cc3c2411077ee629d3ca30cebe36ef4464 Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Thu, 14 May 2015 13:57:01 -0500 Subject: [PATCH 7/7] provider/aws: ElastiCache test updates - rename test to have _basic suffix, so we can run it individually - use us-east-1 for basic test, since that's probably the only region that has Classic - update the indexing of nodes; cache nodes are 4 digits --- .../providers/aws/resource_aws_elasticache_cluster_test.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/builtin/providers/aws/resource_aws_elasticache_cluster_test.go b/builtin/providers/aws/resource_aws_elasticache_cluster_test.go index 2fa38b140..1fb478b26 100644 --- a/builtin/providers/aws/resource_aws_elasticache_cluster_test.go +++ b/builtin/providers/aws/resource_aws_elasticache_cluster_test.go @@ -12,7 +12,7 @@ import ( "github.com/hashicorp/terraform/terraform" ) -func TestAccAWSElasticacheCluster(t *testing.T) { +func TestAccAWSElasticacheCluster_basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, @@ -24,7 +24,7 @@ func TestAccAWSElasticacheCluster(t *testing.T) { testAccCheckAWSElasticacheSecurityGroupExists("aws_elasticache_security_group.bar"), testAccCheckAWSElasticacheClusterExists("aws_elasticache_cluster.bar"), resource.TestCheckResourceAttr( - "aws_elasticache_cluster.bar", "cache_nodes.0.id", "001"), + "aws_elasticache_cluster.bar", "cache_nodes.0.id", "0001"), ), }, }, @@ -95,6 +95,9 @@ func genRandInt() int { } var testAccAWSElasticacheClusterConfig = fmt.Sprintf(` +provider "aws" { + region = "us-east-1" +} resource "aws_security_group" "bar" { name = "tf-test-security-group-%03d" description = "tf-test-security-group-descr"