Skip to content

Commit

Permalink
Added support for ElastiCache reserved cache nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
mousavian committed Mar 7, 2023
1 parent c69d133 commit bb08389
Show file tree
Hide file tree
Showing 8 changed files with 568 additions and 0 deletions.
11 changes: 11 additions & 0 deletions internal/service/elasticache/consts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package elasticache

const (
ResNameTags = "Tags"
)

const (
ReservedCacheNodeStateActive = "active"
ReservedCacheNodeStateRetired = "retired"
ReservedCacheNodeStatePaymentPending = "payment-pending"
)
29 changes: 29 additions & 0 deletions internal/service/elasticache/find.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,3 +337,32 @@ func FindCacheSubnetGroupByName(ctx context.Context, conn *elasticache.ElastiCac

return output.CacheSubnetGroups[0], nil
}

func FindReservedCacheNodeByID(ctx context.Context, conn *elasticache.ElastiCache, id string) (*elasticache.ReservedCacheNode, error) {
input := &elasticache.DescribeReservedCacheNodesInput{
ReservedCacheNodeId: aws.String(id),
}

output, err := conn.DescribeReservedCacheNodesWithContext(ctx, input)

if tfawserr.ErrCodeEquals(err, elasticache.ErrCodeReservedCacheNodeNotFoundFault) {
return nil, &resource.NotFoundError{
LastError: err,
LastRequest: input,
}
}

if err != nil {
return nil, err
}

if output == nil || len(output.ReservedCacheNodes) == 0 || output.ReservedCacheNodes[0] == nil {
return nil, tfresource.NewEmptyResultError(input)
}

if count := len(output.ReservedCacheNodes); count > 1 {
return nil, tfresource.NewTooManyResultsError(count, input)
}

return output.ReservedCacheNodes[0], nil
}
247 changes: 247 additions & 0 deletions internal/service/elasticache/reserved_cache_node.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
package elasticache

import (
"context"
"fmt"
"log"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go/service/elasticache"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"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"
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")
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,
ForceNew: true,
Default: 1,
},
"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,
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 interface{}) diag.Diagnostics {
conn := meta.(*conns.AWSClient).ElastiCacheConn()
defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig
tags := defaultTagsConfig.MergeTags(tftags.New(ctx, d.Get("tags").(map[string]interface{})))

input := &elasticache.PurchaseReservedCacheNodesOfferingInput{
ReservedCacheNodesOfferingId: aws.String(d.Get("offering_id").(string)),
}

if v, ok := d.Get("cache_node_count").(int); ok && v > 0 {
input.CacheNodeCount = aws.Int64(int64(d.Get("cache_node_count").(int)))
}

if v, ok := d.Get("reservation_id").(string); ok && v != "" {
input.ReservedCacheNodeId = aws.String(v)
}

if len(tags) > 0 {
input.Tags = Tags(tags.IgnoreAWS())
}

resp, err := conn.PurchaseReservedCacheNodesOfferingWithContext(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 interface{}) diag.Diagnostics {
conn := meta.(*conns.AWSClient).ElastiCacheConn()
defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig
ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig

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)

tags, err := ListTags(ctx, conn, aws.ToString(reservation.ReservationARN))
tags = tags.IgnoreAWS().IgnoreConfig(ignoreTagsConfig)

if err != nil {
return create.DiagError(names.CE, create.ErrActionReading, ResNameTags, d.Id(), err)
}

//lintignore:AWSR002
if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil {
return create.DiagError(names.CE, create.ErrActionUpdating, ResNameTags, d.Id(), err)
}

if err := d.Set("tags_all", tags.Map()); err != nil {
return create.DiagError(names.CE, create.ErrActionUpdating, ResNameTags, d.Id(), err)
}

return nil
}

func resourceReservedCacheNodeUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
conn := meta.(*conns.AWSClient).ElastiCacheConn()

if d.HasChange("tags") {
o, n := d.GetChange("tags")

if err := UpdateTags(ctx, conn, d.Get("arn").(string), o, n); err != nil {
return create.DiagError(names.ElastiCache, create.ErrActionUpdating, ResNameTags, d.Id(), err)
}
}

if d.HasChange("tags_all") {
o, n := d.GetChange("tags_all")

if err := UpdateTags(ctx, conn, d.Get("arn").(string), o, n); err != nil {
return create.DiagError(names.ElastiCache, create.ErrActionUpdating, ResNameTags, d.Id(), err)
}
}

return resourceReservedCacheNodeRead(ctx, d, meta)
}

func resourceReservedCacheNodeDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
// Reservations cannot be deleted. Removing from state.
log.Printf("[DEBUG] %s %s cannot be deleted. Removing from state.: %s", names.ElastiCache, ResNameReservedCacheNode, d.Id())

return nil
}

func flattenRecurringCharges(recurringCharges []*elasticache.RecurringCharge) []interface{} {
if len(recurringCharges) == 0 {
return []interface{}{}
}

var rawRecurringCharges []interface{}
for _, recurringCharge := range recurringCharges {
rawRecurringCharge := map[string]interface{}{
"recurring_charge_amount": recurringCharge.RecurringChargeAmount,
"recurring_charge_frequency": aws.ToString(recurringCharge.RecurringChargeFrequency),
}

rawRecurringCharges = append(rawRecurringCharges, rawRecurringCharge)
}

return rawRecurringCharges
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package elasticache

import (
"context"
"fmt"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go/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/create"
"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 {
conn := meta.(*conns.AWSClient).ElastiCacheConn()

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.DescribeReservedCacheNodesOfferingsWithContext(ctx, input)

if err != nil {
return create.DiagError(names.ElastiCache, create.ErrActionReading, ResNameReservedCacheNodeOffering, "unknown", err)
}

if len(resp.ReservedCacheNodesOfferings) == 0 {
return diag.Errorf("no %s %s found matching criteria; try different search", names.ElastiCache, ResNameReservedCacheNodeOffering)
}

if len(resp.ReservedCacheNodesOfferings) > 1 {
return diag.Errorf("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 nil
}
Loading

0 comments on commit bb08389

Please sign in to comment.