Skip to content

Commit

Permalink
Merge pull request #18920 from hashicorp/elasticache-redis-6
Browse files Browse the repository at this point in the history
elasticache: Adds Redis 6.x support
  • Loading branch information
gdavison authored Apr 23, 2021
2 parents 354c2aa + 71baa83 commit 2e6e9bf
Show file tree
Hide file tree
Showing 13 changed files with 483 additions and 127 deletions.
11 changes: 11 additions & 0 deletions .changelog/18920.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
```release-note:bug
resource/aws_elasticache_cluster: Allows specifying Redis 6.x
```

```release-note:bug
resource/aws_elasticache_replication_group: Allows specifying Redis 6.x
```

```release-note:enhancement
resource/aws_elasticache_global_replication_group: Adds parameter `engine_version_actual` to match other ElastiCache resources
```
156 changes: 156 additions & 0 deletions aws/elasticache_validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package aws

import (
"context"
"errors"
"fmt"
"regexp"

"github.com/aws/aws-sdk-go/service/elasticache"
multierror "github.com/hashicorp/go-multierror"
gversion "github.com/hashicorp/go-version"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
tfelasticache "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/elasticache"
)

const (
redisVersionPreV6RegexpRaw = `[1-5](\.[[:digit:]]+){2}`
redisVersionPostV6RegexpRaw = `([6-9]|[[:digit:]]{2})\.x`

redisVersionRegexpRaw = redisVersionPreV6RegexpRaw + "|" + redisVersionPostV6RegexpRaw
)

const (
redisVersionRegexpPattern = "^" + redisVersionRegexpRaw + "$"
redisVersionPostV6RegexpPattern = "^" + redisVersionPostV6RegexpRaw + "$"
)

var (
redisVersionRegexp = regexp.MustCompile(redisVersionRegexpPattern)
redisVersionPostV6Regexp = regexp.MustCompile(redisVersionPostV6RegexpPattern)
)

func ValidateElastiCacheRedisVersionString(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)

if !redisVersionRegexp.MatchString(value) {
errors = append(errors, fmt.Errorf("%s: Redis versions must match <major>.x when using version 6 or higher, or <major>.<minor>.<bug-fix>", k))
}

return
}

// NormalizeElastiCacheEngineVersion returns a github.com/hashicorp/go-version Version
// that can handle a regular 1.2.3 version number or a 6.x version number used for
// ElastiCache Redis version 6 and higher
func NormalizeElastiCacheEngineVersion(version string) (*gversion.Version, error) {
if matches := redisVersionPostV6Regexp.FindStringSubmatch(version); matches != nil {
version = matches[1]
}
return gversion.NewVersion(version)
}

// CustomizeDiffElastiCacheEngineVersion causes re-creation of the resource if the version is being downgraded
func CustomizeDiffElastiCacheEngineVersion(_ context.Context, diff *schema.ResourceDiff, v interface{}) error {
if diff.Id() == "" || !diff.HasChange("engine_version") {
return nil
}

o, n := diff.GetChange("engine_version")
oVersion, err := NormalizeElastiCacheEngineVersion(o.(string))
if err != nil {
return fmt.Errorf("error parsing old engine_version: %w", err)
}
nVersion, err := NormalizeElastiCacheEngineVersion(n.(string))
if err != nil {
return fmt.Errorf("error parsing new engine_version: %w", err)
}

if nVersion.GreaterThan(oVersion) {
return nil
}

return diff.ForceNew("engine_version")
}

// CustomizeDiffValidateClusterAZMode validates that `num_cache_nodes` is greater than 1 when `az_mode` is "cross-az"
func CustomizeDiffValidateClusterAZMode(_ context.Context, diff *schema.ResourceDiff, v interface{}) error {
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`)
}

// CustomizeDiffValidateClusterEngineVersion validates the correct format for `engine_version`, based on `engine`
func CustomizeDiffValidateClusterEngineVersion(_ context.Context, diff *schema.ResourceDiff, v interface{}) error {
// Memcached: Versions in format <major>.<minor>.<bug fix>
// Redis: Starting with version 6, must match <major>.x, prior to version 6, <major>.<minor>.<bug fix>
engineVersion, ok := diff.GetOk("engine_version")
if !ok {
return nil
}

var validator schema.SchemaValidateFunc
if v, ok := diff.GetOk("engine"); !ok || v.(string) == tfelasticache.EngineMemcached {
validator = validateVersionString
} else {
validator = ValidateElastiCacheRedisVersionString
}

_, errs := validator(engineVersion, "engine_version")

var err *multierror.Error
err = multierror.Append(err, errs...)
return err.ErrorOrNil()
}

// CustomizeDiffValidateClusterNumCacheNodes validates that `num_cache_nodes` is 1 when `engine` is "redis"
func CustomizeDiffValidateClusterNumCacheNodes(_ context.Context, diff *schema.ResourceDiff, v interface{}) error {
if v, ok := diff.GetOk("engine"); !ok || v.(string) == tfelasticache.EngineMemcached {
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`)
}

// CustomizeDiffClusterMemcachedNodeType causes re-creation when `node_type` is changed and `engine` is "memcached"
func CustomizeDiffClusterMemcachedNodeType(_ context.Context, diff *schema.ResourceDiff, v interface{}) error {
// Engine memcached does not currently support vertical scaling
// https://docs.aws.amazon.com/AmazonElastiCache/latest/mem-ug/Scaling.html#Scaling.Memcached.Vertically
if diff.Id() == "" || !diff.HasChange("node_type") {
return nil
}
if v, ok := diff.GetOk("engine"); !ok || v.(string) == tfelasticache.EngineRedis {
return nil
}
return diff.ForceNew("node_type")
}

// CustomizeDiffValidateClusterMemcachedSnapshotIdentifier validates that `final_snapshot_identifier` is not set when `engine` is "memcached"
func CustomizeDiffValidateClusterMemcachedSnapshotIdentifier(_ context.Context, diff *schema.ResourceDiff, v interface{}) error {
if v, ok := diff.GetOk("engine"); !ok || v.(string) == tfelasticache.EngineRedis {
return nil
}
if _, ok := diff.GetOk("final_snapshot_identifier"); !ok {
return nil
}
return errors.New(`engine "memcached" does not support final_snapshot_identifier`)
}

// CustomizeDiffValidateReplicationGroupAutomaticFailover validates that `automatic_failover_enabled` is set when `multi_az_enabled` is true
func CustomizeDiffValidateReplicationGroupAutomaticFailover(_ context.Context, diff *schema.ResourceDiff, v interface{}) error {
if v := diff.Get("multi_az_enabled").(bool); !v {
return nil
}
if v := diff.Get("automatic_failover_enabled").(bool); !v {
return errors.New(`automatic_failover_enabled must be true if multi_az_enabled is true`)
}
return nil
}
14 changes: 14 additions & 0 deletions aws/internal/service/elasticache/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package elasticache

const (
EngineMemcached = "memcached"
EngineRedis = "redis"
)

// Engine_Values returns all elements of the Engine enum
func Engine_Values() []string {
return []string{
EngineMemcached,
EngineRedis,
}
}
145 changes: 64 additions & 81 deletions aws/resource_aws_elasticache_cluster.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package aws

import (
"context"
"errors"
"fmt"
"log"
Expand All @@ -19,6 +18,7 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags"
tfelasticache "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/elasticache"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/service/elasticache/finder"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/service/elasticache/waiter"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource"
Expand Down Expand Up @@ -115,14 +115,19 @@ func resourceAwsElasticacheCluster() *schema.Resource {
Computed: true,
},
"engine": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ValidateFunc: validation.StringInSlice(tfelasticache.Engine_Values(), false),
},
"engine_version": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"engine_version": {
"engine_version_actual": {
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"maintenance_window": {
Expand Down Expand Up @@ -257,71 +262,12 @@ func resourceAwsElasticacheCluster() *schema.Resource {
},

CustomizeDiff: customdiff.Sequence(
func(_ context.Context, 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(_ context.Context, 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(_ context.Context, 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(_ context.Context, 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/mem-ug/Scaling.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")
},
func(_ context.Context, diff *schema.ResourceDiff, v interface{}) error {
if v, ok := diff.GetOk("engine"); !ok || v.(string) == "redis" {
return nil
}
if _, ok := diff.GetOk("final_snapshot_identifier"); !ok {
return nil
}
return errors.New(`engine "memcached" does not support final_snapshot_identifier`)
},
SetTagsDiff,
CustomizeDiffValidateClusterAZMode,
CustomizeDiffValidateClusterEngineVersion,
CustomizeDiffElastiCacheEngineVersion,
CustomizeDiffValidateClusterNumCacheNodes,
CustomizeDiffClusterMemcachedNodeType,
CustomizeDiffValidateClusterMemcachedSnapshotIdentifier,
),
}
}
Expand Down Expand Up @@ -443,10 +389,16 @@ func resourceAwsElasticacheClusterRead(d *schema.ResourceData, meta interface{})
}

d.Set("cluster_id", c.CacheClusterId)
d.Set("node_type", c.CacheNodeType)

if err := elasticacheSetResourceDataFromCacheCluster(d, c); err != nil {
return err
}

d.Set("snapshot_window", c.SnapshotWindow)
d.Set("snapshot_retention_limit", c.SnapshotRetentionLimit)

d.Set("num_cache_nodes", c.NumCacheNodes)
d.Set("engine", c.Engine)
d.Set("engine_version", c.EngineVersion)

if c.ConfigurationEndpoint != nil {
d.Set("port", c.ConfigurationEndpoint.Port)
d.Set("configuration_endpoint", aws.String(fmt.Sprintf("%s:%d", aws.StringValue(c.ConfigurationEndpoint.Address), aws.Int64Value(c.ConfigurationEndpoint.Port))))
Expand All @@ -459,15 +411,6 @@ func resourceAwsElasticacheClusterRead(d *schema.ResourceData, meta interface{})
d.Set("replication_group_id", c.ReplicationGroupId)
}

d.Set("subnet_group_name", c.CacheSubnetGroupName)
d.Set("security_group_names", flattenElastiCacheSecurityGroupNames(c.CacheSecurityGroups))
d.Set("security_group_ids", flattenElastiCacheSecurityGroupIds(c.SecurityGroups))
if c.CacheParameterGroup != nil {
d.Set("parameter_group_name", c.CacheParameterGroup.CacheParameterGroupName)
}
d.Set("maintenance_window", c.PreferredMaintenanceWindow)
d.Set("snapshot_window", c.SnapshotWindow)
d.Set("snapshot_retention_limit", c.SnapshotRetentionLimit)
if c.NotificationConfiguration != nil {
if *c.NotificationConfiguration.TopicStatus == "active" {
d.Set("notification_topic_arn", c.NotificationConfiguration.TopicArn)
Expand Down Expand Up @@ -506,6 +449,46 @@ func resourceAwsElasticacheClusterRead(d *schema.ResourceData, meta interface{})
return nil
}

func elasticacheSetResourceDataFromCacheCluster(d *schema.ResourceData, c *elasticache.CacheCluster) error {
d.Set("node_type", c.CacheNodeType)

d.Set("engine", c.Engine)
if err := elasticacheSetResourceDataEngineVersionFromCacheCluster(d, c); err != nil {
return err
}

d.Set("subnet_group_name", c.CacheSubnetGroupName)
if err := d.Set("security_group_names", flattenElastiCacheSecurityGroupNames(c.CacheSecurityGroups)); err != nil {
return fmt.Errorf("error setting security_group_names: %w", err)
}
if err := d.Set("security_group_ids", flattenElastiCacheSecurityGroupIds(c.SecurityGroups)); err != nil {
return fmt.Errorf("error setting security_group_ids: %w", err)
}

if c.CacheParameterGroup != nil {
d.Set("parameter_group_name", c.CacheParameterGroup.CacheParameterGroupName)
}

d.Set("maintenance_window", c.PreferredMaintenanceWindow)

return nil
}

func elasticacheSetResourceDataEngineVersionFromCacheCluster(d *schema.ResourceData, c *elasticache.CacheCluster) error {
engineVersion, err := gversion.NewVersion(aws.StringValue(c.EngineVersion))
if err != nil {
return fmt.Errorf("error reading ElastiCache Cache Cluster (%s) engine version: %w", d.Id(), err)
}
if engineVersion.Segments()[0] < 6 {
d.Set("engine_version", engineVersion.String())
} else {
d.Set("engine_version", fmt.Sprintf("%d.x", engineVersion.Segments()[0]))
}
d.Set("engine_version_actual", engineVersion.String())

return nil
}

func resourceAwsElasticacheClusterUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).elasticacheconn

Expand Down
Loading

0 comments on commit 2e6e9bf

Please sign in to comment.