github.com/hobbeswalsh/terraform@v0.3.7-0.20150619183303-ad17cf55a0fa/builtin/providers/aws/resource_aws_elasticache_cluster.go (about) 1 package aws 2 3 import ( 4 "fmt" 5 "log" 6 "sort" 7 "strings" 8 "time" 9 10 "github.com/aws/aws-sdk-go/aws" 11 "github.com/aws/aws-sdk-go/aws/awserr" 12 "github.com/aws/aws-sdk-go/service/elasticache" 13 "github.com/aws/aws-sdk-go/service/iam" 14 "github.com/hashicorp/terraform/helper/hashcode" 15 "github.com/hashicorp/terraform/helper/resource" 16 "github.com/hashicorp/terraform/helper/schema" 17 ) 18 19 func resourceAwsElasticacheCluster() *schema.Resource { 20 return &schema.Resource{ 21 Create: resourceAwsElasticacheClusterCreate, 22 Read: resourceAwsElasticacheClusterRead, 23 Update: resourceAwsElasticacheClusterUpdate, 24 Delete: resourceAwsElasticacheClusterDelete, 25 26 Schema: map[string]*schema.Schema{ 27 "cluster_id": &schema.Schema{ 28 Type: schema.TypeString, 29 Required: true, 30 ForceNew: true, 31 }, 32 "engine": &schema.Schema{ 33 Type: schema.TypeString, 34 Required: true, 35 ForceNew: true, 36 }, 37 "node_type": &schema.Schema{ 38 Type: schema.TypeString, 39 Required: true, 40 ForceNew: true, 41 }, 42 "num_cache_nodes": &schema.Schema{ 43 Type: schema.TypeInt, 44 Required: true, 45 ForceNew: true, 46 }, 47 "parameter_group_name": &schema.Schema{ 48 Type: schema.TypeString, 49 Optional: true, 50 Computed: true, 51 ForceNew: true, 52 }, 53 "port": &schema.Schema{ 54 Type: schema.TypeInt, 55 Required: true, 56 ForceNew: true, 57 }, 58 "engine_version": &schema.Schema{ 59 Type: schema.TypeString, 60 Optional: true, 61 Computed: true, 62 ForceNew: true, 63 }, 64 "subnet_group_name": &schema.Schema{ 65 Type: schema.TypeString, 66 Optional: true, 67 Computed: true, 68 ForceNew: true, 69 }, 70 "security_group_names": &schema.Schema{ 71 Type: schema.TypeSet, 72 Optional: true, 73 Computed: true, 74 ForceNew: true, 75 Elem: &schema.Schema{Type: schema.TypeString}, 76 Set: func(v interface{}) int { 77 return hashcode.String(v.(string)) 78 }, 79 }, 80 "security_group_ids": &schema.Schema{ 81 Type: schema.TypeSet, 82 Optional: true, 83 Computed: true, 84 ForceNew: true, 85 Elem: &schema.Schema{Type: schema.TypeString}, 86 Set: func(v interface{}) int { 87 return hashcode.String(v.(string)) 88 }, 89 }, 90 // Exported Attributes 91 "cache_nodes": &schema.Schema{ 92 Type: schema.TypeList, 93 Computed: true, 94 Elem: &schema.Resource{ 95 Schema: map[string]*schema.Schema{ 96 "id": &schema.Schema{ 97 Type: schema.TypeString, 98 Computed: true, 99 }, 100 "address": &schema.Schema{ 101 Type: schema.TypeString, 102 Computed: true, 103 }, 104 "port": &schema.Schema{ 105 Type: schema.TypeInt, 106 Computed: true, 107 }, 108 }, 109 }, 110 }, 111 112 "tags": tagsSchema(), 113 }, 114 } 115 } 116 117 func resourceAwsElasticacheClusterCreate(d *schema.ResourceData, meta interface{}) error { 118 conn := meta.(*AWSClient).elasticacheconn 119 120 clusterId := d.Get("cluster_id").(string) 121 nodeType := d.Get("node_type").(string) // e.g) cache.m1.small 122 numNodes := int64(d.Get("num_cache_nodes").(int)) // 2 123 engine := d.Get("engine").(string) // memcached 124 engineVersion := d.Get("engine_version").(string) // 1.4.14 125 port := int64(d.Get("port").(int)) // e.g) 11211 126 subnetGroupName := d.Get("subnet_group_name").(string) 127 securityNameSet := d.Get("security_group_names").(*schema.Set) 128 securityIdSet := d.Get("security_group_ids").(*schema.Set) 129 130 securityNames := expandStringList(securityNameSet.List()) 131 securityIds := expandStringList(securityIdSet.List()) 132 133 tags := tagsFromMapEC(d.Get("tags").(map[string]interface{})) 134 req := &elasticache.CreateCacheClusterInput{ 135 CacheClusterID: aws.String(clusterId), 136 CacheNodeType: aws.String(nodeType), 137 NumCacheNodes: aws.Long(numNodes), 138 Engine: aws.String(engine), 139 EngineVersion: aws.String(engineVersion), 140 Port: aws.Long(port), 141 CacheSubnetGroupName: aws.String(subnetGroupName), 142 CacheSecurityGroupNames: securityNames, 143 SecurityGroupIDs: securityIds, 144 Tags: tags, 145 } 146 147 // parameter groups are optional and can be defaulted by AWS 148 if v, ok := d.GetOk("parameter_group_name"); ok { 149 req.CacheParameterGroupName = aws.String(v.(string)) 150 } 151 152 _, err := conn.CreateCacheCluster(req) 153 if err != nil { 154 return fmt.Errorf("Error creating Elasticache: %s", err) 155 } 156 157 pending := []string{"creating"} 158 stateConf := &resource.StateChangeConf{ 159 Pending: pending, 160 Target: "available", 161 Refresh: CacheClusterStateRefreshFunc(conn, d.Id(), "available", pending), 162 Timeout: 10 * time.Minute, 163 Delay: 10 * time.Second, 164 MinTimeout: 3 * time.Second, 165 } 166 167 log.Printf("[DEBUG] Waiting for state to become available: %v", d.Id()) 168 _, sterr := stateConf.WaitForState() 169 if sterr != nil { 170 return fmt.Errorf("Error waiting for elasticache (%s) to be created: %s", d.Id(), sterr) 171 } 172 173 d.SetId(clusterId) 174 175 return resourceAwsElasticacheClusterRead(d, meta) 176 } 177 178 func resourceAwsElasticacheClusterRead(d *schema.ResourceData, meta interface{}) error { 179 conn := meta.(*AWSClient).elasticacheconn 180 req := &elasticache.DescribeCacheClustersInput{ 181 CacheClusterID: aws.String(d.Id()), 182 ShowCacheNodeInfo: aws.Boolean(true), 183 } 184 185 res, err := conn.DescribeCacheClusters(req) 186 if err != nil { 187 return err 188 } 189 190 if len(res.CacheClusters) == 1 { 191 c := res.CacheClusters[0] 192 d.Set("cluster_id", c.CacheClusterID) 193 d.Set("node_type", c.CacheNodeType) 194 d.Set("num_cache_nodes", c.NumCacheNodes) 195 d.Set("engine", c.Engine) 196 d.Set("engine_version", c.EngineVersion) 197 if c.ConfigurationEndpoint != nil { 198 d.Set("port", c.ConfigurationEndpoint.Port) 199 } 200 d.Set("subnet_group_name", c.CacheSubnetGroupName) 201 d.Set("security_group_names", c.CacheSecurityGroups) 202 d.Set("security_group_ids", c.SecurityGroups) 203 d.Set("parameter_group_name", c.CacheParameterGroup) 204 205 if err := setCacheNodeData(d, c); err != nil { 206 return err 207 } 208 // list tags for resource 209 // set tags 210 arn, err := buildECARN(d, meta) 211 if err != nil { 212 log.Printf("[DEBUG] Error building ARN for ElastiCache Cluster, not setting Tags for cluster %s", *c.CacheClusterID) 213 } else { 214 resp, err := conn.ListTagsForResource(&elasticache.ListTagsForResourceInput{ 215 ResourceName: aws.String(arn), 216 }) 217 218 if err != nil { 219 log.Printf("[DEBUG] Error retreiving tags for ARN: %s", arn) 220 } 221 222 var et []*elasticache.Tag 223 if len(resp.TagList) > 0 { 224 et = resp.TagList 225 } 226 d.Set("tags", tagsToMapEC(et)) 227 } 228 } 229 230 return nil 231 } 232 233 func resourceAwsElasticacheClusterUpdate(d *schema.ResourceData, meta interface{}) error { 234 conn := meta.(*AWSClient).elasticacheconn 235 arn, err := buildECARN(d, meta) 236 if err != nil { 237 log.Printf("[DEBUG] Error building ARN for ElastiCache Cluster, not updating Tags for cluster %s", d.Id()) 238 } else { 239 if err := setTagsEC(conn, d, arn); err != nil { 240 return err 241 } 242 } 243 return resourceAwsElasticacheClusterRead(d, meta) 244 } 245 246 func setCacheNodeData(d *schema.ResourceData, c *elasticache.CacheCluster) error { 247 sortedCacheNodes := make([]*elasticache.CacheNode, len(c.CacheNodes)) 248 copy(sortedCacheNodes, c.CacheNodes) 249 sort.Sort(byCacheNodeId(sortedCacheNodes)) 250 251 cacheNodeData := make([]map[string]interface{}, 0, len(sortedCacheNodes)) 252 253 for _, node := range sortedCacheNodes { 254 if node.CacheNodeID == nil || node.Endpoint == nil || node.Endpoint.Address == nil || node.Endpoint.Port == nil { 255 return fmt.Errorf("Unexpected nil pointer in: %#v", node) 256 } 257 cacheNodeData = append(cacheNodeData, map[string]interface{}{ 258 "id": *node.CacheNodeID, 259 "address": *node.Endpoint.Address, 260 "port": int(*node.Endpoint.Port), 261 }) 262 } 263 264 return d.Set("cache_nodes", cacheNodeData) 265 } 266 267 type byCacheNodeId []*elasticache.CacheNode 268 269 func (b byCacheNodeId) Len() int { return len(b) } 270 func (b byCacheNodeId) Swap(i, j int) { b[i], b[j] = b[j], b[i] } 271 func (b byCacheNodeId) Less(i, j int) bool { 272 return b[i].CacheNodeID != nil && b[j].CacheNodeID != nil && 273 *b[i].CacheNodeID < *b[j].CacheNodeID 274 } 275 276 func resourceAwsElasticacheClusterDelete(d *schema.ResourceData, meta interface{}) error { 277 conn := meta.(*AWSClient).elasticacheconn 278 279 req := &elasticache.DeleteCacheClusterInput{ 280 CacheClusterID: aws.String(d.Id()), 281 } 282 _, err := conn.DeleteCacheCluster(req) 283 if err != nil { 284 return err 285 } 286 287 log.Printf("[DEBUG] Waiting for deletion: %v", d.Id()) 288 stateConf := &resource.StateChangeConf{ 289 Pending: []string{"creating", "available", "deleting", "incompatible-parameters", "incompatible-network", "restore-failed"}, 290 Target: "", 291 Refresh: CacheClusterStateRefreshFunc(conn, d.Id(), "", []string{}), 292 Timeout: 10 * time.Minute, 293 Delay: 10 * time.Second, 294 MinTimeout: 3 * time.Second, 295 } 296 297 _, sterr := stateConf.WaitForState() 298 if sterr != nil { 299 return fmt.Errorf("Error waiting for elasticache (%s) to delete: %s", d.Id(), sterr) 300 } 301 302 d.SetId("") 303 304 return nil 305 } 306 307 func CacheClusterStateRefreshFunc(conn *elasticache.ElastiCache, clusterID, givenState string, pending []string) resource.StateRefreshFunc { 308 return func() (interface{}, string, error) { 309 resp, err := conn.DescribeCacheClusters(&elasticache.DescribeCacheClustersInput{ 310 CacheClusterID: aws.String(clusterID), 311 ShowCacheNodeInfo: aws.Boolean(true), 312 }) 313 if err != nil { 314 apierr := err.(awserr.Error) 315 log.Printf("[DEBUG] message: %v, code: %v", apierr.Message(), apierr.Code()) 316 if apierr.Message() == fmt.Sprintf("CacheCluster not found: %v", clusterID) { 317 log.Printf("[DEBUG] Detect deletion") 318 return nil, "", nil 319 } 320 321 log.Printf("[ERROR] CacheClusterStateRefreshFunc: %s", err) 322 return nil, "", err 323 } 324 325 c := resp.CacheClusters[0] 326 log.Printf("[DEBUG] status: %v", *c.CacheClusterStatus) 327 328 // return the current state if it's in the pending array 329 for _, p := range pending { 330 s := *c.CacheClusterStatus 331 if p == s { 332 log.Printf("[DEBUG] Return with status: %v", *c.CacheClusterStatus) 333 return c, p, nil 334 } 335 } 336 337 // return given state if it's not in pending 338 if givenState != "" { 339 // check to make sure we have the node count we're expecting 340 if int64(len(c.CacheNodes)) != *c.NumCacheNodes { 341 log.Printf("[DEBUG] Node count is not what is expected: %d found, %d expected", len(c.CacheNodes), *c.NumCacheNodes) 342 return nil, "creating", nil 343 } 344 // loop the nodes and check their status as well 345 for _, n := range c.CacheNodes { 346 if n.CacheNodeStatus != nil && *n.CacheNodeStatus != "available" { 347 log.Printf("[DEBUG] Node (%s) is not yet available, status: %s", *n.CacheNodeID, *n.CacheNodeStatus) 348 return nil, "creating", nil 349 } 350 } 351 return c, givenState, nil 352 } 353 log.Printf("[DEBUG] current status: %v", *c.CacheClusterStatus) 354 return c, *c.CacheClusterStatus, nil 355 } 356 } 357 358 func buildECARN(d *schema.ResourceData, meta interface{}) (string, error) { 359 iamconn := meta.(*AWSClient).iamconn 360 region := meta.(*AWSClient).region 361 // An zero value GetUserInput{} defers to the currently logged in user 362 resp, err := iamconn.GetUser(&iam.GetUserInput{}) 363 if err != nil { 364 return "", err 365 } 366 userARN := *resp.User.ARN 367 accountID := strings.Split(userARN, ":")[4] 368 arn := fmt.Sprintf("arn:aws:elasticache:%s:%s:cluster:%s", region, accountID, d.Id()) 369 return arn, nil 370 }