Skip to content

Commit 8c58c9b

Browse files
committed
r/aws_budgets_budget: Add PlannedBudgetLimits support
1 parent 3ede5df commit 8c58c9b

File tree

5 files changed

+296
-15
lines changed

5 files changed

+296
-15
lines changed

internal/service/budgets/budget.go

+107-14
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@ package budgets
22

33
import (
44
"fmt"
5-
"log"
6-
"strings"
7-
85
"github.com/aws/aws-sdk-go/aws"
96
"github.com/aws/aws-sdk-go/aws/arn"
107
"github.com/aws/aws-sdk-go/service/budgets"
@@ -17,6 +14,8 @@ import (
1714
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
1815
"github.com/hashicorp/terraform-provider-aws/internal/verify"
1916
"github.com/shopspring/decimal"
17+
"log"
18+
"strings"
2019
)
2120

2221
func ResourceBudget() *schema.Resource {
@@ -143,12 +142,39 @@ func ResourceBudget() *schema.Resource {
143142
},
144143
"limit_amount": {
145144
Type: schema.TypeString,
146-
Required: true,
145+
Optional: true,
146+
Computed: true,
147147
DiffSuppressFunc: suppressEquivalentBudgetLimitAmount,
148+
ConflictsWith: []string{"planned_limit"},
148149
},
149150
"limit_unit": {
150-
Type: schema.TypeString,
151-
Required: true,
151+
Type: schema.TypeString,
152+
Optional: true,
153+
Computed: true,
154+
ConflictsWith: []string{"planned_limit"},
155+
},
156+
"planned_limit": {
157+
Type: schema.TypeSet,
158+
Optional: true,
159+
Elem: &schema.Resource{
160+
Schema: map[string]*schema.Schema{
161+
"start_time": {
162+
Type: schema.TypeString,
163+
Required: true,
164+
ValidateFunc: ValidTimePeriodTimestamp,
165+
},
166+
"amount": {
167+
Type: schema.TypeString,
168+
Required: true,
169+
DiffSuppressFunc: suppressEquivalentBudgetLimitAmount,
170+
},
171+
"unit": {
172+
Type: schema.TypeString,
173+
Required: true,
174+
},
175+
},
176+
},
177+
ConflictsWith: []string{"limit_amount", "limit_unit"},
152178
},
153179
"name": {
154180
Type: schema.TypeString,
@@ -312,6 +338,7 @@ func resourceBudgetRead(d *schema.ResourceData, meta interface{}) error {
312338
d.Set("limit_unit", budget.BudgetLimit.Unit)
313339
}
314340

341+
d.Set("planned_limit", convertPlannedBudgetLimitsToSet(budget.PlannedBudgetLimits))
315342
d.Set("name", budget.BudgetName)
316343
d.Set("name_prefix", create.NamePrefixFromName(aws.StringValue(budget.BudgetName)))
317344

@@ -556,13 +583,58 @@ func convertCostFiltersToStringMap(costFilters map[string][]*string) map[string]
556583
return convertedCostFilters
557584
}
558585

586+
func convertPlannedBudgetLimitsToSet(plannedBudgetLimits map[string]*budgets.Spend) []interface{} {
587+
if plannedBudgetLimits == nil {
588+
return nil
589+
}
590+
591+
convertedPlannedBudgetLimits := make([]interface{}, len(plannedBudgetLimits))
592+
i := 0
593+
594+
for k, v := range plannedBudgetLimits {
595+
if v == nil {
596+
return nil
597+
}
598+
599+
startTime, err := TimePeriodSecondsToString(k)
600+
if err != nil {
601+
return nil
602+
}
603+
604+
convertedPlannedBudgetLimit := make(map[string]string)
605+
convertedPlannedBudgetLimit["start_time"] = startTime
606+
convertedPlannedBudgetLimit["amount"] = *v.Amount
607+
convertedPlannedBudgetLimit["unit"] = *v.Unit
608+
609+
convertedPlannedBudgetLimits[i] = convertedPlannedBudgetLimit
610+
i++
611+
}
612+
613+
return convertedPlannedBudgetLimits
614+
}
615+
559616
func expandBudgetUnmarshal(d *schema.ResourceData) (*budgets.Budget, error) {
560617
budgetName := d.Get("name").(string)
561618
budgetType := d.Get("budget_type").(string)
562-
budgetLimitAmount := d.Get("limit_amount").(string)
563-
budgetLimitUnit := d.Get("limit_unit").(string)
564619
budgetTimeUnit := d.Get("time_unit").(string)
565620
budgetCostFilters := make(map[string][]*string)
621+
var budgetLimit *budgets.Spend
622+
var plannedBudgetLimits map[string]*budgets.Spend
623+
624+
if plannedBudgetLimitsRaw, ok := d.GetOk("planned_limit"); ok {
625+
plannedBudgetLimitsRaw := plannedBudgetLimitsRaw.(*schema.Set).List()
626+
627+
var err error
628+
plannedBudgetLimits, err = expandPlannedBudgetLimitsUnmarshal(plannedBudgetLimitsRaw)
629+
if err != nil {
630+
return nil, err
631+
}
632+
} else {
633+
budgetLimit = &budgets.Spend{
634+
Amount: aws.String(d.Get("limit_amount").(string)),
635+
Unit: aws.String(d.Get("limit_unit").(string)),
636+
}
637+
}
566638

567639
if costFilter, ok := d.GetOk("cost_filter"); ok {
568640
for _, v := range costFilter.(*schema.Set).List() {
@@ -592,16 +664,14 @@ func expandBudgetUnmarshal(d *schema.ResourceData) (*budgets.Budget, error) {
592664
}
593665

594666
budget := &budgets.Budget{
595-
BudgetName: aws.String(budgetName),
596-
BudgetType: aws.String(budgetType),
597-
BudgetLimit: &budgets.Spend{
598-
Amount: aws.String(budgetLimitAmount),
599-
Unit: aws.String(budgetLimitUnit),
600-
},
667+
BudgetName: aws.String(budgetName),
668+
BudgetType: aws.String(budgetType),
669+
PlannedBudgetLimits: plannedBudgetLimits,
601670
TimePeriod: &budgets.TimePeriod{
602671
End: budgetTimePeriodEnd,
603672
Start: budgetTimePeriodStart,
604673
},
674+
BudgetLimit: budgetLimit,
605675
TimeUnit: aws.String(budgetTimeUnit),
606676
CostFilters: budgetCostFilters,
607677
}
@@ -657,6 +727,29 @@ func expandCostTypes(tfMap map[string]interface{}) *budgets.CostTypes {
657727
return apiObject
658728
}
659729

730+
func expandPlannedBudgetLimitsUnmarshal(plannedBudgetLimitsRaw []interface{}) (map[string]*budgets.Spend, error) {
731+
plannedBudgetLimits := make(map[string]*budgets.Spend, len(plannedBudgetLimitsRaw))
732+
733+
for _, plannedBudgetLimit := range plannedBudgetLimitsRaw {
734+
plannedBudgetLimit := plannedBudgetLimit.(map[string]interface{})
735+
736+
key, err := TimePeriodSecondsFromString(plannedBudgetLimit["start_time"].(string))
737+
if err != nil {
738+
return nil, err
739+
}
740+
741+
amount := plannedBudgetLimit["amount"].(string)
742+
unit := plannedBudgetLimit["unit"].(string)
743+
744+
plannedBudgetLimits[key] = &budgets.Spend{
745+
Amount: aws.String(amount),
746+
Unit: aws.String(unit),
747+
}
748+
}
749+
750+
return plannedBudgetLimits, nil
751+
}
752+
660753
func expandBudgetNotificationsUnmarshal(notificationsRaw []interface{}) ([]*budgets.Notification, [][]*budgets.Subscriber) {
661754

662755
notifications := make([]*budgets.Notification, len(notificationsRaw))

internal/service/budgets/budget_test.go

+104
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,55 @@ func TestAccBudgetsBudget_notifications(t *testing.T) {
343343
})
344344
}
345345

346+
func TestAccBudgetsBudget_plannedLimits(t *testing.T) {
347+
var budget budgets.Budget
348+
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
349+
resourceName := "aws_budgets_budget.test"
350+
now := time.Now()
351+
config1, testCheckFuncs1 := generateStartTimes(resourceName, "100.0", now)
352+
config2, testCheckFuncs2 := generateStartTimes(resourceName, "200.0", now)
353+
354+
resource.ParallelTest(t, resource.TestCase{
355+
PreCheck: func() { acctest.PreCheck(t); acctest.PreCheckPartitionHasService(budgets.EndpointsID, t) },
356+
ErrorCheck: acctest.ErrorCheck(t, budgets.EndpointsID),
357+
ProviderFactories: acctest.ProviderFactories,
358+
CheckDestroy: testAccBudgetDestroy,
359+
Steps: []resource.TestStep{
360+
{
361+
Config: testAccBudgetConfig_plannedLimits(rName, config1),
362+
Check: resource.ComposeTestCheckFunc(
363+
append(
364+
testCheckFuncs1,
365+
testAccBudgetExists(resourceName, &budget),
366+
resource.TestCheckResourceAttr(resourceName, "name", rName),
367+
resource.TestCheckResourceAttr(resourceName, "budget_type", "COST"),
368+
resource.TestCheckResourceAttr(resourceName, "time_unit", "MONTHLY"),
369+
resource.TestCheckResourceAttr(resourceName, "planned_limit.#", "12"),
370+
)...,
371+
),
372+
},
373+
{
374+
ResourceName: resourceName,
375+
ImportState: true,
376+
ImportStateVerify: true,
377+
},
378+
{
379+
Config: testAccBudgetConfig_plannedLimitsUpdated(rName, config2),
380+
Check: resource.ComposeTestCheckFunc(
381+
append(
382+
testCheckFuncs2,
383+
testAccBudgetExists(resourceName, &budget),
384+
resource.TestCheckResourceAttr(resourceName, "name", rName),
385+
resource.TestCheckResourceAttr(resourceName, "budget_type", "COST"),
386+
resource.TestCheckResourceAttr(resourceName, "time_unit", "MONTHLY"),
387+
resource.TestCheckResourceAttr(resourceName, "planned_limit.#", "12"),
388+
)...,
389+
),
390+
},
391+
},
392+
})
393+
}
394+
346395
func testAccBudgetExists(resourceName string, v *budgets.Budget) resource.TestCheckFunc {
347396
return func(s *terraform.State) error {
348397
rs, ok := s.RootModule().Resources[resourceName]
@@ -555,3 +604,58 @@ resource "aws_budgets_budget" "test" {
555604
}
556605
`, rName, emailAddress1)
557606
}
607+
608+
func testAccBudgetConfig_plannedLimits(rName, config string) string {
609+
return fmt.Sprintf(`
610+
resource "aws_budgets_budget" "test" {
611+
name = %[1]q
612+
budget_type = "COST"
613+
time_unit = "MONTHLY"
614+
%[2]s
615+
}
616+
`, rName, config)
617+
}
618+
619+
func testAccBudgetConfig_plannedLimitsUpdated(rName, config string) string {
620+
return fmt.Sprintf(`
621+
resource "aws_budgets_budget" "test" {
622+
name = %[1]q
623+
budget_type = "COST"
624+
time_unit = "MONTHLY"
625+
%[2]s
626+
}
627+
`, rName, config)
628+
}
629+
630+
func generateStartTimes(resourceName, amount string, now time.Time) (string, []resource.TestCheckFunc) {
631+
startTimes := make([]time.Time, 12)
632+
633+
year, month, _ := now.Date()
634+
startTimes[0] = time.Date(year, month, 1, 0, 0, 0, 0, time.UTC)
635+
636+
for i := 1; i < len(startTimes); i++ {
637+
startTimes[i] = startTimes[i-1].AddDate(0, 1, 0)
638+
}
639+
640+
configBuilder := strings.Builder{}
641+
for i := 0; i < len(startTimes); i++ {
642+
configBuilder.WriteString(fmt.Sprintf(`
643+
planned_limit {
644+
start_time = %[1]q
645+
amount = %[2]q
646+
unit = "USD"
647+
}
648+
`, tfbudgets.TimePeriodTimestampToString(&startTimes[i]), amount))
649+
}
650+
651+
testCheckFuncs := make([]resource.TestCheckFunc, len(startTimes))
652+
for i := 0; i < len(startTimes); i++ {
653+
testCheckFuncs[i] = resource.TestCheckTypeSetElemNestedAttrs(resourceName, "planned_limit.*", map[string]string{
654+
"start_time": tfbudgets.TimePeriodTimestampToString(&startTimes[i]),
655+
"amount": amount,
656+
"unit": "USD",
657+
})
658+
}
659+
660+
return configBuilder.String(), testCheckFuncs
661+
}

internal/service/budgets/time.go

+26
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package budgets
22

33
import (
44
"fmt"
5+
"strconv"
56
"time"
67

78
"github.com/aws/aws-sdk-go/aws"
@@ -33,6 +34,31 @@ func TimePeriodTimestampToString(ts *time.Time) string {
3334
return aws.TimeValue(ts).Format(timePeriodLayout)
3435
}
3536

37+
func TimePeriodSecondsFromString(s string) (string, error) {
38+
if s == "" {
39+
return "", nil
40+
}
41+
42+
ts, err := time.Parse(timePeriodLayout, s)
43+
44+
if err != nil {
45+
return "", err
46+
}
47+
48+
return strconv.FormatInt(aws.Time(ts).Unix(), 10), nil
49+
}
50+
51+
func TimePeriodSecondsToString(s string) (string, error) {
52+
startTime, err := strconv.ParseInt(s, 10, 64)
53+
if err != nil {
54+
return "", err
55+
}
56+
57+
startTime = startTime * 1000
58+
59+
return aws.SecondsTimeValue(&startTime).UTC().Format(timePeriodLayout), nil
60+
}
61+
3662
func ValidTimePeriodTimestamp(v interface{}, k string) (ws []string, errors []error) {
3763
_, err := time.Parse(timePeriodLayout, v.(string))
3864

internal/service/budgets/time_test.go

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package budgets
2+
3+
import "testing"
4+
5+
func TestTimePeriodSecondsFromString(t *testing.T) {
6+
seconds, err := TimePeriodSecondsFromString("2020-03-01_00:00")
7+
if err != nil {
8+
t.Errorf("unexpected error: %s", err)
9+
}
10+
11+
want := "1583020800"
12+
if seconds != want {
13+
t.Errorf("got %s, expected %s", seconds, want)
14+
}
15+
}
16+
17+
func TestTimePeriodSecondsToString(t *testing.T) {
18+
ts, err := TimePeriodSecondsToString("1583020800")
19+
if err != nil {
20+
t.Errorf("unexpected error: %s", err)
21+
}
22+
23+
want := "2020-03-01_00:00"
24+
if ts != want {
25+
t.Errorf("got %s, expected %s", ts, want)
26+
}
27+
}

0 commit comments

Comments
 (0)