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