From f9e4cc942457efcc0927ae6184e81cf26a5a98e0 Mon Sep 17 00:00:00 2001 From: Rahman Mousavian Date: Wed, 8 Mar 2023 01:18:24 +1000 Subject: [PATCH 01/19] Added support for ElastiCache reserved cache nodes --- docs/acc-test-environment-variables.md | 1 + internal/service/elasticache/consts.go | 6 + internal/service/elasticache/exports_test.go | 1 + .../elasticache/reserved_cache_node.go | 277 ++++++++++++++++++ ...eserved_cache_node_offering_data_source.go | 98 +++++++ ...ed_cache_node_offering_data_source_test.go | 45 +++ .../elasticache/reserved_cache_node_test.go | 99 +++++++ .../elasticache/service_package_gen.go | 11 + 8 files changed, 538 insertions(+) create mode 100644 internal/service/elasticache/reserved_cache_node.go create mode 100644 internal/service/elasticache/reserved_cache_node_offering_data_source.go create mode 100644 internal/service/elasticache/reserved_cache_node_offering_data_source_test.go create mode 100644 internal/service/elasticache/reserved_cache_node_test.go diff --git a/docs/acc-test-environment-variables.md b/docs/acc-test-environment-variables.md index 20c9a0e1b69..60dd1418e7e 100644 --- a/docs/acc-test-environment-variables.md +++ b/docs/acc-test-environment-variables.md @@ -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 | diff --git a/internal/service/elasticache/consts.go b/internal/service/elasticache/consts.go index 439a7f5b420..0c8e2dd96d0 100644 --- a/internal/service/elasticache/consts.go +++ b/internal/service/elasticache/consts.go @@ -15,3 +15,9 @@ func engine_Values() []string { engineRedis, } } + +const ( + reservedCacheNodeStateActive = "active" + reservedCacheNodeStateRetired = "retired" + reservedCacheNodeStatePaymentPending = "payment-pending" +) diff --git a/internal/service/elasticache/exports_test.go b/internal/service/elasticache/exports_test.go index c4d719c6f15..c256fac3107 100644 --- a/internal/service/elasticache/exports_test.go +++ b/internal/service/elasticache/exports_test.go @@ -21,6 +21,7 @@ var ( FindCacheSubnetGroupByName = findCacheSubnetGroupByName FindGlobalReplicationGroupByID = findGlobalReplicationGroupByID FindReplicationGroupByID = findReplicationGroupByID + FindReservedCacheNodeByID = findReservedCacheNodeByID FindServerlessCacheByID = findServerlessCacheByID FindUserByID = findUserByID FindUserGroupByID = findUserGroupByID diff --git a/internal/service/elasticache/reserved_cache_node.go b/internal/service/elasticache/reserved_cache_node.go new file mode 100644 index 00000000000..e7ebe98edfd --- /dev/null +++ b/internal/service/elasticache/reserved_cache_node.go @@ -0,0 +1,277 @@ +package elasticache + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/elasticache" + awstypes "github.com/aws/aws-sdk-go-v2/service/elasticache/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/internal/verify" + "github.com/hashicorp/terraform-provider-aws/names" +) + +const ( + ResNameReservedCacheNode = "Reserved Cache Node" +) + +// @SDKResource("aws_elasticache_reserved_cache_node") +// @Tags(identifierAttribute="arn") +// @Testing(tagsTests=false) +func ResourceReservedCacheNode() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceReservedCacheNodeCreate, + ReadWithoutTimeout: resourceReservedCacheNodeRead, + UpdateWithoutTimeout: resourceReservedCacheNodeUpdate, + DeleteWithoutTimeout: resourceReservedCacheNodeDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(30 * time.Minute), + Update: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(1 * time.Minute), + }, + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "cache_node_type": { + Type: schema.TypeString, + Computed: true, + }, + "duration": { + Type: schema.TypeInt, + Computed: true, + }, + "fixed_price": { + Type: schema.TypeFloat, + Computed: true, + }, + "cache_node_count": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ForceNew: true, + }, + "offering_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "offering_type": { + Type: schema.TypeString, + Computed: true, + }, + "product_description": { + Type: schema.TypeString, + Computed: true, + }, + "recurring_charges": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "recurring_charge_amount": { + Type: schema.TypeInt, + Computed: true, + }, + "recurring_charge_frequency": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "reservation_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "start_time": { + Type: schema.TypeString, + Computed: true, + }, + "state": { + Type: schema.TypeString, + Computed: true, + }, + "usage_price": { + Type: schema.TypeFloat, + Computed: true, + }, + "tags": tftags.TagsSchema(), + "tags_all": tftags.TagsSchemaComputed(), + }, + + CustomizeDiff: verify.SetTagsDiff, + } +} + +func resourceReservedCacheNodeCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + conn := meta.(*conns.AWSClient).ElastiCacheClient(ctx) + + input := elasticache.PurchaseReservedCacheNodesOfferingInput{ + ReservedCacheNodesOfferingId: aws.String(d.Get("offering_id").(string)), + Tags: getTagsIn(ctx), + } + + if v, ok := d.Get("cache_node_count").(int); ok && v > 0 { + input.CacheNodeCount = aws.Int32(int32(d.Get("cache_node_count").(int))) + } + + if v, ok := d.Get("reservation_id").(string); ok && v != "" { + input.ReservedCacheNodeId = aws.String(v) + } + + resp, err := conn.PurchaseReservedCacheNodesOffering(ctx, &input) + if err != nil { + return create.DiagError(names.ElastiCache, create.ErrActionCreating, ResNameReservedCacheNode, fmt.Sprintf("offering_id: %s, reservation_id: %s", d.Get("offering_id").(string), d.Get("reservation_id").(string)), err) + } + + d.SetId(aws.ToString(resp.ReservedCacheNode.ReservedCacheNodeId)) + + if err := waitReservedCacheNodeCreated(ctx, conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { + return create.DiagError(names.ElastiCache, create.ErrActionWaitingForCreation, ResNameReservedCacheNode, d.Id(), err) + } + + return resourceReservedCacheNodeRead(ctx, d, meta) +} + +func resourceReservedCacheNodeRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + conn := meta.(*conns.AWSClient).ElastiCacheClient(ctx) + + reservation, err := findReservedCacheNodeByID(ctx, conn, d.Id()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + create.LogNotFoundRemoveState(names.ElastiCache, create.ErrActionReading, ResNameReservedCacheNode, d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return create.DiagError(names.ElastiCache, create.ErrActionReading, ResNameReservedCacheNode, d.Id(), err) + } + + d.Set("arn", reservation.ReservationARN) + d.Set("cache_node_type", reservation.CacheNodeType) + d.Set("duration", reservation.Duration) + d.Set("fixed_price", reservation.FixedPrice) + d.Set("cache_node_count", reservation.CacheNodeCount) + d.Set("offering_id", reservation.ReservedCacheNodesOfferingId) + d.Set("offering_type", reservation.OfferingType) + d.Set("product_description", reservation.ProductDescription) + d.Set("recurring_charges", flattenRecurringCharges(reservation.RecurringCharges)) + d.Set("reservation_id", reservation.ReservedCacheNodeId) + d.Set("start_time", (reservation.StartTime).Format(time.RFC3339)) + d.Set("state", reservation.State) + d.Set("usage_price", reservation.UsagePrice) + + return nil +} + +func resourceReservedCacheNodeUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + var diags diag.Diagnostics + + // Tags only. + + return append(diags, resourceReservedCacheNodeRead(ctx, d, meta)...) +} + +func resourceReservedCacheNodeDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + log.Printf("[DEBUG] %s %s cannot be deleted. Removing from state.: %s", names.ElastiCache, ResNameReservedCacheNode, d.Id()) + + return nil +} + +func flattenRecurringCharges(recurringCharges []awstypes.RecurringCharge) []any { + if len(recurringCharges) == 0 { + return []any{} + } + + var rawRecurringCharges []any + for _, recurringCharge := range recurringCharges { + rawRecurringCharge := map[string]any{ + "recurring_charge_amount": recurringCharge.RecurringChargeAmount, + "recurring_charge_frequency": aws.ToString(recurringCharge.RecurringChargeFrequency), + } + + rawRecurringCharges = append(rawRecurringCharges, rawRecurringCharge) + } + + return rawRecurringCharges +} + +func findReservedCacheNodeByID(ctx context.Context, conn *elasticache.Client, id string) (result awstypes.ReservedCacheNode, err error) { + input := elasticache.DescribeReservedCacheNodesInput{ + ReservedCacheNodeId: aws.String(id), + } + + output, err := conn.DescribeReservedCacheNodes(ctx, &input) + + if errs.IsA[*awstypes.ReservedCacheNodeNotFoundFault](err) { + return result, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { + return result, err + } + + if output == nil || len(output.ReservedCacheNodes) == 0 { + return result, tfresource.NewEmptyResultError(input) + } + + if count := len(output.ReservedCacheNodes); count > 1 { + return result, tfresource.NewTooManyResultsError(count, input) + } + + return output.ReservedCacheNodes[0], nil +} + +func waitReservedCacheNodeCreated(ctx context.Context, conn *elasticache.Client, id string, timeout time.Duration) error { + stateConf := &retry.StateChangeConf{ + Pending: []string{ + reservedCacheNodeStatePaymentPending, + }, + Target: []string{reservedCacheNodeStateActive}, + Refresh: statusReservedCacheNode(ctx, conn, id), + NotFoundChecks: 5, + Timeout: timeout, + MinTimeout: 10 * time.Second, + Delay: 30 * time.Second, + } + + _, err := stateConf.WaitForStateContext(ctx) + + return err +} + +func statusReservedCacheNode(ctx context.Context, conn *elasticache.Client, id string) retry.StateRefreshFunc { + return func() (any, string, error) { + output, err := findReservedCacheNodeByID(ctx, conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.ToString(output.State), nil + } +} diff --git a/internal/service/elasticache/reserved_cache_node_offering_data_source.go b/internal/service/elasticache/reserved_cache_node_offering_data_source.go new file mode 100644 index 00000000000..97f33db9965 --- /dev/null +++ b/internal/service/elasticache/reserved_cache_node_offering_data_source.go @@ -0,0 +1,98 @@ +package elasticache + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/elasticache" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" + "github.com/hashicorp/terraform-provider-aws/names" +) + +const ( + ResNameReservedCacheNodeOffering = "Reserved Cache Node Offering" +) + +// @SDKDataSource("aws_elasticache_reserved_cache_node_offering") +func DataSourceReservedCacheNodeOffering() *schema.Resource { + return &schema.Resource{ + ReadWithoutTimeout: dataSourceReservedCacheNodeOfferingRead, + Schema: map[string]*schema.Schema{ + "cache_node_type": { + Type: schema.TypeString, + Required: true, + }, + "duration": { + Type: schema.TypeInt, + Required: true, + }, + "fixed_price": { + Type: schema.TypeFloat, + Computed: true, + }, + "offering_id": { + Type: schema.TypeString, + Computed: true, + }, + "offering_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + "Light Utilization", + "Medium Utilization", + "Heavy Utilization", + "Partial Upfront", + "All Upfront", + "No Upfront", + }, false), + }, + "product_description": { + Type: schema.TypeString, + Required: true, + }, + }, + } +} + +func dataSourceReservedCacheNodeOfferingRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + + conn := meta.(*conns.AWSClient).ElastiCacheClient(ctx) + + input := elasticache.DescribeReservedCacheNodesOfferingsInput{ + CacheNodeType: aws.String(d.Get("cache_node_type").(string)), + Duration: aws.String(fmt.Sprint(d.Get("duration").(int))), + OfferingType: aws.String(d.Get("offering_type").(string)), + ProductDescription: aws.String(d.Get("product_description").(string)), + } + + resp, err := conn.DescribeReservedCacheNodesOfferings(ctx, &input) + if err != nil { + return sdkdiag.AppendErrorf(diags, "reading ElastiCache Reserved Cache Node Offering: %s", err) + } + + if len(resp.ReservedCacheNodesOfferings) == 0 { + return sdkdiag.AppendErrorf(diags, "no %s %s found matching criteria; try different search", names.ElastiCache, ResNameReservedCacheNodeOffering) + } + + if len(resp.ReservedCacheNodesOfferings) > 1 { + return sdkdiag.AppendErrorf(diags, "More than one %s %s found matching criteria; try different search", names.ElastiCache, ResNameReservedCacheNodeOffering) + } + + offering := resp.ReservedCacheNodesOfferings[0] + + d.SetId(aws.ToString(offering.ReservedCacheNodesOfferingId)) + d.Set("cache_node_type", offering.CacheNodeType) + d.Set("duration", offering.Duration) + d.Set("fixed_price", offering.FixedPrice) + d.Set("offering_type", offering.OfferingType) + d.Set("product_description", offering.ProductDescription) + d.Set("offering_id", offering.ReservedCacheNodesOfferingId) + + return diags +} diff --git a/internal/service/elasticache/reserved_cache_node_offering_data_source_test.go b/internal/service/elasticache/reserved_cache_node_offering_data_source_test.go new file mode 100644 index 00000000000..22aa68739d7 --- /dev/null +++ b/internal/service/elasticache/reserved_cache_node_offering_data_source_test.go @@ -0,0 +1,45 @@ +package elasticache_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccElastiCacheReservedNodeOffering_basic(t *testing.T) { + ctx := acctest.Context(t) + dataSourceName := "data.aws_elasticache_reserved_cache_node_offering.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: nil, + ErrorCheck: acctest.ErrorCheck(t, names.ElastiCacheServiceID), + Steps: []resource.TestStep{ + { + Config: testAccReservedNodeOfferingConfig_basic(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "cache_node_type", "cache.t2.micro"), + resource.TestCheckResourceAttr(dataSourceName, "duration", "31536000"), + resource.TestCheckResourceAttrSet(dataSourceName, "fixed_price"), + resource.TestCheckResourceAttrSet(dataSourceName, "offering_id"), + resource.TestCheckResourceAttr(dataSourceName, "offering_type", "Heavy Utilization"), + resource.TestCheckResourceAttr(dataSourceName, "product_description", "redis"), + ), + }, + }, + }) +} + +func testAccReservedNodeOfferingConfig_basic() string { + return ` +data "aws_elasticache_reserved_cache_node_offering" "test" { + cache_node_type = "cache.t2.micro" + duration = 31536000 + offering_type = "Heavy Utilization" + product_description = "redis" +} +` +} diff --git a/internal/service/elasticache/reserved_cache_node_test.go b/internal/service/elasticache/reserved_cache_node_test.go new file mode 100644 index 00000000000..609a6e2dd48 --- /dev/null +++ b/internal/service/elasticache/reserved_cache_node_test.go @@ -0,0 +1,99 @@ +package elasticache_test + +import ( + "context" + "fmt" + "os" + "regexp" + "testing" + + awstypes "github.com/aws/aws-sdk-go-v2/service/elasticache/types" + sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfelasticache "github.com/hashicorp/terraform-provider-aws/internal/service/elasticache" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccElastiCacheReservedCacheNode_basic(t *testing.T) { + ctx := acctest.Context(t) + if os.Getenv("TF_TEST_ELASTICACHE_RESERVED_CACHE_NODE") == "" { + t.Skip("Environment variable TF_TEST_ELASTICACHE_RESERVED_CACHE_NODE is not set") + } + + var reservation awstypes.ReservedCacheNode + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_elasticache_reserved_cache_node.test" + dataSourceName := "data.aws_elasticache_reserved_cache_node_offering.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: nil, + ErrorCheck: acctest.ErrorCheck(t, names.ElastiCacheServiceID), + Steps: []resource.TestStep{ + { + Config: testAccReservedInstanceConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccReservedInstanceExists(ctx, resourceName, &reservation), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "elasticache", regexp.MustCompile(`reserved-instance:.+`)), + resource.TestCheckResourceAttrPair(dataSourceName, "cache_node_type", resourceName, "cache_node_type"), + resource.TestCheckResourceAttrPair(dataSourceName, "duration", resourceName, "duration"), + resource.TestCheckResourceAttrPair(dataSourceName, "fixed_price", resourceName, "fixed_price"), + resource.TestCheckResourceAttr(resourceName, "cache_node_count", acctest.Ct1), + resource.TestCheckResourceAttrPair(dataSourceName, "offering_id", resourceName, "offering_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "offering_type", resourceName, "offering_type"), + resource.TestCheckResourceAttrPair(dataSourceName, "product_description", resourceName, "product_description"), + resource.TestCheckResourceAttrSet(resourceName, "recurring_charges"), + resource.TestCheckResourceAttr(resourceName, "reservation_id", rName), + resource.TestCheckResourceAttrSet(resourceName, "start_time"), + resource.TestCheckResourceAttrSet(resourceName, "state"), + resource.TestCheckResourceAttrSet(resourceName, "usage_price"), + ), + }, + }, + }) +} + +func testAccReservedInstanceExists(ctx context.Context, n string, reservation *awstypes.ReservedCacheNode) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).ElastiCacheClient(ctx) + + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ElastiCache Reserved Cache Node reservation id is set") + } + + resp, err := tfelasticache.FindReservedCacheNodeByID(ctx, conn, rs.Primary.ID) + + if err != nil { + return err + } + + *reservation = resp + + return nil + } +} + +func testAccReservedInstanceConfig_basic(rName string) string { + return fmt.Sprintf(` +resource "aws_elasticache_reserved_cache_node" "test" { + offering_id = data.aws_elasticache_reserved_cache_node_offering.test.offering_id + reservation_id = %[1]q +} + +data "aws_elasticache_reserved_cache_node_offering" "test" { + cache_node_type = "cache.t2.micro" + duration = 31536000 + offering_type = "No Upfront" + product_description = "redis" +} +`, rName) +} diff --git a/internal/service/elasticache/service_package_gen.go b/internal/service/elasticache/service_package_gen.go index dc973a3bc2f..6bade066dca 100644 --- a/internal/service/elasticache/service_package_gen.go +++ b/internal/service/elasticache/service_package_gen.go @@ -42,6 +42,10 @@ func (p *servicePackage) SDKDataSources(ctx context.Context) []*types.ServicePac TypeName: "aws_elasticache_replication_group", Name: "Replication Group", }, + { + Factory: DataSourceReservedCacheNodeOffering, + TypeName: "aws_elasticache_reserved_cache_node_offering", + }, { Factory: dataSourceSubnetGroup, TypeName: "aws_elasticache_subnet_group", @@ -86,6 +90,13 @@ func (p *servicePackage) SDKResources(ctx context.Context) []*types.ServicePacka IdentifierAttribute: names.AttrARN, }, }, + { + Factory: ResourceReservedCacheNode, + TypeName: "aws_elasticache_reserved_cache_node", + Tags: &types.ServicePackageResourceTags{ + IdentifierAttribute: names.AttrARN, + }, + }, { Factory: resourceSubnetGroup, TypeName: "aws_elasticache_subnet_group", From e37a4364a2179f3eadf4e7d2a54b79daf1ea640a Mon Sep 17 00:00:00 2001 From: Rahman Mousavian Date: Wed, 8 Mar 2023 01:27:48 +1000 Subject: [PATCH 02/19] Added changelog --- .changelog/29832.txt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changelog/29832.txt diff --git a/.changelog/29832.txt b/.changelog/29832.txt new file mode 100644 index 00000000000..bd5fb3ccb30 --- /dev/null +++ b/.changelog/29832.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_elasticache_reserved_cache_node +``` + +```release-note:new-data-source +aws_elasticache_reserved_cache_node_offering +``` From 095aea49cb13729d3739aabeb52815eea80e12ca Mon Sep 17 00:00:00 2001 From: Rahman Mousavian Date: Wed, 8 Mar 2023 02:09:42 +1000 Subject: [PATCH 03/19] Generated service package & fixed tests --- .../reserved_cache_node_offering_data_source_test.go | 8 ++++---- internal/service/elasticache/reserved_cache_node_test.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/service/elasticache/reserved_cache_node_offering_data_source_test.go b/internal/service/elasticache/reserved_cache_node_offering_data_source_test.go index 22aa68739d7..ab2cc853579 100644 --- a/internal/service/elasticache/reserved_cache_node_offering_data_source_test.go +++ b/internal/service/elasticache/reserved_cache_node_offering_data_source_test.go @@ -21,11 +21,11 @@ func TestAccElastiCacheReservedNodeOffering_basic(t *testing.T) { { Config: testAccReservedNodeOfferingConfig_basic(), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(dataSourceName, "cache_node_type", "cache.t2.micro"), + resource.TestCheckResourceAttr(dataSourceName, "cache_node_type", "cache.t4g.small"), resource.TestCheckResourceAttr(dataSourceName, "duration", "31536000"), resource.TestCheckResourceAttrSet(dataSourceName, "fixed_price"), resource.TestCheckResourceAttrSet(dataSourceName, "offering_id"), - resource.TestCheckResourceAttr(dataSourceName, "offering_type", "Heavy Utilization"), + resource.TestCheckResourceAttr(dataSourceName, "offering_type", "No Upfront"), resource.TestCheckResourceAttr(dataSourceName, "product_description", "redis"), ), }, @@ -36,9 +36,9 @@ func TestAccElastiCacheReservedNodeOffering_basic(t *testing.T) { func testAccReservedNodeOfferingConfig_basic() string { return ` data "aws_elasticache_reserved_cache_node_offering" "test" { - cache_node_type = "cache.t2.micro" + cache_node_type = "cache.t4g.small" duration = 31536000 - offering_type = "Heavy Utilization" + offering_type = "No Upfront" product_description = "redis" } ` diff --git a/internal/service/elasticache/reserved_cache_node_test.go b/internal/service/elasticache/reserved_cache_node_test.go index 609a6e2dd48..976f1f6b347 100644 --- a/internal/service/elasticache/reserved_cache_node_test.go +++ b/internal/service/elasticache/reserved_cache_node_test.go @@ -90,7 +90,7 @@ resource "aws_elasticache_reserved_cache_node" "test" { } data "aws_elasticache_reserved_cache_node_offering" "test" { - cache_node_type = "cache.t2.micro" + cache_node_type = "cache.t4g.small" duration = 31536000 offering_type = "No Upfront" product_description = "redis" From ce0fe831e5cd7509b3d87ebb3ec9b22efa5aef42 Mon Sep 17 00:00:00 2001 From: Rahman Mousavian Date: Wed, 8 Mar 2023 20:11:01 +1000 Subject: [PATCH 04/19] Added docs --- ...reserved_cache_node_offering.html.markdown | 39 ++++++++++ ...sticache_reserved_cache_node.html.markdown | 77 +++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 website/docs/d/elasticache_reserved_cache_node_offering.html.markdown create mode 100644 website/docs/r/elasticache_reserved_cache_node.html.markdown diff --git a/website/docs/d/elasticache_reserved_cache_node_offering.html.markdown b/website/docs/d/elasticache_reserved_cache_node_offering.html.markdown new file mode 100644 index 00000000000..574854fdd95 --- /dev/null +++ b/website/docs/d/elasticache_reserved_cache_node_offering.html.markdown @@ -0,0 +1,39 @@ +--- +subcategory: "ElastiCache" +layout: "aws" +page_title: "AWS: aws_elasticache_reserved_cache_node_offering" +description: |- + Information about a single ElastiCache Reserved Cache Node Offering. +--- + +# Data Source: aws_elasticache_reserved_cache_node_offering + +Information about a single ElastiCache Reserved Cache Node Offering. + +## Example Usage + +```terraform +data "aws_elasticache_reserved_cache_node_offering" "test" { + cache_node_type = "cache.t4g.small" + duration = 31536000 + offering_type = "No Upfront" + product_description = "redis" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `cache_node_type` - (Required) Node type for the reserved cache node. +* `duration` - (Required) Duration of the reservation in seconds. +* `offering_type` - (Required) Offering type of this reserved cache node. +* `product_description` - (Required) Description of the reserved cache node. + +## Attribute Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - Unique identifier for the reservation. Same as `offering_id`. +* `fixed_price` - Fixed price charged for this reserved cache node. +* `offering_id` - Unique identifier for the reservation. diff --git a/website/docs/r/elasticache_reserved_cache_node.html.markdown b/website/docs/r/elasticache_reserved_cache_node.html.markdown new file mode 100644 index 00000000000..18a8bca36c9 --- /dev/null +++ b/website/docs/r/elasticache_reserved_cache_node.html.markdown @@ -0,0 +1,77 @@ +--- +subcategory: "ElastiCache" +layout: "aws" +page_title: "AWS: aws_elasticache_reserved_cache_node" +description: |- + Manages an ElastiCache Reserved Cache Node +--- + +# Resource: aws_elasticache_reserved_cache_node + +Manages an ElastiCache Reserved Cache Node. + +~> **NOTE:** Once created, a reservation is valid for the `duration` of the provided `offering_id` and cannot be deleted. Performing a `destroy` will only remove the resource from state. For more information see [ElastiCache Reserved Nodes Documentation](https://aws.amazon.com/elasticache/reserved-cache-nodes/) and [PurchaseReservedCacheNodesOffering](https://docs.aws.amazon.com/AmazonElastiCache/latest/APIReference/API_PurchaseReservedCacheNodesOffering.html). + +~> **NOTE:** Due to the expense of testing this resource, we provide it as best effort. If you find it useful, and have the ability to help test or notice issues, consider reaching out to us on [GitHub](https://github.com/hashicorp/terraform-provider-aws). + +## Example Usage + +```terraform +data "aws_elasticache_reserved_cache_node_offering" "test" { + cache_node_type = "cache.t4g.small" + duration = 31536000 + offering_type = "No Upfront" + product_description = "redis" +} + +resource "aws_elasticache_reserved_cache_node" "example" { + offering_id = data.aws_elasticache_reserved_cache_node_offering.test.offering_id + reservation_id = "optionalCustomReservationID" + cache_node_count = 3 +} +``` + +## Argument Reference + +The following arguments are required: + +* `offering_id` - (Required) ID of the reserved cache node offering to purchase. To determine an `offering_id`, see the `aws_elasticache_reserved_cache_node_offering` data source. + +The following arguments are optional: + +* `instance_count` - (Optional) Number of cache node instances to reserve. Default value is `1`. +* `reservation_id` - (Optional) Customer-specified identifier to track this reservation. +* `tags` - (Optional) Map of tags to assign to the reservation. If configured with a provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `arn` - ARN for the reserved cache node. +* `id` - Unique identifier for the reservation. same as `reservation_id`. +* `duration` - Duration of the reservation in seconds. +* `fixed_price` – Fixed price charged for this reserved cache node. +* `cache_node_type` - Cache node type for the reserved cache nodes. +* `offering_type` - Offering type of this reserved cache node. +* `product_description` - Description of the reserved cache node. +* `recurring_charges` - Recurring price charged to run this reserved cache node. +* `start_time` - Time the reservation started. +* `state` - State of the reserved cache node. +* `usage_price` - Hourly price charged for this reserved cache node. +* `tags_all` - Map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block). + +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +- `create` - (Default `30m`) +- `update` - (Default `10m`) +- `delete` - (Default `1m`) + +## Import + +RDS DB Instance Reservations can be imported using the `instance_id`, e.g., + +``` +$ terraform import aws_elasticache_reserved_cache_node.reservation_node CustomReservationID +``` From 9527bd5a8abfd169bfcbbea709bf2202781872a3 Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Mon, 16 Sep 2024 14:49:37 -0700 Subject: [PATCH 05/19] Semgrep fixes --- .../elasticache/reserved_cache_node.go | 40 +++++++++++-------- ...eserved_cache_node_offering_data_source.go | 6 +-- ...ed_cache_node_offering_data_source_test.go | 2 +- .../elasticache/reserved_cache_node_test.go | 10 ++--- 4 files changed, 32 insertions(+), 26 deletions(-) diff --git a/internal/service/elasticache/reserved_cache_node.go b/internal/service/elasticache/reserved_cache_node.go index e7ebe98edfd..0b2bc421e65 100644 --- a/internal/service/elasticache/reserved_cache_node.go +++ b/internal/service/elasticache/reserved_cache_node.go @@ -43,7 +43,7 @@ func ResourceReservedCacheNode() *schema.Resource { Delete: schema.DefaultTimeout(1 * time.Minute), }, Schema: map[string]*schema.Schema{ - "arn": { + names.AttrARN: { Type: schema.TypeString, Computed: true, }, @@ -51,7 +51,7 @@ func ResourceReservedCacheNode() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "duration": { + names.AttrDuration: { Type: schema.TypeInt, Computed: true, }, @@ -100,11 +100,11 @@ func ResourceReservedCacheNode() *schema.Resource { Computed: true, ForceNew: true, }, - "start_time": { + names.AttrStartTime: { Type: schema.TypeString, Computed: true, }, - "state": { + names.AttrState: { Type: schema.TypeString, Computed: true, }, @@ -112,8 +112,8 @@ func ResourceReservedCacheNode() *schema.Resource { Type: schema.TypeFloat, Computed: true, }, - "tags": tftags.TagsSchema(), - "tags_all": tftags.TagsSchemaComputed(), + names.AttrTags: tftags.TagsSchema(), + names.AttrTagsAll: tftags.TagsSchemaComputed(), }, CustomizeDiff: verify.SetTagsDiff, @@ -121,6 +121,8 @@ func ResourceReservedCacheNode() *schema.Resource { } func resourceReservedCacheNodeCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).ElastiCacheClient(ctx) input := elasticache.PurchaseReservedCacheNodesOfferingInput{ @@ -138,19 +140,21 @@ func resourceReservedCacheNodeCreate(ctx context.Context, d *schema.ResourceData resp, err := conn.PurchaseReservedCacheNodesOffering(ctx, &input) if err != nil { - return create.DiagError(names.ElastiCache, create.ErrActionCreating, ResNameReservedCacheNode, fmt.Sprintf("offering_id: %s, reservation_id: %s", d.Get("offering_id").(string), d.Get("reservation_id").(string)), err) + return create.AppendDiagError(diags, names.ElastiCache, create.ErrActionCreating, ResNameReservedCacheNode, fmt.Sprintf("offering_id: %s, reservation_id: %s", d.Get("offering_id").(string), d.Get("reservation_id").(string)), err) } d.SetId(aws.ToString(resp.ReservedCacheNode.ReservedCacheNodeId)) if err := waitReservedCacheNodeCreated(ctx, conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { - return create.DiagError(names.ElastiCache, create.ErrActionWaitingForCreation, ResNameReservedCacheNode, d.Id(), err) + return create.AppendDiagError(diags, names.ElastiCache, create.ErrActionWaitingForCreation, ResNameReservedCacheNode, d.Id(), err) } - return resourceReservedCacheNodeRead(ctx, d, meta) + return append(diags, resourceReservedCacheNodeRead(ctx, d, meta)...) } func resourceReservedCacheNodeRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).ElastiCacheClient(ctx) reservation, err := findReservedCacheNodeByID(ctx, conn, d.Id()) @@ -158,16 +162,16 @@ func resourceReservedCacheNodeRead(ctx context.Context, d *schema.ResourceData, if !d.IsNewResource() && tfresource.NotFound(err) { create.LogNotFoundRemoveState(names.ElastiCache, create.ErrActionReading, ResNameReservedCacheNode, d.Id()) d.SetId("") - return nil + return diags } if err != nil { - return create.DiagError(names.ElastiCache, create.ErrActionReading, ResNameReservedCacheNode, d.Id(), err) + return create.AppendDiagError(diags, names.ElastiCache, create.ErrActionReading, ResNameReservedCacheNode, d.Id(), err) } - d.Set("arn", reservation.ReservationARN) + d.Set(names.AttrARN, reservation.ReservationARN) d.Set("cache_node_type", reservation.CacheNodeType) - d.Set("duration", reservation.Duration) + d.Set(names.AttrDuration, reservation.Duration) d.Set("fixed_price", reservation.FixedPrice) d.Set("cache_node_count", reservation.CacheNodeCount) d.Set("offering_id", reservation.ReservedCacheNodesOfferingId) @@ -175,11 +179,11 @@ func resourceReservedCacheNodeRead(ctx context.Context, d *schema.ResourceData, d.Set("product_description", reservation.ProductDescription) d.Set("recurring_charges", flattenRecurringCharges(reservation.RecurringCharges)) d.Set("reservation_id", reservation.ReservedCacheNodeId) - d.Set("start_time", (reservation.StartTime).Format(time.RFC3339)) - d.Set("state", reservation.State) + d.Set(names.AttrStartTime, (reservation.StartTime).Format(time.RFC3339)) + d.Set(names.AttrState, reservation.State) d.Set("usage_price", reservation.UsagePrice) - return nil + return diags } func resourceReservedCacheNodeUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { @@ -191,9 +195,11 @@ func resourceReservedCacheNodeUpdate(ctx context.Context, d *schema.ResourceData } func resourceReservedCacheNodeDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + var diags diag.Diagnostics + log.Printf("[DEBUG] %s %s cannot be deleted. Removing from state.: %s", names.ElastiCache, ResNameReservedCacheNode, d.Id()) - return nil + return diags } func flattenRecurringCharges(recurringCharges []awstypes.RecurringCharge) []any { diff --git a/internal/service/elasticache/reserved_cache_node_offering_data_source.go b/internal/service/elasticache/reserved_cache_node_offering_data_source.go index 97f33db9965..967c8c97349 100644 --- a/internal/service/elasticache/reserved_cache_node_offering_data_source.go +++ b/internal/service/elasticache/reserved_cache_node_offering_data_source.go @@ -27,7 +27,7 @@ func DataSourceReservedCacheNodeOffering() *schema.Resource { Type: schema.TypeString, Required: true, }, - "duration": { + names.AttrDuration: { Type: schema.TypeInt, Required: true, }, @@ -66,7 +66,7 @@ func dataSourceReservedCacheNodeOfferingRead(ctx context.Context, d *schema.Reso input := elasticache.DescribeReservedCacheNodesOfferingsInput{ CacheNodeType: aws.String(d.Get("cache_node_type").(string)), - Duration: aws.String(fmt.Sprint(d.Get("duration").(int))), + Duration: aws.String(fmt.Sprint(d.Get(names.AttrDuration).(int))), OfferingType: aws.String(d.Get("offering_type").(string)), ProductDescription: aws.String(d.Get("product_description").(string)), } @@ -88,7 +88,7 @@ func dataSourceReservedCacheNodeOfferingRead(ctx context.Context, d *schema.Reso d.SetId(aws.ToString(offering.ReservedCacheNodesOfferingId)) d.Set("cache_node_type", offering.CacheNodeType) - d.Set("duration", offering.Duration) + d.Set(names.AttrDuration, offering.Duration) d.Set("fixed_price", offering.FixedPrice) d.Set("offering_type", offering.OfferingType) d.Set("product_description", offering.ProductDescription) diff --git a/internal/service/elasticache/reserved_cache_node_offering_data_source_test.go b/internal/service/elasticache/reserved_cache_node_offering_data_source_test.go index ab2cc853579..d38a1a18616 100644 --- a/internal/service/elasticache/reserved_cache_node_offering_data_source_test.go +++ b/internal/service/elasticache/reserved_cache_node_offering_data_source_test.go @@ -22,7 +22,7 @@ func TestAccElastiCacheReservedNodeOffering_basic(t *testing.T) { Config: testAccReservedNodeOfferingConfig_basic(), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(dataSourceName, "cache_node_type", "cache.t4g.small"), - resource.TestCheckResourceAttr(dataSourceName, "duration", "31536000"), + resource.TestCheckResourceAttr(dataSourceName, names.AttrDuration, "31536000"), resource.TestCheckResourceAttrSet(dataSourceName, "fixed_price"), resource.TestCheckResourceAttrSet(dataSourceName, "offering_id"), resource.TestCheckResourceAttr(dataSourceName, "offering_type", "No Upfront"), diff --git a/internal/service/elasticache/reserved_cache_node_test.go b/internal/service/elasticache/reserved_cache_node_test.go index 976f1f6b347..24d6135d6eb 100644 --- a/internal/service/elasticache/reserved_cache_node_test.go +++ b/internal/service/elasticache/reserved_cache_node_test.go @@ -4,9 +4,9 @@ import ( "context" "fmt" "os" - "regexp" "testing" + "github.com/YakDriver/regexache" awstypes "github.com/aws/aws-sdk-go-v2/service/elasticache/types" sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -38,9 +38,9 @@ func TestAccElastiCacheReservedCacheNode_basic(t *testing.T) { Config: testAccReservedInstanceConfig_basic(rName), Check: resource.ComposeTestCheckFunc( testAccReservedInstanceExists(ctx, resourceName, &reservation), - acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "elasticache", regexp.MustCompile(`reserved-instance:.+`)), + acctest.MatchResourceAttrRegionalARN(resourceName, names.AttrARN, "elasticache", regexache.MustCompile(`reserved-instance:.+`)), resource.TestCheckResourceAttrPair(dataSourceName, "cache_node_type", resourceName, "cache_node_type"), - resource.TestCheckResourceAttrPair(dataSourceName, "duration", resourceName, "duration"), + resource.TestCheckResourceAttrPair(dataSourceName, names.AttrDuration, resourceName, names.AttrDuration), resource.TestCheckResourceAttrPair(dataSourceName, "fixed_price", resourceName, "fixed_price"), resource.TestCheckResourceAttr(resourceName, "cache_node_count", acctest.Ct1), resource.TestCheckResourceAttrPair(dataSourceName, "offering_id", resourceName, "offering_id"), @@ -48,8 +48,8 @@ func TestAccElastiCacheReservedCacheNode_basic(t *testing.T) { resource.TestCheckResourceAttrPair(dataSourceName, "product_description", resourceName, "product_description"), resource.TestCheckResourceAttrSet(resourceName, "recurring_charges"), resource.TestCheckResourceAttr(resourceName, "reservation_id", rName), - resource.TestCheckResourceAttrSet(resourceName, "start_time"), - resource.TestCheckResourceAttrSet(resourceName, "state"), + resource.TestCheckResourceAttrSet(resourceName, names.AttrStartTime), + resource.TestCheckResourceAttrSet(resourceName, names.AttrState), resource.TestCheckResourceAttrSet(resourceName, "usage_price"), ), }, From fc0667b47f2d0844b2ee2af91666e79335737333 Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Tue, 17 Sep 2024 10:59:44 -0700 Subject: [PATCH 06/19] Fix `tfsdk2fw` --- tools/tfsdk2fw/datasource.gtpl | 2 +- tools/tfsdk2fw/go.mod | 1 + tools/tfsdk2fw/go.sum | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/tfsdk2fw/datasource.gtpl b/tools/tfsdk2fw/datasource.gtpl index 6c891876384..6f1d575d325 100644 --- a/tools/tfsdk2fw/datasource.gtpl +++ b/tools/tfsdk2fw/datasource.gtpl @@ -13,7 +13,7 @@ import ( {{if .ImportProviderFrameworkTypes }}fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types"{{- end}} ) -// @FrameworkDataSource +// @FrameworkDataSource("{{ .TFTypeName }}") func newDataSource{{ .Name }}(context.Context) (datasource.DataSourceWithConfigure, error) { return &dataSource{{ .Name }}{}, nil } diff --git a/tools/tfsdk2fw/go.mod b/tools/tfsdk2fw/go.mod index e7cc2deeebb..c4bd0885f80 100644 --- a/tools/tfsdk2fw/go.mod +++ b/tools/tfsdk2fw/go.mod @@ -203,6 +203,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/pcaconnectorad v1.7.6 // indirect github.com/aws/aws-sdk-go-v2/service/pcs v1.0.2 // indirect github.com/aws/aws-sdk-go-v2/service/pinpoint v1.32.6 // indirect + github.com/aws/aws-sdk-go-v2/service/pinpointsmsvoicev2 v1.12.8 // indirect github.com/aws/aws-sdk-go-v2/service/pipes v1.15.0 // indirect github.com/aws/aws-sdk-go-v2/service/polly v1.43.2 // indirect github.com/aws/aws-sdk-go-v2/service/pricing v1.30.6 // indirect diff --git a/tools/tfsdk2fw/go.sum b/tools/tfsdk2fw/go.sum index 8fdfeea5309..c137367e34b 100644 --- a/tools/tfsdk2fw/go.sum +++ b/tools/tfsdk2fw/go.sum @@ -392,6 +392,8 @@ github.com/aws/aws-sdk-go-v2/service/pcs v1.0.2 h1:+PSbd/wTgCueA9agqNNeSmVoOgcgA github.com/aws/aws-sdk-go-v2/service/pcs v1.0.2/go.mod h1:acm3akB4exauzjZeKNonTwkxCPIdWT1LWLRM09eZP7c= github.com/aws/aws-sdk-go-v2/service/pinpoint v1.32.6 h1:S5SxTH9Ue7cwK9O76RQKkt9xY+zapTJv6dutXEyKOGQ= github.com/aws/aws-sdk-go-v2/service/pinpoint v1.32.6/go.mod h1:2yK6vZtj8t8tmEOk2/XBk/7oC9QggiRIDhwt1rUNkPE= +github.com/aws/aws-sdk-go-v2/service/pinpointsmsvoicev2 v1.12.8 h1:3GiUwkpy6GXMqVdfIfbWkBR86dOsd38obv4sBwyRxZ8= +github.com/aws/aws-sdk-go-v2/service/pinpointsmsvoicev2 v1.12.8/go.mod h1:Ek88Y1SlTvTDgX9L7DWUPfQIYtT++3eqK7cMK0TdW8Q= github.com/aws/aws-sdk-go-v2/service/pipes v1.15.0 h1:2P3Y9TFqZP2V8rJquXMEcXQ3D2Ybdvj+qD9wG9m0Sio= github.com/aws/aws-sdk-go-v2/service/pipes v1.15.0/go.mod h1:JKl45FQijnuqkji3jAlVTH0tRTbYYZSUb00P9HClkRg= github.com/aws/aws-sdk-go-v2/service/polly v1.43.2 h1:AmoLJRNIJQvN4CcXPhLwXPaDOnke2EXAWe9T+MNloEE= From cf564341f73c2c71fc2930077a90b3426cbea5fd Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Tue, 17 Sep 2024 12:26:34 -0700 Subject: [PATCH 07/19] Converts `aws_elasticache_reserved_cache_node_offering` to Framework --- ...eserved_cache_node_offering_data_source.go | 133 ++++++++++-------- .../elasticache/service_package_gen.go | 10 +- 2 files changed, 81 insertions(+), 62 deletions(-) diff --git a/internal/service/elasticache/reserved_cache_node_offering_data_source.go b/internal/service/elasticache/reserved_cache_node_offering_data_source.go index 967c8c97349..4855806add3 100644 --- a/internal/service/elasticache/reserved_cache_node_offering_data_source.go +++ b/internal/service/elasticache/reserved_cache_node_offering_data_source.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package elasticache import ( @@ -6,93 +9,109 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/elasticache" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - "github.com/hashicorp/terraform-provider-aws/internal/conns" - "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/names" ) -const ( - ResNameReservedCacheNodeOffering = "Reserved Cache Node Offering" -) +// @FrameworkDataSource("aws_elasticache_reserved_cache_node_offering") +func newDataSourceReservedCacheNodeOffering(context.Context) (datasource.DataSourceWithConfigure, error) { + return &dataSourceReservedCacheNodeOffering{}, nil +} + +type dataSourceReservedCacheNodeOffering struct { + framework.DataSourceWithConfigure +} + +func (d *dataSourceReservedCacheNodeOffering) Metadata(_ context.Context, request datasource.MetadataRequest, response *datasource.MetadataResponse) { + response.TypeName = "aws_elasticache_reserved_cache_node_offering" +} -// @SDKDataSource("aws_elasticache_reserved_cache_node_offering") -func DataSourceReservedCacheNodeOffering() *schema.Resource { - return &schema.Resource{ - ReadWithoutTimeout: dataSourceReservedCacheNodeOfferingRead, - Schema: map[string]*schema.Schema{ - "cache_node_type": { - Type: schema.TypeString, +func (d *dataSourceReservedCacheNodeOffering) Schema(ctx context.Context, request datasource.SchemaRequest, response *datasource.SchemaResponse) { + response.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "cache_node_type": schema.StringAttribute{ Required: true, }, - names.AttrDuration: { - Type: schema.TypeInt, + names.AttrDuration: schema.Int32Attribute{ Required: true, }, - "fixed_price": { - Type: schema.TypeFloat, + "fixed_price": schema.Float64Attribute{ Computed: true, }, - "offering_id": { - Type: schema.TypeString, + "offering_id": schema.StringAttribute{ Computed: true, }, - "offering_type": { - Type: schema.TypeString, + "offering_type": schema.StringAttribute{ Required: true, - ValidateFunc: validation.StringInSlice([]string{ - "Light Utilization", - "Medium Utilization", - "Heavy Utilization", - "Partial Upfront", - "All Upfront", - "No Upfront", - }, false), + Validators: []validator.String{ + stringvalidator.OneOf( + "Light Utilization", + "Medium Utilization", + "Heavy Utilization", + "Partial Upfront", + "All Upfront", + "No Upfront", + ), + }, }, - "product_description": { - Type: schema.TypeString, + "product_description": schema.StringAttribute{ Required: true, }, }, } } -func dataSourceReservedCacheNodeOfferingRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - var diags diag.Diagnostics - - conn := meta.(*conns.AWSClient).ElastiCacheClient(ctx) +// Read is called when the provider must read data source values in order to update state. +// Config values should be read from the ReadRequest and new state values set on the ReadResponse. +func (d *dataSourceReservedCacheNodeOffering) Read(ctx context.Context, request datasource.ReadRequest, response *datasource.ReadResponse) { + var data dataSourceReservedCacheNodeOfferingData - input := elasticache.DescribeReservedCacheNodesOfferingsInput{ - CacheNodeType: aws.String(d.Get("cache_node_type").(string)), - Duration: aws.String(fmt.Sprint(d.Get(names.AttrDuration).(int))), - OfferingType: aws.String(d.Get("offering_type").(string)), - ProductDescription: aws.String(d.Get("product_description").(string)), + response.Diagnostics.Append(request.Config.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return } + conn := d.Meta().ElastiCacheClient(ctx) + + flexOpt := flex.WithFieldNamePrefix("ReservedCacheNodes") + + var input elasticache.DescribeReservedCacheNodesOfferingsInput + response.Diagnostics.Append(flex.Expand(ctx, data, &input, flexOpt)...) + + input.Duration = aws.String(fmt.Sprint(data.Duration.ValueInt32())) + resp, err := conn.DescribeReservedCacheNodesOfferings(ctx, &input) if err != nil { - return sdkdiag.AppendErrorf(diags, "reading ElastiCache Reserved Cache Node Offering: %s", err) + response.Diagnostics.AddError("reading ElastiCache Reserved Cache Node Offering", err.Error()) + return } - if len(resp.ReservedCacheNodesOfferings) == 0 { - return sdkdiag.AppendErrorf(diags, "no %s %s found matching criteria; try different search", names.ElastiCache, ResNameReservedCacheNodeOffering) + offering, err := tfresource.AssertSingleValueResult(resp.ReservedCacheNodesOfferings) + if err != nil { + response.Diagnostics.AddError("reading ElastiCache Reserved Cache Node Offering", err.Error()) + return } - if len(resp.ReservedCacheNodesOfferings) > 1 { - return sdkdiag.AppendErrorf(diags, "More than one %s %s found matching criteria; try different search", names.ElastiCache, ResNameReservedCacheNodeOffering) + response.Diagnostics.Append(flex.Flatten(ctx, offering, &data, flexOpt)...) + if response.Diagnostics.HasError() { + return } - offering := resp.ReservedCacheNodesOfferings[0] - - d.SetId(aws.ToString(offering.ReservedCacheNodesOfferingId)) - d.Set("cache_node_type", offering.CacheNodeType) - d.Set(names.AttrDuration, offering.Duration) - d.Set("fixed_price", offering.FixedPrice) - d.Set("offering_type", offering.OfferingType) - d.Set("product_description", offering.ProductDescription) - d.Set("offering_id", offering.ReservedCacheNodesOfferingId) + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} - return diags +type dataSourceReservedCacheNodeOfferingData struct { + CacheNodeType types.String `tfsdk:"cache_node_type"` + Duration types.Int32 `tfsdk:"duration" autoflex:"-"` + FixedPrice types.Float64 `tfsdk:"fixed_price"` + OfferingID types.String `tfsdk:"offering_id"` + OfferingType types.String `tfsdk:"offering_type"` + ProductDescription types.String `tfsdk:"product_description"` } diff --git a/internal/service/elasticache/service_package_gen.go b/internal/service/elasticache/service_package_gen.go index 6bade066dca..b9a16b81553 100644 --- a/internal/service/elasticache/service_package_gen.go +++ b/internal/service/elasticache/service_package_gen.go @@ -15,7 +15,11 @@ import ( type servicePackage struct{} func (p *servicePackage) FrameworkDataSources(ctx context.Context) []*types.ServicePackageFrameworkDataSource { - return []*types.ServicePackageFrameworkDataSource{} + return []*types.ServicePackageFrameworkDataSource{ + { + Factory: newDataSourceReservedCacheNodeOffering, + }, + } } func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.ServicePackageFrameworkResource { @@ -42,10 +46,6 @@ func (p *servicePackage) SDKDataSources(ctx context.Context) []*types.ServicePac TypeName: "aws_elasticache_replication_group", Name: "Replication Group", }, - { - Factory: DataSourceReservedCacheNodeOffering, - TypeName: "aws_elasticache_reserved_cache_node_offering", - }, { Factory: dataSourceSubnetGroup, TypeName: "aws_elasticache_subnet_group", From 9ba012b75dfd0223d8395e826151cec936863198 Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Tue, 17 Sep 2024 12:26:45 -0700 Subject: [PATCH 08/19] `copywrite headers` --- internal/service/elasticache/reserved_cache_node.go | 3 +++ .../reserved_cache_node_offering_data_source_test.go | 3 +++ internal/service/elasticache/reserved_cache_node_test.go | 3 +++ 3 files changed, 9 insertions(+) diff --git a/internal/service/elasticache/reserved_cache_node.go b/internal/service/elasticache/reserved_cache_node.go index 0b2bc421e65..b598d95c88d 100644 --- a/internal/service/elasticache/reserved_cache_node.go +++ b/internal/service/elasticache/reserved_cache_node.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package elasticache import ( diff --git a/internal/service/elasticache/reserved_cache_node_offering_data_source_test.go b/internal/service/elasticache/reserved_cache_node_offering_data_source_test.go index d38a1a18616..553bd018516 100644 --- a/internal/service/elasticache/reserved_cache_node_offering_data_source_test.go +++ b/internal/service/elasticache/reserved_cache_node_offering_data_source_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package elasticache_test import ( diff --git a/internal/service/elasticache/reserved_cache_node_test.go b/internal/service/elasticache/reserved_cache_node_test.go index 24d6135d6eb..9c1fae5434c 100644 --- a/internal/service/elasticache/reserved_cache_node_test.go +++ b/internal/service/elasticache/reserved_cache_node_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package elasticache_test import ( From 9a9f80b5d07405cba27dea2bb5b95934eb9c7ba2 Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Tue, 17 Sep 2024 16:17:59 -0700 Subject: [PATCH 09/19] Converts `aws_elasticache_reserved_cache_node` to Framework --- .../elasticache/reserved_cache_node.go | 294 +++++++++--------- .../elasticache/reserved_cache_node_test.go | 65 +++- .../elasticache/service_package_gen.go | 13 +- 3 files changed, 217 insertions(+), 155 deletions(-) diff --git a/internal/service/elasticache/reserved_cache_node.go b/internal/service/elasticache/reserved_cache_node.go index b598d95c88d..088199a71b7 100644 --- a/internal/service/elasticache/reserved_cache_node.go +++ b/internal/service/elasticache/reserved_cache_node.go @@ -6,221 +6,235 @@ package elasticache import ( "context" "fmt" - "log" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/elasticache" awstypes "github.com/aws/aws-sdk-go-v2/service/elasticache/types" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int32planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-provider-aws/internal/conns" - "github.com/hashicorp/terraform-provider-aws/internal/create" "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" - "github.com/hashicorp/terraform-provider-aws/internal/verify" "github.com/hashicorp/terraform-provider-aws/names" ) -const ( - ResNameReservedCacheNode = "Reserved Cache Node" -) - -// @SDKResource("aws_elasticache_reserved_cache_node") +// @FrameworkResource("aws_elasticache_reserved_cache_node") // @Tags(identifierAttribute="arn") // @Testing(tagsTests=false) -func ResourceReservedCacheNode() *schema.Resource { - return &schema.Resource{ - CreateWithoutTimeout: resourceReservedCacheNodeCreate, - ReadWithoutTimeout: resourceReservedCacheNodeRead, - UpdateWithoutTimeout: resourceReservedCacheNodeUpdate, - DeleteWithoutTimeout: resourceReservedCacheNodeDelete, - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - Timeouts: &schema.ResourceTimeout{ - Create: schema.DefaultTimeout(30 * time.Minute), - Update: schema.DefaultTimeout(10 * time.Minute), - Delete: schema.DefaultTimeout(1 * time.Minute), - }, - Schema: map[string]*schema.Schema{ - names.AttrARN: { - Type: schema.TypeString, +func newResourceReservedCacheNode(context.Context) (resource.ResourceWithConfigure, error) { + r := &resourceReservedCacheNode{} + r.SetDefaultCreateTimeout(30 * time.Minute) + r.SetDefaultUpdateTimeout(10 * time.Minute) + r.SetDefaultDeleteTimeout(1 * time.Minute) + + return r, nil +} + +type resourceReservedCacheNode struct { + framework.ResourceWithConfigure + framework.WithNoOpUpdate[resourceReservedCacheNodeModel] + framework.WithNoOpDelete + framework.WithTimeouts +} + +func (r *resourceReservedCacheNode) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "aws_elasticache_reserved_cache_node" +} + +func (r *resourceReservedCacheNode) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) { + response.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + names.AttrARN: schema.StringAttribute{ Computed: true, }, - "cache_node_type": { - Type: schema.TypeString, + "cache_node_count": schema.Int32Attribute{ + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Int32{ + int32planmodifier.RequiresReplace(), + }, + }, + "cache_node_type": schema.StringAttribute{ Computed: true, }, - names.AttrDuration: { - Type: schema.TypeInt, + names.AttrDuration: schema.Int32Attribute{ Computed: true, }, - "fixed_price": { - Type: schema.TypeFloat, + "fixed_price": schema.Float64Attribute{ Computed: true, }, - "cache_node_count": { - Type: schema.TypeInt, + names.AttrID: schema.StringAttribute{ Optional: true, Computed: true, - ForceNew: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, - "offering_id": { - Type: schema.TypeString, + "reserved_cache_nodes_offering_id": schema.StringAttribute{ Required: true, - ForceNew: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, - "offering_type": { - Type: schema.TypeString, + "offering_type": schema.StringAttribute{ Computed: true, }, - "product_description": { - Type: schema.TypeString, + "product_description": schema.StringAttribute{ Computed: true, }, - "recurring_charges": { - Type: schema.TypeList, - Computed: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "recurring_charge_amount": { - Type: schema.TypeInt, - Computed: true, - }, - "recurring_charge_frequency": { - Type: schema.TypeString, - Computed: true, - }, - }, + "recurring_charges": schema.ListAttribute{ + CustomType: fwtypes.NewListNestedObjectTypeOf[recurringChargeModel](ctx), + Computed: true, + ElementType: types.ObjectType{ + AttrTypes: fwtypes.AttributeTypesMust[recurringChargeModel](ctx), }, }, - "reservation_id": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - names.AttrStartTime: { - Type: schema.TypeString, + names.AttrStartTime: schema.StringAttribute{ Computed: true, }, - names.AttrState: { - Type: schema.TypeString, + names.AttrState: schema.StringAttribute{ Computed: true, }, - "usage_price": { - Type: schema.TypeFloat, + names.AttrTags: tftags.TagsAttribute(), + names.AttrTagsAll: tftags.TagsAttributeComputedOnly(), + "usage_price": schema.Float64Attribute{ Computed: true, }, - names.AttrTags: tftags.TagsSchema(), - names.AttrTagsAll: tftags.TagsSchemaComputed(), }, - CustomizeDiff: verify.SetTagsDiff, + Blocks: map[string]schema.Block{ + names.AttrTimeouts: timeouts.Block(ctx, timeouts.Opts{ + Create: true, + Update: true, + Delete: true, + }), + }, } } -func resourceReservedCacheNodeCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - var diags diag.Diagnostics - - conn := meta.(*conns.AWSClient).ElastiCacheClient(ctx) +// Create is called when the provider must create a new resource. +// Config and planned state values should be read from the CreateRequest and new state values set on the CreateResponse. +func (r *resourceReservedCacheNode) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + var data resourceReservedCacheNodeModel - input := elasticache.PurchaseReservedCacheNodesOfferingInput{ - ReservedCacheNodesOfferingId: aws.String(d.Get("offering_id").(string)), - Tags: getTagsIn(ctx), + response.Diagnostics.Append(request.Plan.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return } - if v, ok := d.Get("cache_node_count").(int); ok && v > 0 { - input.CacheNodeCount = aws.Int32(int32(d.Get("cache_node_count").(int))) - } + conn := r.Meta().ElastiCacheClient(ctx) - if v, ok := d.Get("reservation_id").(string); ok && v != "" { - input.ReservedCacheNodeId = aws.String(v) - } + var input elasticache.PurchaseReservedCacheNodesOfferingInput + response.Diagnostics.Append(flex.Expand(ctx, data, &input, r.flexOpts()...)...) + + input.Tags = getTagsIn(ctx) resp, err := conn.PurchaseReservedCacheNodesOffering(ctx, &input) if err != nil { - return create.AppendDiagError(diags, names.ElastiCache, create.ErrActionCreating, ResNameReservedCacheNode, fmt.Sprintf("offering_id: %s, reservation_id: %s", d.Get("offering_id").(string), d.Get("reservation_id").(string)), err) + response.Diagnostics.AddError( + "Creating ElastiCache Reserved Cache Node", + fmt.Sprintf("Could not create ElastiCache Reserved Cache Node with Offering ID %q\nError: %s", data.ReservedCacheNodesOfferingID.ValueString(), err.Error()), + ) + return } - d.SetId(aws.ToString(resp.ReservedCacheNode.ReservedCacheNodeId)) + createTimeout := r.CreateTimeout(ctx, data.Timeouts) + if err := waitReservedCacheNodeCreated(ctx, conn, aws.ToString(resp.ReservedCacheNode.ReservedCacheNodeId), createTimeout); err != nil { + response.Diagnostics.AddError( + "Creating ElastiCache Reserved Cache Node", + fmt.Sprintf("Creating ElastiCache Reserved Cache Node with Offering ID %q failed while waiting for completion.\nError: %s", data.ReservedCacheNodesOfferingID.ValueString(), err.Error()), + ) + return + } - if err := waitReservedCacheNodeCreated(ctx, conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { - return create.AppendDiagError(diags, names.ElastiCache, create.ErrActionWaitingForCreation, ResNameReservedCacheNode, d.Id(), err) + response.Diagnostics.Append(flex.Flatten(ctx, resp.ReservedCacheNode, &data, r.flexOpts()...)...) + if response.Diagnostics.HasError() { + return } - return append(diags, resourceReservedCacheNodeRead(ctx, d, meta)...) + response.Diagnostics.Append(response.State.Set(ctx, &data)...) } -func resourceReservedCacheNodeRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - var diags diag.Diagnostics +func (r *resourceReservedCacheNode) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + var data resourceReservedCacheNodeModel - conn := meta.(*conns.AWSClient).ElastiCacheClient(ctx) + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } - reservation, err := findReservedCacheNodeByID(ctx, conn, d.Id()) + conn := r.Meta().ElastiCacheClient(ctx) - if !d.IsNewResource() && tfresource.NotFound(err) { - create.LogNotFoundRemoveState(names.ElastiCache, create.ErrActionReading, ResNameReservedCacheNode, d.Id()) - d.SetId("") - return diags + reservation, err := findReservedCacheNodeByID(ctx, conn, data.ID.ValueString()) + + if tfresource.NotFound(err) { + response.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + response.State.RemoveResource(ctx) + return } if err != nil { - return create.AppendDiagError(diags, names.ElastiCache, create.ErrActionReading, ResNameReservedCacheNode, d.Id(), err) + response.Diagnostics.AddError(fmt.Sprintf("reading ElastiCache Reserved Cache Node (%s)", data.ID.ValueString()), err.Error()) + return } - d.Set(names.AttrARN, reservation.ReservationARN) - d.Set("cache_node_type", reservation.CacheNodeType) - d.Set(names.AttrDuration, reservation.Duration) - d.Set("fixed_price", reservation.FixedPrice) - d.Set("cache_node_count", reservation.CacheNodeCount) - d.Set("offering_id", reservation.ReservedCacheNodesOfferingId) - d.Set("offering_type", reservation.OfferingType) - d.Set("product_description", reservation.ProductDescription) - d.Set("recurring_charges", flattenRecurringCharges(reservation.RecurringCharges)) - d.Set("reservation_id", reservation.ReservedCacheNodeId) - d.Set(names.AttrStartTime, (reservation.StartTime).Format(time.RFC3339)) - d.Set(names.AttrState, reservation.State) - d.Set("usage_price", reservation.UsagePrice) - - return diags -} - -func resourceReservedCacheNodeUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - var diags diag.Diagnostics - - // Tags only. + response.Diagnostics.Append(flex.Flatten(ctx, reservation, &data)...) + if response.Diagnostics.HasError() { + return + } - return append(diags, resourceReservedCacheNodeRead(ctx, d, meta)...) + response.Diagnostics.Append(response.State.Set(ctx, &data)...) } -func resourceReservedCacheNodeDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - var diags diag.Diagnostics - - log.Printf("[DEBUG] %s %s cannot be deleted. Removing from state.: %s", names.ElastiCache, ResNameReservedCacheNode, d.Id()) +func (r *resourceReservedCacheNode) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root(names.AttrID), request, response) +} - return diags +func (r *resourceReservedCacheNode) ModifyPlan(ctx context.Context, request resource.ModifyPlanRequest, response *resource.ModifyPlanResponse) { + r.SetTagsAll(ctx, request, response) } -func flattenRecurringCharges(recurringCharges []awstypes.RecurringCharge) []any { - if len(recurringCharges) == 0 { - return []any{} +func (r *resourceReservedCacheNode) flexOpts() []flex.AutoFlexOptionsFunc { + return []flex.AutoFlexOptionsFunc{ + flex.WithFieldNamePrefix("ReservedCacheNode"), } +} - var rawRecurringCharges []any - for _, recurringCharge := range recurringCharges { - rawRecurringCharge := map[string]any{ - "recurring_charge_amount": recurringCharge.RecurringChargeAmount, - "recurring_charge_frequency": aws.ToString(recurringCharge.RecurringChargeFrequency), - } - - rawRecurringCharges = append(rawRecurringCharges, rawRecurringCharge) - } +type resourceReservedCacheNodeModel struct { + ARN types.String `tfsdk:"arn"` + CacheNodeCount types.Int32 `tfsdk:"cache_node_count"` + CacheNodeType types.String `tfsdk:"cache_node_type"` + Duration types.Int32 `tfsdk:"duration"` + FixedPrice types.Float64 `tfsdk:"fixed_price"` + ID types.String `tfsdk:"id"` + ReservedCacheNodesOfferingID types.String `tfsdk:"reserved_cache_nodes_offering_id"` + OfferingType types.String `tfsdk:"offering_type"` + ProductDescription types.String `tfsdk:"product_description"` + RecurringCharges fwtypes.ListNestedObjectValueOf[recurringChargeModel] `tfsdk:"recurring_charges"` + StartTime types.String `tfsdk:"start_time"` + State types.String `tfsdk:"state"` + Tags tftags.Map `tfsdk:"tags"` + TagsAll tftags.Map `tfsdk:"tags_all"` + UsagePrice types.Float64 `tfsdk:"usage_price"` + + Timeouts timeouts.Value `tfsdk:"timeouts"` +} - return rawRecurringCharges +type recurringChargeModel struct { + RecurringChargeAmount types.Float64 `tfsdk:"recurring_charge_amount"` + RecurringChargeFrequency types.String `tfsdk:"recurring_charge_frequency"` } func findReservedCacheNodeByID(ctx context.Context, conn *elasticache.Client, id string) (result awstypes.ReservedCacheNode, err error) { diff --git a/internal/service/elasticache/reserved_cache_node_test.go b/internal/service/elasticache/reserved_cache_node_test.go index 9c1fae5434c..590af408b4f 100644 --- a/internal/service/elasticache/reserved_cache_node_test.go +++ b/internal/service/elasticache/reserved_cache_node_test.go @@ -13,7 +13,10 @@ import ( awstypes "github.com/aws/aws-sdk-go-v2/service/elasticache/types" sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" "github.com/hashicorp/terraform-provider-aws/internal/acctest" "github.com/hashicorp/terraform-provider-aws/internal/conns" tfelasticache "github.com/hashicorp/terraform-provider-aws/internal/service/elasticache" @@ -27,7 +30,6 @@ func TestAccElastiCacheReservedCacheNode_basic(t *testing.T) { } var reservation awstypes.ReservedCacheNode - rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_elasticache_reserved_cache_node.test" dataSourceName := "data.aws_elasticache_reserved_cache_node_offering.test" @@ -38,23 +40,55 @@ func TestAccElastiCacheReservedCacheNode_basic(t *testing.T) { ErrorCheck: acctest.ErrorCheck(t, names.ElastiCacheServiceID), Steps: []resource.TestStep{ { - Config: testAccReservedInstanceConfig_basic(rName), + Config: testAccReservedInstanceConfig_basic(), Check: resource.ComposeTestCheckFunc( testAccReservedInstanceExists(ctx, resourceName, &reservation), acctest.MatchResourceAttrRegionalARN(resourceName, names.AttrARN, "elasticache", regexache.MustCompile(`reserved-instance:.+`)), + resource.TestCheckResourceAttr(resourceName, "cache_node_count", acctest.Ct1), resource.TestCheckResourceAttrPair(dataSourceName, "cache_node_type", resourceName, "cache_node_type"), resource.TestCheckResourceAttrPair(dataSourceName, names.AttrDuration, resourceName, names.AttrDuration), resource.TestCheckResourceAttrPair(dataSourceName, "fixed_price", resourceName, "fixed_price"), - resource.TestCheckResourceAttr(resourceName, "cache_node_count", acctest.Ct1), - resource.TestCheckResourceAttrPair(dataSourceName, "offering_id", resourceName, "offering_id"), + resource.TestCheckResourceAttrSet(resourceName, names.AttrID), + resource.TestCheckResourceAttrPair(dataSourceName, "reserved_cache_nodes_offering_id", resourceName, "offering_id"), resource.TestCheckResourceAttrPair(dataSourceName, "offering_type", resourceName, "offering_type"), resource.TestCheckResourceAttrPair(dataSourceName, "product_description", resourceName, "product_description"), resource.TestCheckResourceAttrSet(resourceName, "recurring_charges"), - resource.TestCheckResourceAttr(resourceName, "reservation_id", rName), resource.TestCheckResourceAttrSet(resourceName, names.AttrStartTime), resource.TestCheckResourceAttrSet(resourceName, names.AttrState), resource.TestCheckResourceAttrSet(resourceName, "usage_price"), ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTags), knownvalue.MapExact(map[string]knownvalue.Check{})), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTagsAll), knownvalue.MapExact(map[string]knownvalue.Check{})), + }, + }, + }, + }) +} + +func TestAccElastiCacheReservedCacheNode_ID(t *testing.T) { + ctx := acctest.Context(t) + if os.Getenv("TF_TEST_ELASTICACHE_RESERVED_CACHE_NODE") == "" { + t.Skip("Environment variable TF_TEST_ELASTICACHE_RESERVED_CACHE_NODE is not set") + } + + var reservation awstypes.ReservedCacheNode + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_elasticache_reserved_cache_node.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: nil, + ErrorCheck: acctest.ErrorCheck(t, names.ElastiCacheServiceID), + Steps: []resource.TestStep{ + { + Config: testAccReservedInstanceConfig_ID(rName), + Check: resource.ComposeTestCheckFunc( + testAccReservedInstanceExists(ctx, resourceName, &reservation), + resource.TestCheckResourceAttr(resourceName, names.AttrID, rName), + resource.TestCheckResourceAttrSet(resourceName, "usage_price"), + ), }, }, }) @@ -85,11 +119,26 @@ func testAccReservedInstanceExists(ctx context.Context, n string, reservation *a } } -func testAccReservedInstanceConfig_basic(rName string) string { - return fmt.Sprintf(` +func testAccReservedInstanceConfig_basic() string { + return ` resource "aws_elasticache_reserved_cache_node" "test" { offering_id = data.aws_elasticache_reserved_cache_node_offering.test.offering_id - reservation_id = %[1]q +} + +data "aws_elasticache_reserved_cache_node_offering" "test" { + cache_node_type = "cache.t4g.small" + duration = 31536000 + offering_type = "No Upfront" + product_description = "redis" +} +` +} + +func testAccReservedInstanceConfig_ID(rName string) string { + return fmt.Sprintf(` +resource "aws_elasticache_reserved_cache_node" "test" { + offering_id = data.aws_elasticache_reserved_cache_node_offering.test.offering_id + id = %[1]q } data "aws_elasticache_reserved_cache_node_offering" "test" { diff --git a/internal/service/elasticache/service_package_gen.go b/internal/service/elasticache/service_package_gen.go index b9a16b81553..88d85fd6e45 100644 --- a/internal/service/elasticache/service_package_gen.go +++ b/internal/service/elasticache/service_package_gen.go @@ -24,6 +24,12 @@ func (p *servicePackage) FrameworkDataSources(ctx context.Context) []*types.Serv func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.ServicePackageFrameworkResource { return []*types.ServicePackageFrameworkResource{ + { + Factory: newResourceReservedCacheNode, + Tags: &types.ServicePackageResourceTags{ + IdentifierAttribute: names.AttrARN, + }, + }, { Factory: newServerlessCacheResource, Name: "Serverless Cache", @@ -90,13 +96,6 @@ func (p *servicePackage) SDKResources(ctx context.Context) []*types.ServicePacka IdentifierAttribute: names.AttrARN, }, }, - { - Factory: ResourceReservedCacheNode, - TypeName: "aws_elasticache_reserved_cache_node", - Tags: &types.ServicePackageResourceTags{ - IdentifierAttribute: names.AttrARN, - }, - }, { Factory: resourceSubnetGroup, TypeName: "aws_elasticache_subnet_group", From e7d7a80be51d1f4a092c64d252e2963268c0d495 Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Wed, 18 Sep 2024 12:45:04 -0700 Subject: [PATCH 10/19] Adds `noflatten` AutoFlex struct tag --- docs/data-handling-and-conversion.md | 15 +++++++++++++++ internal/framework/flex/autoflex.go | 7 +++++++ internal/framework/flex/tags.go | 4 ++++ 3 files changed, 26 insertions(+) diff --git a/docs/data-handling-and-conversion.md b/docs/data-handling-and-conversion.md index a5ffa5bfa67..e35db243be5 100644 --- a/docs/data-handling-and-conversion.md +++ b/docs/data-handling-and-conversion.md @@ -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. diff --git a/internal/framework/flex/autoflex.go b/internal/framework/flex/autoflex.go index 4b160e06068..176c4c521ef 100644 --- a/internal/framework/flex/autoflex.go +++ b/internal/framework/flex/autoflex.go @@ -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{ diff --git a/internal/framework/flex/tags.go b/internal/framework/flex/tags.go index e32542b063f..e4710cb00cc 100644 --- a/internal/framework/flex/tags.go +++ b/internal/framework/flex/tags.go @@ -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") +} From 020109fd7e09027d11722279f2dc4c9778f19e24 Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Wed, 18 Sep 2024 12:47:47 -0700 Subject: [PATCH 11/19] Adds RFC3339 duration type --- internal/framework/types/rfc3339_duration.go | 158 ++++++++++++++++++ .../framework/types/rfc3339_duration_test.go | 135 +++++++++++++++ ...eserved_cache_node_offering_data_source.go | 32 ++-- ...ed_cache_node_offering_data_source_test.go | 4 +- ...reserved_cache_node_offering.html.markdown | 7 +- 5 files changed, 317 insertions(+), 19 deletions(-) create mode 100644 internal/framework/types/rfc3339_duration.go create mode 100644 internal/framework/types/rfc3339_duration_test.go diff --git a/internal/framework/types/rfc3339_duration.go b/internal/framework/types/rfc3339_duration.go new file mode 100644 index 00000000000..d1135d0ee8d --- /dev/null +++ b/internal/framework/types/rfc3339_duration.go @@ -0,0 +1,158 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package types + +import ( + "context" + "fmt" + + "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, + } +} + +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(), + ) + } +} diff --git a/internal/framework/types/rfc3339_duration_test.go b/internal/framework/types/rfc3339_duration_test.go new file mode 100644 index 00000000000..8cd0f3de37a --- /dev/null +++ b/internal/framework/types/rfc3339_duration_test.go @@ -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) + } + }) + } +} diff --git a/internal/service/elasticache/reserved_cache_node_offering_data_source.go b/internal/service/elasticache/reserved_cache_node_offering_data_source.go index 4855806add3..b34b31f4add 100644 --- a/internal/service/elasticache/reserved_cache_node_offering_data_source.go +++ b/internal/service/elasticache/reserved_cache_node_offering_data_source.go @@ -5,9 +5,7 @@ package elasticache import ( "context" - "fmt" - "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/elasticache" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource" @@ -16,6 +14,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-provider-aws/internal/framework" "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/names" ) @@ -39,8 +38,12 @@ func (d *dataSourceReservedCacheNodeOffering) Schema(ctx context.Context, reques "cache_node_type": schema.StringAttribute{ Required: true, }, - names.AttrDuration: schema.Int32Attribute{ - Required: true, + names.AttrDuration: schema.StringAttribute{ + CustomType: fwtypes.RFC3339DurationType, + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf("P1Y", "P3Y"), + }, }, "fixed_price": schema.Float64Attribute{ Computed: true, @@ -71,7 +74,7 @@ func (d *dataSourceReservedCacheNodeOffering) Schema(ctx context.Context, reques // Read is called when the provider must read data source values in order to update state. // Config values should be read from the ReadRequest and new state values set on the ReadResponse. func (d *dataSourceReservedCacheNodeOffering) Read(ctx context.Context, request datasource.ReadRequest, response *datasource.ReadResponse) { - var data dataSourceReservedCacheNodeOfferingData + var data dataSourceReservedCacheNodeOfferingModel response.Diagnostics.Append(request.Config.Get(ctx, &data)...) if response.Diagnostics.HasError() { @@ -84,8 +87,9 @@ func (d *dataSourceReservedCacheNodeOffering) Read(ctx context.Context, request var input elasticache.DescribeReservedCacheNodesOfferingsInput response.Diagnostics.Append(flex.Expand(ctx, data, &input, flexOpt)...) - - input.Duration = aws.String(fmt.Sprint(data.Duration.ValueInt32())) + if response.Diagnostics.HasError() { + return + } resp, err := conn.DescribeReservedCacheNodesOfferings(ctx, &input) if err != nil { @@ -107,11 +111,11 @@ func (d *dataSourceReservedCacheNodeOffering) Read(ctx context.Context, request response.Diagnostics.Append(response.State.Set(ctx, &data)...) } -type dataSourceReservedCacheNodeOfferingData struct { - CacheNodeType types.String `tfsdk:"cache_node_type"` - Duration types.Int32 `tfsdk:"duration" autoflex:"-"` - FixedPrice types.Float64 `tfsdk:"fixed_price"` - OfferingID types.String `tfsdk:"offering_id"` - OfferingType types.String `tfsdk:"offering_type"` - ProductDescription types.String `tfsdk:"product_description"` +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"` } diff --git a/internal/service/elasticache/reserved_cache_node_offering_data_source_test.go b/internal/service/elasticache/reserved_cache_node_offering_data_source_test.go index 553bd018516..1e45423e480 100644 --- a/internal/service/elasticache/reserved_cache_node_offering_data_source_test.go +++ b/internal/service/elasticache/reserved_cache_node_offering_data_source_test.go @@ -25,7 +25,7 @@ func TestAccElastiCacheReservedNodeOffering_basic(t *testing.T) { Config: testAccReservedNodeOfferingConfig_basic(), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(dataSourceName, "cache_node_type", "cache.t4g.small"), - resource.TestCheckResourceAttr(dataSourceName, names.AttrDuration, "31536000"), + resource.TestCheckResourceAttr(dataSourceName, names.AttrDuration, "P1Y"), resource.TestCheckResourceAttrSet(dataSourceName, "fixed_price"), resource.TestCheckResourceAttrSet(dataSourceName, "offering_id"), resource.TestCheckResourceAttr(dataSourceName, "offering_type", "No Upfront"), @@ -40,7 +40,7 @@ func testAccReservedNodeOfferingConfig_basic() string { return ` data "aws_elasticache_reserved_cache_node_offering" "test" { cache_node_type = "cache.t4g.small" - duration = 31536000 + duration = "P1Y" offering_type = "No Upfront" product_description = "redis" } diff --git a/website/docs/d/elasticache_reserved_cache_node_offering.html.markdown b/website/docs/d/elasticache_reserved_cache_node_offering.html.markdown index 574854fdd95..8501bb4cfc7 100644 --- a/website/docs/d/elasticache_reserved_cache_node_offering.html.markdown +++ b/website/docs/d/elasticache_reserved_cache_node_offering.html.markdown @@ -13,9 +13,9 @@ Information about a single ElastiCache Reserved Cache Node Offering. ## Example Usage ```terraform -data "aws_elasticache_reserved_cache_node_offering" "test" { +data "aws_elasticache_reserved_cache_node_offering" "example" { cache_node_type = "cache.t4g.small" - duration = 31536000 + duration = "P1Y" offering_type = "No Upfront" product_description = "redis" } @@ -26,7 +26,8 @@ data "aws_elasticache_reserved_cache_node_offering" "test" { The following arguments are supported: * `cache_node_type` - (Required) Node type for the reserved cache node. -* `duration` - (Required) Duration of the reservation in seconds. +* `duration` - (Required) Duration of the reservation in RFC3339 duration format. + Valid values are `P1Y` (one year) and `P3Y` (three years). * `offering_type` - (Required) Offering type of this reserved cache node. * `product_description` - (Required) Description of the reserved cache node. From 17fcb325d3d03e5c778f841f68cbefe493f6abdf Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Wed, 18 Sep 2024 12:48:29 -0700 Subject: [PATCH 12/19] Adds validation for `product_description` --- .../elasticache/reserved_cache_node_offering_data_source.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/service/elasticache/reserved_cache_node_offering_data_source.go b/internal/service/elasticache/reserved_cache_node_offering_data_source.go index b34b31f4add..ef28c11da54 100644 --- a/internal/service/elasticache/reserved_cache_node_offering_data_source.go +++ b/internal/service/elasticache/reserved_cache_node_offering_data_source.go @@ -66,6 +66,9 @@ func (d *dataSourceReservedCacheNodeOffering) Schema(ctx context.Context, reques }, "product_description": schema.StringAttribute{ Required: true, + Validators: []validator.String{ + stringvalidator.OneOf(engine_Values()...), + }, }, }, } From 0641226a8cb4edc9ced4bfe4c6e4f0d9d291c43c Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Wed, 18 Sep 2024 12:48:56 -0700 Subject: [PATCH 13/19] Updates `cache_node_type` documentation --- ...elasticache_reserved_cache_node_offering.html.markdown | 8 +++++++- website/docs/r/elasticache_cluster.html.markdown | 5 ++++- .../docs/r/elasticache_replication_group.html.markdown | 5 ++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/website/docs/d/elasticache_reserved_cache_node_offering.html.markdown b/website/docs/d/elasticache_reserved_cache_node_offering.html.markdown index 8501bb4cfc7..d9a9af4fba1 100644 --- a/website/docs/d/elasticache_reserved_cache_node_offering.html.markdown +++ b/website/docs/d/elasticache_reserved_cache_node_offering.html.markdown @@ -26,10 +26,16 @@ data "aws_elasticache_reserved_cache_node_offering" "example" { The following arguments are supported: * `cache_node_type` - (Required) Node type for the reserved cache node. + See AWS documentation for information on [supported node types for Redis](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/CacheNodes.SupportedTypes.html) and [guidance on selecting node types for Redis](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/nodes-select-size.html). + See AWS documentation for information on [supported node types for Memcached](https://docs.aws.amazon.com/AmazonElastiCache/latest/mem-ug/CacheNodes.SupportedTypes.html) and [guidance on selecting node types for Memcached](https://docs.aws.amazon.com/AmazonElastiCache/latest/mem-ug/nodes-select-size.html). * `duration` - (Required) Duration of the reservation in RFC3339 duration format. Valid values are `P1Y` (one year) and `P3Y` (three years). * `offering_type` - (Required) Offering type of this reserved cache node. -* `product_description` - (Required) Description of the reserved cache node. + For the latest generation of nodes (e.g. M5, R5, T4 and newer) valid values are `No Upfront`, `Partial Upfront`, and `All Upfront`. + For other current generation nodes (i.e. T2, M3, M4, R3, or R4) the only valid value is `Heavy Utilization`. + For previous generation modes (i.e. T1, M1, M2, or C1) valid values are `Heavy Utilization`, `Medium Utilization`, and `Light Utilization`. +* `product_description` - (Required) Engine type for the reserved cache node. + Valid values are `redis` and `memcached`. ## Attribute Reference diff --git a/website/docs/r/elasticache_cluster.html.markdown b/website/docs/r/elasticache_cluster.html.markdown index cf39d8ee06b..2068bac4fa2 100644 --- a/website/docs/r/elasticache_cluster.html.markdown +++ b/website/docs/r/elasticache_cluster.html.markdown @@ -141,7 +141,10 @@ The following arguments are required: * `cluster_id` – (Required) Group identifier. ElastiCache converts this name to lowercase. Changing this value will re-create the resource. * `engine` – (Optional, Required if `replication_group_id` is not specified) Name of the cache engine to be used for this cache cluster. Valid values are `memcached` or `redis`. -* `node_type` – (Required unless `replication_group_id` is provided) The instance class used. See AWS documentation for information on [supported node types for Redis](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/CacheNodes.SupportedTypes.html) and [guidance on selecting node types for Redis](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/nodes-select-size.html). See AWS documentation for information on [supported node types for Memcached](https://docs.aws.amazon.com/AmazonElastiCache/latest/mem-ug/CacheNodes.SupportedTypes.html) and [guidance on selecting node types for Memcached](https://docs.aws.amazon.com/AmazonElastiCache/latest/mem-ug/nodes-select-size.html). For Memcached, changing this value will re-create the resource. +* `node_type` – (Required unless `replication_group_id` is provided) The instance class used. + See AWS documentation for information on [supported node types for Redis](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/CacheNodes.SupportedTypes.html) and [guidance on selecting node types for Redis](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/nodes-select-size.html). + See AWS documentation for information on [supported node types for Memcached](https://docs.aws.amazon.com/AmazonElastiCache/latest/mem-ug/CacheNodes.SupportedTypes.html) and [guidance on selecting node types for Memcached](https://docs.aws.amazon.com/AmazonElastiCache/latest/mem-ug/nodes-select-size.html). + For Memcached, changing this value will re-create the resource. * `num_cache_nodes` – (Required unless `replication_group_id` is provided) The initial number of cache nodes that the cache cluster will have. For Redis, this value must be 1. For Memcached, this value must be between 1 and 40. If this number is reduced on subsequent runs, the highest numbered nodes will be removed. * `parameter_group_name` – (Required unless `replication_group_id` is provided) The name of the parameter group to associate with this cache cluster. diff --git a/website/docs/r/elasticache_replication_group.html.markdown b/website/docs/r/elasticache_replication_group.html.markdown index c10a52ed666..47d94a7b4be 100644 --- a/website/docs/r/elasticache_replication_group.html.markdown +++ b/website/docs/r/elasticache_replication_group.html.markdown @@ -220,7 +220,10 @@ The following arguments are optional: If `true`, `automatic_failover_enabled` must also be enabled. Defaults to `false`. * `network_type` - (Optional) The IP versions for cache cluster connections. Valid values are `ipv4`, `ipv6` or `dual_stack`. -* `node_type` - (Optional) Instance class to be used. See AWS documentation for information on [supported node types](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/CacheNodes.SupportedTypes.html) and [guidance on selecting node types](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/nodes-select-size.html). Required unless `global_replication_group_id` is set. Cannot be set if `global_replication_group_id` is set. +* `node_type` - (Optional) Instance class to be used. + See AWS documentation for information on [supported node types](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/CacheNodes.SupportedTypes.html) and [guidance on selecting node types](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/nodes-select-size.html). + Required unless `global_replication_group_id` is set. + Cannot be set if `global_replication_group_id` is set. * `notification_topic_arn` – (Optional) ARN of an SNS topic to send ElastiCache notifications to. Example: `arn:aws:sns:us-east-1:012345678999:my_sns_topic` * `num_cache_clusters` - (Optional) Number of cache clusters (primary and replicas) this replication group will have. If `automatic_failover_enabled` or `multi_az_enabled` are `true`, must be at least 2. From c1f7e7cd1b42471f8a300b5f83fbe9bc2832a0dc Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Wed, 18 Sep 2024 13:26:21 -0700 Subject: [PATCH 14/19] Updates `aws_elasticache_reserved_cache_node` to use RFC3339 duration for `duration` --- internal/framework/types/rfc3339_duration.go | 10 ++++ .../elasticache/reserved_cache_node.go | 15 ++++-- internal/types/duration/duration.go | 20 ++++++++ internal/types/duration/duration_test.go | 47 +++++++++++++++++++ ...sticache_reserved_cache_node.html.markdown | 4 +- 5 files changed, 91 insertions(+), 5 deletions(-) diff --git a/internal/framework/types/rfc3339_duration.go b/internal/framework/types/rfc3339_duration.go index d1135d0ee8d..8df8092d9d2 100644 --- a/internal/framework/types/rfc3339_duration.go +++ b/internal/framework/types/rfc3339_duration.go @@ -6,6 +6,7 @@ package types import ( "context" "fmt" + "time" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/attr/xattr" @@ -117,6 +118,15 @@ func RFC3339DurationValue(value string) RFC3339Duration { } } +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 diff --git a/internal/service/elasticache/reserved_cache_node.go b/internal/service/elasticache/reserved_cache_node.go index 088199a71b7..9716af3eb14 100644 --- a/internal/service/elasticache/reserved_cache_node.go +++ b/internal/service/elasticache/reserved_cache_node.go @@ -64,12 +64,14 @@ func (r *resourceReservedCacheNode) Schema(ctx context.Context, request resource Computed: true, PlanModifiers: []planmodifier.Int32{ int32planmodifier.RequiresReplace(), + int32planmodifier.UseStateForUnknown(), }, }, "cache_node_type": schema.StringAttribute{ - Computed: true, + CustomType: fwtypes.RFC3339DurationType, + Computed: true, }, - names.AttrDuration: schema.Int32Attribute{ + names.AttrDuration: schema.StringAttribute{ Computed: true, }, "fixed_price": schema.Float64Attribute{ @@ -80,6 +82,7 @@ func (r *resourceReservedCacheNode) Schema(ctx context.Context, request resource Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), + stringplanmodifier.UseStateForUnknown(), }, }, "reserved_cache_nodes_offering_id": schema.StringAttribute{ @@ -164,6 +167,9 @@ func (r *resourceReservedCacheNode) Create(ctx context.Context, request resource return } + duration := time.Duration(aws.ToInt32(resp.ReservedCacheNode.Duration)) * time.Second + data.Duration = fwtypes.RFC3339DurationTimeDurationValue(duration) + response.Diagnostics.Append(response.State.Set(ctx, &data)...) } @@ -195,6 +201,9 @@ func (r *resourceReservedCacheNode) Read(ctx context.Context, request resource.R return } + duration := time.Duration(aws.ToInt32(reservation.Duration)) * time.Second + data.Duration = fwtypes.RFC3339DurationTimeDurationValue(duration) + response.Diagnostics.Append(response.State.Set(ctx, &data)...) } @@ -216,7 +225,7 @@ type resourceReservedCacheNodeModel struct { ARN types.String `tfsdk:"arn"` CacheNodeCount types.Int32 `tfsdk:"cache_node_count"` CacheNodeType types.String `tfsdk:"cache_node_type"` - Duration types.Int32 `tfsdk:"duration"` + Duration fwtypes.RFC3339Duration `tfsdk:"duration" autoflex:",noflatten"` FixedPrice types.Float64 `tfsdk:"fixed_price"` ID types.String `tfsdk:"id"` ReservedCacheNodesOfferingID types.String `tfsdk:"reserved_cache_nodes_offering_id"` diff --git a/internal/types/duration/duration.go b/internal/types/duration/duration.go index c282862b186..4b5345b15a9 100644 --- a/internal/types/duration/duration.go +++ b/internal/types/duration/duration.go @@ -64,6 +64,26 @@ func Parse(s string) (Duration, error) { return duration, nil } +// NewFromTimeDuration converts a time.Duration to a duration.Duration. +// Only the years and days fields are populated. +func NewFromTimeDuration(t time.Duration) (result Duration) { + u := uint64(t) + + const ( + day = uint64(24 * time.Hour) + year = uint64(365 * day) + ) + + if u >= year { + result.years = int(u / year) + u = u % year + } + + result.days = int(u / day) + + return result +} + func (d Duration) String() string { var b strings.Builder b.WriteString("P") diff --git a/internal/types/duration/duration_test.go b/internal/types/duration/duration_test.go index fa963e568d5..1f24743c6c8 100644 --- a/internal/types/duration/duration_test.go +++ b/internal/types/duration/duration_test.go @@ -96,6 +96,53 @@ func TestParse(t *testing.T) { } } +func TestNewFromTimeDuration(t *testing.T) { + t.Parallel() + + const ( + day = 24 * time.Hour + year = 365 * day + ) + + testcases := map[string]struct { + input time.Duration + expected Duration + }{ + // Single + "years only": { + input: 2 * year, + expected: Duration{years: 2}, + }, + "days only": { + input: 21 * day, + expected: Duration{days: 21}, + }, + + // Multiple + "years days": { + input: 1*year + 15*day, + expected: Duration{years: 1, days: 15}, + }, + + "zero": { + input: 0, + expected: Duration{}, + }, + } + + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + duration := NewFromTimeDuration(tc.input) + + if !duration.equal(tc.expected) { + t.Errorf("expected %q, got %q", tc.expected, duration) + } + }) + } +} + func TestSub(t *testing.T) { t.Parallel() diff --git a/website/docs/r/elasticache_reserved_cache_node.html.markdown b/website/docs/r/elasticache_reserved_cache_node.html.markdown index 18a8bca36c9..d58a64727d1 100644 --- a/website/docs/r/elasticache_reserved_cache_node.html.markdown +++ b/website/docs/r/elasticache_reserved_cache_node.html.markdown @@ -19,7 +19,7 @@ Manages an ElastiCache Reserved Cache Node. ```terraform data "aws_elasticache_reserved_cache_node_offering" "test" { cache_node_type = "cache.t4g.small" - duration = 31536000 + duration = "P1Y" offering_type = "No Upfront" product_description = "redis" } @@ -49,7 +49,7 @@ In addition to all arguments above, the following attributes are exported: * `arn` - ARN for the reserved cache node. * `id` - Unique identifier for the reservation. same as `reservation_id`. -* `duration` - Duration of the reservation in seconds. +* `duration` - Duration of the reservation as an RFC3339 duration. * `fixed_price` – Fixed price charged for this reserved cache node. * `cache_node_type` - Cache node type for the reserved cache nodes. * `offering_type` - Offering type of this reserved cache node. From 7ff6d93b1c888364e0754131eeb8d1e340c98a38 Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Wed, 18 Sep 2024 13:26:37 -0700 Subject: [PATCH 15/19] Updates `aws_elasticache_reserved_cache_node` documentation --- ...sticache_reserved_cache_node.html.markdown | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/website/docs/r/elasticache_reserved_cache_node.html.markdown b/website/docs/r/elasticache_reserved_cache_node.html.markdown index d58a64727d1..be96943f0a5 100644 --- a/website/docs/r/elasticache_reserved_cache_node.html.markdown +++ b/website/docs/r/elasticache_reserved_cache_node.html.markdown @@ -17,7 +17,7 @@ Manages an ElastiCache Reserved Cache Node. ## Example Usage ```terraform -data "aws_elasticache_reserved_cache_node_offering" "test" { +data "aws_elasticache_reserved_cache_node_offering" "example" { cache_node_type = "cache.t4g.small" duration = "P1Y" offering_type = "No Upfront" @@ -25,9 +25,9 @@ data "aws_elasticache_reserved_cache_node_offering" "test" { } resource "aws_elasticache_reserved_cache_node" "example" { - offering_id = data.aws_elasticache_reserved_cache_node_offering.test.offering_id - reservation_id = "optionalCustomReservationID" - cache_node_count = 3 + reserved_cache_nodes_offering_id = data.aws_elasticache_reserved_cache_node_offering.example.offering_id + id = "optionalCustomReservationID" + cache_node_count = 3 } ``` @@ -35,12 +35,15 @@ resource "aws_elasticache_reserved_cache_node" "example" { The following arguments are required: -* `offering_id` - (Required) ID of the reserved cache node offering to purchase. To determine an `offering_id`, see the `aws_elasticache_reserved_cache_node_offering` data source. +* `reserved_cache_nodes_offering_id` - (Required) ID of the reserved cache node offering to purchase. + To determine an `reserved_cache_nodes_offering_id`, see the `aws_elasticache_reserved_cache_node_offering` data source. The following arguments are optional: -* `instance_count` - (Optional) Number of cache node instances to reserve. Default value is `1`. -* `reservation_id` - (Optional) Customer-specified identifier to track this reservation. +* `instance_count` - (Optional) Number of cache node instances to reserve. + Default value is `1`. +* `id` - (Optional) Customer-specified identifier to track this reservation. + If not specified, AWS will assign a random ID. * `tags` - (Optional) Map of tags to assign to the reservation. If configured with a provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. ## Attributes Reference @@ -48,12 +51,11 @@ The following arguments are optional: In addition to all arguments above, the following attributes are exported: * `arn` - ARN for the reserved cache node. -* `id` - Unique identifier for the reservation. same as `reservation_id`. * `duration` - Duration of the reservation as an RFC3339 duration. * `fixed_price` – Fixed price charged for this reserved cache node. -* `cache_node_type` - Cache node type for the reserved cache nodes. +* `cache_node_type` - Node type for the reserved cache nodes. * `offering_type` - Offering type of this reserved cache node. -* `product_description` - Description of the reserved cache node. +* `product_description` - Engine type for the reserved cache node. * `recurring_charges` - Recurring price charged to run this reserved cache node. * `start_time` - Time the reservation started. * `state` - State of the reserved cache node. From afb84297bf3451b320d70703a583d9e22cf91dac Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Wed, 18 Sep 2024 13:45:57 -0700 Subject: [PATCH 16/19] `terrafmt` --- internal/service/elasticache/reserved_cache_node_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/service/elasticache/reserved_cache_node_test.go b/internal/service/elasticache/reserved_cache_node_test.go index 590af408b4f..47b22253e9e 100644 --- a/internal/service/elasticache/reserved_cache_node_test.go +++ b/internal/service/elasticache/reserved_cache_node_test.go @@ -122,7 +122,7 @@ func testAccReservedInstanceExists(ctx context.Context, n string, reservation *a func testAccReservedInstanceConfig_basic() string { return ` resource "aws_elasticache_reserved_cache_node" "test" { - offering_id = data.aws_elasticache_reserved_cache_node_offering.test.offering_id + offering_id = data.aws_elasticache_reserved_cache_node_offering.test.offering_id } data "aws_elasticache_reserved_cache_node_offering" "test" { From e2f7a46bd9bfa00e06f97bbc0b483eed88e333b8 Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Wed, 18 Sep 2024 13:46:10 -0700 Subject: [PATCH 17/19] Linting fixes --- internal/types/duration/duration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/types/duration/duration.go b/internal/types/duration/duration.go index 4b5345b15a9..d235ad7edaf 100644 --- a/internal/types/duration/duration.go +++ b/internal/types/duration/duration.go @@ -71,7 +71,7 @@ func NewFromTimeDuration(t time.Duration) (result Duration) { const ( day = uint64(24 * time.Hour) - year = uint64(365 * day) + year = 365 * day ) if u >= year { From 520bd33438940e728987767833c4fbe0d9a41474 Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Wed, 18 Sep 2024 13:48:07 -0700 Subject: [PATCH 18/19] Documentation fix --- website/docs/r/elasticache_reserved_cache_node.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/r/elasticache_reserved_cache_node.html.markdown b/website/docs/r/elasticache_reserved_cache_node.html.markdown index be96943f0a5..ebd1c175407 100644 --- a/website/docs/r/elasticache_reserved_cache_node.html.markdown +++ b/website/docs/r/elasticache_reserved_cache_node.html.markdown @@ -46,7 +46,7 @@ The following arguments are optional: If not specified, AWS will assign a random ID. * `tags` - (Optional) Map of tags to assign to the reservation. If configured with a provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. -## Attributes Reference +## Attribute Reference In addition to all arguments above, the following attributes are exported: From 5e466edaed37d2053373ea3cf7a5dfa36e7f4201 Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Wed, 18 Sep 2024 15:23:36 -0700 Subject: [PATCH 19/19] Fixes documentation --- .../elasticache_reserved_cache_node.html.markdown | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/website/docs/r/elasticache_reserved_cache_node.html.markdown b/website/docs/r/elasticache_reserved_cache_node.html.markdown index ebd1c175407..3030ad5305b 100644 --- a/website/docs/r/elasticache_reserved_cache_node.html.markdown +++ b/website/docs/r/elasticache_reserved_cache_node.html.markdown @@ -48,7 +48,7 @@ The following arguments are optional: ## Attribute Reference -In addition to all arguments above, the following attributes are exported: +This resource exports the following attributes in addition to the arguments above: * `arn` - ARN for the reserved cache node. * `duration` - Duration of the reservation as an RFC3339 duration. @@ -72,8 +72,17 @@ In addition to all arguments above, the following attributes are exported: ## Import -RDS DB Instance Reservations can be imported using the `instance_id`, e.g., +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import ElastiCache Reserved Cache Nodes using the `id`. For example: +```terraform +import { + to = aws_elasticache_reserved_cache_node.example + id = "CustomReservationID" +} ``` -$ terraform import aws_elasticache_reserved_cache_node.reservation_node CustomReservationID + +Using `terraform import`, import ElastiCache Reserved Cache Node using the `id`. For example: + +```console +% terraform import aws_elasticache_reserved_cache_node.example CustomReservationID ```