github.com/danp/terraform@v0.9.5-0.20170426144147-39d740081351/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  }