diff --git a/aws/resource_aws_elasticache_cluster.go b/aws/resource_aws_elasticache_cluster.go index 9e778189b49b..78011b4b7690 100644 --- a/aws/resource_aws_elasticache_cluster.go +++ b/aws/resource_aws_elasticache_cluster.go @@ -1,6 +1,7 @@ package aws import ( + "errors" "fmt" "log" "sort" @@ -10,6 +11,8 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/elasticache" + gversion "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform/helper/customdiff" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/validation" @@ -151,6 +154,10 @@ func resourceAwsElasticacheCluster() *schema.Resource { Type: schema.TypeString, Optional: true, Computed: true, + ValidateFunc: validation.StringInSlice([]string{ + elasticache.AZModeCrossAz, + elasticache.AZModeSingleAz, + }, false), } resourceSchema["availability_zone"] = &schema.Schema{ @@ -210,6 +217,85 @@ func resourceAwsElasticacheCluster() *schema.Resource { }, Schema: resourceSchema, + + CustomizeDiff: customdiff.Sequence( + func(diff *schema.ResourceDiff, v interface{}) error { + // Plan time validation for az_mode + // InvalidParameterCombination: Must specify at least two cache nodes in order to specify AZ Mode of 'cross-az'. + if v, ok := diff.GetOk("az_mode"); !ok || v.(string) != elasticache.AZModeCrossAz { + return nil + } + if v, ok := diff.GetOk("num_cache_nodes"); !ok || v.(int) != 1 { + return nil + } + return errors.New(`az_mode "cross-az" is not supported with num_cache_nodes = 1`) + }, + func(diff *schema.ResourceDiff, v interface{}) error { + // Plan time validation for engine_version + // InvalidParameterCombination: Cannot modify memcached from 1.4.33 to 1.4.24 + // InvalidParameterCombination: Cannot modify redis from 3.2.6 to 3.2.4 + if diff.Id() == "" || !diff.HasChange("engine_version") { + return nil + } + o, n := diff.GetChange("engine_version") + oVersion, err := gversion.NewVersion(o.(string)) + if err != nil { + return err + } + nVersion, err := gversion.NewVersion(n.(string)) + if err != nil { + return err + } + if nVersion.GreaterThan(oVersion) { + return nil + } + return diff.ForceNew("engine_version") + }, + func(diff *schema.ResourceDiff, v interface{}) error { + // Plan time validation for node_type + // InvalidParameterCombination: Instance type cache.t2.micro can only be created in a VPC. + nodeType, nodeTypeOk := diff.GetOk("node_type") + if !nodeTypeOk { + return nil + } + vpcOnlyNodeTypes := []string{ + "cache.t2.micro", + "cache.t2.small", + "cache.t2.medium", + } + if _, ok := diff.GetOk("subnet_group_name"); !ok { + for _, vpcOnlyNodeType := range vpcOnlyNodeTypes { + if nodeType == vpcOnlyNodeType { + return fmt.Errorf("node_type %q can only be created in a VPC", nodeType) + } + } + } + return nil + }, + func(diff *schema.ResourceDiff, v interface{}) error { + // Plan time validation for num_cache_nodes + // InvalidParameterValue: Cannot create a Redis cluster with a NumCacheNodes parameter greater than 1. + if v, ok := diff.GetOk("engine"); !ok || v.(string) == "memcached" { + return nil + } + if v, ok := diff.GetOk("num_cache_nodes"); !ok || v.(int) == 1 { + return nil + } + return errors.New(`engine "redis" does not support num_cache_nodes > 1`) + }, + func(diff *schema.ResourceDiff, v interface{}) error { + // Engine memcached does not currently support vertical scaling + // InvalidParameterCombination: Scaling is not supported for engine memcached + // https://docs.aws.amazon.com/AmazonElastiCache/latest/UserGuide/Scaling.Memcached.html#Scaling.Memcached.Vertically + if diff.Id() == "" || !diff.HasChange("node_type") { + return nil + } + if v, ok := diff.GetOk("engine"); !ok || v.(string) == "redis" { + return nil + } + return diff.ForceNew("node_type") + }, + ), } } @@ -480,9 +566,6 @@ func resourceAwsElasticacheClusterUpdate(d *schema.ResourceData, meta interface{ oraw, nraw := d.GetChange("num_cache_nodes") o := oraw.(int) n := nraw.(int) - if v, ok := d.GetOk("az_mode"); ok && v.(string) == "cross-az" && n == 1 { - return fmt.Errorf("[WARN] Error updateing Elasticache cluster (%s), error: Cross-AZ mode is not supported in a single cache node.", d.Id()) - } if n < o { log.Printf("[INFO] Cluster %s is marked for Decreasing cache nodes from %d to %d", d.Id(), o, n) nodesToRemove := getCacheNodesToRemove(d, o, o-n) diff --git a/aws/resource_aws_elasticache_cluster_test.go b/aws/resource_aws_elasticache_cluster_test.go index 8dbe2da6d32a..25957192a7e8 100644 --- a/aws/resource_aws_elasticache_cluster_test.go +++ b/aws/resource_aws_elasticache_cluster_test.go @@ -1,7 +1,10 @@ package aws import ( + "errors" "fmt" + "os" + "regexp" "strings" "testing" @@ -152,6 +155,268 @@ func TestAccAWSElasticacheCluster_multiAZInVpc(t *testing.T) { }) } +func TestAccAWSElasticacheCluster_AZMode_Memcached_Ec2Classic(t *testing.T) { + oldvar := os.Getenv("AWS_DEFAULT_REGION") + os.Setenv("AWS_DEFAULT_REGION", "us-east-1") + defer os.Setenv("AWS_DEFAULT_REGION", oldvar) + + var cluster elasticache.CacheCluster + rName := fmt.Sprintf("tf-acc-test-%s", acctest.RandString(8)) + resourceName := "aws_elasticache_cluster.bar" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccEC2ClassicPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSElasticacheClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSElasticacheClusterConfig_AZMode_Memcached_Ec2Classic(rName, "unknown"), + ExpectError: regexp.MustCompile(`expected az_mode to be one of .*, got unknown`), + }, + { + Config: testAccAWSElasticacheClusterConfig_AZMode_Memcached_Ec2Classic(rName, "cross-az"), + ExpectError: regexp.MustCompile(`az_mode "cross-az" is not supported with num_cache_nodes = 1`), + }, + { + Config: testAccAWSElasticacheClusterConfig_AZMode_Memcached_Ec2Classic(rName, "single-az"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSElasticacheClusterExists(resourceName, &cluster), + resource.TestCheckResourceAttr(resourceName, "az_mode", "single-az"), + ), + }, + { + Config: testAccAWSElasticacheClusterConfig_AZMode_Memcached_Ec2Classic(rName, "cross-az"), + ExpectError: regexp.MustCompile(`az_mode "cross-az" is not supported with num_cache_nodes = 1`), + }, + }, + }) +} + +func TestAccAWSElasticacheCluster_AZMode_Redis_Ec2Classic(t *testing.T) { + oldvar := os.Getenv("AWS_DEFAULT_REGION") + os.Setenv("AWS_DEFAULT_REGION", "us-east-1") + defer os.Setenv("AWS_DEFAULT_REGION", oldvar) + + var cluster elasticache.CacheCluster + rName := fmt.Sprintf("tf-acc-test-%s", acctest.RandString(8)) + resourceName := "aws_elasticache_cluster.bar" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccEC2ClassicPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSElasticacheClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSElasticacheClusterConfig_AZMode_Redis_Ec2Classic(rName, "unknown"), + ExpectError: regexp.MustCompile(`expected az_mode to be one of .*, got unknown`), + }, + { + Config: testAccAWSElasticacheClusterConfig_AZMode_Redis_Ec2Classic(rName, "cross-az"), + ExpectError: regexp.MustCompile(`az_mode "cross-az" is not supported with num_cache_nodes = 1`), + }, + { + Config: testAccAWSElasticacheClusterConfig_AZMode_Redis_Ec2Classic(rName, "single-az"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSElasticacheClusterExists(resourceName, &cluster), + resource.TestCheckResourceAttr(resourceName, "az_mode", "single-az"), + ), + }, + }, + }) +} + +func TestAccAWSElasticacheCluster_EngineVersion_Memcached_Ec2Classic(t *testing.T) { + oldvar := os.Getenv("AWS_DEFAULT_REGION") + os.Setenv("AWS_DEFAULT_REGION", "us-east-1") + defer os.Setenv("AWS_DEFAULT_REGION", oldvar) + + var pre, mid, post elasticache.CacheCluster + rName := fmt.Sprintf("tf-acc-test-%s", acctest.RandString(8)) + resourceName := "aws_elasticache_cluster.bar" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccEC2ClassicPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSElasticacheClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSElasticacheClusterConfig_EngineVersion_Memcached_Ec2Classic(rName, "1.4.33"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSElasticacheClusterExists(resourceName, &pre), + resource.TestCheckResourceAttr(resourceName, "engine_version", "1.4.33"), + ), + }, + { + Config: testAccAWSElasticacheClusterConfig_EngineVersion_Memcached_Ec2Classic(rName, "1.4.24"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSElasticacheClusterExists(resourceName, &mid), + testAccCheckAWSElasticacheClusterRecreated(&pre, &mid), + resource.TestCheckResourceAttr(resourceName, "engine_version", "1.4.24"), + ), + }, + { + Config: testAccAWSElasticacheClusterConfig_EngineVersion_Memcached_Ec2Classic(rName, "1.4.34"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSElasticacheClusterExists(resourceName, &post), + testAccCheckAWSElasticacheClusterNotRecreated(&mid, &post), + resource.TestCheckResourceAttr(resourceName, "engine_version", "1.4.34"), + ), + }, + }, + }) +} + +func TestAccAWSElasticacheCluster_EngineVersion_Redis_Ec2Classic(t *testing.T) { + oldvar := os.Getenv("AWS_DEFAULT_REGION") + os.Setenv("AWS_DEFAULT_REGION", "us-east-1") + defer os.Setenv("AWS_DEFAULT_REGION", oldvar) + + var pre, mid, post elasticache.CacheCluster + rName := fmt.Sprintf("tf-acc-test-%s", acctest.RandString(8)) + resourceName := "aws_elasticache_cluster.bar" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccEC2ClassicPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSElasticacheClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSElasticacheClusterConfig_EngineVersion_Redis_Ec2Classic(rName, "3.2.6"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSElasticacheClusterExists(resourceName, &pre), + resource.TestCheckResourceAttr(resourceName, "engine_version", "3.2.6"), + ), + }, + { + Config: testAccAWSElasticacheClusterConfig_EngineVersion_Redis_Ec2Classic(rName, "3.2.4"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSElasticacheClusterExists(resourceName, &mid), + testAccCheckAWSElasticacheClusterRecreated(&pre, &mid), + resource.TestCheckResourceAttr(resourceName, "engine_version", "3.2.4"), + ), + }, + { + Config: testAccAWSElasticacheClusterConfig_EngineVersion_Redis_Ec2Classic(rName, "3.2.10"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSElasticacheClusterExists(resourceName, &post), + testAccCheckAWSElasticacheClusterNotRecreated(&mid, &post), + resource.TestCheckResourceAttr(resourceName, "engine_version", "3.2.10"), + ), + }, + }, + }) +} + +func TestAccAWSElasticacheCluster_NodeTypeResize_Memcached_Ec2Classic(t *testing.T) { + oldvar := os.Getenv("AWS_DEFAULT_REGION") + os.Setenv("AWS_DEFAULT_REGION", "us-east-1") + defer os.Setenv("AWS_DEFAULT_REGION", oldvar) + + var pre, post elasticache.CacheCluster + rName := fmt.Sprintf("tf-acc-test-%s", acctest.RandString(8)) + resourceName := "aws_elasticache_cluster.bar" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccEC2ClassicPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSElasticacheClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSElasticacheClusterConfig_NodeType_Memcached_Ec2Classic(rName, "cache.t2.micro"), + ExpectError: regexp.MustCompile(`node_type "cache.t2.micro" can only be created in a VPC`), + }, + { + Config: testAccAWSElasticacheClusterConfig_NodeType_Memcached_Ec2Classic(rName, "cache.t2.small"), + ExpectError: regexp.MustCompile(`node_type "cache.t2.small" can only be created in a VPC`), + }, + { + Config: testAccAWSElasticacheClusterConfig_NodeType_Memcached_Ec2Classic(rName, "cache.t2.medium"), + ExpectError: regexp.MustCompile(`node_type "cache.t2.medium" can only be created in a VPC`), + }, + { + Config: testAccAWSElasticacheClusterConfig_NodeType_Memcached_Ec2Classic(rName, "cache.m3.medium"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSElasticacheClusterExists(resourceName, &pre), + resource.TestCheckResourceAttr(resourceName, "node_type", "cache.m3.medium"), + ), + }, + { + Config: testAccAWSElasticacheClusterConfig_NodeType_Memcached_Ec2Classic(rName, "cache.m3.large"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSElasticacheClusterExists(resourceName, &post), + testAccCheckAWSElasticacheClusterRecreated(&pre, &post), + resource.TestCheckResourceAttr(resourceName, "node_type", "cache.m3.large"), + ), + }, + }, + }) +} + +func TestAccAWSElasticacheCluster_NodeTypeResize_Redis_Ec2Classic(t *testing.T) { + oldvar := os.Getenv("AWS_DEFAULT_REGION") + os.Setenv("AWS_DEFAULT_REGION", "us-east-1") + defer os.Setenv("AWS_DEFAULT_REGION", oldvar) + + var pre, post elasticache.CacheCluster + rName := fmt.Sprintf("tf-acc-test-%s", acctest.RandString(8)) + resourceName := "aws_elasticache_cluster.bar" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccEC2ClassicPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSElasticacheClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSElasticacheClusterConfig_NodeType_Redis_Ec2Classic(rName, "cache.t2.micro"), + ExpectError: regexp.MustCompile(`node_type "cache.t2.micro" can only be created in a VPC`), + }, + { + Config: testAccAWSElasticacheClusterConfig_NodeType_Redis_Ec2Classic(rName, "cache.t2.small"), + ExpectError: regexp.MustCompile(`node_type "cache.t2.small" can only be created in a VPC`), + }, + { + Config: testAccAWSElasticacheClusterConfig_NodeType_Redis_Ec2Classic(rName, "cache.t2.medium"), + ExpectError: regexp.MustCompile(`node_type "cache.t2.medium" can only be created in a VPC`), + }, + { + Config: testAccAWSElasticacheClusterConfig_NodeType_Redis_Ec2Classic(rName, "cache.m3.medium"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSElasticacheClusterExists(resourceName, &pre), + resource.TestCheckResourceAttr(resourceName, "node_type", "cache.m3.medium"), + ), + }, + { + Config: testAccAWSElasticacheClusterConfig_NodeType_Redis_Ec2Classic(rName, "cache.m3.large"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSElasticacheClusterExists(resourceName, &post), + testAccCheckAWSElasticacheClusterNotRecreated(&pre, &post), + resource.TestCheckResourceAttr(resourceName, "node_type", "cache.m3.large"), + ), + }, + }, + }) +} + +func TestAccAWSElasticacheCluster_NumCacheNodes_Redis_Ec2Classic(t *testing.T) { + oldvar := os.Getenv("AWS_DEFAULT_REGION") + os.Setenv("AWS_DEFAULT_REGION", "us-east-1") + defer os.Setenv("AWS_DEFAULT_REGION", oldvar) + + rName := fmt.Sprintf("tf-acc-test-%s", acctest.RandString(8)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccEC2ClassicPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSElasticacheClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSElasticacheClusterConfig_NumCacheNodes_Redis_Ec2Classic(rName, 2), + ExpectError: regexp.MustCompile(`engine "redis" does not support num_cache_nodes > 1`), + }, + }, + }) +} + func testAccCheckAWSElasticacheClusterAttributes(v *elasticache.CacheCluster) resource.TestCheckFunc { return func(s *terraform.State) error { if v.NotificationConfiguration == nil { @@ -166,6 +431,26 @@ func testAccCheckAWSElasticacheClusterAttributes(v *elasticache.CacheCluster) re } } +func testAccCheckAWSElasticacheClusterNotRecreated(i, j *elasticache.CacheCluster) resource.TestCheckFunc { + return func(s *terraform.State) error { + if aws.TimeValue(i.CacheClusterCreateTime) != aws.TimeValue(j.CacheClusterCreateTime) { + return errors.New("Elasticache Cluster was recreated") + } + + return nil + } +} + +func testAccCheckAWSElasticacheClusterRecreated(i, j *elasticache.CacheCluster) resource.TestCheckFunc { + return func(s *terraform.State) error { + if aws.TimeValue(i.CacheClusterCreateTime) == aws.TimeValue(j.CacheClusterCreateTime) { + return errors.New("Elasticache Cluster was not recreated") + } + + return nil + } +} + func testAccCheckAWSElasticacheClusterDestroy(s *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).elasticacheconn @@ -526,3 +811,105 @@ resource "aws_elasticache_cluster" "bar" { ] } `, acctest.RandInt(), acctest.RandInt(), acctest.RandString(10)) + +func testAccAWSElasticacheClusterConfig_AZMode_Memcached_Ec2Classic(rName, azMode string) string { + return fmt.Sprintf(` +resource "aws_elasticache_cluster" "bar" { + apply_immediately = true + az_mode = "%[2]s" + cluster_id = "%[1]s" + engine = "memcached" + node_type = "cache.m3.medium" + num_cache_nodes = 1 + parameter_group_name = "default.memcached1.4" + port = 11211 +} +`, rName, azMode) +} + +func testAccAWSElasticacheClusterConfig_AZMode_Redis_Ec2Classic(rName, azMode string) string { + return fmt.Sprintf(` +resource "aws_elasticache_cluster" "bar" { + apply_immediately = true + az_mode = "%[2]s" + cluster_id = "%[1]s" + engine = "redis" + node_type = "cache.m3.medium" + num_cache_nodes = 1 + parameter_group_name = "default.redis3.2" + port = 6379 +} +`, rName, azMode) +} + +func testAccAWSElasticacheClusterConfig_EngineVersion_Memcached_Ec2Classic(rName, engineVersion string) string { + return fmt.Sprintf(` +resource "aws_elasticache_cluster" "bar" { + apply_immediately = true + cluster_id = "%[1]s" + engine = "memcached" + engine_version = "%[2]s" + node_type = "cache.m3.medium" + num_cache_nodes = 1 + parameter_group_name = "default.memcached1.4" + port = 11211 +} +`, rName, engineVersion) +} + +func testAccAWSElasticacheClusterConfig_EngineVersion_Redis_Ec2Classic(rName, engineVersion string) string { + return fmt.Sprintf(` +resource "aws_elasticache_cluster" "bar" { + apply_immediately = true + cluster_id = "%[1]s" + engine = "redis" + engine_version = "%[2]s" + node_type = "cache.m3.medium" + num_cache_nodes = 1 + parameter_group_name = "default.redis3.2" + port = 6379 +} +`, rName, engineVersion) +} + +func testAccAWSElasticacheClusterConfig_NodeType_Memcached_Ec2Classic(rName, nodeType string) string { + return fmt.Sprintf(` +resource "aws_elasticache_cluster" "bar" { + apply_immediately = true + cluster_id = "%[1]s" + engine = "memcached" + node_type = "%[2]s" + num_cache_nodes = 1 + parameter_group_name = "default.memcached1.4" + port = 11211 +} +`, rName, nodeType) +} + +func testAccAWSElasticacheClusterConfig_NodeType_Redis_Ec2Classic(rName, nodeType string) string { + return fmt.Sprintf(` +resource "aws_elasticache_cluster" "bar" { + apply_immediately = true + cluster_id = "%[1]s" + engine = "redis" + node_type = "%[2]s" + num_cache_nodes = 1 + parameter_group_name = "default.redis3.2" + port = 6379 +} +`, rName, nodeType) +} + +func testAccAWSElasticacheClusterConfig_NumCacheNodes_Redis_Ec2Classic(rName string, numCacheNodes int) string { + return fmt.Sprintf(` +resource "aws_elasticache_cluster" "bar" { + apply_immediately = true + cluster_id = "%[1]s" + engine = "redis" + node_type = "cache.m3.medium" + num_cache_nodes = %[2]d + parameter_group_name = "default.redis3.2" + port = 6379 +} +`, rName, numCacheNodes) +}