github.com/bendemaree/terraform@v0.5.4-0.20150613200311-f50d97d6eee6/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  			"engine": &schema.Schema{
    33  				Type:     schema.TypeString,
    34  				Required: true,
    35  				ForceNew: 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  				ForceNew: true,
    46  			},
    47  			"parameter_group_name": &schema.Schema{
    48  				Type:     schema.TypeString,
    49  				Optional: true,
    50  				Computed: true,
    51  				ForceNew: true,
    52  			},
    53  			"port": &schema.Schema{
    54  				Type:     schema.TypeInt,
    55  				Required: true,
    56  				ForceNew: true,
    57  			},
    58  			"engine_version": &schema.Schema{
    59  				Type:     schema.TypeString,
    60  				Optional: true,
    61  				Computed: true,
    62  				ForceNew: true,
    63  			},
    64  			"subnet_group_name": &schema.Schema{
    65  				Type:     schema.TypeString,
    66  				Optional: true,
    67  				Computed: true,
    68  				ForceNew: true,
    69  			},
    70  			"security_group_names": &schema.Schema{
    71  				Type:     schema.TypeSet,
    72  				Optional: true,
    73  				Computed: true,
    74  				ForceNew: true,
    75  				Elem:     &schema.Schema{Type: schema.TypeString},
    76  				Set: func(v interface{}) int {
    77  					return hashcode.String(v.(string))
    78  				},
    79  			},
    80  			"security_group_ids": &schema.Schema{
    81  				Type:     schema.TypeSet,
    82  				Optional: true,
    83  				Computed: true,
    84  				ForceNew: true,
    85  				Elem:     &schema.Schema{Type: schema.TypeString},
    86  				Set: func(v interface{}) int {
    87  					return hashcode.String(v.(string))
    88  				},
    89  			},
    90  			// Exported Attributes
    91  			"cache_nodes": &schema.Schema{
    92  				Type:     schema.TypeList,
    93  				Computed: true,
    94  				Elem: &schema.Resource{
    95  					Schema: map[string]*schema.Schema{
    96  						"id": &schema.Schema{
    97  							Type:     schema.TypeString,
    98  							Computed: true,
    99  						},
   100  						"address": &schema.Schema{
   101  							Type:     schema.TypeString,
   102  							Computed: true,
   103  						},
   104  						"port": &schema.Schema{
   105  							Type:     schema.TypeInt,
   106  							Computed: true,
   107  						},
   108  					},
   109  				},
   110  			},
   111  
   112  			"tags": tagsSchema(),
   113  		},
   114  	}
   115  }
   116  
   117  func resourceAwsElasticacheClusterCreate(d *schema.ResourceData, meta interface{}) error {
   118  	conn := meta.(*AWSClient).elasticacheconn
   119  
   120  	clusterId := d.Get("cluster_id").(string)
   121  	nodeType := d.Get("node_type").(string)           // e.g) cache.m1.small
   122  	numNodes := int64(d.Get("num_cache_nodes").(int)) // 2
   123  	engine := d.Get("engine").(string)                // memcached
   124  	engineVersion := d.Get("engine_version").(string) // 1.4.14
   125  	port := int64(d.Get("port").(int))                // e.g) 11211
   126  	subnetGroupName := d.Get("subnet_group_name").(string)
   127  	securityNameSet := d.Get("security_group_names").(*schema.Set)
   128  	securityIdSet := d.Get("security_group_ids").(*schema.Set)
   129  
   130  	securityNames := expandStringList(securityNameSet.List())
   131  	securityIds := expandStringList(securityIdSet.List())
   132  
   133  	tags := tagsFromMapEC(d.Get("tags").(map[string]interface{}))
   134  	req := &elasticache.CreateCacheClusterInput{
   135  		CacheClusterID:          aws.String(clusterId),
   136  		CacheNodeType:           aws.String(nodeType),
   137  		NumCacheNodes:           aws.Long(numNodes),
   138  		Engine:                  aws.String(engine),
   139  		EngineVersion:           aws.String(engineVersion),
   140  		Port:                    aws.Long(port),
   141  		CacheSubnetGroupName:    aws.String(subnetGroupName),
   142  		CacheSecurityGroupNames: securityNames,
   143  		SecurityGroupIDs:        securityIds,
   144  		Tags:                    tags,
   145  	}
   146  
   147  	// parameter groups are optional and can be defaulted by AWS
   148  	if v, ok := d.GetOk("parameter_group_name"); ok {
   149  		req.CacheParameterGroupName = aws.String(v.(string))
   150  	}
   151  
   152  	_, err := conn.CreateCacheCluster(req)
   153  	if err != nil {
   154  		return fmt.Errorf("Error creating Elasticache: %s", err)
   155  	}
   156  
   157  	pending := []string{"creating"}
   158  	stateConf := &resource.StateChangeConf{
   159  		Pending:    pending,
   160  		Target:     "available",
   161  		Refresh:    CacheClusterStateRefreshFunc(conn, d.Id(), "available", pending),
   162  		Timeout:    10 * time.Minute,
   163  		Delay:      10 * time.Second,
   164  		MinTimeout: 3 * time.Second,
   165  	}
   166  
   167  	log.Printf("[DEBUG] Waiting for state to become available: %v", d.Id())
   168  	_, sterr := stateConf.WaitForState()
   169  	if sterr != nil {
   170  		return fmt.Errorf("Error waiting for elasticache (%s) to be created: %s", d.Id(), sterr)
   171  	}
   172  
   173  	d.SetId(clusterId)
   174  
   175  	return resourceAwsElasticacheClusterRead(d, meta)
   176  }
   177  
   178  func resourceAwsElasticacheClusterRead(d *schema.ResourceData, meta interface{}) error {
   179  	conn := meta.(*AWSClient).elasticacheconn
   180  	req := &elasticache.DescribeCacheClustersInput{
   181  		CacheClusterID:    aws.String(d.Id()),
   182  		ShowCacheNodeInfo: aws.Boolean(true),
   183  	}
   184  
   185  	res, err := conn.DescribeCacheClusters(req)
   186  	if err != nil {
   187  		return err
   188  	}
   189  
   190  	if len(res.CacheClusters) == 1 {
   191  		c := res.CacheClusters[0]
   192  		d.Set("cluster_id", c.CacheClusterID)
   193  		d.Set("node_type", c.CacheNodeType)
   194  		d.Set("num_cache_nodes", c.NumCacheNodes)
   195  		d.Set("engine", c.Engine)
   196  		d.Set("engine_version", c.EngineVersion)
   197  		if c.ConfigurationEndpoint != nil {
   198  			d.Set("port", c.ConfigurationEndpoint.Port)
   199  		}
   200  		d.Set("subnet_group_name", c.CacheSubnetGroupName)
   201  		d.Set("security_group_names", c.CacheSecurityGroups)
   202  		d.Set("security_group_ids", c.SecurityGroups)
   203  		d.Set("parameter_group_name", c.CacheParameterGroup)
   204  
   205  		if err := setCacheNodeData(d, c); err != nil {
   206  			return err
   207  		}
   208  		// list tags for resource
   209  		// set tags
   210  		arn, err := buildECARN(d, meta)
   211  		if err != nil {
   212  			log.Printf("[DEBUG] Error building ARN for ElastiCache Cluster, not setting Tags for cluster %s", *c.CacheClusterID)
   213  		} else {
   214  			resp, err := conn.ListTagsForResource(&elasticache.ListTagsForResourceInput{
   215  				ResourceName: aws.String(arn),
   216  			})
   217  
   218  			if err != nil {
   219  				log.Printf("[DEBUG] Error retreiving tags for ARN: %s", arn)
   220  			}
   221  
   222  			var et []*elasticache.Tag
   223  			if len(resp.TagList) > 0 {
   224  				et = resp.TagList
   225  			}
   226  			d.Set("tags", tagsToMapEC(et))
   227  		}
   228  	}
   229  
   230  	return nil
   231  }
   232  
   233  func resourceAwsElasticacheClusterUpdate(d *schema.ResourceData, meta interface{}) error {
   234  	conn := meta.(*AWSClient).elasticacheconn
   235  	arn, err := buildECARN(d, meta)
   236  	if err != nil {
   237  		log.Printf("[DEBUG] Error building ARN for ElastiCache Cluster, not updating Tags for cluster %s", d.Id())
   238  	} else {
   239  		if err := setTagsEC(conn, d, arn); err != nil {
   240  			return err
   241  		}
   242  	}
   243  	return resourceAwsElasticacheClusterRead(d, meta)
   244  }
   245  
   246  func setCacheNodeData(d *schema.ResourceData, c *elasticache.CacheCluster) error {
   247  	sortedCacheNodes := make([]*elasticache.CacheNode, len(c.CacheNodes))
   248  	copy(sortedCacheNodes, c.CacheNodes)
   249  	sort.Sort(byCacheNodeId(sortedCacheNodes))
   250  
   251  	cacheNodeData := make([]map[string]interface{}, 0, len(sortedCacheNodes))
   252  
   253  	for _, node := range sortedCacheNodes {
   254  		if node.CacheNodeID == nil || node.Endpoint == nil || node.Endpoint.Address == nil || node.Endpoint.Port == nil {
   255  			return fmt.Errorf("Unexpected nil pointer in: %#v", node)
   256  		}
   257  		cacheNodeData = append(cacheNodeData, map[string]interface{}{
   258  			"id":      *node.CacheNodeID,
   259  			"address": *node.Endpoint.Address,
   260  			"port":    int(*node.Endpoint.Port),
   261  		})
   262  	}
   263  
   264  	return d.Set("cache_nodes", cacheNodeData)
   265  }
   266  
   267  type byCacheNodeId []*elasticache.CacheNode
   268  
   269  func (b byCacheNodeId) Len() int      { return len(b) }
   270  func (b byCacheNodeId) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
   271  func (b byCacheNodeId) Less(i, j int) bool {
   272  	return b[i].CacheNodeID != nil && b[j].CacheNodeID != nil &&
   273  		*b[i].CacheNodeID < *b[j].CacheNodeID
   274  }
   275  
   276  func resourceAwsElasticacheClusterDelete(d *schema.ResourceData, meta interface{}) error {
   277  	conn := meta.(*AWSClient).elasticacheconn
   278  
   279  	req := &elasticache.DeleteCacheClusterInput{
   280  		CacheClusterID: aws.String(d.Id()),
   281  	}
   282  	_, err := conn.DeleteCacheCluster(req)
   283  	if err != nil {
   284  		return err
   285  	}
   286  
   287  	log.Printf("[DEBUG] Waiting for deletion: %v", d.Id())
   288  	stateConf := &resource.StateChangeConf{
   289  		Pending:    []string{"creating", "available", "deleting", "incompatible-parameters", "incompatible-network", "restore-failed"},
   290  		Target:     "",
   291  		Refresh:    CacheClusterStateRefreshFunc(conn, d.Id(), "", []string{}),
   292  		Timeout:    10 * time.Minute,
   293  		Delay:      10 * time.Second,
   294  		MinTimeout: 3 * time.Second,
   295  	}
   296  
   297  	_, sterr := stateConf.WaitForState()
   298  	if sterr != nil {
   299  		return fmt.Errorf("Error waiting for elasticache (%s) to delete: %s", d.Id(), sterr)
   300  	}
   301  
   302  	d.SetId("")
   303  
   304  	return nil
   305  }
   306  
   307  func CacheClusterStateRefreshFunc(conn *elasticache.ElastiCache, clusterID, givenState string, pending []string) resource.StateRefreshFunc {
   308  	return func() (interface{}, string, error) {
   309  		resp, err := conn.DescribeCacheClusters(&elasticache.DescribeCacheClustersInput{
   310  			CacheClusterID:    aws.String(clusterID),
   311  			ShowCacheNodeInfo: aws.Boolean(true),
   312  		})
   313  		if err != nil {
   314  			apierr := err.(awserr.Error)
   315  			log.Printf("[DEBUG] message: %v, code: %v", apierr.Message(), apierr.Code())
   316  			if apierr.Message() == fmt.Sprintf("CacheCluster not found: %v", clusterID) {
   317  				log.Printf("[DEBUG] Detect deletion")
   318  				return nil, "", nil
   319  			}
   320  
   321  			log.Printf("[ERROR] CacheClusterStateRefreshFunc: %s", err)
   322  			return nil, "", err
   323  		}
   324  
   325  		c := resp.CacheClusters[0]
   326  		log.Printf("[DEBUG] status: %v", *c.CacheClusterStatus)
   327  
   328  		// return the current state if it's in the pending array
   329  		for _, p := range pending {
   330  			s := *c.CacheClusterStatus
   331  			if p == s {
   332  				log.Printf("[DEBUG] Return with status: %v", *c.CacheClusterStatus)
   333  				return c, p, nil
   334  			}
   335  		}
   336  
   337  		// return given state if it's not in pending
   338  		if givenState != "" {
   339  			// check to make sure we have the node count we're expecting
   340  			if int64(len(c.CacheNodes)) != *c.NumCacheNodes {
   341  				log.Printf("[DEBUG] Node count is not what is expected: %d found, %d expected", len(c.CacheNodes), *c.NumCacheNodes)
   342  				return nil, "creating", nil
   343  			}
   344  			// loop the nodes and check their status as well
   345  			for _, n := range c.CacheNodes {
   346  				if n.CacheNodeStatus != nil && *n.CacheNodeStatus != "available" {
   347  					log.Printf("[DEBUG] Node (%s) is not yet available, status: %s", *n.CacheNodeID, *n.CacheNodeStatus)
   348  					return nil, "creating", nil
   349  				}
   350  			}
   351  			return c, givenState, nil
   352  		}
   353  		log.Printf("[DEBUG] current status: %v", *c.CacheClusterStatus)
   354  		return c, *c.CacheClusterStatus, nil
   355  	}
   356  }
   357  
   358  func buildECARN(d *schema.ResourceData, meta interface{}) (string, error) {
   359  	iamconn := meta.(*AWSClient).iamconn
   360  	region := meta.(*AWSClient).region
   361  	// An zero value GetUserInput{} defers to the currently logged in user
   362  	resp, err := iamconn.GetUser(&iam.GetUserInput{})
   363  	if err != nil {
   364  		return "", err
   365  	}
   366  	userARN := *resp.User.ARN
   367  	accountID := strings.Split(userARN, ":")[4]
   368  	arn := fmt.Sprintf("arn:aws:elasticache:%s:%s:cluster:%s", region, accountID, d.Id())
   369  	return arn, nil
   370  }