github.com/anuaimi/terraform@v0.6.4-0.20150904235404-2bf9aec61da8/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 }, 36 "node_type": &schema.Schema{ 37 Type: schema.TypeString, 38 Required: true, 39 ForceNew: true, 40 }, 41 "num_cache_nodes": &schema.Schema{ 42 Type: schema.TypeInt, 43 Required: true, 44 }, 45 "parameter_group_name": &schema.Schema{ 46 Type: schema.TypeString, 47 Optional: true, 48 Computed: true, 49 }, 50 "port": &schema.Schema{ 51 Type: schema.TypeInt, 52 Required: true, 53 ForceNew: true, 54 }, 55 "engine_version": &schema.Schema{ 56 Type: schema.TypeString, 57 Optional: true, 58 Computed: true, 59 }, 60 "maintenance_window": &schema.Schema{ 61 Type: schema.TypeString, 62 Optional: true, 63 Computed: true, 64 }, 65 "subnet_group_name": &schema.Schema{ 66 Type: schema.TypeString, 67 Optional: true, 68 Computed: true, 69 ForceNew: true, 70 }, 71 "security_group_names": &schema.Schema{ 72 Type: schema.TypeSet, 73 Optional: true, 74 Computed: true, 75 ForceNew: true, 76 Elem: &schema.Schema{Type: schema.TypeString}, 77 Set: func(v interface{}) int { 78 return hashcode.String(v.(string)) 79 }, 80 }, 81 "security_group_ids": &schema.Schema{ 82 Type: schema.TypeSet, 83 Optional: true, 84 Computed: 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 // A single-element string list containing an Amazon Resource Name (ARN) that 113 // uniquely identifies a Redis RDB snapshot file stored in Amazon S3. The snapshot 114 // file will be used to populate the node group. 115 // 116 // See also: 117 // https://github.com/aws/aws-sdk-go/blob/4862a174f7fc92fb523fc39e68f00b87d91d2c3d/service/elasticache/api.go#L2079 118 "snapshot_arns": &schema.Schema{ 119 Type: schema.TypeSet, 120 Optional: true, 121 ForceNew: true, 122 Elem: &schema.Schema{Type: schema.TypeString}, 123 Set: func(v interface{}) int { 124 return hashcode.String(v.(string)) 125 }, 126 }, 127 128 "tags": tagsSchema(), 129 130 // apply_immediately is used to determine when the update modifications 131 // take place. 132 // See http://docs.aws.amazon.com/AmazonElastiCache/latest/APIReference/API_ModifyCacheCluster.html 133 "apply_immediately": &schema.Schema{ 134 Type: schema.TypeBool, 135 Optional: true, 136 Computed: true, 137 }, 138 }, 139 } 140 } 141 142 func resourceAwsElasticacheClusterCreate(d *schema.ResourceData, meta interface{}) error { 143 conn := meta.(*AWSClient).elasticacheconn 144 145 clusterId := d.Get("cluster_id").(string) 146 nodeType := d.Get("node_type").(string) // e.g) cache.m1.small 147 numNodes := int64(d.Get("num_cache_nodes").(int)) // 2 148 engine := d.Get("engine").(string) // memcached 149 engineVersion := d.Get("engine_version").(string) // 1.4.14 150 port := int64(d.Get("port").(int)) // e.g) 11211 151 subnetGroupName := d.Get("subnet_group_name").(string) 152 securityNameSet := d.Get("security_group_names").(*schema.Set) 153 securityIdSet := d.Get("security_group_ids").(*schema.Set) 154 155 securityNames := expandStringList(securityNameSet.List()) 156 securityIds := expandStringList(securityIdSet.List()) 157 158 tags := tagsFromMapEC(d.Get("tags").(map[string]interface{})) 159 req := &elasticache.CreateCacheClusterInput{ 160 CacheClusterId: aws.String(clusterId), 161 CacheNodeType: aws.String(nodeType), 162 NumCacheNodes: aws.Int64(numNodes), 163 Engine: aws.String(engine), 164 EngineVersion: aws.String(engineVersion), 165 Port: aws.Int64(port), 166 CacheSubnetGroupName: aws.String(subnetGroupName), 167 CacheSecurityGroupNames: securityNames, 168 SecurityGroupIds: securityIds, 169 Tags: tags, 170 } 171 172 // parameter groups are optional and can be defaulted by AWS 173 if v, ok := d.GetOk("parameter_group_name"); ok { 174 req.CacheParameterGroupName = aws.String(v.(string)) 175 } 176 177 if v, ok := d.GetOk("maintenance_window"); ok { 178 req.PreferredMaintenanceWindow = aws.String(v.(string)) 179 } 180 181 snaps := d.Get("snapshot_arns").(*schema.Set).List() 182 if len(snaps) > 0 { 183 s := expandStringList(snaps) 184 req.SnapshotArns = s 185 log.Printf("[DEBUG] Restoring Redis cluster from S3 snapshot: %#v", s) 186 } 187 188 resp, err := conn.CreateCacheCluster(req) 189 if err != nil { 190 return fmt.Errorf("Error creating Elasticache: %s", err) 191 } 192 193 d.SetId(*resp.CacheCluster.CacheClusterId) 194 195 pending := []string{"creating"} 196 stateConf := &resource.StateChangeConf{ 197 Pending: pending, 198 Target: "available", 199 Refresh: cacheClusterStateRefreshFunc(conn, d.Id(), "available", pending), 200 Timeout: 10 * time.Minute, 201 Delay: 10 * time.Second, 202 MinTimeout: 3 * time.Second, 203 } 204 205 log.Printf("[DEBUG] Waiting for state to become available: %v", d.Id()) 206 _, sterr := stateConf.WaitForState() 207 if sterr != nil { 208 return fmt.Errorf("Error waiting for elasticache (%s) to be created: %s", d.Id(), sterr) 209 } 210 211 return resourceAwsElasticacheClusterRead(d, meta) 212 } 213 214 func resourceAwsElasticacheClusterRead(d *schema.ResourceData, meta interface{}) error { 215 conn := meta.(*AWSClient).elasticacheconn 216 req := &elasticache.DescribeCacheClustersInput{ 217 CacheClusterId: aws.String(d.Id()), 218 ShowCacheNodeInfo: aws.Bool(true), 219 } 220 221 res, err := conn.DescribeCacheClusters(req) 222 if err != nil { 223 return err 224 } 225 226 if len(res.CacheClusters) == 1 { 227 c := res.CacheClusters[0] 228 d.Set("cluster_id", c.CacheClusterId) 229 d.Set("node_type", c.CacheNodeType) 230 d.Set("num_cache_nodes", c.NumCacheNodes) 231 d.Set("engine", c.Engine) 232 d.Set("engine_version", c.EngineVersion) 233 if c.ConfigurationEndpoint != nil { 234 d.Set("port", c.ConfigurationEndpoint.Port) 235 } 236 d.Set("subnet_group_name", c.CacheSubnetGroupName) 237 d.Set("security_group_names", c.CacheSecurityGroups) 238 d.Set("security_group_ids", c.SecurityGroups) 239 d.Set("parameter_group_name", c.CacheParameterGroup) 240 d.Set("maintenance_window", c.PreferredMaintenanceWindow) 241 242 if err := setCacheNodeData(d, c); err != nil { 243 return err 244 } 245 // list tags for resource 246 // set tags 247 arn, err := buildECARN(d, meta) 248 if err != nil { 249 log.Printf("[DEBUG] Error building ARN for ElastiCache Cluster, not setting Tags for cluster %s", *c.CacheClusterId) 250 } else { 251 resp, err := conn.ListTagsForResource(&elasticache.ListTagsForResourceInput{ 252 ResourceName: aws.String(arn), 253 }) 254 255 if err != nil { 256 log.Printf("[DEBUG] Error retreiving tags for ARN: %s", arn) 257 } 258 259 var et []*elasticache.Tag 260 if len(resp.TagList) > 0 { 261 et = resp.TagList 262 } 263 d.Set("tags", tagsToMapEC(et)) 264 } 265 } 266 267 return nil 268 } 269 270 func resourceAwsElasticacheClusterUpdate(d *schema.ResourceData, meta interface{}) error { 271 conn := meta.(*AWSClient).elasticacheconn 272 arn, err := buildECARN(d, meta) 273 if err != nil { 274 log.Printf("[DEBUG] Error building ARN for ElastiCache Cluster, not updating Tags for cluster %s", d.Id()) 275 } else { 276 if err := setTagsEC(conn, d, arn); err != nil { 277 return err 278 } 279 } 280 281 req := &elasticache.ModifyCacheClusterInput{ 282 CacheClusterId: aws.String(d.Id()), 283 ApplyImmediately: aws.Bool(d.Get("apply_immediately").(bool)), 284 } 285 286 requestUpdate := false 287 if d.HasChange("security_group_ids") { 288 if attr := d.Get("security_group_ids").(*schema.Set); attr.Len() > 0 { 289 req.SecurityGroupIds = expandStringList(attr.List()) 290 requestUpdate = true 291 } 292 } 293 294 if d.HasChange("parameter_group_name") { 295 req.CacheParameterGroupName = aws.String(d.Get("parameter_group_name").(string)) 296 requestUpdate = true 297 } 298 299 if d.HasChange("maintenance_window") { 300 req.PreferredMaintenanceWindow = aws.String(d.Get("maintenance_window").(string)) 301 requestUpdate = true 302 } 303 304 if d.HasChange("engine_version") { 305 req.EngineVersion = aws.String(d.Get("engine_version").(string)) 306 requestUpdate = true 307 } 308 309 if d.HasChange("num_cache_nodes") { 310 req.NumCacheNodes = aws.Int64(int64(d.Get("num_cache_nodes").(int))) 311 requestUpdate = true 312 } 313 314 if requestUpdate { 315 log.Printf("[DEBUG] Modifying ElastiCache Cluster (%s), opts:\n%s", d.Id(), req) 316 _, err := conn.ModifyCacheCluster(req) 317 if err != nil { 318 return fmt.Errorf("[WARN] Error updating ElastiCache cluster (%s), error: %s", d.Id(), err) 319 } 320 321 log.Printf("[DEBUG] Waiting for update: %s", d.Id()) 322 pending := []string{"modifying", "rebooting cache cluster nodes", "snapshotting"} 323 stateConf := &resource.StateChangeConf{ 324 Pending: pending, 325 Target: "available", 326 Refresh: cacheClusterStateRefreshFunc(conn, d.Id(), "available", pending), 327 Timeout: 5 * time.Minute, 328 Delay: 5 * time.Second, 329 MinTimeout: 3 * time.Second, 330 } 331 332 _, sterr := stateConf.WaitForState() 333 if sterr != nil { 334 return fmt.Errorf("Error waiting for elasticache (%s) to update: %s", d.Id(), sterr) 335 } 336 } 337 338 return resourceAwsElasticacheClusterRead(d, meta) 339 } 340 341 func setCacheNodeData(d *schema.ResourceData, c *elasticache.CacheCluster) error { 342 sortedCacheNodes := make([]*elasticache.CacheNode, len(c.CacheNodes)) 343 copy(sortedCacheNodes, c.CacheNodes) 344 sort.Sort(byCacheNodeId(sortedCacheNodes)) 345 346 cacheNodeData := make([]map[string]interface{}, 0, len(sortedCacheNodes)) 347 348 for _, node := range sortedCacheNodes { 349 if node.CacheNodeId == nil || node.Endpoint == nil || node.Endpoint.Address == nil || node.Endpoint.Port == nil { 350 return fmt.Errorf("Unexpected nil pointer in: %s", node) 351 } 352 cacheNodeData = append(cacheNodeData, map[string]interface{}{ 353 "id": *node.CacheNodeId, 354 "address": *node.Endpoint.Address, 355 "port": int(*node.Endpoint.Port), 356 }) 357 } 358 359 return d.Set("cache_nodes", cacheNodeData) 360 } 361 362 type byCacheNodeId []*elasticache.CacheNode 363 364 func (b byCacheNodeId) Len() int { return len(b) } 365 func (b byCacheNodeId) Swap(i, j int) { b[i], b[j] = b[j], b[i] } 366 func (b byCacheNodeId) Less(i, j int) bool { 367 return b[i].CacheNodeId != nil && b[j].CacheNodeId != nil && 368 *b[i].CacheNodeId < *b[j].CacheNodeId 369 } 370 371 func resourceAwsElasticacheClusterDelete(d *schema.ResourceData, meta interface{}) error { 372 conn := meta.(*AWSClient).elasticacheconn 373 374 req := &elasticache.DeleteCacheClusterInput{ 375 CacheClusterId: aws.String(d.Id()), 376 } 377 _, err := conn.DeleteCacheCluster(req) 378 if err != nil { 379 return err 380 } 381 382 log.Printf("[DEBUG] Waiting for deletion: %v", d.Id()) 383 stateConf := &resource.StateChangeConf{ 384 Pending: []string{"creating", "available", "deleting", "incompatible-parameters", "incompatible-network", "restore-failed"}, 385 Target: "", 386 Refresh: cacheClusterStateRefreshFunc(conn, d.Id(), "", []string{}), 387 Timeout: 10 * time.Minute, 388 Delay: 10 * time.Second, 389 MinTimeout: 3 * time.Second, 390 } 391 392 _, sterr := stateConf.WaitForState() 393 if sterr != nil { 394 return fmt.Errorf("Error waiting for elasticache (%s) to delete: %s", d.Id(), sterr) 395 } 396 397 d.SetId("") 398 399 return nil 400 } 401 402 func cacheClusterStateRefreshFunc(conn *elasticache.ElastiCache, clusterID, givenState string, pending []string) resource.StateRefreshFunc { 403 return func() (interface{}, string, error) { 404 resp, err := conn.DescribeCacheClusters(&elasticache.DescribeCacheClustersInput{ 405 CacheClusterId: aws.String(clusterID), 406 ShowCacheNodeInfo: aws.Bool(true), 407 }) 408 if err != nil { 409 apierr := err.(awserr.Error) 410 log.Printf("[DEBUG] message: %v, code: %v", apierr.Message(), apierr.Code()) 411 if apierr.Message() == fmt.Sprintf("CacheCluster not found: %v", clusterID) { 412 log.Printf("[DEBUG] Detect deletion") 413 return nil, "", nil 414 } 415 416 log.Printf("[ERROR] CacheClusterStateRefreshFunc: %s", err) 417 return nil, "", err 418 } 419 420 if len(resp.CacheClusters) == 0 { 421 return nil, "", fmt.Errorf("[WARN] Error: no Cache Clusters found for id (%s)", clusterID) 422 } 423 424 var c *elasticache.CacheCluster 425 for _, cluster := range resp.CacheClusters { 426 if *cluster.CacheClusterId == clusterID { 427 log.Printf("[DEBUG] Found matching ElastiCache cluster: %s", *cluster.CacheClusterId) 428 c = cluster 429 } 430 } 431 432 if c == nil { 433 return nil, "", fmt.Errorf("[WARN] Error: no matching Elastic Cache cluster for id (%s)", clusterID) 434 } 435 436 log.Printf("[DEBUG] ElastiCache Cluster (%s) status: %v", clusterID, *c.CacheClusterStatus) 437 438 // return the current state if it's in the pending array 439 for _, p := range pending { 440 log.Printf("[DEBUG] ElastiCache: checking pending state (%s) for cluster (%s), cluster status: %s", pending, clusterID, *c.CacheClusterStatus) 441 s := *c.CacheClusterStatus 442 if p == s { 443 log.Printf("[DEBUG] Return with status: %v", *c.CacheClusterStatus) 444 return c, p, nil 445 } 446 } 447 448 // return given state if it's not in pending 449 if givenState != "" { 450 log.Printf("[DEBUG] ElastiCache: checking given state (%s) of cluster (%s) against cluster status (%s)", givenState, clusterID, *c.CacheClusterStatus) 451 // check to make sure we have the node count we're expecting 452 if int64(len(c.CacheNodes)) != *c.NumCacheNodes { 453 log.Printf("[DEBUG] Node count is not what is expected: %d found, %d expected", len(c.CacheNodes), *c.NumCacheNodes) 454 return nil, "creating", nil 455 } 456 457 log.Printf("[DEBUG] Node count matched (%d)", len(c.CacheNodes)) 458 // loop the nodes and check their status as well 459 for _, n := range c.CacheNodes { 460 log.Printf("[DEBUG] Checking cache node for status: %s", n) 461 if n.CacheNodeStatus != nil && *n.CacheNodeStatus != "available" { 462 log.Printf("[DEBUG] Node (%s) is not yet available, status: %s", *n.CacheNodeId, *n.CacheNodeStatus) 463 return nil, "creating", nil 464 } 465 log.Printf("[DEBUG] Cache node not in expected state") 466 } 467 log.Printf("[DEBUG] ElastiCache returning given state (%s), cluster: %s", givenState, c) 468 return c, givenState, nil 469 } 470 log.Printf("[DEBUG] current status: %v", *c.CacheClusterStatus) 471 return c, *c.CacheClusterStatus, nil 472 } 473 } 474 475 func buildECARN(d *schema.ResourceData, meta interface{}) (string, error) { 476 iamconn := meta.(*AWSClient).iamconn 477 region := meta.(*AWSClient).region 478 // An zero value GetUserInput{} defers to the currently logged in user 479 resp, err := iamconn.GetUser(&iam.GetUserInput{}) 480 if err != nil { 481 return "", err 482 } 483 userARN := *resp.User.Arn 484 accountID := strings.Split(userARN, ":")[4] 485 arn := fmt.Sprintf("arn:aws:elasticache:%s:%s:cluster:%s", region, accountID, d.Id()) 486 return arn, nil 487 }