Skip to content

Commit 5e7785b

Browse files
committed
Merge pull request #8777 from stefansundin/appautoscaling_scheduled_action-capacity-fix
2 parents b430579 + 16a4749 commit 5e7785b

5 files changed

Lines changed: 515 additions & 107 deletions

File tree

.changelog/8777.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
```release-note:enhancement
2+
resource/aws_appautoscaling_scheduled_action: No longer re-creates when changes can be updated in-place.
3+
```
4+
5+
```release-note:enhancement
6+
resource/aws_appautoscaling_scheduled_action: Allows setting leaving `min_capacity` or `max_capacity` unset.
7+
```
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package finder
2+
3+
import (
4+
"github.com/aws/aws-sdk-go/aws"
5+
"github.com/aws/aws-sdk-go/service/applicationautoscaling"
6+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
7+
)
8+
9+
func ScheduledAction(conn *applicationautoscaling.ApplicationAutoScaling, name, serviceNamespace, resourceId string) (*applicationautoscaling.ScheduledAction, error) {
10+
var result *applicationautoscaling.ScheduledAction
11+
12+
input := &applicationautoscaling.DescribeScheduledActionsInput{
13+
ScheduledActionNames: []*string{aws.String(name)},
14+
ServiceNamespace: aws.String(serviceNamespace),
15+
ResourceId: aws.String(resourceId),
16+
}
17+
err := conn.DescribeScheduledActionsPages(input, func(page *applicationautoscaling.DescribeScheduledActionsOutput, lastPage bool) bool {
18+
if page == nil {
19+
return !lastPage
20+
}
21+
22+
for _, item := range page.ScheduledActions {
23+
if item == nil {
24+
continue
25+
}
26+
27+
if name == aws.StringValue(item.ScheduledActionName) {
28+
result = item
29+
return false
30+
}
31+
}
32+
33+
return !lastPage
34+
})
35+
if err != nil {
36+
return nil, err
37+
}
38+
39+
if result == nil {
40+
return nil, &resource.NotFoundError{
41+
LastRequest: input,
42+
}
43+
}
44+
45+
return result, nil
46+
}

aws/resource_aws_appautoscaling_scheduled_action.go

Lines changed: 142 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,25 @@ package aws
33
import (
44
"fmt"
55
"log"
6+
"strconv"
67
"time"
78

89
"github.com/aws/aws-sdk-go/aws"
910
"github.com/aws/aws-sdk-go/service/applicationautoscaling"
11+
"github.com/hashicorp/aws-sdk-go-base/tfawserr"
1012
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
1113
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1214
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
15+
"github.com/terraform-providers/terraform-provider-aws/aws/internal/experimental/nullable"
16+
"github.com/terraform-providers/terraform-provider-aws/aws/internal/service/applicationautoscaling/finder"
17+
"github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource"
1318
)
1419

1520
func resourceAwsAppautoscalingScheduledAction() *schema.Resource {
1621
return &schema.Resource{
1722
Create: resourceAwsAppautoscalingScheduledActionPut,
1823
Read: resourceAwsAppautoscalingScheduledActionRead,
24+
Update: resourceAwsAppautoscalingScheduledActionPut,
1925
Delete: resourceAwsAppautoscalingScheduledActionDelete,
2026

2127
Schema: map[string]*schema.Schema{
@@ -36,54 +42,57 @@ func resourceAwsAppautoscalingScheduledAction() *schema.Resource {
3642
},
3743
"scalable_dimension": {
3844
Type: schema.TypeString,
39-
Optional: true,
45+
Required: true,
4046
ForceNew: true,
4147
},
4248
"scalable_target_action": {
4349
Type: schema.TypeList,
44-
Optional: true,
45-
ForceNew: true,
50+
Required: true,
4651
MaxItems: 1,
4752
Elem: &schema.Resource{
4853
Schema: map[string]*schema.Schema{
4954
"max_capacity": {
50-
Type: schema.TypeInt,
51-
Optional: true,
52-
ForceNew: true,
55+
Type: nullable.TypeNullableInt,
56+
Optional: true,
57+
ValidateFunc: nullable.ValidateTypeStringNullableIntAtLeast(0),
58+
AtLeastOneOf: []string{
59+
"scalable_target_action.0.max_capacity",
60+
"scalable_target_action.0.min_capacity",
61+
},
5362
},
5463
"min_capacity": {
55-
Type: schema.TypeInt,
56-
Optional: true,
57-
ForceNew: true,
64+
Type: nullable.TypeNullableInt,
65+
Optional: true,
66+
ValidateFunc: nullable.ValidateTypeStringNullableIntAtLeast(0),
67+
AtLeastOneOf: []string{
68+
"scalable_target_action.0.max_capacity",
69+
"scalable_target_action.0.min_capacity",
70+
},
5871
},
5972
},
6073
},
6174
},
6275
"schedule": {
6376
Type: schema.TypeString,
64-
Optional: true,
65-
ForceNew: true,
77+
Required: true,
6678
},
6779
// The AWS API normalizes start_time and end_time to UTC. Uses
6880
// suppressEquivalentTime to allow any timezone to be used.
6981
"start_time": {
7082
Type: schema.TypeString,
7183
Optional: true,
72-
ForceNew: true,
7384
ValidateFunc: validation.IsRFC3339Time,
7485
DiffSuppressFunc: suppressEquivalentTime,
7586
},
7687
"end_time": {
7788
Type: schema.TypeString,
7889
Optional: true,
79-
ForceNew: true,
8090
ValidateFunc: validation.IsRFC3339Time,
8191
DiffSuppressFunc: suppressEquivalentTime,
8292
},
8393
"timezone": {
8494
Type: schema.TypeString,
8595
Optional: true,
86-
ForceNew: true,
8796
Default: "UTC",
8897
},
8998
"arn": {
@@ -101,109 +110,119 @@ func resourceAwsAppautoscalingScheduledActionPut(d *schema.ResourceData, meta in
101110
ScheduledActionName: aws.String(d.Get("name").(string)),
102111
ServiceNamespace: aws.String(d.Get("service_namespace").(string)),
103112
ResourceId: aws.String(d.Get("resource_id").(string)),
104-
Timezone: aws.String(d.Get("timezone").(string)),
105-
}
106-
if v, ok := d.GetOk("scalable_dimension"); ok {
107-
input.ScalableDimension = aws.String(v.(string))
108-
}
109-
if v, ok := d.GetOk("schedule"); ok {
110-
input.Schedule = aws.String(v.(string))
113+
ScalableDimension: aws.String(d.Get("scalable_dimension").(string)),
111114
}
112-
if v, ok := d.GetOk("scalable_target_action"); ok {
113-
sta := &applicationautoscaling.ScalableTargetAction{}
114-
raw := v.([]interface{})[0].(map[string]interface{})
115-
if max, ok := raw["max_capacity"]; ok {
116-
sta.MaxCapacity = aws.Int64(int64(max.(int)))
117-
}
118-
if min, ok := raw["min_capacity"]; ok {
119-
sta.MinCapacity = aws.Int64(int64(min.(int)))
120-
}
121-
input.ScalableTargetAction = sta
115+
116+
needsPut := true
117+
if d.IsNewResource() {
118+
appautoscalingScheduledActionPopulateInputForCreate(input, d)
119+
} else {
120+
needsPut = appautoscalingScheduledActionPopulateInputForUpdate(input, d)
122121
}
123-
if v, ok := d.GetOk("start_time"); ok {
124-
t, err := time.Parse(time.RFC3339, v.(string))
125-
if err != nil {
126-
return fmt.Errorf("Error Parsing Appautoscaling Scheduled Action Start Time: %w", err)
122+
123+
if needsPut {
124+
err := resource.Retry(5*time.Minute, func() *resource.RetryError {
125+
_, err := conn.PutScheduledAction(input)
126+
if err != nil {
127+
if tfawserr.ErrCodeEquals(err, applicationautoscaling.ErrCodeObjectNotFoundException) {
128+
return resource.RetryableError(err)
129+
}
130+
return resource.NonRetryableError(err)
131+
}
132+
return nil
133+
})
134+
if isResourceTimeoutError(err) {
135+
_, err = conn.PutScheduledAction(input)
127136
}
128-
input.StartTime = aws.Time(t)
129-
}
130-
if v, ok := d.GetOk("end_time"); ok {
131-
t, err := time.Parse(time.RFC3339, v.(string))
132137
if err != nil {
133-
return fmt.Errorf("Error Parsing Appautoscaling Scheduled Action End Time: %w", err)
138+
return fmt.Errorf("error putting Application Auto Scaling scheduled action: %w", err)
134139
}
135-
input.EndTime = aws.Time(t)
136-
}
137140

138-
err := resource.Retry(5*time.Minute, func() *resource.RetryError {
139-
_, err := conn.PutScheduledAction(input)
140-
if err != nil {
141-
if isAWSErr(err, applicationautoscaling.ErrCodeObjectNotFoundException, "") {
142-
return resource.RetryableError(err)
143-
}
144-
return resource.NonRetryableError(err)
141+
if d.IsNewResource() {
142+
d.SetId(d.Get("name").(string) + "-" + d.Get("service_namespace").(string) + "-" + d.Get("resource_id").(string))
145143
}
146-
return nil
147-
})
148-
if isResourceTimeoutError(err) {
149-
_, err = conn.PutScheduledAction(input)
150144
}
151145

152-
if err != nil {
153-
return fmt.Errorf("error putting scheduled action: %w", err)
154-
}
155-
156-
d.SetId(d.Get("name").(string) + "-" + d.Get("service_namespace").(string) + "-" + d.Get("resource_id").(string))
157146
return resourceAwsAppautoscalingScheduledActionRead(d, meta)
158147
}
159148

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

163-
saName := d.Get("name").(string)
164-
input := &applicationautoscaling.DescribeScheduledActionsInput{
165-
ResourceId: aws.String(d.Get("resource_id").(string)),
166-
ScheduledActionNames: []*string{aws.String(saName)},
167-
ServiceNamespace: aws.String(d.Get("service_namespace").(string)),
154+
if v, ok := d.GetOk("start_time"); ok {
155+
t, _ := time.Parse(time.RFC3339, v.(string))
156+
input.StartTime = aws.Time(t)
168157
}
169-
resp, err := conn.DescribeScheduledActions(input)
170-
if err != nil {
171-
return fmt.Errorf("error describing Application Auto Scaling Scheduled Action (%s): %w", d.Id(), err)
158+
if v, ok := d.GetOk("end_time"); ok {
159+
t, _ := time.Parse(time.RFC3339, v.(string))
160+
input.EndTime = aws.Time(t)
172161
}
162+
}
173163

174-
var scheduledAction *applicationautoscaling.ScheduledAction
164+
func appautoscalingScheduledActionPopulateInputForUpdate(input *applicationautoscaling.PutScheduledActionInput, d *schema.ResourceData) bool {
165+
hasChange := false
175166

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

180-
for _, sa := range resp.ScheduledActions {
181-
if sa == nil {
182-
continue
183-
}
172+
if d.HasChange("scalable_target_action") {
173+
input.ScalableTargetAction = expandScalableTargetAction(d.Get("scalable_target_action").([]interface{}))
174+
hasChange = true
175+
}
176+
177+
if d.HasChange("timezone") {
178+
input.Timezone = aws.String(d.Get("timezone").(string))
179+
hasChange = true
180+
}
184181

185-
if aws.StringValue(sa.ScheduledActionName) == saName {
186-
scheduledAction = sa
187-
break
182+
if d.HasChange("start_time") {
183+
if v, ok := d.GetOk("start_time"); ok {
184+
t, _ := time.Parse(time.RFC3339, v.(string))
185+
input.StartTime = aws.Time(t)
186+
hasChange = true
187+
}
188+
}
189+
if d.HasChange("end_time") {
190+
if v, ok := d.GetOk("end_time"); ok {
191+
t, _ := time.Parse(time.RFC3339, v.(string))
192+
input.EndTime = aws.Time(t)
193+
hasChange = true
188194
}
189195
}
190196

191-
if scheduledAction == nil {
192-
log.Printf("[WARN] Application Autoscaling Scheduled Action (%s) not found, removing from state", d.Id())
197+
return hasChange
198+
}
199+
200+
func resourceAwsAppautoscalingScheduledActionRead(d *schema.ResourceData, meta interface{}) error {
201+
conn := meta.(*AWSClient).appautoscalingconn
202+
203+
scheduledAction, err := finder.ScheduledAction(conn, d.Get("name").(string), d.Get("service_namespace").(string), d.Get("resource_id").(string))
204+
if tfresource.NotFound(err) {
205+
log.Printf("[WARN] Application Auto Scaling Scheduled Action (%s) not found, removing from state", d.Id())
193206
d.SetId("")
194207
return nil
195208
}
209+
if err != nil {
210+
return fmt.Errorf("error describing Application Auto Scaling Scheduled Action (%s): %w", d.Id(), err)
211+
}
196212

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

217+
d.Set("schedule", scheduledAction.Schedule)
199218
if scheduledAction.StartTime != nil {
200219
d.Set("start_time", scheduledAction.StartTime.Format(time.RFC3339))
201220
}
202221
if scheduledAction.EndTime != nil {
203222
d.Set("end_time", scheduledAction.EndTime.Format(time.RFC3339))
204223
}
205-
206224
d.Set("timezone", scheduledAction.Timezone)
225+
d.Set("arn", scheduledAction.ScheduledActionARN)
207226

208227
return nil
209228
}
@@ -221,12 +240,51 @@ func resourceAwsAppautoscalingScheduledActionDelete(d *schema.ResourceData, meta
221240
}
222241
_, err := conn.DeleteScheduledAction(input)
223242
if err != nil {
224-
if isAWSErr(err, applicationautoscaling.ErrCodeObjectNotFoundException, "") {
225-
log.Printf("[WARN] Application Autoscaling Scheduled Action (%s) already gone, removing from state", d.Id())
243+
if tfawserr.ErrCodeEquals(err, applicationautoscaling.ErrCodeObjectNotFoundException) {
244+
log.Printf("[WARN] Application Auto Scaling scheduled action (%s) not found, removing from state", d.Id())
226245
return nil
227246
}
228247
return err
229248
}
230249

231250
return nil
232251
}
252+
253+
func expandScalableTargetAction(l []interface{}) *applicationautoscaling.ScalableTargetAction {
254+
if len(l) == 0 || l[0] == nil {
255+
return nil
256+
}
257+
258+
m := l[0].(map[string]interface{})
259+
260+
result := &applicationautoscaling.ScalableTargetAction{}
261+
262+
if v, ok := m["max_capacity"]; ok {
263+
if v, null, _ := nullable.Int(v.(string)).Value(); !null {
264+
result.MaxCapacity = aws.Int64(v)
265+
}
266+
}
267+
if v, ok := m["min_capacity"]; ok {
268+
if v, null, _ := nullable.Int(v.(string)).Value(); !null {
269+
result.MinCapacity = aws.Int64(v)
270+
}
271+
}
272+
273+
return result
274+
}
275+
276+
func flattenScalableTargetAction(cfg *applicationautoscaling.ScalableTargetAction) []interface{} {
277+
if cfg == nil {
278+
return []interface{}{}
279+
}
280+
281+
m := make(map[string]interface{})
282+
if cfg.MaxCapacity != nil {
283+
m["max_capacity"] = strconv.FormatInt(aws.Int64Value(cfg.MaxCapacity), 10)
284+
}
285+
if cfg.MinCapacity != nil {
286+
m["min_capacity"] = strconv.FormatInt(aws.Int64Value(cfg.MinCapacity), 10)
287+
}
288+
289+
return []interface{}{m}
290+
}

0 commit comments

Comments
 (0)