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