github.com/koding/terraform@v0.6.4-0.20170608090606-5d7e0339779d/builtin/providers/aws/resource_aws_elasticache_replication_group.go (about) 1 package aws 2 3 import ( 4 "fmt" 5 "log" 6 "regexp" 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/hashicorp/terraform/helper/resource" 14 "github.com/hashicorp/terraform/helper/schema" 15 ) 16 17 func resourceAwsElasticacheReplicationGroup() *schema.Resource { 18 19 resourceSchema := resourceAwsElastiCacheCommonSchema() 20 21 resourceSchema["replication_group_id"] = &schema.Schema{ 22 Type: schema.TypeString, 23 Required: true, 24 ForceNew: true, 25 ValidateFunc: validateAwsElastiCacheReplicationGroupId, 26 } 27 28 resourceSchema["automatic_failover_enabled"] = &schema.Schema{ 29 Type: schema.TypeBool, 30 Optional: true, 31 Default: false, 32 } 33 34 resourceSchema["auto_minor_version_upgrade"] = &schema.Schema{ 35 Type: schema.TypeBool, 36 Optional: true, 37 Default: true, 38 } 39 40 resourceSchema["replication_group_description"] = &schema.Schema{ 41 Type: schema.TypeString, 42 Required: true, 43 } 44 45 resourceSchema["number_cache_clusters"] = &schema.Schema{ 46 Type: schema.TypeInt, 47 Computed: true, 48 Optional: true, 49 ForceNew: true, 50 } 51 52 resourceSchema["primary_endpoint_address"] = &schema.Schema{ 53 Type: schema.TypeString, 54 Computed: true, 55 } 56 57 resourceSchema["configuration_endpoint_address"] = &schema.Schema{ 58 Type: schema.TypeString, 59 Computed: true, 60 } 61 62 resourceSchema["cluster_mode"] = &schema.Schema{ 63 Type: schema.TypeSet, 64 Optional: true, 65 MaxItems: 1, 66 Elem: &schema.Resource{ 67 Schema: map[string]*schema.Schema{ 68 "replicas_per_node_group": { 69 Type: schema.TypeInt, 70 Required: true, 71 ForceNew: true, 72 }, 73 "num_node_groups": { 74 Type: schema.TypeInt, 75 Required: true, 76 ForceNew: true, 77 }, 78 }, 79 }, 80 } 81 82 resourceSchema["engine"].Required = false 83 resourceSchema["engine"].Optional = true 84 resourceSchema["engine"].Default = "redis" 85 resourceSchema["engine"].ValidateFunc = validateAwsElastiCacheReplicationGroupEngine 86 87 return &schema.Resource{ 88 Create: resourceAwsElasticacheReplicationGroupCreate, 89 Read: resourceAwsElasticacheReplicationGroupRead, 90 Update: resourceAwsElasticacheReplicationGroupUpdate, 91 Delete: resourceAwsElasticacheReplicationGroupDelete, 92 Importer: &schema.ResourceImporter{ 93 State: schema.ImportStatePassthrough, 94 }, 95 96 Schema: resourceSchema, 97 } 98 } 99 100 func resourceAwsElasticacheReplicationGroupCreate(d *schema.ResourceData, meta interface{}) error { 101 conn := meta.(*AWSClient).elasticacheconn 102 103 tags := tagsFromMapEC(d.Get("tags").(map[string]interface{})) 104 params := &elasticache.CreateReplicationGroupInput{ 105 ReplicationGroupId: aws.String(d.Get("replication_group_id").(string)), 106 ReplicationGroupDescription: aws.String(d.Get("replication_group_description").(string)), 107 AutomaticFailoverEnabled: aws.Bool(d.Get("automatic_failover_enabled").(bool)), 108 AutoMinorVersionUpgrade: aws.Bool(d.Get("auto_minor_version_upgrade").(bool)), 109 CacheNodeType: aws.String(d.Get("node_type").(string)), 110 Engine: aws.String(d.Get("engine").(string)), 111 Port: aws.Int64(int64(d.Get("port").(int))), 112 Tags: tags, 113 } 114 115 if v, ok := d.GetOk("engine_version"); ok { 116 params.EngineVersion = aws.String(v.(string)) 117 } 118 119 preferred_azs := d.Get("availability_zones").(*schema.Set).List() 120 if len(preferred_azs) > 0 { 121 azs := expandStringList(preferred_azs) 122 params.PreferredCacheClusterAZs = azs 123 } 124 125 if v, ok := d.GetOk("parameter_group_name"); ok { 126 params.CacheParameterGroupName = aws.String(v.(string)) 127 } 128 129 if v, ok := d.GetOk("subnet_group_name"); ok { 130 params.CacheSubnetGroupName = aws.String(v.(string)) 131 } 132 133 security_group_names := d.Get("security_group_names").(*schema.Set).List() 134 if len(security_group_names) > 0 { 135 params.CacheSecurityGroupNames = expandStringList(security_group_names) 136 } 137 138 security_group_ids := d.Get("security_group_ids").(*schema.Set).List() 139 if len(security_group_ids) > 0 { 140 params.SecurityGroupIds = expandStringList(security_group_ids) 141 } 142 143 snaps := d.Get("snapshot_arns").(*schema.Set).List() 144 if len(snaps) > 0 { 145 params.SnapshotArns = expandStringList(snaps) 146 } 147 148 if v, ok := d.GetOk("maintenance_window"); ok { 149 params.PreferredMaintenanceWindow = aws.String(v.(string)) 150 } 151 152 if v, ok := d.GetOk("notification_topic_arn"); ok { 153 params.NotificationTopicArn = aws.String(v.(string)) 154 } 155 156 if v, ok := d.GetOk("snapshot_retention_limit"); ok { 157 params.SnapshotRetentionLimit = aws.Int64(int64(v.(int))) 158 } 159 160 if v, ok := d.GetOk("snapshot_window"); ok { 161 params.SnapshotWindow = aws.String(v.(string)) 162 } 163 164 if v, ok := d.GetOk("snapshot_name"); ok { 165 params.SnapshotName = aws.String(v.(string)) 166 } 167 168 clusterMode, clusterModeOk := d.GetOk("cluster_mode") 169 cacheClusters, cacheClustersOk := d.GetOk("number_cache_clusters") 170 171 if !clusterModeOk && !cacheClustersOk || clusterModeOk && cacheClustersOk { 172 return fmt.Errorf("Either `number_cache_clusters` or `cluster_mode` must be set") 173 } 174 175 if clusterModeOk { 176 clusterModeAttributes := clusterMode.(*schema.Set).List() 177 attributes := clusterModeAttributes[0].(map[string]interface{}) 178 179 if v, ok := attributes["num_node_groups"]; ok { 180 params.NumNodeGroups = aws.Int64(int64(v.(int))) 181 } 182 183 if v, ok := attributes["replicas_per_node_group"]; ok { 184 params.ReplicasPerNodeGroup = aws.Int64(int64(v.(int))) 185 } 186 } 187 188 if cacheClustersOk { 189 params.NumCacheClusters = aws.Int64(int64(cacheClusters.(int))) 190 } 191 192 resp, err := conn.CreateReplicationGroup(params) 193 if err != nil { 194 return fmt.Errorf("Error creating Elasticache Replication Group: %s", err) 195 } 196 197 d.SetId(*resp.ReplicationGroup.ReplicationGroupId) 198 199 pending := []string{"creating", "modifying", "restoring", "snapshotting"} 200 stateConf := &resource.StateChangeConf{ 201 Pending: pending, 202 Target: []string{"available"}, 203 Refresh: cacheReplicationGroupStateRefreshFunc(conn, d.Id(), "available", pending), 204 Timeout: 40 * time.Minute, 205 MinTimeout: 10 * time.Second, 206 Delay: 30 * 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 replication group (%s) to be created: %s", d.Id(), sterr) 213 } 214 215 return resourceAwsElasticacheReplicationGroupRead(d, meta) 216 } 217 218 func resourceAwsElasticacheReplicationGroupRead(d *schema.ResourceData, meta interface{}) error { 219 conn := meta.(*AWSClient).elasticacheconn 220 req := &elasticache.DescribeReplicationGroupsInput{ 221 ReplicationGroupId: aws.String(d.Id()), 222 } 223 224 res, err := conn.DescribeReplicationGroups(req) 225 if err != nil { 226 if eccErr, ok := err.(awserr.Error); ok && eccErr.Code() == "ReplicationGroupNotFoundFault" { 227 log.Printf("[WARN] Elasticache Replication Group (%s) not found", d.Id()) 228 d.SetId("") 229 return nil 230 } 231 232 return err 233 } 234 235 var rgp *elasticache.ReplicationGroup 236 for _, r := range res.ReplicationGroups { 237 if *r.ReplicationGroupId == d.Id() { 238 rgp = r 239 } 240 } 241 242 if rgp == nil { 243 log.Printf("[WARN] Replication Group (%s) not found", d.Id()) 244 d.SetId("") 245 return nil 246 } 247 248 if *rgp.Status == "deleting" { 249 log.Printf("[WARN] The Replication Group %q is currently in the `deleting` state", d.Id()) 250 d.SetId("") 251 return nil 252 } 253 254 if rgp.AutomaticFailover != nil { 255 switch strings.ToLower(*rgp.AutomaticFailover) { 256 case "disabled", "disabling": 257 d.Set("automatic_failover_enabled", false) 258 case "enabled", "enabling": 259 d.Set("automatic_failover_enabled", true) 260 default: 261 log.Printf("Unknown AutomaticFailover state %s", *rgp.AutomaticFailover) 262 } 263 } 264 265 d.Set("replication_group_description", rgp.Description) 266 d.Set("number_cache_clusters", len(rgp.MemberClusters)) 267 d.Set("replication_group_id", rgp.ReplicationGroupId) 268 269 if rgp.NodeGroups != nil { 270 if len(rgp.NodeGroups[0].NodeGroupMembers) == 0 { 271 return nil 272 } 273 274 cacheCluster := *rgp.NodeGroups[0].NodeGroupMembers[0] 275 276 res, err := conn.DescribeCacheClusters(&elasticache.DescribeCacheClustersInput{ 277 CacheClusterId: cacheCluster.CacheClusterId, 278 ShowCacheNodeInfo: aws.Bool(true), 279 }) 280 if err != nil { 281 return err 282 } 283 284 if len(res.CacheClusters) == 0 { 285 return nil 286 } 287 288 c := res.CacheClusters[0] 289 d.Set("node_type", c.CacheNodeType) 290 d.Set("engine", c.Engine) 291 d.Set("engine_version", c.EngineVersion) 292 d.Set("subnet_group_name", c.CacheSubnetGroupName) 293 d.Set("security_group_names", flattenElastiCacheSecurityGroupNames(c.CacheSecurityGroups)) 294 d.Set("security_group_ids", flattenElastiCacheSecurityGroupIds(c.SecurityGroups)) 295 296 if c.CacheParameterGroup != nil { 297 d.Set("parameter_group_name", c.CacheParameterGroup.CacheParameterGroupName) 298 } 299 300 d.Set("maintenance_window", c.PreferredMaintenanceWindow) 301 d.Set("snapshot_window", rgp.SnapshotWindow) 302 d.Set("snapshot_retention_limit", rgp.SnapshotRetentionLimit) 303 304 if rgp.ConfigurationEndpoint != nil { 305 d.Set("port", rgp.ConfigurationEndpoint.Port) 306 d.Set("configuration_endpoint_address", rgp.ConfigurationEndpoint.Address) 307 } else { 308 d.Set("port", rgp.NodeGroups[0].PrimaryEndpoint.Port) 309 d.Set("primary_endpoint_address", rgp.NodeGroups[0].PrimaryEndpoint.Address) 310 } 311 312 d.Set("auto_minor_version_upgrade", c.AutoMinorVersionUpgrade) 313 } 314 315 return nil 316 } 317 318 func resourceAwsElasticacheReplicationGroupUpdate(d *schema.ResourceData, meta interface{}) error { 319 conn := meta.(*AWSClient).elasticacheconn 320 321 requestUpdate := false 322 params := &elasticache.ModifyReplicationGroupInput{ 323 ApplyImmediately: aws.Bool(d.Get("apply_immediately").(bool)), 324 ReplicationGroupId: aws.String(d.Id()), 325 } 326 327 if d.HasChange("replication_group_description") { 328 params.ReplicationGroupDescription = aws.String(d.Get("replication_group_description").(string)) 329 requestUpdate = true 330 } 331 332 if d.HasChange("automatic_failover_enabled") { 333 params.AutomaticFailoverEnabled = aws.Bool(d.Get("automatic_failover_enabled").(bool)) 334 requestUpdate = true 335 } 336 337 if d.HasChange("auto_minor_version_upgrade") { 338 params.AutoMinorVersionUpgrade = aws.Bool(d.Get("auto_minor_version_upgrade").(bool)) 339 requestUpdate = true 340 } 341 342 if d.HasChange("security_group_ids") { 343 if attr := d.Get("security_group_ids").(*schema.Set); attr.Len() > 0 { 344 params.SecurityGroupIds = expandStringList(attr.List()) 345 requestUpdate = true 346 } 347 } 348 349 if d.HasChange("security_group_names") { 350 if attr := d.Get("security_group_names").(*schema.Set); attr.Len() > 0 { 351 params.CacheSecurityGroupNames = expandStringList(attr.List()) 352 requestUpdate = true 353 } 354 } 355 356 if d.HasChange("maintenance_window") { 357 params.PreferredMaintenanceWindow = aws.String(d.Get("maintenance_window").(string)) 358 requestUpdate = true 359 } 360 361 if d.HasChange("notification_topic_arn") { 362 params.NotificationTopicArn = aws.String(d.Get("notification_topic_arn").(string)) 363 requestUpdate = true 364 } 365 366 if d.HasChange("parameter_group_name") { 367 params.CacheParameterGroupName = aws.String(d.Get("parameter_group_name").(string)) 368 requestUpdate = true 369 } 370 371 if d.HasChange("engine_version") { 372 params.EngineVersion = aws.String(d.Get("engine_version").(string)) 373 requestUpdate = true 374 } 375 376 if d.HasChange("snapshot_retention_limit") { 377 // This is a real hack to set the Snapshotting Cluster ID to be the first Cluster in the RG 378 o, _ := d.GetChange("snapshot_retention_limit") 379 if o.(int) == 0 { 380 params.SnapshottingClusterId = aws.String(fmt.Sprintf("%s-001", d.Id())) 381 } 382 383 params.SnapshotRetentionLimit = aws.Int64(int64(d.Get("snapshot_retention_limit").(int))) 384 requestUpdate = true 385 } 386 387 if d.HasChange("snapshot_window") { 388 params.SnapshotWindow = aws.String(d.Get("snapshot_window").(string)) 389 requestUpdate = true 390 } 391 392 if d.HasChange("node_type") { 393 params.CacheNodeType = aws.String(d.Get("node_type").(string)) 394 requestUpdate = true 395 } 396 397 if requestUpdate { 398 _, err := conn.ModifyReplicationGroup(params) 399 if err != nil { 400 return fmt.Errorf("Error updating Elasticache replication group: %s", err) 401 } 402 403 pending := []string{"creating", "modifying", "snapshotting"} 404 stateConf := &resource.StateChangeConf{ 405 Pending: pending, 406 Target: []string{"available"}, 407 Refresh: cacheReplicationGroupStateRefreshFunc(conn, d.Id(), "available", pending), 408 Timeout: 40 * time.Minute, 409 MinTimeout: 10 * time.Second, 410 Delay: 30 * time.Second, 411 } 412 413 log.Printf("[DEBUG] Waiting for state to become available: %v", d.Id()) 414 _, sterr := stateConf.WaitForState() 415 if sterr != nil { 416 return fmt.Errorf("Error waiting for elasticache replication group (%s) to be created: %s", d.Id(), sterr) 417 } 418 } 419 return resourceAwsElasticacheReplicationGroupRead(d, meta) 420 } 421 422 func resourceAwsElasticacheReplicationGroupDelete(d *schema.ResourceData, meta interface{}) error { 423 conn := meta.(*AWSClient).elasticacheconn 424 425 req := &elasticache.DeleteReplicationGroupInput{ 426 ReplicationGroupId: aws.String(d.Id()), 427 } 428 429 _, err := conn.DeleteReplicationGroup(req) 430 if err != nil { 431 if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "ReplicationGroupNotFoundFault" { 432 d.SetId("") 433 return nil 434 } 435 436 return fmt.Errorf("Error deleting Elasticache replication group: %s", err) 437 } 438 439 log.Printf("[DEBUG] Waiting for deletion: %v", d.Id()) 440 stateConf := &resource.StateChangeConf{ 441 Pending: []string{"creating", "available", "deleting"}, 442 Target: []string{}, 443 Refresh: cacheReplicationGroupStateRefreshFunc(conn, d.Id(), "", []string{}), 444 Timeout: 40 * time.Minute, 445 MinTimeout: 10 * time.Second, 446 Delay: 30 * time.Second, 447 } 448 449 _, sterr := stateConf.WaitForState() 450 if sterr != nil { 451 return fmt.Errorf("Error waiting for replication group (%s) to delete: %s", d.Id(), sterr) 452 } 453 454 return nil 455 } 456 457 func cacheReplicationGroupStateRefreshFunc(conn *elasticache.ElastiCache, replicationGroupId, givenState string, pending []string) resource.StateRefreshFunc { 458 return func() (interface{}, string, error) { 459 resp, err := conn.DescribeReplicationGroups(&elasticache.DescribeReplicationGroupsInput{ 460 ReplicationGroupId: aws.String(replicationGroupId), 461 }) 462 if err != nil { 463 if eccErr, ok := err.(awserr.Error); ok && eccErr.Code() == "ReplicationGroupNotFoundFault" { 464 log.Printf("[DEBUG] Replication Group Not Found") 465 return nil, "", nil 466 } 467 468 log.Printf("[ERROR] cacheClusterReplicationGroupStateRefreshFunc: %s", err) 469 return nil, "", err 470 } 471 472 if len(resp.ReplicationGroups) == 0 { 473 return nil, "", fmt.Errorf("[WARN] Error: no Cache Replication Groups found for id (%s)", replicationGroupId) 474 } 475 476 var rg *elasticache.ReplicationGroup 477 for _, replicationGroup := range resp.ReplicationGroups { 478 if *replicationGroup.ReplicationGroupId == replicationGroupId { 479 log.Printf("[DEBUG] Found matching ElastiCache Replication Group: %s", *replicationGroup.ReplicationGroupId) 480 rg = replicationGroup 481 } 482 } 483 484 if rg == nil { 485 return nil, "", fmt.Errorf("[WARN] Error: no matching ElastiCache Replication Group for id (%s)", replicationGroupId) 486 } 487 488 log.Printf("[DEBUG] ElastiCache Replication Group (%s) status: %v", replicationGroupId, *rg.Status) 489 490 // return the current state if it's in the pending array 491 for _, p := range pending { 492 log.Printf("[DEBUG] ElastiCache: checking pending state (%s) for Replication Group (%s), Replication Group status: %s", pending, replicationGroupId, *rg.Status) 493 s := *rg.Status 494 if p == s { 495 log.Printf("[DEBUG] Return with status: %v", *rg.Status) 496 return s, p, nil 497 } 498 } 499 500 return rg, *rg.Status, nil 501 } 502 } 503 504 func validateAwsElastiCacheReplicationGroupEngine(v interface{}, k string) (ws []string, errors []error) { 505 if strings.ToLower(v.(string)) != "redis" { 506 errors = append(errors, fmt.Errorf("The only acceptable Engine type when using Replication Groups is Redis")) 507 } 508 return 509 } 510 511 func validateAwsElastiCacheReplicationGroupId(v interface{}, k string) (ws []string, errors []error) { 512 value := v.(string) 513 if (len(value) < 1) || (len(value) > 20) { 514 errors = append(errors, fmt.Errorf( 515 "%q must contain from 1 to 20 alphanumeric characters or hyphens", k)) 516 } 517 if !regexp.MustCompile(`^[0-9a-zA-Z-]+$`).MatchString(value) { 518 errors = append(errors, fmt.Errorf( 519 "only alphanumeric characters and hyphens allowed in %q", k)) 520 } 521 if !regexp.MustCompile(`^[a-z]`).MatchString(value) { 522 errors = append(errors, fmt.Errorf( 523 "first character of %q must be a letter", k)) 524 } 525 if regexp.MustCompile(`--`).MatchString(value) { 526 errors = append(errors, fmt.Errorf( 527 "%q cannot contain two consecutive hyphens", k)) 528 } 529 if regexp.MustCompile(`-$`).MatchString(value) { 530 errors = append(errors, fmt.Errorf( 531 "%q cannot end with a hyphen", k)) 532 } 533 return 534 }