From 931e3ecda05e74603fce1b0a45715e91cf1a3bc7 Mon Sep 17 00:00:00 2001 From: drfaust92 Date: Thu, 27 May 2021 21:39:37 +0300 Subject: [PATCH 01/10] inital commit --- aws/internal/service/budgets/finder/finder.go | 27 + aws/internal/service/budgets/id.go | 14 + aws/provider.go | 1 + aws/resource_aws_budgets_budget_action.go | 606 ++++++++++++++++++ ...resource_aws_budgets_budget_action_test.go | 261 ++++++++ 5 files changed, 909 insertions(+) create mode 100644 aws/internal/service/budgets/finder/finder.go create mode 100644 aws/internal/service/budgets/id.go create mode 100644 aws/resource_aws_budgets_budget_action.go create mode 100644 aws/resource_aws_budgets_budget_action_test.go diff --git a/aws/internal/service/budgets/finder/finder.go b/aws/internal/service/budgets/finder/finder.go new file mode 100644 index 00000000000..2c399fd8e10 --- /dev/null +++ b/aws/internal/service/budgets/finder/finder.go @@ -0,0 +1,27 @@ +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" +) + +func ActionById(conn *budgets.Budgets, id string) (*budgets.DescribeBudgetActionOutput, error) { + accountID, actionID, budgetName, err := tfbudgets.DecodeBudgetsBudgetActionID(id) + if err != nil { + return nil, err + } + + input := &budgets.DescribeBudgetActionInput{ + BudgetName: aws.String(budgetName), + AccountId: aws.String(accountID), + ActionId: aws.String(actionID), + } + + out, err := conn.DescribeBudgetAction(input) + if err != nil { + return nil, err + } + + return out, nil +} diff --git a/aws/internal/service/budgets/id.go b/aws/internal/service/budgets/id.go new file mode 100644 index 00000000000..63d140c7fae --- /dev/null +++ b/aws/internal/service/budgets/id.go @@ -0,0 +1,14 @@ +package glue + +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) + } + return parts[0], parts[1], parts[2], nil +} diff --git a/aws/provider.go b/aws/provider.go index b47fadaea28..b64305a8b89 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -532,6 +532,7 @@ func Provider() *schema.Provider { "aws_backup_vault_notifications": resourceAwsBackupVaultNotifications(), "aws_backup_vault_policy": resourceAwsBackupVaultPolicy(), "aws_budgets_budget": resourceAwsBudgetsBudget(), + "aws_budgets_budget_action": resourceAwsBudgetsBudgetAction(), "aws_cloud9_environment_ec2": resourceAwsCloud9EnvironmentEc2(), "aws_cloudformation_stack": resourceAwsCloudFormationStack(), "aws_cloudformation_stack_set": resourceAwsCloudFormationStackSet(), diff --git a/aws/resource_aws_budgets_budget_action.go b/aws/resource_aws_budgets_budget_action.go new file mode 100644 index 00000000000..744ec4992e6 --- /dev/null +++ b/aws/resource_aws_budgets_budget_action.go @@ -0,0 +1,606 @@ +package aws + +import ( + "fmt" + "log" + "regexp" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" + "github.com/aws/aws-sdk-go/service/budgets" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + tfbudgets "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/budgets" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/budgets/finder" +) + +func resourceAwsBudgetsBudgetAction() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsBudgetsBudgetActionCreate, + Read: resourceAwsBudgetsBudgetActionRead, + Update: resourceAwsBudgetsBudgetActionUpdate, + Delete: resourceAwsBudgetsBudgetActionDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "account_id": { + Type: schema.TypeString, + Computed: true, + Optional: true, + ForceNew: true, + ValidateFunc: validateAwsAccountId, + }, + "action_id": { + Type: schema.TypeString, + Computed: true, + }, + "action_threshold": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "action_threshold_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(budgets.ThresholdType_Values(), false), + }, + "action_threshold_value": { + Type: schema.TypeFloat, + Required: true, + ValidateFunc: validation.FloatBetween(0, 40000000000), + }, + }, + }, + }, + "action_type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(budgets.ActionType_Values(), false), + }, + "approval_model": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(budgets.ApprovalModel_Values(), false), + }, + "budget_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.All( + validation.StringLenBetween(1, 100), + validation.StringMatch(regexp.MustCompile(`[^:\\]+`), "The ':' and '\\' characters aren't allowed."), + ), + }, + "definition": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "iam_action_definition": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "policy_arn": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateArn, + }, + "groups": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 100, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "roles": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 100, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "users": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 100, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + "ssm_action_definition": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "action_sub_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(budgets.ActionSubType_Values(), false), + }, + "instance_ids": { + Type: schema.TypeSet, + Required: true, + MaxItems: 100, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "region": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + "scp_action_definition": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "policy_id": { + Type: schema.TypeString, + Required: true, + }, + "target_ids": { + Type: schema.TypeSet, + Required: true, + MaxItems: 100, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + }, + }, + }, + "execution_role_arn": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateArn, + }, + "notification_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(budgets.NotificationType_Values(), false), + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + "subscriber": { + Type: schema.TypeSet, + Required: true, + MaxItems: 11, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "address": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.All( + validation.StringLenBetween(1, 2147483647), + validation.StringMatch(regexp.MustCompile(`(.*[\n\r\t\f\ ]?)*`), "Can't contain line breaks."), + )}, + "subscription_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(budgets.SubscriptionType_Values(), false), + }, + }, + }, + }, + }, + } +} + +func resourceAwsBudgetsBudgetActionCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).budgetconn + + var accountID string + if v, ok := d.GetOk("account_id"); ok { + accountID = v.(string) + } else { + accountID = meta.(*AWSClient).accountid + } + + input := &budgets.CreateBudgetActionInput{ + AccountId: aws.String(accountID), + BudgetName: aws.String(d.Get("budget_name").(string)), + ActionType: aws.String(d.Get("action_type").(string)), + ApprovalModel: aws.String(d.Get("approval_model").(string)), + ExecutionRoleArn: aws.String(d.Get("execution_role_arn").(string)), + NotificationType: aws.String(d.Get("notification_type").(string)), + ActionThreshold: expandAwsBudgetsBudgetActionActionThreshold(d.Get("action_threshold").([]interface{})), + Subscribers: expandAwsBudgetsBudgetActionSubscriber(d.Get("subscriber").(*schema.Set)), + Definition: expandAwsBudgetsBudgetActionActionDefinition(d.Get("definition").([]interface{})), + } + + var output *budgets.CreateBudgetActionOutput + _, err := retryOnAwsCode(budgets.ErrCodeAccessDeniedException, func() (interface{}, error) { + var err error + output, err = conn.CreateBudgetAction(input) + return output, err + }) + if err != nil { + return fmt.Errorf("create budget failed: %v", err) + } + + d.SetId(fmt.Sprintf("%s:%s:%s", aws.StringValue(output.AccountId), aws.StringValue(output.ActionId), aws.StringValue(output.BudgetName))) + + return resourceAwsBudgetsBudgetActionRead(d, meta) +} + +func resourceAwsBudgetsBudgetActionRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).budgetconn + out, err := finder.ActionById(conn, d.Id()) + if isAWSErr(err, budgets.ErrCodeNotFoundException, "") { + log.Printf("[WARN] Budget Action %s not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("describe Budget Action failed: %w", err) + } + + action := out.Action + if action == nil { + log.Printf("[WARN] Budget Action %s not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + budgetName := aws.StringValue(out.BudgetName) + actId := aws.StringValue(action.ActionId) + d.Set("account_id", out.AccountId) + d.Set("budget_name", budgetName) + d.Set("action_id", actId) + d.Set("action_type", action.ActionType) + d.Set("approval_model", action.ApprovalModel) + d.Set("execution_role_arn", action.ExecutionRoleArn) + d.Set("notification_type", action.NotificationType) + d.Set("status", action.Status) + + if err := d.Set("subscriber", flattenAwsBudgetsBudgetActionSubscriber(action.Subscribers)); err != nil { + return fmt.Errorf("error setting subscriber: %w", err) + } + + if err := d.Set("definition", flattenAwsBudgetsBudgetActionDefinition(action.Definition)); err != nil { + return fmt.Errorf("error setting definition: %w", err) + } + + if err := d.Set("action_threshold", flattenAwsBudgetsBudgetActionActionThreshold(action.ActionThreshold)); err != nil { + return fmt.Errorf("error setting action_threshold: %w", err) + } + + arn := arn.ARN{ + Partition: meta.(*AWSClient).partition, + Service: "budgetservice", + AccountID: meta.(*AWSClient).accountid, + Resource: fmt.Sprintf("budget/%s/action/%s", budgetName, actId), + } + d.Set("arn", arn.String()) + + return nil +} + +func resourceAwsBudgetsBudgetActionUpdate(d *schema.ResourceData, meta interface{}) error { + accountID, actionID, budgetName, err := tfbudgets.DecodeBudgetsBudgetActionID(d.Id()) + if err != nil { + return err + } + + conn := meta.(*AWSClient).budgetconn + input := &budgets.UpdateBudgetActionInput{ + BudgetName: aws.String(budgetName), + AccountId: aws.String(accountID), + ActionId: aws.String(actionID), + } + + if d.HasChange("approval_model") { + input.ApprovalModel = aws.String(d.Get("approval_model").(string)) + } + + if d.HasChange("execution_role_arn") { + input.ExecutionRoleArn = aws.String(d.Get("execution_role_arn").(string)) + } + + if d.HasChange("notification_type") { + input.NotificationType = aws.String(d.Get("notification_type").(string)) + } + + if d.HasChange("action_threshold") { + input.ActionThreshold = expandAwsBudgetsBudgetActionActionThreshold(d.Get("action_threshold").([]interface{})) + } + + if d.HasChange("subscriber") { + input.Subscribers = expandAwsBudgetsBudgetActionSubscriber(d.Get("subscriber").(*schema.Set)) + } + + if d.HasChange("definition") { + input.Definition = expandAwsBudgetsBudgetActionActionDefinition(d.Get("definition").([]interface{})) + } + + _, err = conn.UpdateBudgetAction(input) + if err != nil { + return fmt.Errorf("Updating Budget Action failed: %w", err) + } + + return resourceAwsBudgetsBudgetActionRead(d, meta) +} + +func resourceAwsBudgetsBudgetActionDelete(d *schema.ResourceData, meta interface{}) error { + accountID, actionID, budgetName, err := tfbudgets.DecodeBudgetsBudgetActionID(d.Id()) + if err != nil { + return err + } + + conn := meta.(*AWSClient).budgetconn + _, err = conn.DeleteBudgetAction(&budgets.DeleteBudgetActionInput{ + BudgetName: aws.String(budgetName), + AccountId: aws.String(accountID), + ActionId: aws.String(actionID), + }) + if err != nil { + if isAWSErr(err, budgets.ErrCodeNotFoundException, "") { + log.Printf("[INFO] Budget Action %s could not be found. skipping delete.", d.Id()) + return nil + } + + return fmt.Errorf("Deleting Budget Action failed: %w", err) + } + + return nil +} + +func expandAwsBudgetsBudgetActionActionThreshold(l []interface{}) *budgets.ActionThreshold { + if len(l) == 0 || l[0] == nil { + return nil + } + + m := l[0].(map[string]interface{}) + + config := &budgets.ActionThreshold{} + + if v, ok := m["action_threshold_type"].(string); ok && v != "" { + config.ActionThresholdType = aws.String(v) + } + + if v, ok := m["action_threshold_value"].(float64); ok { + config.ActionThresholdValue = aws.Float64(v) + } + + return config +} + +func expandAwsBudgetsBudgetActionSubscriber(l *schema.Set) []*budgets.Subscriber { + if l.Len() == 0 { + return []*budgets.Subscriber{} + } + + items := []*budgets.Subscriber{} + + for _, m := range l.List() { + config := &budgets.Subscriber{} + raw := m.(map[string]interface{}) + + if v, ok := raw["address"].(string); ok && v != "" { + config.Address = aws.String(v) + } + + if v, ok := raw["subscription_type"].(string); ok { + config.SubscriptionType = aws.String(v) + } + + items = append(items, config) + } + + return items +} + +func expandAwsBudgetsBudgetActionActionDefinition(l []interface{}) *budgets.Definition { + if len(l) == 0 || l[0] == nil { + return nil + } + + m := l[0].(map[string]interface{}) + + config := &budgets.Definition{} + + if v, ok := m["ssm_action_definition"].([]interface{}); ok && len(v) > 0 { + config.SsmActionDefinition = expandAwsBudgetsBudgetActionActionSsmActionDefinition(v) + } + + if v, ok := m["scp_action_definition"].([]interface{}); ok && len(v) > 0 { + config.ScpActionDefinition = expandAwsBudgetsBudgetActionActionScpActionDefinition(v) + } + + if v, ok := m["iam_action_definition"].([]interface{}); ok && len(v) > 0 { + config.IamActionDefinition = expandAwsBudgetsBudgetActionActionIamActionDefinition(v) + } + + return config +} + +func expandAwsBudgetsBudgetActionActionScpActionDefinition(l []interface{}) *budgets.ScpActionDefinition { + if len(l) == 0 || l[0] == nil { + return nil + } + + m := l[0].(map[string]interface{}) + + config := &budgets.ScpActionDefinition{} + + if v, ok := m["policy_id"].(string); ok && v != "" { + config.PolicyId = aws.String(v) + } + + if v, ok := m["target_ids"].(*schema.Set); ok && v.Len() > 0 { + config.TargetIds = expandStringSet(v) + } + + return config +} + +func expandAwsBudgetsBudgetActionActionSsmActionDefinition(l []interface{}) *budgets.SsmActionDefinition { + if len(l) == 0 || l[0] == nil { + return nil + } + + m := l[0].(map[string]interface{}) + + config := &budgets.SsmActionDefinition{} + + if v, ok := m["action_sub_type"].(string); ok && v != "" { + config.ActionSubType = aws.String(v) + } + + if v, ok := m["region"].(string); ok && v != "" { + config.Region = aws.String(v) + } + + if v, ok := m["instance_ids"].(*schema.Set); ok && v.Len() > 0 { + config.InstanceIds = expandStringSet(v) + } + + return config +} + +func expandAwsBudgetsBudgetActionActionIamActionDefinition(l []interface{}) *budgets.IamActionDefinition { + if len(l) == 0 || l[0] == nil { + return nil + } + + m := l[0].(map[string]interface{}) + + config := &budgets.IamActionDefinition{} + + if v, ok := m["policy_arn"].(string); ok && v != "" { + config.PolicyArn = aws.String(v) + } + + if v, ok := m["groups"].(*schema.Set); ok && v.Len() > 0 { + config.Groups = expandStringSet(v) + } + + if v, ok := m["roles"].(*schema.Set); ok && v.Len() > 0 { + config.Roles = expandStringSet(v) + } + + if v, ok := m["users"].(*schema.Set); ok && v.Len() > 0 { + config.Users = expandStringSet(v) + } + + return config +} + +func flattenAwsBudgetsBudgetActionSubscriber(configured []*budgets.Subscriber) []map[string]interface{} { + dataResources := make([]map[string]interface{}, 0, len(configured)) + + for _, raw := range configured { + item := make(map[string]interface{}) + item["address"] = aws.StringValue(raw.Address) + item["subscription_type"] = aws.StringValue(raw.SubscriptionType) + + dataResources = append(dataResources, item) + } + + return dataResources +} + +func flattenAwsBudgetsBudgetActionActionThreshold(lt *budgets.ActionThreshold) []map[string]interface{} { + if lt == nil { + return []map[string]interface{}{} + } + + attrs := map[string]interface{}{ + "action_threshold_type": aws.StringValue(lt.ActionThresholdType), + "action_threshold_value": aws.Float64Value(lt.ActionThresholdValue), + } + + return []map[string]interface{}{attrs} +} + +func flattenAwsBudgetsBudgetActionIamActionDefinition(lt *budgets.IamActionDefinition) []map[string]interface{} { + if lt == nil { + return []map[string]interface{}{} + } + + attrs := map[string]interface{}{ + "policy_arn": aws.StringValue(lt.PolicyArn), + } + + if lt.Users != nil && len(lt.Users) > 0 { + attrs["users"] = flattenStringSet(lt.Users) + } + + if lt.Roles != nil && len(lt.Roles) > 0 { + attrs["roles"] = flattenStringSet(lt.Roles) + } + + if lt.Groups != nil && len(lt.Groups) > 0 { + attrs["groups"] = flattenStringSet(lt.Groups) + } + + return []map[string]interface{}{attrs} +} + +func flattenAwsBudgetsBudgetActionScpActionDefinition(lt *budgets.ScpActionDefinition) []map[string]interface{} { + if lt == nil { + return []map[string]interface{}{} + } + + attrs := map[string]interface{}{ + "policy_id": aws.StringValue(lt.PolicyId), + } + + if lt.TargetIds != nil && len(lt.TargetIds) > 0 { + attrs["target_ids"] = flattenStringSet(lt.TargetIds) + } + + return []map[string]interface{}{attrs} +} + +func flattenAwsBudgetsBudgetActionSsmActionDefinition(lt *budgets.SsmActionDefinition) []map[string]interface{} { + if lt == nil { + return []map[string]interface{}{} + } + + attrs := map[string]interface{}{ + "action_sub_type": aws.StringValue(lt.ActionSubType), + "instance_ids": flattenStringSet(lt.InstanceIds), + "region": aws.StringValue(lt.Region), + } + + return []map[string]interface{}{attrs} +} + +func flattenAwsBudgetsBudgetActionDefinition(lt *budgets.Definition) []map[string]interface{} { + if lt == nil { + return []map[string]interface{}{} + } + + attrs := map[string]interface{}{} + + if lt.SsmActionDefinition != nil { + attrs["ssm_action_definition"] = flattenAwsBudgetsBudgetActionSsmActionDefinition(lt.SsmActionDefinition) + } + + if lt.IamActionDefinition != nil { + attrs["iam_action_definition"] = flattenAwsBudgetsBudgetActionIamActionDefinition(lt.IamActionDefinition) + } + + if lt.ScpActionDefinition != nil { + attrs["scp_action_definition"] = flattenAwsBudgetsBudgetActionScpActionDefinition(lt.ScpActionDefinition) + } + + return []map[string]interface{}{attrs} +} diff --git a/aws/resource_aws_budgets_budget_action_test.go b/aws/resource_aws_budgets_budget_action_test.go new file mode 100644 index 00000000000..17258472fb9 --- /dev/null +++ b/aws/resource_aws_budgets_budget_action_test.go @@ -0,0 +1,261 @@ +package aws + +import ( + "fmt" + "log" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/budgets" + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/budgets/finder" +) + +func init() { + resource.AddTestSweepers("aws_budgets_budget_action", &resource.Sweeper{ + Name: "aws_budgets_budget_action", + F: testSweepBudgetsBudgetActionss, + }) +} + +func testSweepBudgetsBudgetActionss(region string) error { + client, err := sharedClientForRegion(region) + if err != nil { + return fmt.Errorf("error getting client: %w", err) + } + conn := client.(*AWSClient).budgetconn + accountID := client.(*AWSClient).accountid + input := &budgets.DescribeBudgetsInput{ + AccountId: aws.String(accountID), + } + var sweeperErrs *multierror.Error + + for { + output, err := conn.DescribeBudgets(input) + if testSweepSkipSweepError(err) { + log.Printf("[WARN] Skipping Budgets sweep for %s: %s", region, err) + return sweeperErrs.ErrorOrNil() // In case we have completed some pages, but had errors + } + if err != nil { + sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error retrieving Budgets: %w", err)) + return sweeperErrs + } + + for _, budget := range output.Budgets { + name := aws.StringValue(budget.BudgetName) + + log.Printf("[INFO] Deleting Budget: %s", name) + _, err := conn.DeleteBudget(&budgets.DeleteBudgetInput{ + AccountId: aws.String(accountID), + BudgetName: aws.String(name), + }) + if isAWSErr(err, budgets.ErrCodeNotFoundException, "") { + continue + } + if err != nil { + sweeperErr := fmt.Errorf("error deleting Budget (%s): %w", name, err) + log.Printf("[ERROR] %s", sweeperErr) + sweeperErrs = multierror.Append(sweeperErrs, sweeperErr) + continue + } + } + + if aws.StringValue(output.NextToken) == "" { + break + } + input.NextToken = output.NextToken + } + + return sweeperErrs.ErrorOrNil() +} + +func TestAccAWSBudgetsBudgetAction_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_budgets_budget_action.test" + var conf budgets.Action + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(budgets.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, budgets.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccAWSBudgetsBudgetActionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSBudgetsBudgetActionConfigBasic(rName), + Check: resource.ComposeTestCheckFunc( + testAccAWSBudgetsBudgetActionExists(resourceName, &conf), + testAccMatchResourceAttrGlobalARN(resourceName, "arn", "budgetservice", regexp.MustCompile(fmt.Sprintf(`budget/%s/action/.+`, rName))), + resource.TestCheckResourceAttrPair(resourceName, "budget_name", "aws_budgets_budget.test", "name"), + resource.TestCheckResourceAttrPair(resourceName, "execution_role_arn", "aws_iam_role.test", "arn"), + resource.TestCheckResourceAttr(resourceName, "action_type", "APPLY_IAM_POLICY"), + resource.TestCheckResourceAttr(resourceName, "approval_model", "AUTOMATIC"), + resource.TestCheckResourceAttr(resourceName, "notification_type", "ACTUAL"), + resource.TestCheckResourceAttr(resourceName, "action_threshold.#", "1"), + resource.TestCheckResourceAttr(resourceName, "action_threshold.0.action_threshold_type", "ABSOLUTE_VALUE"), + resource.TestCheckResourceAttr(resourceName, "action_threshold.0.action_threshold_value", "100"), + resource.TestCheckResourceAttr(resourceName, "definition.#", "1"), + resource.TestCheckResourceAttr(resourceName, "definition.0.iam_action_definition.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "definition.0.iam_action_definition.0.policy_arn", "aws_iam_policy.test", "arn"), + resource.TestCheckResourceAttr(resourceName, "definition.0.iam_action_definition.0.roles.#", "1"), + resource.TestCheckResourceAttr(resourceName, "subscriber.#", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSBudgetsBudgetAction_disappears(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_budgets_budget_action.test" + var conf budgets.Action + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(budgets.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, budgets.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccAWSBudgetsBudgetActionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSBudgetsBudgetActionConfigBasic(rName), + Check: resource.ComposeTestCheckFunc( + testAccAWSBudgetsBudgetActionExists(resourceName, &conf), + testAccCheckResourceDisappears(testAccProvider, resourceAwsBudgetsBudgetAction(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccAWSBudgetsBudgetActionExists(resourceName string, config *budgets.Action) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + conn := testAccProvider.Meta().(*AWSClient).budgetconn + out, err := finder.ActionById(conn, rs.Primary.ID) + + if err != nil { + return fmt.Errorf("Describe budget action error: %v", err) + } + + if out.Action == nil { + return fmt.Errorf("No budget Action returned %v in %v", out.Action, out) + } + + *out.Action = *config + + return nil + } +} + +func testAccAWSBudgetsBudgetActionDestroy(s *terraform.State) error { + meta := testAccProvider.Meta() + conn := meta.(*AWSClient).budgetconn + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_budgets_budget_action" { + continue + } + + _, err := finder.ActionById(conn, rs.Primary.ID) + if !isAWSErr(err, budgets.ErrCodeNotFoundException, "") { + return fmt.Errorf("Budget Action '%s' was not deleted properly", rs.Primary.ID) + } + } + + return nil +} + +func testAccAWSBudgetsBudgetActionConfigBasic(rName string) string { + return fmt.Sprintf(` +resource "aws_iam_policy" "test" { + name = %[1]q + description = "My test policy" + + policy = < Date: Thu, 27 May 2021 21:47:22 +0300 Subject: [PATCH 02/10] sweeper --- ...resource_aws_budgets_budget_action_test.go | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/aws/resource_aws_budgets_budget_action_test.go b/aws/resource_aws_budgets_budget_action_test.go index 17258472fb9..820b67218a5 100644 --- a/aws/resource_aws_budgets_budget_action_test.go +++ b/aws/resource_aws_budgets_budget_action_test.go @@ -29,13 +29,13 @@ func testSweepBudgetsBudgetActionss(region string) error { } conn := client.(*AWSClient).budgetconn accountID := client.(*AWSClient).accountid - input := &budgets.DescribeBudgetsInput{ + input := &budgets.DescribeBudgetActionsForAccountInput{ AccountId: aws.String(accountID), } var sweeperErrs *multierror.Error for { - output, err := conn.DescribeBudgets(input) + output, err := conn.DescribeBudgetActionsForAccount(input) if testSweepSkipSweepError(err) { log.Printf("[WARN] Skipping Budgets sweep for %s: %s", region, err) return sweeperErrs.ErrorOrNil() // In case we have completed some pages, but had errors @@ -45,19 +45,18 @@ func testSweepBudgetsBudgetActionss(region string) error { return sweeperErrs } - for _, budget := range output.Budgets { - name := aws.StringValue(budget.BudgetName) + for _, action := range output.Actions { + name := aws.StringValue(action.BudgetName) + log.Printf("[INFO] Deleting Budget Action: %s", name) + id := fmt.Sprintf("%s:%s:%s", accountID, aws.StringValue(action.ActionId), name) - log.Printf("[INFO] Deleting Budget: %s", name) - _, err := conn.DeleteBudget(&budgets.DeleteBudgetInput{ - AccountId: aws.String(accountID), - BudgetName: aws.String(name), - }) - if isAWSErr(err, budgets.ErrCodeNotFoundException, "") { - continue - } + r := resourceAwsBudgetsBudgetAction() + d := r.Data(nil) + d.SetId(id) + + err := r.Delete(d, client) if err != nil { - sweeperErr := fmt.Errorf("error deleting Budget (%s): %w", name, err) + sweeperErr := fmt.Errorf("error deleting Budget Action (%s): %w", name, err) log.Printf("[ERROR] %s", sweeperErr) sweeperErrs = multierror.Append(sweeperErrs, sweeperErr) continue From dd77e8dca988f3b57c2414792fd931a76093baed Mon Sep 17 00:00:00 2001 From: drfaust92 Date: Thu, 27 May 2021 22:10:56 +0300 Subject: [PATCH 03/10] docs --- .../r/budgets_budget_action.html.markdown | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 website/docs/r/budgets_budget_action.html.markdown diff --git a/website/docs/r/budgets_budget_action.html.markdown b/website/docs/r/budgets_budget_action.html.markdown new file mode 100644 index 00000000000..ef0255d1128 --- /dev/null +++ b/website/docs/r/budgets_budget_action.html.markdown @@ -0,0 +1,157 @@ +--- +subcategory: "Budgets" +layout: "aws" +page_title: "AWS: aws_budgets_budget_action" +description: |- + Provides a budgets budget action resource. +--- + +# Resource: aws_budgets_budget_action + +Provides a budgets budget resource. Budgets use the cost visualisation provided by Cost Explorer to show you the status of your budgets, to provide forecasts of your estimated costs, and to track your AWS usage, including your free tier usage. + +## Example Usage + +```terraform +resource "aws_budgets_budget_action" "example" { + budget_name = aws_budgets_budget.example.name + action_type = "APPLY_IAM_POLICY" + approval_model = "AUTOMATIC" + notification_type = "ACTUAL" + execution_role_arn = aws_iam_role.example.arn + + action_threshold { + action_threshold_type = "ABSOLUTE_VALUE" + action_threshold_value = 100 + } + + definition { + iam_action_definition { + policy_arn = aws_iam_policy.example.arn + roles = [aws_iam_role.example.name] + } + } + + subscriber { + address = "example@example.example" + subscription_type = "EMAIL" + } +} + +resource "aws_iam_policy" "example" { + name = "example" + description = "My example policy" + + policy = < Date: Thu, 27 May 2021 22:13:03 +0300 Subject: [PATCH 04/10] changelog --- .changelog/19554.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/19554.txt diff --git a/.changelog/19554.txt b/.changelog/19554.txt new file mode 100644 index 00000000000..9696311995f --- /dev/null +++ b/.changelog/19554.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_budgets_budget_action +``` From 3978294583b5cdc159886be39c7ae7e0eb27ebf8 Mon Sep 17 00:00:00 2001 From: drfaust92 Date: Thu, 27 May 2021 22:15:30 +0300 Subject: [PATCH 05/10] fmt --- aws/resource_aws_budgets_budget_action_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/aws/resource_aws_budgets_budget_action_test.go b/aws/resource_aws_budgets_budget_action_test.go index 820b67218a5..e8ee74e0949 100644 --- a/aws/resource_aws_budgets_budget_action_test.go +++ b/aws/resource_aws_budgets_budget_action_test.go @@ -241,19 +241,19 @@ resource "aws_budgets_budget_action" "test" { action_threshold { action_threshold_type = "ABSOLUTE_VALUE" - action_threshold_value = 100 + action_threshold_value = 100 } definition { iam_action_definition { - policy_arn = aws_iam_policy.test.arn - roles = [aws_iam_role.test.name] - } + policy_arn = aws_iam_policy.test.arn + roles = [aws_iam_role.test.name] + } } subscriber { address = "test@test.test" - subscription_type = "EMAIL" + subscription_type = "EMAIL" } } `, rName) From a372d27142ca0c08b81907b6a7ed8510f64b0654 Mon Sep 17 00:00:00 2001 From: drfaust92 Date: Thu, 27 May 2021 22:32:19 +0300 Subject: [PATCH 06/10] docs fmt --- website/docs/r/budgets_budget_action.html.markdown | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/website/docs/r/budgets_budget_action.html.markdown b/website/docs/r/budgets_budget_action.html.markdown index ef0255d1128..0bd4b8f3ad5 100644 --- a/website/docs/r/budgets_budget_action.html.markdown +++ b/website/docs/r/budgets_budget_action.html.markdown @@ -22,19 +22,19 @@ resource "aws_budgets_budget_action" "example" { action_threshold { action_threshold_type = "ABSOLUTE_VALUE" - action_threshold_value = 100 + action_threshold_value = 100 } definition { iam_action_definition { - policy_arn = aws_iam_policy.example.arn - roles = [aws_iam_role.example.name] - } + policy_arn = aws_iam_policy.example.arn + roles = [aws_iam_role.example.name] + } } subscriber { address = "example@example.example" - subscription_type = "EMAIL" + subscription_type = "EMAIL" } } From 879eaeb4fcbb03e1cff26b18708a65b0c4d2308d Mon Sep 17 00:00:00 2001 From: Ilia Lazebnik Date: Tue, 8 Jun 2021 16:32:50 +0300 Subject: [PATCH 07/10] Update website/docs/r/budgets_budget_action.html.markdown Co-authored-by: Kit Ewbank --- website/docs/r/budgets_budget_action.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/r/budgets_budget_action.html.markdown b/website/docs/r/budgets_budget_action.html.markdown index 0bd4b8f3ad5..ea00d90522f 100644 --- a/website/docs/r/budgets_budget_action.html.markdown +++ b/website/docs/r/budgets_budget_action.html.markdown @@ -3,7 +3,7 @@ subcategory: "Budgets" layout: "aws" page_title: "AWS: aws_budgets_budget_action" description: |- - Provides a budgets budget action resource. + Provides a budget action resource. --- # Resource: aws_budgets_budget_action From fcdbbc419fd33bebacf857535d3361139af8df70 Mon Sep 17 00:00:00 2001 From: Ilia Lazebnik Date: Tue, 8 Jun 2021 16:33:04 +0300 Subject: [PATCH 08/10] Update website/docs/r/budgets_budget_action.html.markdown Co-authored-by: Kit Ewbank --- website/docs/r/budgets_budget_action.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/r/budgets_budget_action.html.markdown b/website/docs/r/budgets_budget_action.html.markdown index ea00d90522f..e3a3be012bc 100644 --- a/website/docs/r/budgets_budget_action.html.markdown +++ b/website/docs/r/budgets_budget_action.html.markdown @@ -8,7 +8,7 @@ description: |- # Resource: aws_budgets_budget_action -Provides a budgets budget resource. Budgets use the cost visualisation provided by Cost Explorer to show you the status of your budgets, to provide forecasts of your estimated costs, and to track your AWS usage, including your free tier usage. +Provides a budget action resource. Budget actions are cost savings controls that run either automatically on your behalf or by using a workflow approval process. ## Example Usage From cf696decbc907f2d3666fce3295ae627febd04f5 Mon Sep 17 00:00:00 2001 From: drfaust92 Date: Tue, 8 Jun 2021 21:49:57 +0300 Subject: [PATCH 09/10] add waiters --- aws/internal/service/budgets/waiter/status.go | 24 +++++++++++++ aws/internal/service/budgets/waiter/waiter.go | 36 +++++++++++++++++++ aws/resource_aws_budgets_budget_action.go | 11 +++++- 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 aws/internal/service/budgets/waiter/status.go create mode 100644 aws/internal/service/budgets/waiter/waiter.go diff --git a/aws/internal/service/budgets/waiter/status.go b/aws/internal/service/budgets/waiter/status.go new file mode 100644 index 00000000000..46f79acfb23 --- /dev/null +++ b/aws/internal/service/budgets/waiter/status.go @@ -0,0 +1,24 @@ +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" +) + +func ActionStatus(conn *budgets.Budgets, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + out, err := finder.ActionById(conn, id) + 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 + } +} diff --git a/aws/internal/service/budgets/waiter/waiter.go b/aws/internal/service/budgets/waiter/waiter.go new file mode 100644 index 00000000000..532ca43667a --- /dev/null +++ b/aws/internal/service/budgets/waiter/waiter.go @@ -0,0 +1,36 @@ +package waiter + +import ( + "time" + + "github.com/aws/aws-sdk-go/service/budgets" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const ( + ActionAvailableTimeout = 2 * time.Minute +) + +func ActionAvailable(conn *budgets.Budgets, id string) (*budgets.Action, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + budgets.ActionStatusExecutionInProgress, + budgets.ActionStatusPending, + }, + Target: []string{ + budgets.ActionStatusStandby, + budgets.ActionStatusExecutionSuccess, + budgets.ActionStatusExecutionFailure, + }, + Refresh: ActionStatus(conn, id), + Timeout: ActionAvailableTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*budgets.Action); ok { + return v, err + } + + return nil, err +} diff --git a/aws/resource_aws_budgets_budget_action.go b/aws/resource_aws_budgets_budget_action.go index 744ec4992e6..2cceb30a7ae 100644 --- a/aws/resource_aws_budgets_budget_action.go +++ b/aws/resource_aws_budgets_budget_action.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" tfbudgets "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/budgets" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/budgets/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/budgets/waiter" ) func resourceAwsBudgetsBudgetAction() *schema.Resource { @@ -230,11 +231,15 @@ func resourceAwsBudgetsBudgetActionCreate(d *schema.ResourceData, meta interface return output, err }) if err != nil { - return fmt.Errorf("create budget failed: %v", err) + return fmt.Errorf("create Budget Action failed: %v", err) } d.SetId(fmt.Sprintf("%s:%s:%s", aws.StringValue(output.AccountId), aws.StringValue(output.ActionId), aws.StringValue(output.BudgetName))) + if _, err := waiter.ActionAvailable(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for Budget Action (%s) creation: %w", d.Id(), err) + } + return resourceAwsBudgetsBudgetActionRead(d, meta) } @@ -334,6 +339,10 @@ func resourceAwsBudgetsBudgetActionUpdate(d *schema.ResourceData, meta interface return fmt.Errorf("Updating Budget Action failed: %w", err) } + if _, err := waiter.ActionAvailable(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for Budget Action (%s) update: %w", d.Id(), err) + } + return resourceAwsBudgetsBudgetActionRead(d, meta) } From 4f0153ddec349a01e797215b6d4d7915570a163b Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Mon, 14 Jun 2021 14:37:01 -0400 Subject: [PATCH 10/10] r/aws_budgets_budget_action: Tweak waiter statuses. --- aws/internal/service/budgets/waiter/waiter.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aws/internal/service/budgets/waiter/waiter.go b/aws/internal/service/budgets/waiter/waiter.go index 532ca43667a..7aa9e7bc63b 100644 --- a/aws/internal/service/budgets/waiter/waiter.go +++ b/aws/internal/service/budgets/waiter/waiter.go @@ -15,12 +15,12 @@ func ActionAvailable(conn *budgets.Budgets, id string) (*budgets.Action, error) stateConf := &resource.StateChangeConf{ Pending: []string{ budgets.ActionStatusExecutionInProgress, - budgets.ActionStatusPending, + budgets.ActionStatusStandby, }, Target: []string{ - budgets.ActionStatusStandby, budgets.ActionStatusExecutionSuccess, budgets.ActionStatusExecutionFailure, + budgets.ActionStatusPending, }, Refresh: ActionStatus(conn, id), Timeout: ActionAvailableTimeout,