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