github.com/econnell/terraform@v0.5.4-0.20150722160631-78eb236786a4/builtin/providers/aws/resource_aws_elasticache_cluster.go (about)

     1  package aws
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"sort"
     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/aws/awsutil"
    13  	"github.com/aws/aws-sdk-go/service/elasticache"
    14  	"github.com/aws/aws-sdk-go/service/iam"
    15  	"github.com/hashicorp/terraform/helper/hashcode"
    16  	"github.com/hashicorp/terraform/helper/resource"
    17  	"github.com/hashicorp/terraform/helper/schema"
    18  )
    19  
    20  func resourceAwsElasticacheCluster() *schema.Resource {
    21  	return &schema.Resource{
    22  		Create: resourceAwsElasticacheClusterCreate,
    23  		Read:   resourceAwsElasticacheClusterRead,
    24  		Update: resourceAwsElasticacheClusterUpdate,
    25  		Delete: resourceAwsElasticacheClusterDelete,
    26  
    27  		Schema: map[string]*schema.Schema{
    28  			"cluster_id": &schema.Schema{
    29  				Type:     schema.TypeString,
    30  				Required: true,
    31  				ForceNew: true,
    32  			},
    33  			"engine": &schema.Schema{
    34  				Type:     schema.TypeString,
    35  				Required: true,
    36  			},
    37  			"node_type": &schema.Schema{
    38  				Type:     schema.TypeString,
    39  				Required: true,
    40  				ForceNew: true,
    41  			},
    42  			"num_cache_nodes": &schema.Schema{
    43  				Type:     schema.TypeInt,
    44  				Required: true,
    45  			},
    46  			"parameter_group_name": &schema.Schema{
    47  				Type:     schema.TypeString,
    48  				Optional: true,
    49  				Computed: true,
    50  			},
    51  			"port": &schema.Schema{
    52  				Type:     schema.TypeInt,
    53  				Required: true,
    54  				ForceNew: true,
    55  			},
    56  			"engine_version": &schema.Schema{
    57  				Type:     schema.TypeString,
    58  				Optional: true,
    59  				Computed: true,
    60  			},
    61  			"maintenance_window": &schema.Schema{
    62  				Type:     schema.TypeString,
    63  				Optional: true,
    64  				Computed: true,
    65  			},
    66  			"subnet_group_name": &schema.Schema{
    67  				Type:     schema.TypeString,
    68  				Optional: true,
    69  				Computed: true,
    70  				ForceNew: true,
    71  			},
    72  			"security_group_names": &schema.Schema{
    73  				Type:     schema.TypeSet,
    74  				Optional: true,
    75  				Computed: true,
    76  				ForceNew: true,
    77  				Elem:     &schema.Schema{Type: schema.TypeString},
    78  				Set: func(v interface{}) int {
    79  					return hashcode.String(v.(string))
    80  				},
    81  			},
    82  			"security_group_ids": &schema.Schema{
    83  				Type:     schema.TypeSet,
    84  				Optional: true,
    85  				Computed: true,
    86  				Elem:     &schema.Schema{Type: schema.TypeString},
    87  				Set: func(v interface{}) int {
    88  					return hashcode.String(v.(string))
    89  				},
    90  			},
    91  			// Exported Attributes
    92  			"cache_nodes": &schema.Schema{
    93  				Type:     schema.TypeList,
    94  				Computed: true,
    95  				Elem: &schema.Resource{
    96  					Schema: map[string]*schema.Schema{
    97  						"id": &schema.Schema{
    98  							Type:     schema.TypeString,
    99  							Computed: true,
   100  						},
   101  						"address": &schema.Schema{
   102  							Type:     schema.TypeString,
   103  							Computed: true,
   104  						},
   105  						"port": &schema.Schema{
   106  							Type:     schema.TypeInt,
   107  							Computed: true,
   108  						},
   109  					},
   110  				},
   111  			},
   112  
   113  			// A single-element string list containing an Amazon Resource Name (ARN) that
   114  			// uniquely identifies a Redis RDB snapshot file stored in Amazon S3. The snapshot
   115  			// file will be used to populate the node group.
   116  			//
   117  			// See also:
   118  			// https://github.com/aws/aws-sdk-go/blob/4862a174f7fc92fb523fc39e68f00b87d91d2c3d/service/elasticache/api.go#L2079
   119  			"snapshot_arns": &schema.Schema{
   120  				Type:     schema.TypeSet,
   121  				Optional: true,
   122  				ForceNew: true,
   123  				Elem:     &schema.Schema{Type: schema.TypeString},
   124  				Set: func(v interface{}) int {
   125  					return hashcode.String(v.(string))
   126  				},
   127  			},
   128  
   129  			"tags": tagsSchema(),
   130  
   131  			// apply_immediately is used to determine when the update modifications
   132  			// take place.
   133  			// See http://docs.aws.amazon.com/AmazonElastiCache/latest/APIReference/API_ModifyCacheCluster.html
   134  			"apply_immediately": &schema.Schema{
   135  				Type:     schema.TypeBool,
   136  				Optional: true,
   137  				Computed: true,
   138  			},
   139  		},
   140  	}
   141  }
   142  
   143  func resourceAwsElasticacheClusterCreate(d *schema.ResourceData, meta interface{}) error {
   144  	conn := meta.(*AWSClient).elasticacheconn
   145  
   146  	clusterId := d.Get("cluster_id").(string)
   147  	nodeType := d.Get("node_type").(string)           // e.g) cache.m1.small
   148  	numNodes := int64(d.Get("num_cache_nodes").(int)) // 2
   149  	engine := d.Get("engine").(string)                // memcached
   150  	engineVersion := d.Get("engine_version").(string) // 1.4.14
   151  	port := int64(d.Get("port").(int))                // e.g) 11211
   152  	subnetGroupName := d.Get("subnet_group_name").(string)
   153  	securityNameSet := d.Get("security_group_names").(*schema.Set)
   154  	securityIdSet := d.Get("security_group_ids").(*schema.Set)
   155  
   156  	securityNames := expandStringList(securityNameSet.List())
   157  	securityIds := expandStringList(securityIdSet.List())
   158  
   159  	tags := tagsFromMapEC(d.Get("tags").(map[string]interface{}))
   160  	req := &elasticache.CreateCacheClusterInput{
   161  		CacheClusterID:          aws.String(clusterId),
   162  		CacheNodeType:           aws.String(nodeType),
   163  		NumCacheNodes:           aws.Long(numNodes),
   164  		Engine:                  aws.String(engine),
   165  		EngineVersion:           aws.String(engineVersion),
   166  		Port:                    aws.Long(port),
   167  		CacheSubnetGroupName:    aws.String(subnetGroupName),
   168  		CacheSecurityGroupNames: securityNames,
   169  		SecurityGroupIDs:        securityIds,
   170  		Tags:                    tags,
   171  	}
   172  
   173  	// parameter groups are optional and can be defaulted by AWS
   174  	if v, ok := d.GetOk("parameter_group_name"); ok {
   175  		req.CacheParameterGroupName = aws.String(v.(string))
   176  	}
   177  
   178  	if v, ok := d.GetOk("maintenance_window"); ok {
   179  		req.PreferredMaintenanceWindow = aws.String(v.(string))
   180  	}
   181  
   182  	snaps := d.Get("snapshot_arns").(*schema.Set).List()
   183  	if len(snaps) > 0 {
   184  		s := expandStringList(snaps)
   185  		req.SnapshotARNs = s
   186  		log.Printf("[DEBUG] Restoring Redis cluster from S3 snapshot: %#v", s)
   187  	}
   188  
   189  	_, err := conn.CreateCacheCluster(req)
   190  	if err != nil {
   191  		return fmt.Errorf("Error creating Elasticache: %s", err)
   192  	}
   193  
   194  	pending := []string{"creating"}
   195  	stateConf := &resource.StateChangeConf{
   196  		Pending:    pending,
   197  		Target:     "available",
   198  		Refresh:    cacheClusterStateRefreshFunc(conn, d.Id(), "available", pending),
   199  		Timeout:    10 * time.Minute,
   200  		Delay:      10 * time.Second,
   201  		MinTimeout: 3 * time.Second,
   202  	}
   203  
   204  	log.Printf("[DEBUG] Waiting for state to become available: %v", d.Id())
   205  	_, sterr := stateConf.WaitForState()
   206  	if sterr != nil {
   207  		return fmt.Errorf("Error waiting for elasticache (%s) to be created: %s", d.Id(), sterr)
   208  	}
   209  
   210  	d.SetId(clusterId)
   211  
   212  	return resourceAwsElasticacheClusterRead(d, meta)
   213  }
   214  
   215  func resourceAwsElasticacheClusterRead(d *schema.ResourceData, meta interface{}) error {
   216  	conn := meta.(*AWSClient).elasticacheconn
   217  	req := &elasticache.DescribeCacheClustersInput{
   218  		CacheClusterID:    aws.String(d.Id()),
   219  		ShowCacheNodeInfo: aws.Boolean(true),
   220  	}
   221  
   222  	res, err := conn.DescribeCacheClusters(req)
   223  	if err != nil {
   224  		return err
   225  	}
   226  
   227  	if len(res.CacheClusters) == 1 {
   228  		c := res.CacheClusters[0]
   229  		d.Set("cluster_id", c.CacheClusterID)
   230  		d.Set("node_type", c.CacheNodeType)
   231  		d.Set("num_cache_nodes", c.NumCacheNodes)
   232  		d.Set("engine", c.Engine)
   233  		d.Set("engine_version", c.EngineVersion)
   234  		if c.ConfigurationEndpoint != nil {
   235  			d.Set("port", c.ConfigurationEndpoint.Port)
   236  		}
   237  		d.Set("subnet_group_name", c.CacheSubnetGroupName)
   238  		d.Set("security_group_names", c.CacheSecurityGroups)
   239  		d.Set("security_group_ids", c.SecurityGroups)
   240  		d.Set("parameter_group_name", c.CacheParameterGroup)
   241  		d.Set("maintenance_window", c.PreferredMaintenanceWindow)
   242  
   243  		if err := setCacheNodeData(d, c); err != nil {
   244  			return err
   245  		}
   246  		// list tags for resource
   247  		// set tags
   248  		arn, err := buildECARN(d, meta)
   249  		if err != nil {
   250  			log.Printf("[DEBUG] Error building ARN for ElastiCache Cluster, not setting Tags for cluster %s", *c.CacheClusterID)
   251  		} else {
   252  			resp, err := conn.ListTagsForResource(&elasticache.ListTagsForResourceInput{
   253  				ResourceName: aws.String(arn),
   254  			})
   255  
   256  			if err != nil {
   257  				log.Printf("[DEBUG] Error retreiving tags for ARN: %s", arn)
   258  			}
   259  
   260  			var et []*elasticache.Tag
   261  			if len(resp.TagList) > 0 {
   262  				et = resp.TagList
   263  			}
   264  			d.Set("tags", tagsToMapEC(et))
   265  		}
   266  	}
   267  
   268  	return nil
   269  }
   270  
   271  func resourceAwsElasticacheClusterUpdate(d *schema.ResourceData, meta interface{}) error {
   272  	conn := meta.(*AWSClient).elasticacheconn
   273  	arn, err := buildECARN(d, meta)
   274  	if err != nil {
   275  		log.Printf("[DEBUG] Error building ARN for ElastiCache Cluster, not updating Tags for cluster %s", d.Id())
   276  	} else {
   277  		if err := setTagsEC(conn, d, arn); err != nil {
   278  			return err
   279  		}
   280  	}
   281  
   282  	req := &elasticache.ModifyCacheClusterInput{
   283  		CacheClusterID:   aws.String(d.Id()),
   284  		ApplyImmediately: aws.Boolean(d.Get("apply_immediately").(bool)),
   285  	}
   286  
   287  	requestUpdate := false
   288  	if d.HasChange("security_group_ids") {
   289  		if attr := d.Get("security_group_ids").(*schema.Set); attr.Len() > 0 {
   290  			req.SecurityGroupIDs = expandStringList(attr.List())
   291  			requestUpdate = true
   292  		}
   293  	}
   294  
   295  	if d.HasChange("parameter_group_name") {
   296  		req.CacheParameterGroupName = aws.String(d.Get("parameter_group_name").(string))
   297  		requestUpdate = true
   298  	}
   299  
   300  	if d.HasChange("maintenance_window") {
   301  		req.PreferredMaintenanceWindow = aws.String(d.Get("maintenance_window").(string))
   302  		requestUpdate = true
   303  	}
   304  
   305  	if d.HasChange("engine_version") {
   306  		req.EngineVersion = aws.String(d.Get("engine_version").(string))
   307  		requestUpdate = true
   308  	}
   309  
   310  	if d.HasChange("num_cache_nodes") {
   311  		req.NumCacheNodes = aws.Long(int64(d.Get("num_cache_nodes").(int)))
   312  		requestUpdate = true
   313  	}
   314  
   315  	if requestUpdate {
   316  		log.Printf("[DEBUG] Modifying ElastiCache Cluster (%s), opts:\n%s", d.Id(), awsutil.StringValue(req))
   317  		_, err := conn.ModifyCacheCluster(req)
   318  		if err != nil {
   319  			return fmt.Errorf("[WARN] Error updating ElastiCache cluster (%s), error: %s", d.Id(), err)
   320  		}
   321  
   322  		log.Printf("[DEBUG] Waiting for update: %s", d.Id())
   323  		pending := []string{"modifying", "rebooting cache cluster nodes", "snapshotting"}
   324  		stateConf := &resource.StateChangeConf{
   325  			Pending:    pending,
   326  			Target:     "available",
   327  			Refresh:    cacheClusterStateRefreshFunc(conn, d.Id(), "available", pending),
   328  			Timeout:    5 * time.Minute,
   329  			Delay:      5 * time.Second,
   330  			MinTimeout: 3 * time.Second,
   331  		}
   332  
   333  		_, sterr := stateConf.WaitForState()
   334  		if sterr != nil {
   335  			return fmt.Errorf("Error waiting for elasticache (%s) to update: %s", d.Id(), sterr)
   336  		}
   337  	}
   338  
   339  	return resourceAwsElasticacheClusterRead(d, meta)
   340  }
   341  
   342  func setCacheNodeData(d *schema.ResourceData, c *elasticache.CacheCluster) error {
   343  	sortedCacheNodes := make([]*elasticache.CacheNode, len(c.CacheNodes))
   344  	copy(sortedCacheNodes, c.CacheNodes)
   345  	sort.Sort(byCacheNodeId(sortedCacheNodes))
   346  
   347  	cacheNodeData := make([]map[string]interface{}, 0, len(sortedCacheNodes))
   348  
   349  	for _, node := range sortedCacheNodes {
   350  		if node.CacheNodeID == nil || node.Endpoint == nil || node.Endpoint.Address == nil || node.Endpoint.Port == nil {
   351  			return fmt.Errorf("Unexpected nil pointer in: %s", awsutil.StringValue(node))
   352  		}
   353  		cacheNodeData = append(cacheNodeData, map[string]interface{}{
   354  			"id":      *node.CacheNodeID,
   355  			"address": *node.Endpoint.Address,
   356  			"port":    int(*node.Endpoint.Port),
   357  		})
   358  	}
   359  
   360  	return d.Set("cache_nodes", cacheNodeData)
   361  }
   362  
   363  type byCacheNodeId []*elasticache.CacheNode
   364  
   365  func (b byCacheNodeId) Len() int      { return len(b) }
   366  func (b byCacheNodeId) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
   367  func (b byCacheNodeId) Less(i, j int) bool {
   368  	return b[i].CacheNodeID != nil && b[j].CacheNodeID != nil &&
   369  		*b[i].CacheNodeID < *b[j].CacheNodeID
   370  }
   371  
   372  func resourceAwsElasticacheClusterDelete(d *schema.ResourceData, meta interface{}) error {
   373  	conn := meta.(*AWSClient).elasticacheconn
   374  
   375  	req := &elasticache.DeleteCacheClusterInput{
   376  		CacheClusterID: aws.String(d.Id()),
   377  	}
   378  	_, err := conn.DeleteCacheCluster(req)
   379  	if err != nil {
   380  		return err
   381  	}
   382  
   383  	log.Printf("[DEBUG] Waiting for deletion: %v", d.Id())
   384  	stateConf := &resource.StateChangeConf{
   385  		Pending:    []string{"creating", "available", "deleting", "incompatible-parameters", "incompatible-network", "restore-failed"},
   386  		Target:     "",
   387  		Refresh:    cacheClusterStateRefreshFunc(conn, d.Id(), "", []string{}),
   388  		Timeout:    10 * time.Minute,
   389  		Delay:      10 * time.Second,
   390  		MinTimeout: 3 * time.Second,
   391  	}
   392  
   393  	_, sterr := stateConf.WaitForState()
   394  	if sterr != nil {
   395  		return fmt.Errorf("Error waiting for elasticache (%s) to delete: %s", d.Id(), sterr)
   396  	}
   397  
   398  	d.SetId("")
   399  
   400  	return nil
   401  }
   402  
   403  func cacheClusterStateRefreshFunc(conn *elasticache.ElastiCache, clusterID, givenState string, pending []string) resource.StateRefreshFunc {
   404  	return func() (interface{}, string, error) {
   405  		resp, err := conn.DescribeCacheClusters(&elasticache.DescribeCacheClustersInput{
   406  			CacheClusterID:    aws.String(clusterID),
   407  			ShowCacheNodeInfo: aws.Boolean(true),
   408  		})
   409  		if err != nil {
   410  			apierr := err.(awserr.Error)
   411  			log.Printf("[DEBUG] message: %v, code: %v", apierr.Message(), apierr.Code())
   412  			if apierr.Message() == fmt.Sprintf("CacheCluster not found: %v", clusterID) {
   413  				log.Printf("[DEBUG] Detect deletion")
   414  				return nil, "", nil
   415  			}
   416  
   417  			log.Printf("[ERROR] CacheClusterStateRefreshFunc: %s", err)
   418  			return nil, "", err
   419  		}
   420  
   421  		c := resp.CacheClusters[0]
   422  		log.Printf("[DEBUG] status: %v", *c.CacheClusterStatus)
   423  
   424  		// return the current state if it's in the pending array
   425  		for _, p := range pending {
   426  			s := *c.CacheClusterStatus
   427  			if p == s {
   428  				log.Printf("[DEBUG] Return with status: %v", *c.CacheClusterStatus)
   429  				return c, p, nil
   430  			}
   431  		}
   432  
   433  		// return given state if it's not in pending
   434  		if givenState != "" {
   435  			// check to make sure we have the node count we're expecting
   436  			if int64(len(c.CacheNodes)) != *c.NumCacheNodes {
   437  				log.Printf("[DEBUG] Node count is not what is expected: %d found, %d expected", len(c.CacheNodes), *c.NumCacheNodes)
   438  				return nil, "creating", nil
   439  			}
   440  			// loop the nodes and check their status as well
   441  			for _, n := range c.CacheNodes {
   442  				if n.CacheNodeStatus != nil && *n.CacheNodeStatus != "available" {
   443  					log.Printf("[DEBUG] Node (%s) is not yet available, status: %s", *n.CacheNodeID, *n.CacheNodeStatus)
   444  					return nil, "creating", nil
   445  				}
   446  			}
   447  			return c, givenState, nil
   448  		}
   449  		log.Printf("[DEBUG] current status: %v", *c.CacheClusterStatus)
   450  		return c, *c.CacheClusterStatus, nil
   451  	}
   452  }
   453  
   454  func buildECARN(d *schema.ResourceData, meta interface{}) (string, error) {
   455  	iamconn := meta.(*AWSClient).iamconn
   456  	region := meta.(*AWSClient).region
   457  	// An zero value GetUserInput{} defers to the currently logged in user
   458  	resp, err := iamconn.GetUser(&iam.GetUserInput{})
   459  	if err != nil {
   460  		return "", err
   461  	}
   462  	userARN := *resp.User.ARN
   463  	accountID := strings.Split(userARN, ":")[4]
   464  	arn := fmt.Sprintf("arn:aws:elasticache:%s:%s:cluster:%s", region, accountID, d.Id())
   465  	return arn, nil
   466  }