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