Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multiple values per one cost filter in aws_budgets_budget resource #9092

Merged
merged 15 commits into from
Jul 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .changelog/9092.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
```release-note:enhancement
resource/aws_budgets_budget: Add the `cost_filter` argument which allows multiple `values` to be specified per filter. This new argument will eventually replace the `cost_filters` argument
```

```release-note:enhancement
resource/aws_budgets_budget: Change `time_period_start` to an optional argument. If you don't specify a start date, AWS defaults to the start of your chosen time period
```

```release-note:bug
resource/aws_budgets_budget: Change the service name in the `arn` attribute from `budgetservice` to `budgets`
```

```release-note:bug
resource/aws_budgets_budget: Suppress plan differences with trailing zeroes for `limit_amount`
```

```release-note:bug
resource/aws_budgets_budget_action: Change the service name in the `arn` attribute from `budgetservice` to `budgets`
```
143 changes: 136 additions & 7 deletions aws/internal/service/budgets/finder/finder.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,154 @@ package finder
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/budgets"
tfbudgets "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/budgets"
"github.com/hashicorp/aws-sdk-go-base/tfawserr"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

func ActionById(conn *budgets.Budgets, id string) (*budgets.DescribeBudgetActionOutput, error) {
accountID, actionID, budgetName, err := tfbudgets.DecodeBudgetsBudgetActionID(id)
func ActionByAccountIDActionIDAndBudgetName(conn *budgets.Budgets, accountID, actionID, budgetName string) (*budgets.Action, error) {
input := &budgets.DescribeBudgetActionInput{
AccountId: aws.String(accountID),
ActionId: aws.String(actionID),
BudgetName: aws.String(budgetName),
}

output, err := conn.DescribeBudgetAction(input)

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

if err != nil {
return nil, err
}

input := &budgets.DescribeBudgetActionInput{
if output == nil || output.Action == nil {
return nil, &resource.NotFoundError{
Message: "Empty result",
LastRequest: input,
}
}

return output.Action, nil
}

func BudgetByAccountIDAndBudgetName(conn *budgets.Budgets, accountID, budgetName string) (*budgets.Budget, error) {
input := &budgets.DescribeBudgetInput{
AccountId: aws.String(accountID),
BudgetName: aws.String(budgetName),
}

output, err := conn.DescribeBudget(input)

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

if err != nil {
return nil, err
}

if output == nil || output.Budget == nil {
return nil, &resource.NotFoundError{
Message: "Empty result",
LastRequest: input,
}
}

return output.Budget, nil
}

func NotificationsByAccountIDAndBudgetName(conn *budgets.Budgets, accountID, budgetName string) ([]*budgets.Notification, error) {
input := &budgets.DescribeNotificationsForBudgetInput{
AccountId: aws.String(accountID),
ActionId: aws.String(actionID),
BudgetName: aws.String(budgetName),
}
var output []*budgets.Notification

err := conn.DescribeNotificationsForBudgetPages(input, func(page *budgets.DescribeNotificationsForBudgetOutput, lastPage bool) bool {
if page == nil {
return !lastPage
}

for _, notification := range page.Notifications {
if notification == nil {
continue
}

output = append(output, notification)
}

return !lastPage
})

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

out, err := conn.DescribeBudgetAction(input)
if err != nil {
return nil, err
}

return out, nil
if len(output) == 0 {
return nil, &resource.NotFoundError{
Message: "Empty result",
LastRequest: input,
}
}

return output, nil
}

func SubscribersByAccountIDBudgetNameAndNotification(conn *budgets.Budgets, accountID, budgetName string, notification *budgets.Notification) ([]*budgets.Subscriber, error) {
input := &budgets.DescribeSubscribersForNotificationInput{
AccountId: aws.String(accountID),
BudgetName: aws.String(budgetName),
Notification: notification,
}
var output []*budgets.Subscriber

err := conn.DescribeSubscribersForNotificationPages(input, func(page *budgets.DescribeSubscribersForNotificationOutput, lastPage bool) bool {
if page == nil {
return !lastPage
}

for _, subscriber := range page.Subscribers {
if subscriber == nil {
continue
}

output = append(output, subscriber)
}

return !lastPage
})

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

if err != nil {
return nil, err
}

if len(output) == 0 {
return nil, &resource.NotFoundError{
Message: "Empty result",
LastRequest: input,
}
}

return output, nil
}
42 changes: 36 additions & 6 deletions aws/internal/service/budgets/id.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,44 @@
package glue
package budgets

import (
"fmt"
"strings"
)

func DecodeBudgetsBudgetActionID(id string) (string, string, string, error) {
parts := strings.Split(id, ":")
if len(parts) != 3 {
return "", "", "", fmt.Errorf("Unexpected format of ID (%q), expected AccountID:ActionID:BudgetName", id)
const budgetActionResourceIDSeparator = ":"

func BudgetActionCreateResourceID(accountID, actionID, budgetName string) string {
parts := []string{accountID, actionID, budgetName}
id := strings.Join(parts, budgetActionResourceIDSeparator)

return id
}

func BudgetActionParseResourceID(id string) (string, string, string, error) {
parts := strings.Split(id, budgetActionResourceIDSeparator)

if len(parts) == 3 && parts[0] != "" && parts[1] != "" && parts[2] != "" {
return parts[0], parts[1], parts[2], nil
}
return parts[0], parts[1], parts[2], nil

return "", "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected AccountID%[2]sActionID%[2]sBudgetName", id, budgetActionResourceIDSeparator)
}

const budgetResourceIDSeparator = ":"

func BudgetCreateResourceID(accountID, budgetName string) string {
parts := []string{accountID, budgetName}
id := strings.Join(parts, budgetResourceIDSeparator)

return id
}

func BudgetParseResourceID(id string) (string, string, error) {
parts := strings.Split(id, budgetResourceIDSeparator)

if len(parts) == 2 && parts[0] != "" && parts[1] != "" {
return parts[0], parts[1], nil
}

return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected AccountID%[2]sBudgetName", id, budgetActionResourceIDSeparator)
}
44 changes: 44 additions & 0 deletions aws/internal/service/budgets/time.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package budgets

import (
"fmt"
"time"

"github.com/aws/aws-sdk-go/aws"
)

const (
timePeriodLayout = "2006-01-02_15:04"
)

func TimePeriodTimestampFromString(s string) (*time.Time, error) {
if s == "" {
return nil, nil
}

ts, err := time.Parse(timePeriodLayout, s)

if err != nil {
return nil, err
}

return aws.Time(ts), nil
}

func TimePeriodTimestampToString(ts *time.Time) string {
if ts == nil {
return ""
}

return aws.TimeValue(ts).Format(timePeriodLayout)
}

func ValidateTimePeriodTimestamp(v interface{}, k string) (ws []string, errors []error) {
_, err := time.Parse(timePeriodLayout, v.(string))

if err != nil {
errors = append(errors, fmt.Errorf("%q cannot be parsed as %q: %w", k, timePeriodLayout, err))
}

return
}
17 changes: 9 additions & 8 deletions aws/internal/service/budgets/waiter/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,23 @@ package waiter
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/budgets"
"github.com/hashicorp/aws-sdk-go-base/tfawserr"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/service/budgets/finder"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource"
)

func ActionStatus(conn *budgets.Budgets, id string) resource.StateRefreshFunc {
func ActionStatus(conn *budgets.Budgets, accountID, actionID, budgetName string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
out, err := finder.ActionById(conn, id)
output, err := finder.ActionByAccountIDActionIDAndBudgetName(conn, accountID, actionID, budgetName)

if tfresource.NotFound(err) {
return nil, "", nil
}

if err != nil {
if tfawserr.ErrCodeEquals(err, budgets.ErrCodeNotFoundException) {
return nil, "", nil
}
return nil, "", err
}

action := out.Action
return action, aws.StringValue(action.Status), err
return output, aws.StringValue(output.Status), nil
}
}
4 changes: 2 additions & 2 deletions aws/internal/service/budgets/waiter/waiter.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const (
ActionAvailableTimeout = 2 * time.Minute
)

func ActionAvailable(conn *budgets.Budgets, id string) (*budgets.Action, error) {
func ActionAvailable(conn *budgets.Budgets, accountID, actionID, budgetName string) (*budgets.Action, error) {
stateConf := &resource.StateChangeConf{
Pending: []string{
budgets.ActionStatusExecutionInProgress,
Expand All @@ -22,7 +22,7 @@ func ActionAvailable(conn *budgets.Budgets, id string) (*budgets.Action, error)
budgets.ActionStatusExecutionFailure,
budgets.ActionStatusPending,
},
Refresh: ActionStatus(conn, id),
Refresh: ActionStatus(conn, accountID, actionID, budgetName),
Timeout: ActionAvailableTimeout,
}

Expand Down
Loading