Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

elasticache: Adds Redis 6.x support #18920

Merged
merged 7 commits into from
Apr 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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