github.com/jrperritt/terraform@v0.1.1-0.20170525065507-96f391dafc38/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  		cacheCluster := *rgp.NodeGroups[0].NodeGroupMembers[0]
   271  
   272  		res, err := conn.DescribeCacheClusters(&elasticache.DescribeCacheClustersInput{
   273  			CacheClusterId:    cacheCluster.CacheClusterId,
   274  			ShowCacheNodeInfo: aws.Bool(true),
   275  		})
   276  		if err != nil {
   277  			return err
   278  		}
   279  
   280  		if len(res.CacheClusters) == 0 {
   281  			return nil
   282  		}
   283  
   284  		c := res.CacheClusters[0]
   285  		d.Set("node_type", c.CacheNodeType)
   286  		d.Set("engine", c.Engine)
   287  		d.Set("engine_version", c.EngineVersion)
   288  		d.Set("subnet_group_name", c.CacheSubnetGroupName)
   289  		d.Set("security_group_names", flattenElastiCacheSecurityGroupNames(c.CacheSecurityGroups))
   290  		d.Set("security_group_ids", flattenElastiCacheSecurityGroupIds(c.SecurityGroups))
   291  
   292  		if c.CacheParameterGroup != nil {
   293  			d.Set("parameter_group_name", c.CacheParameterGroup.CacheParameterGroupName)
   294  		}
   295  
   296  		d.Set("maintenance_window", c.PreferredMaintenanceWindow)
   297  		d.Set("snapshot_window", rgp.SnapshotWindow)
   298  		d.Set("snapshot_retention_limit", rgp.SnapshotRetentionLimit)
   299  
   300  		if rgp.ConfigurationEndpoint != nil {
   301  			d.Set("port", rgp.ConfigurationEndpoint.Port)
   302  			d.Set("configuration_endpoint_address", rgp.ConfigurationEndpoint.Address)
   303  		} else {
   304  			d.Set("port", rgp.NodeGroups[0].PrimaryEndpoint.Port)
   305  			d.Set("primary_endpoint_address", rgp.NodeGroups[0].PrimaryEndpoint.Address)
   306  		}
   307  
   308  		d.Set("auto_minor_version_upgrade", c.AutoMinorVersionUpgrade)
   309  	}
   310  
   311  	return nil
   312  }
   313  
   314  func resourceAwsElasticacheReplicationGroupUpdate(d *schema.ResourceData, meta interface{}) error {
   315  	conn := meta.(*AWSClient).elasticacheconn
   316  
   317  	requestUpdate := false
   318  	params := &elasticache.ModifyReplicationGroupInput{
   319  		ApplyImmediately:   aws.Bool(d.Get("apply_immediately").(bool)),
   320  		ReplicationGroupId: aws.String(d.Id()),
   321  	}
   322  
   323  	if d.HasChange("replication_group_description") {
   324  		params.ReplicationGroupDescription = aws.String(d.Get("replication_group_description").(string))
   325  		requestUpdate = true
   326  	}
   327  
   328  	if d.HasChange("automatic_failover_enabled") {
   329  		params.AutomaticFailoverEnabled = aws.Bool(d.Get("automatic_failover_enabled").(bool))
   330  		requestUpdate = true
   331  	}
   332  
   333  	if d.HasChange("auto_minor_version_upgrade") {
   334  		params.AutoMinorVersionUpgrade = aws.Bool(d.Get("auto_minor_version_upgrade").(bool))
   335  		requestUpdate = true
   336  	}
   337  
   338  	if d.HasChange("security_group_ids") {
   339  		if attr := d.Get("security_group_ids").(*schema.Set); attr.Len() > 0 {
   340  			params.SecurityGroupIds = expandStringList(attr.List())
   341  			requestUpdate = true
   342  		}
   343  	}
   344  
   345  	if d.HasChange("security_group_names") {
   346  		if attr := d.Get("security_group_names").(*schema.Set); attr.Len() > 0 {
   347  			params.CacheSecurityGroupNames = expandStringList(attr.List())
   348  			requestUpdate = true
   349  		}
   350  	}
   351  
   352  	if d.HasChange("maintenance_window") {
   353  		params.PreferredMaintenanceWindow = aws.String(d.Get("maintenance_window").(string))
   354  		requestUpdate = true
   355  	}
   356  
   357  	if d.HasChange("notification_topic_arn") {
   358  		params.NotificationTopicArn = aws.String(d.Get("notification_topic_arn").(string))
   359  		requestUpdate = true
   360  	}
   361  
   362  	if d.HasChange("parameter_group_name") {
   363  		params.CacheParameterGroupName = aws.String(d.Get("parameter_group_name").(string))
   364  		requestUpdate = true
   365  	}
   366  
   367  	if d.HasChange("engine_version") {
   368  		params.EngineVersion = aws.String(d.Get("engine_version").(string))
   369  		requestUpdate = true
   370  	}
   371  
   372  	if d.HasChange("snapshot_retention_limit") {
   373  		// This is a real hack to set the Snapshotting Cluster ID to be the first Cluster in the RG
   374  		o, _ := d.GetChange("snapshot_retention_limit")
   375  		if o.(int) == 0 {
   376  			params.SnapshottingClusterId = aws.String(fmt.Sprintf("%s-001", d.Id()))
   377  		}
   378  
   379  		params.SnapshotRetentionLimit = aws.Int64(int64(d.Get("snapshot_retention_limit").(int)))
   380  		requestUpdate = true
   381  	}
   382  
   383  	if d.HasChange("snapshot_window") {
   384  		params.SnapshotWindow = aws.String(d.Get("snapshot_window").(string))
   385  		requestUpdate = true
   386  	}
   387  
   388  	if d.HasChange("node_type") {
   389  		params.CacheNodeType = aws.String(d.Get("node_type").(string))
   390  		requestUpdate = true
   391  	}
   392  
   393  	if requestUpdate {
   394  		_, err := conn.ModifyReplicationGroup(params)
   395  		if err != nil {
   396  			return fmt.Errorf("Error updating Elasticache replication group: %s", err)
   397  		}
   398  
   399  		pending := []string{"creating", "modifying", "snapshotting"}
   400  		stateConf := &resource.StateChangeConf{
   401  			Pending:    pending,
   402  			Target:     []string{"available"},
   403  			Refresh:    cacheReplicationGroupStateRefreshFunc(conn, d.Id(), "available", pending),
   404  			Timeout:    40 * time.Minute,
   405  			MinTimeout: 10 * time.Second,
   406  			Delay:      30 * time.Second,
   407  		}
   408  
   409  		log.Printf("[DEBUG] Waiting for state to become available: %v", d.Id())
   410  		_, sterr := stateConf.WaitForState()
   411  		if sterr != nil {
   412  			return fmt.Errorf("Error waiting for elasticache replication group (%s) to be created: %s", d.Id(), sterr)
   413  		}
   414  	}
   415  	return resourceAwsElasticacheReplicationGroupRead(d, meta)
   416  }
   417  
   418  func resourceAwsElasticacheReplicationGroupDelete(d *schema.ResourceData, meta interface{}) error {
   419  	conn := meta.(*AWSClient).elasticacheconn
   420  
   421  	req := &elasticache.DeleteReplicationGroupInput{
   422  		ReplicationGroupId: aws.String(d.Id()),
   423  	}
   424  
   425  	_, err := conn.DeleteReplicationGroup(req)
   426  	if err != nil {
   427  		if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "ReplicationGroupNotFoundFault" {
   428  			d.SetId("")
   429  			return nil
   430  		}
   431  
   432  		return fmt.Errorf("Error deleting Elasticache replication group: %s", err)
   433  	}
   434  
   435  	log.Printf("[DEBUG] Waiting for deletion: %v", d.Id())
   436  	stateConf := &resource.StateChangeConf{
   437  		Pending:    []string{"creating", "available", "deleting"},
   438  		Target:     []string{},
   439  		Refresh:    cacheReplicationGroupStateRefreshFunc(conn, d.Id(), "", []string{}),
   440  		Timeout:    40 * time.Minute,
   441  		MinTimeout: 10 * time.Second,
   442  		Delay:      30 * time.Second,
   443  	}
   444  
   445  	_, sterr := stateConf.WaitForState()
   446  	if sterr != nil {
   447  		return fmt.Errorf("Error waiting for replication group (%s) to delete: %s", d.Id(), sterr)
   448  	}
   449  
   450  	return nil
   451  }
   452  
   453  func cacheReplicationGroupStateRefreshFunc(conn *elasticache.ElastiCache, replicationGroupId, givenState string, pending []string) resource.StateRefreshFunc {
   454  	return func() (interface{}, string, error) {
   455  		resp, err := conn.DescribeReplicationGroups(&elasticache.DescribeReplicationGroupsInput{
   456  			ReplicationGroupId: aws.String(replicationGroupId),
   457  		})
   458  		if err != nil {
   459  			if eccErr, ok := err.(awserr.Error); ok && eccErr.Code() == "ReplicationGroupNotFoundFault" {
   460  				log.Printf("[DEBUG] Replication Group Not Found")
   461  				return nil, "", nil
   462  			}
   463  
   464  			log.Printf("[ERROR] cacheClusterReplicationGroupStateRefreshFunc: %s", err)
   465  			return nil, "", err
   466  		}
   467  
   468  		if len(resp.ReplicationGroups) == 0 {
   469  			return nil, "", fmt.Errorf("[WARN] Error: no Cache Replication Groups found for id (%s)", replicationGroupId)
   470  		}
   471  
   472  		var rg *elasticache.ReplicationGroup
   473  		for _, replicationGroup := range resp.ReplicationGroups {
   474  			if *replicationGroup.ReplicationGroupId == replicationGroupId {
   475  				log.Printf("[DEBUG] Found matching ElastiCache Replication Group: %s", *replicationGroup.ReplicationGroupId)
   476  				rg = replicationGroup
   477  			}
   478  		}
   479  
   480  		if rg == nil {
   481  			return nil, "", fmt.Errorf("[WARN] Error: no matching ElastiCache Replication Group for id (%s)", replicationGroupId)
   482  		}
   483  
   484  		log.Printf("[DEBUG] ElastiCache Replication Group (%s) status: %v", replicationGroupId, *rg.Status)
   485  
   486  		// return the current state if it's in the pending array
   487  		for _, p := range pending {
   488  			log.Printf("[DEBUG] ElastiCache: checking pending state (%s) for Replication Group (%s), Replication Group status: %s", pending, replicationGroupId, *rg.Status)
   489  			s := *rg.Status
   490  			if p == s {
   491  				log.Printf("[DEBUG] Return with status: %v", *rg.Status)
   492  				return s, p, nil
   493  			}
   494  		}
   495  
   496  		return rg, *rg.Status, nil
   497  	}
   498  }
   499  
   500  func validateAwsElastiCacheReplicationGroupEngine(v interface{}, k string) (ws []string, errors []error) {
   501  	if strings.ToLower(v.(string)) != "redis" {
   502  		errors = append(errors, fmt.Errorf("The only acceptable Engine type when using Replication Groups is Redis"))
   503  	}
   504  	return
   505  }
   506  
   507  func validateAwsElastiCacheReplicationGroupId(v interface{}, k string) (ws []string, errors []error) {
   508  	value := v.(string)
   509  	if (len(value) < 1) || (len(value) > 20) {
   510  		errors = append(errors, fmt.Errorf(
   511  			"%q must contain from 1 to 20 alphanumeric characters or hyphens", k))
   512  	}
   513  	if !regexp.MustCompile(`^[0-9a-zA-Z-]+$`).MatchString(value) {
   514  		errors = append(errors, fmt.Errorf(
   515  			"only alphanumeric characters and hyphens allowed in %q", k))
   516  	}
   517  	if !regexp.MustCompile(`^[a-z]`).MatchString(value) {
   518  		errors = append(errors, fmt.Errorf(
   519  			"first character of %q must be a letter", k))
   520  	}
   521  	if regexp.MustCompile(`--`).MatchString(value) {
   522  		errors = append(errors, fmt.Errorf(
   523  			"%q cannot contain two consecutive hyphens", k))
   524  	}
   525  	if regexp.MustCompile(`-$`).MatchString(value) {
   526  		errors = append(errors, fmt.Errorf(
   527  			"%q cannot end with a hyphen", k))
   528  	}
   529  	return
   530  }