Skip to content

Commit

Permalink
Merge pull request hashicorp#8777 from stefansundin/appautoscaling_sc…
Browse files Browse the repository at this point in the history
…heduled_action-capacity-fix
  • Loading branch information
gdavison committed Mar 8, 2021
2 parents b430579 + 16a4749 commit 5e7785b
Show file tree
Hide file tree
Showing 5 changed files with 515 additions and 107 deletions.
7 changes: 7 additions & 0 deletions .changelog/8777.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```release-note:enhancement
resource/aws_appautoscaling_scheduled_action: No longer re-creates when changes can be updated in-place.
```

```release-note:enhancement
resource/aws_appautoscaling_scheduled_action: Allows setting leaving `min_capacity` or `max_capacity` unset.
```
46 changes: 46 additions & 0 deletions aws/internal/service/applicationautoscaling/finder/finder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package finder

import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/applicationautoscaling"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

func ScheduledAction(conn *applicationautoscaling.ApplicationAutoScaling, name, serviceNamespace, resourceId string) (*applicationautoscaling.ScheduledAction, error) {
var result *applicationautoscaling.ScheduledAction

input := &applicationautoscaling.DescribeScheduledActionsInput{
ScheduledActionNames: []*string{aws.String(name)},
ServiceNamespace: aws.String(serviceNamespace),
ResourceId: aws.String(resourceId),
}
err := conn.DescribeScheduledActionsPages(input, func(page *applicationautoscaling.DescribeScheduledActionsOutput, lastPage bool) bool {
if page == nil {
return !lastPage
}

for _, item := range page.ScheduledActions {
if item == nil {
continue
}

if name == aws.StringValue(item.ScheduledActionName) {
result = item
return false
}
}

return !lastPage
})
if err != nil {
return nil, err
}

if result == nil {
return nil, &resource.NotFoundError{
LastRequest: input,
}
}

return result, nil
}
226 changes: 142 additions & 84 deletions aws/resource_aws_appautoscaling_scheduled_action.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,25 @@ package aws
import (
"fmt"
"log"
"strconv"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/applicationautoscaling"
"github.com/hashicorp/aws-sdk-go-base/tfawserr"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/experimental/nullable"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/service/applicationautoscaling/finder"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource"
)

func resourceAwsAppautoscalingScheduledAction() *schema.Resource {
return &schema.Resource{
Create: resourceAwsAppautoscalingScheduledActionPut,
Read: resourceAwsAppautoscalingScheduledActionRead,
Update: resourceAwsAppautoscalingScheduledActionPut,
Delete: resourceAwsAppautoscalingScheduledActionDelete,

Schema: map[string]*schema.Schema{
Expand All @@ -36,54 +42,57 @@ func resourceAwsAppautoscalingScheduledAction() *schema.Resource {
},
"scalable_dimension": {
Type: schema.TypeString,
Optional: true,
Required: true,
ForceNew: true,
},
"scalable_target_action": {
Type: schema.TypeList,
Optional: true,
ForceNew: true,
Required: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"max_capacity": {
Type: schema.TypeInt,
Optional: true,
ForceNew: true,
Type: nullable.TypeNullableInt,
Optional: true,
ValidateFunc: nullable.ValidateTypeStringNullableIntAtLeast(0),
AtLeastOneOf: []string{
"scalable_target_action.0.max_capacity",
"scalable_target_action.0.min_capacity",
},
},
"min_capacity": {
Type: schema.TypeInt,
Optional: true,
ForceNew: true,
Type: nullable.TypeNullableInt,
Optional: true,
ValidateFunc: nullable.ValidateTypeStringNullableIntAtLeast(0),
AtLeastOneOf: []string{
"scalable_target_action.0.max_capacity",
"scalable_target_action.0.min_capacity",
},
},
},
},
},
"schedule": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Required: true,
},
// The AWS API normalizes start_time and end_time to UTC. Uses
// suppressEquivalentTime to allow any timezone to be used.
"start_time": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ValidateFunc: validation.IsRFC3339Time,
DiffSuppressFunc: suppressEquivalentTime,
},
"end_time": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ValidateFunc: validation.IsRFC3339Time,
DiffSuppressFunc: suppressEquivalentTime,
},
"timezone": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Default: "UTC",
},
"arn": {
Expand All @@ -101,109 +110,119 @@ func resourceAwsAppautoscalingScheduledActionPut(d *schema.ResourceData, meta in
ScheduledActionName: aws.String(d.Get("name").(string)),
ServiceNamespace: aws.String(d.Get("service_namespace").(string)),
ResourceId: aws.String(d.Get("resource_id").(string)),
Timezone: aws.String(d.Get("timezone").(string)),
}
if v, ok := d.GetOk("scalable_dimension"); ok {
input.ScalableDimension = aws.String(v.(string))
}
if v, ok := d.GetOk("schedule"); ok {
input.Schedule = aws.String(v.(string))
ScalableDimension: aws.String(d.Get("scalable_dimension").(string)),
}
if v, ok := d.GetOk("scalable_target_action"); ok {
sta := &applicationautoscaling.ScalableTargetAction{}
raw := v.([]interface{})[0].(map[string]interface{})
if max, ok := raw["max_capacity"]; ok {
sta.MaxCapacity = aws.Int64(int64(max.(int)))
}
if min, ok := raw["min_capacity"]; ok {
sta.MinCapacity = aws.Int64(int64(min.(int)))
}
input.ScalableTargetAction = sta

needsPut := true
if d.IsNewResource() {
appautoscalingScheduledActionPopulateInputForCreate(input, d)
} else {
needsPut = appautoscalingScheduledActionPopulateInputForUpdate(input, d)
}
if v, ok := d.GetOk("start_time"); ok {
t, err := time.Parse(time.RFC3339, v.(string))
if err != nil {
return fmt.Errorf("Error Parsing Appautoscaling Scheduled Action Start Time: %w", err)

if needsPut {
err := resource.Retry(5*time.Minute, func() *resource.RetryError {
_, err := conn.PutScheduledAction(input)
if err != nil {
if tfawserr.ErrCodeEquals(err, applicationautoscaling.ErrCodeObjectNotFoundException) {
return resource.RetryableError(err)
}
return resource.NonRetryableError(err)
}
return nil
})
if isResourceTimeoutError(err) {
_, err = conn.PutScheduledAction(input)
}
input.StartTime = aws.Time(t)
}
if v, ok := d.GetOk("end_time"); ok {
t, err := time.Parse(time.RFC3339, v.(string))
if err != nil {
return fmt.Errorf("Error Parsing Appautoscaling Scheduled Action End Time: %w", err)
return fmt.Errorf("error putting Application Auto Scaling scheduled action: %w", err)
}
input.EndTime = aws.Time(t)
}

err := resource.Retry(5*time.Minute, func() *resource.RetryError {
_, err := conn.PutScheduledAction(input)
if err != nil {
if isAWSErr(err, applicationautoscaling.ErrCodeObjectNotFoundException, "") {
return resource.RetryableError(err)
}
return resource.NonRetryableError(err)
if d.IsNewResource() {
d.SetId(d.Get("name").(string) + "-" + d.Get("service_namespace").(string) + "-" + d.Get("resource_id").(string))
}
return nil
})
if isResourceTimeoutError(err) {
_, err = conn.PutScheduledAction(input)
}

if err != nil {
return fmt.Errorf("error putting scheduled action: %w", err)
}

d.SetId(d.Get("name").(string) + "-" + d.Get("service_namespace").(string) + "-" + d.Get("resource_id").(string))
return resourceAwsAppautoscalingScheduledActionRead(d, meta)
}

func resourceAwsAppautoscalingScheduledActionRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).appautoscalingconn
func appautoscalingScheduledActionPopulateInputForCreate(input *applicationautoscaling.PutScheduledActionInput, d *schema.ResourceData) {
input.Schedule = aws.String(d.Get("schedule").(string))
input.ScalableTargetAction = expandScalableTargetAction(d.Get("scalable_target_action").([]interface{}))
input.Timezone = aws.String(d.Get("timezone").(string))

saName := d.Get("name").(string)
input := &applicationautoscaling.DescribeScheduledActionsInput{
ResourceId: aws.String(d.Get("resource_id").(string)),
ScheduledActionNames: []*string{aws.String(saName)},
ServiceNamespace: aws.String(d.Get("service_namespace").(string)),
if v, ok := d.GetOk("start_time"); ok {
t, _ := time.Parse(time.RFC3339, v.(string))
input.StartTime = aws.Time(t)
}
resp, err := conn.DescribeScheduledActions(input)
if err != nil {
return fmt.Errorf("error describing Application Auto Scaling Scheduled Action (%s): %w", d.Id(), err)
if v, ok := d.GetOk("end_time"); ok {
t, _ := time.Parse(time.RFC3339, v.(string))
input.EndTime = aws.Time(t)
}
}

var scheduledAction *applicationautoscaling.ScheduledAction
func appautoscalingScheduledActionPopulateInputForUpdate(input *applicationautoscaling.PutScheduledActionInput, d *schema.ResourceData) bool {
hasChange := false

if resp == nil {
return fmt.Errorf("error describing Application Auto Scaling Scheduled Action (%s): empty response", d.Id())
if d.HasChange("schedule") {
input.Schedule = aws.String(d.Get("schedule").(string))
hasChange = true
}

for _, sa := range resp.ScheduledActions {
if sa == nil {
continue
}
if d.HasChange("scalable_target_action") {
input.ScalableTargetAction = expandScalableTargetAction(d.Get("scalable_target_action").([]interface{}))
hasChange = true
}

if d.HasChange("timezone") {
input.Timezone = aws.String(d.Get("timezone").(string))
hasChange = true
}

if aws.StringValue(sa.ScheduledActionName) == saName {
scheduledAction = sa
break
if d.HasChange("start_time") {
if v, ok := d.GetOk("start_time"); ok {
t, _ := time.Parse(time.RFC3339, v.(string))
input.StartTime = aws.Time(t)
hasChange = true
}
}
if d.HasChange("end_time") {
if v, ok := d.GetOk("end_time"); ok {
t, _ := time.Parse(time.RFC3339, v.(string))
input.EndTime = aws.Time(t)
hasChange = true
}
}

if scheduledAction == nil {
log.Printf("[WARN] Application Autoscaling Scheduled Action (%s) not found, removing from state", d.Id())
return hasChange
}

func resourceAwsAppautoscalingScheduledActionRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).appautoscalingconn

scheduledAction, err := finder.ScheduledAction(conn, d.Get("name").(string), d.Get("service_namespace").(string), d.Get("resource_id").(string))
if tfresource.NotFound(err) {
log.Printf("[WARN] Application Auto Scaling Scheduled Action (%s) not found, removing from state", d.Id())
d.SetId("")
return nil
}
if err != nil {
return fmt.Errorf("error describing Application Auto Scaling Scheduled Action (%s): %w", d.Id(), err)
}

d.Set("arn", scheduledAction.ScheduledActionARN)
if err := d.Set("scalable_target_action", flattenScalableTargetAction(scheduledAction.ScalableTargetAction)); err != nil {
return fmt.Errorf("error setting scalable_target_action: %w", err)
}

d.Set("schedule", scheduledAction.Schedule)
if scheduledAction.StartTime != nil {
d.Set("start_time", scheduledAction.StartTime.Format(time.RFC3339))
}
if scheduledAction.EndTime != nil {
d.Set("end_time", scheduledAction.EndTime.Format(time.RFC3339))
}

d.Set("timezone", scheduledAction.Timezone)
d.Set("arn", scheduledAction.ScheduledActionARN)

return nil
}
Expand All @@ -221,12 +240,51 @@ func resourceAwsAppautoscalingScheduledActionDelete(d *schema.ResourceData, meta
}
_, err := conn.DeleteScheduledAction(input)
if err != nil {
if isAWSErr(err, applicationautoscaling.ErrCodeObjectNotFoundException, "") {
log.Printf("[WARN] Application Autoscaling Scheduled Action (%s) already gone, removing from state", d.Id())
if tfawserr.ErrCodeEquals(err, applicationautoscaling.ErrCodeObjectNotFoundException) {
log.Printf("[WARN] Application Auto Scaling scheduled action (%s) not found, removing from state", d.Id())
return nil
}
return err
}

return nil
}

func expandScalableTargetAction(l []interface{}) *applicationautoscaling.ScalableTargetAction {
if len(l) == 0 || l[0] == nil {
return nil
}

m := l[0].(map[string]interface{})

result := &applicationautoscaling.ScalableTargetAction{}

if v, ok := m["max_capacity"]; ok {
if v, null, _ := nullable.Int(v.(string)).Value(); !null {
result.MaxCapacity = aws.Int64(v)
}
}
if v, ok := m["min_capacity"]; ok {
if v, null, _ := nullable.Int(v.(string)).Value(); !null {
result.MinCapacity = aws.Int64(v)
}
}

return result
}

func flattenScalableTargetAction(cfg *applicationautoscaling.ScalableTargetAction) []interface{} {
if cfg == nil {
return []interface{}{}
}

m := make(map[string]interface{})
if cfg.MaxCapacity != nil {
m["max_capacity"] = strconv.FormatInt(aws.Int64Value(cfg.MaxCapacity), 10)
}
if cfg.MinCapacity != nil {
m["min_capacity"] = strconv.FormatInt(aws.Int64Value(cfg.MinCapacity), 10)
}

return []interface{}{m}
}
Loading

0 comments on commit 5e7785b

Please sign in to comment.