Skip to content

Commit

Permalink
Merge pull request #29832 from mousavian/f-elasticache-reserved-cache…
Browse files Browse the repository at this point in the history
…-node

Added Support for ElastiCache Reserved Cache Nodes
  • Loading branch information
gdavison authored Sep 18, 2024
2 parents 5bdfd97 + 5e466ed commit 10f8b4c
Show file tree
Hide file tree
Showing 21 changed files with 1,197 additions and 4 deletions.
7 changes: 7 additions & 0 deletions .changelog/29832.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```release-note:new-resource
aws_elasticache_reserved_cache_node
```

```release-note:new-data-source
aws_elasticache_reserved_cache_node_offering
```
1 change: 1 addition & 0 deletions docs/acc-test-environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,4 @@ Environment variables (beyond standard AWS Go SDK ones) used by acceptance testi
| `TF_AWS_LICENSE_MANAGER_GRANT_LICENSE_ARN` | ARN for a License Manager license imported into the current account. |
| `TF_AWS_LICENSE_MANAGER_GRANT_PRINCIPAL` | ARN of a principal to share the License Manager license with. Either a root user, Organization, or Organizational Unit. |
| `TF_TEST_CLOUDFRONT_RETAIN` | Flag to disable but dangle CloudFront Distributions during testing to reduce feedback time (must be manually destroyed afterwards) |
| `TF_TEST_ELASTICACHE_RESERVED_CACHE_NODE` | Flag to enable resource tests for ElastiCache reserved nodes. Set to `1` to run tests |
15 changes: 15 additions & 0 deletions docs/data-handling-and-conversion.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,21 @@ type scheduleModel struct {
}
```

To ignore a field when flattening, but include it when expanding, use the option `noflatten`.

For example, from the struct `dataSourceReservedCacheNodeOfferingModel` for the ElastiCache Reserved Cache Node Offering:

```go
type dataSourceReservedCacheNodeOfferingModel struct {
CacheNodeType types.String `tfsdk:"cache_node_type"`
Duration fwtypes.RFC3339Duration `tfsdk:"duration" autoflex:",noflatten"`
FixedPrice types.Float64 `tfsdk:"fixed_price"`
OfferingID types.String `tfsdk:"offering_id"`
OfferingType types.String `tfsdk:"offering_type"`
ProductDescription types.String `tfsdk:"product_description"`
}
```

#### Overriding Default Behavior

In some cases, flattening and expanding need conditional handling.
Expand Down
7 changes: 7 additions & 0 deletions internal/framework/flex/autoflex.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,13 @@ func autoFlexConvertStruct(ctx context.Context, sourcePath path.Path, from any,
})
continue
}
if toOpts.NoFlatten() {
tflog.SubsystemTrace(ctx, subsystemName, "Skipping noflatten target field", map[string]any{
logAttrKeySourceFieldname: fieldName,
logAttrKeyTargetFieldname: toFieldName,
})
continue
}
if !toFieldVal.CanSet() {
// Corresponding field value can't be changed.
tflog.SubsystemDebug(ctx, subsystemName, "Field cannot be set", map[string]any{
Expand Down
4 changes: 4 additions & 0 deletions internal/framework/flex/tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,7 @@ func (o tagOptions) Legacy() bool {
func (o tagOptions) OmitEmpty() bool {
return o.Contains("omitempty")
}

func (o tagOptions) NoFlatten() bool {
return o.Contains("noflatten")
}
168 changes: 168 additions & 0 deletions internal/framework/types/rfc3339_duration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package types

import (
"context"
"fmt"
"time"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/attr/xattr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-go/tftypes"
"github.com/hashicorp/terraform-provider-aws/internal/types/duration"
)

var (
_ basetypes.StringTypable = (*rfc3339DurationType)(nil)
)

type rfc3339DurationType struct {
basetypes.StringType
}

var (
RFC3339DurationType = rfc3339DurationType{}
)

func (t rfc3339DurationType) Equal(o attr.Type) bool {
other, ok := o.(rfc3339DurationType)

if !ok {
return false
}

return t.StringType.Equal(other.StringType)
}

func (rfc3339DurationType) String() string {
return "RFC3339DurationType"
}

func (t rfc3339DurationType) ValueFromString(_ context.Context, in types.String) (basetypes.StringValuable, diag.Diagnostics) {
var diags diag.Diagnostics

if in.IsNull() {
return RFC3339DurationNull(), diags
}
if in.IsUnknown() {
return RFC3339DurationUnknown(), diags
}

valueString := in.ValueString()
if _, err := duration.Parse(valueString); err != nil {
return RFC3339DurationUnknown(), diags // Must not return validation errors
}

return RFC3339DurationValue(valueString), diags
}

func (t rfc3339DurationType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
attrValue, err := t.StringType.ValueFromTerraform(ctx, in)

if err != nil {
return nil, err
}

stringValue, ok := attrValue.(basetypes.StringValue)

if !ok {
return nil, fmt.Errorf("unexpected value type of %T", attrValue)
}

stringValuable, diags := t.ValueFromString(ctx, stringValue)

if diags.HasError() {
return nil, fmt.Errorf("unexpected error converting StringValue to StringValuable: %v", diags)
}

return stringValuable, nil
}

func (rfc3339DurationType) ValueType(context.Context) attr.Value {
return RFC3339Duration{}
}

var (
_ basetypes.StringValuable = (*RFC3339Duration)(nil)
_ xattr.ValidateableAttribute = (*RFC3339Duration)(nil)
)

func RFC3339DurationNull() RFC3339Duration {
return RFC3339Duration{StringValue: basetypes.NewStringNull()}
}

func RFC3339DurationUnknown() RFC3339Duration {
return RFC3339Duration{StringValue: basetypes.NewStringUnknown()}
}

// DurationValue initializes a new RFC3339Duration type with the provided value
//
// This function does not return diagnostics, and therefore invalid duration values
// are not handled during construction. Invalid values will be detected by the
// ValidateAttribute method, called by the ValidateResourceConfig RPC during
// operations like `terraform validate`, `plan`, or `apply`.
func RFC3339DurationValue(value string) RFC3339Duration {
// swallow any RFC3339Duration parsing errors here and just pass along the
// zero value duration.Duration. Invalid values will be handled downstream
// by the ValidateAttribute method.
v, _ := duration.Parse(value)

return RFC3339Duration{
StringValue: basetypes.NewStringValue(value),
value: v,
}
}

func RFC3339DurationTimeDurationValue(value time.Duration) RFC3339Duration {
v := duration.NewFromTimeDuration(value)

return RFC3339Duration{
StringValue: basetypes.NewStringValue(v.String()),
value: v,
}
}

type RFC3339Duration struct {
basetypes.StringValue
value duration.Duration
}

func (v RFC3339Duration) Equal(o attr.Value) bool {
other, ok := o.(RFC3339Duration)

if !ok {
return false
}

return v.StringValue.Equal(other.StringValue)
}

func (RFC3339Duration) Type(context.Context) attr.Type {
return RFC3339DurationType
}

// ValueDuration returns the known duration.Duration value. If RFC3339Duration is null or unknown, returns 0.
func (v RFC3339Duration) ValueDuration() duration.Duration {
return v.value
}

func (v RFC3339Duration) ValidateAttribute(ctx context.Context, req xattr.ValidateAttributeRequest, resp *xattr.ValidateAttributeResponse) {
if v.IsNull() || v.IsUnknown() {
return
}

if _, err := duration.Parse(v.ValueString()); err != nil {
resp.Diagnostics.AddAttributeError(
req.Path,
"Invalid Duration Value",
"The provided value cannot be parsed as a Duration.\n\n"+
"Path: "+req.Path.String()+"\n"+
"Error: "+err.Error(),
)
}
}
135 changes: 135 additions & 0 deletions internal/framework/types/rfc3339_duration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package types_test

import (
"context"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/attr/xattr"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-go/tftypes"
fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types"
)

func TestRFC3339DurationTypeValueFromTerraform(t *testing.T) {
t.Parallel()

tests := map[string]struct {
val tftypes.Value
expected attr.Value
}{
"null value": {
val: tftypes.NewValue(tftypes.String, nil),
expected: fwtypes.RFC3339DurationNull(),
},
"unknown value": {
val: tftypes.NewValue(tftypes.String, tftypes.UnknownValue),
expected: fwtypes.RFC3339DurationUnknown(),
},
"valid duration": {
val: tftypes.NewValue(tftypes.String, "P2Y"),
expected: fwtypes.RFC3339DurationValue("P2Y"),
},
"invalid duration": {
val: tftypes.NewValue(tftypes.String, "not ok"),
expected: fwtypes.RFC3339DurationUnknown(),
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()

ctx := context.Background()
val, err := fwtypes.RFC3339DurationType.ValueFromTerraform(ctx, test.val)

if err != nil {
t.Fatalf("got unexpected error: %s", err)
}

if diff := cmp.Diff(val, test.expected); diff != "" {
t.Errorf("unexpected diff (+wanted, -got): %s", diff)
}
})
}
}

func TestRFC3339DurationValidateAttribute(t *testing.T) {
t.Parallel()

type testCase struct {
val fwtypes.RFC3339Duration
expectError bool
}
tests := map[string]testCase{
"unknown": {
val: fwtypes.RFC3339DurationUnknown(),
},
"null": {
val: fwtypes.RFC3339DurationNull(),
},
"valid": {
val: fwtypes.RFC3339DurationValue("P2Y"),
},
"invalid": {
val: fwtypes.RFC3339DurationValue("not ok"),
expectError: true,
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()

ctx := context.Background()

req := xattr.ValidateAttributeRequest{}
resp := xattr.ValidateAttributeResponse{}

test.val.ValidateAttribute(ctx, req, &resp)
if resp.Diagnostics.HasError() != test.expectError {
t.Errorf("resp.Diagnostics.HasError() = %t, want = %t", resp.Diagnostics.HasError(), test.expectError)
}
})
}
}

func TestRFC3339DurationToStringValue(t *testing.T) {
t.Parallel()

tests := map[string]struct {
duration fwtypes.RFC3339Duration
expected types.String
}{
"value": {
duration: fwtypes.RFC3339DurationValue("P2Y"),
expected: types.StringValue("P2Y"),
},
"null": {
duration: fwtypes.RFC3339DurationNull(),
expected: types.StringNull(),
},
"unknown": {
duration: fwtypes.RFC3339DurationUnknown(),
expected: types.StringUnknown(),
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()

ctx := context.Background()

s, _ := test.duration.ToStringValue(ctx)

if !test.expected.Equal(s) {
t.Fatalf("expected %#v to equal %#v", s, test.expected)
}
})
}
}
6 changes: 6 additions & 0 deletions internal/service/elasticache/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,9 @@ func engine_Values() []string {
engineRedis,
}
}

const (
reservedCacheNodeStateActive = "active"
reservedCacheNodeStateRetired = "retired"
reservedCacheNodeStatePaymentPending = "payment-pending"
)
1 change: 1 addition & 0 deletions internal/service/elasticache/exports_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ var (
FindCacheSubnetGroupByName = findCacheSubnetGroupByName
FindGlobalReplicationGroupByID = findGlobalReplicationGroupByID
FindReplicationGroupByID = findReplicationGroupByID
FindReservedCacheNodeByID = findReservedCacheNodeByID
FindServerlessCacheByID = findServerlessCacheByID
FindUserByID = findUserByID
FindUserGroupByID = findUserGroupByID
Expand Down
Loading

0 comments on commit 10f8b4c

Please sign in to comment.