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

Added Support for ElastiCache Reserved Cache Nodes #29832

Merged
merged 19 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from 18 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
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
Loading