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  }