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