diff --git a/.changelog/37361.txt b/.changelog/37361.txt new file mode 100644 index 00000000000..c425fbe3cf6 --- /dev/null +++ b/.changelog/37361.txt @@ -0,0 +1,9 @@ +```release-note:enhancement +resource/aws_budgets_budget: Add `tags` argument +``` +```release-note:enhancement +resource/aws_budgets_budget_action: Add `tags` argument +``` +```release-note:enhancement +data-source/aws_budgets_budget: Add `tags` attribute +``` diff --git a/internal/service/budgets/budget.go b/internal/service/budgets/budget.go index f11e6a90b27..b67b2b065f7 100644 --- a/internal/service/budgets/budget.go +++ b/internal/service/budgets/budget.go @@ -26,6 +26,7 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/errs" "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" "github.com/hashicorp/terraform-provider-aws/internal/flex" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/internal/verify" "github.com/hashicorp/terraform-provider-aws/names" @@ -33,6 +34,7 @@ import ( ) // @SDKResource("aws_budgets_budget") +// @Tags(identifierAttribute="arn") func ResourceBudget() *schema.Resource { return &schema.Resource{ CreateWithoutTimeout: resourceBudgetCreate, @@ -272,6 +274,8 @@ func ResourceBudget() *schema.Resource { }, ConflictsWith: []string{"limit_amount", "limit_unit"}, }, + names.AttrTags: tftags.TagsSchema(), + names.AttrTagsAll: tftags.TagsSchemaComputed(), "time_period_end": { Type: schema.TypeString, Optional: true, @@ -290,6 +294,7 @@ func ResourceBudget() *schema.Resource { ValidateDiagFunc: enum.Validate[awstypes.TimeUnit](), }, }, + CustomizeDiff: verify.SetTagsDiff, } } @@ -313,8 +318,9 @@ func resourceBudgetCreate(ctx context.Context, d *schema.ResourceData, meta inte } _, err = conn.CreateBudget(ctx, &budgets.CreateBudgetInput{ - AccountId: aws.String(accountID), - Budget: budget, + AccountId: aws.String(accountID), + Budget: budget, + ResourceTags: getTagsIn(ctx), }) if err != nil { diff --git a/internal/service/budgets/budget_action.go b/internal/service/budgets/budget_action.go index 8957f71378f..04db7bbe2b2 100644 --- a/internal/service/budgets/budget_action.go +++ b/internal/service/budgets/budget_action.go @@ -24,12 +24,14 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/errs" "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" "github.com/hashicorp/terraform-provider-aws/internal/flex" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/internal/verify" "github.com/hashicorp/terraform-provider-aws/names" ) // @SDKResource("aws_budgets_budget_action") +// @Tags(identifierAttribute="arn") func ResourceBudgetAction() *schema.Resource { return &schema.Resource{ CreateWithoutTimeout: resourceBudgetActionCreate, @@ -221,7 +223,10 @@ func ResourceBudgetAction() *schema.Resource { }, }, }, + names.AttrTags: tftags.TagsSchema(), + names.AttrTagsAll: tftags.TagsSchemaComputed(), }, + CustomizeDiff: verify.SetTagsDiff, } } @@ -243,6 +248,7 @@ func resourceBudgetActionCreate(ctx context.Context, d *schema.ResourceData, met ExecutionRoleArn: aws.String(d.Get("execution_role_arn").(string)), NotificationType: awstypes.NotificationType(d.Get("notification_type").(string)), Subscribers: expandBudgetActionSubscriber(d.Get("subscriber").(*schema.Set)), + ResourceTags: getTagsIn(ctx), } outputRaw, err := tfresource.RetryWhenIsA[*awstypes.AccessDeniedException](ctx, propagationTimeout, func() (interface{}, error) { @@ -324,40 +330,42 @@ func resourceBudgetActionUpdate(ctx context.Context, d *schema.ResourceData, met return sdkdiag.AppendFromErr(diags, err) } - input := &budgets.UpdateBudgetActionInput{ - AccountId: aws.String(accountID), - ActionId: aws.String(actionID), - BudgetName: aws.String(budgetName), - } + if d.HasChangesExcept(names.AttrTags, names.AttrTagsAll) { + input := &budgets.UpdateBudgetActionInput{ + AccountId: aws.String(accountID), + ActionId: aws.String(actionID), + BudgetName: aws.String(budgetName), + } - if d.HasChange("action_threshold") { - input.ActionThreshold = expandBudgetActionActionThreshold(d.Get("action_threshold").([]interface{})) - } + if d.HasChange("action_threshold") { + input.ActionThreshold = expandBudgetActionActionThreshold(d.Get("action_threshold").([]interface{})) + } - if d.HasChange("approval_model") { - input.ApprovalModel = awstypes.ApprovalModel(d.Get("approval_model").(string)) - } + if d.HasChange("approval_model") { + input.ApprovalModel = awstypes.ApprovalModel(d.Get("approval_model").(string)) + } - if d.HasChange("definition") { - input.Definition = expandBudgetActionActionDefinition(d.Get("definition").([]interface{})) - } + if d.HasChange("definition") { + input.Definition = expandBudgetActionActionDefinition(d.Get("definition").([]interface{})) + } - if d.HasChange("execution_role_arn") { - input.ExecutionRoleArn = aws.String(d.Get("execution_role_arn").(string)) - } + if d.HasChange("execution_role_arn") { + input.ExecutionRoleArn = aws.String(d.Get("execution_role_arn").(string)) + } - if d.HasChange("notification_type") { - input.NotificationType = awstypes.NotificationType(d.Get("notification_type").(string)) - } + if d.HasChange("notification_type") { + input.NotificationType = awstypes.NotificationType(d.Get("notification_type").(string)) + } - if d.HasChange("subscriber") { - input.Subscribers = expandBudgetActionSubscriber(d.Get("subscriber").(*schema.Set)) - } + if d.HasChange("subscriber") { + input.Subscribers = expandBudgetActionSubscriber(d.Get("subscriber").(*schema.Set)) + } - _, err = conn.UpdateBudgetAction(ctx, input) + _, err = conn.UpdateBudgetAction(ctx, input) - if err != nil { - return sdkdiag.AppendErrorf(diags, "updating Budget Action (%s): %s", d.Id(), err) + if err != nil { + return sdkdiag.AppendErrorf(diags, "updating Budget Action (%s): %s", d.Id(), err) + } } return append(diags, resourceBudgetActionRead(ctx, d, meta)...) diff --git a/internal/service/budgets/budget_action_test.go b/internal/service/budgets/budget_action_test.go index 4e2f1ec8a1f..6f2ef65f35c 100644 --- a/internal/service/budgets/budget_action_test.go +++ b/internal/service/budgets/budget_action_test.go @@ -152,6 +152,54 @@ func TestAccBudgetsBudgetAction_triggeredManual(t *testing.T) { }) } +func TestAccBudgetsBudgetAction_tags(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_budgets_budget_action.test" + var conf awstypes.Action + + const thresholdValue = "1000000000" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); acctest.PreCheckPartitionHasService(t, names.BudgetsEndpointID) }, + ErrorCheck: acctest.ErrorCheck(t, names.BudgetsServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckBudgetActionDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccBudgetActionConfig_tags1(rName, string(awstypes.ApprovalModelManual), thresholdValue, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccBudgetActionExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccBudgetActionConfig_tags2(rName, string(awstypes.ApprovalModelManual), thresholdValue, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccBudgetActionExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccBudgetActionConfig_tags1(rName, string(awstypes.ApprovalModelManual), thresholdValue, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccBudgetActionExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + func TestAccBudgetsBudgetAction_disappears(t *testing.T) { ctx := acctest.Context(t) rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) @@ -243,8 +291,68 @@ func testAccCheckBudgetActionDestroy(ctx context.Context) resource.TestCheckFunc } } -func testAccBudgetActionConfig_basic(rName, approvalModel, thresholdValue string) string { +func testAccBudgetActionConfig_base(rName string) string { return fmt.Sprintf(` +resource "aws_budgets_budget" "test" { + name = %[1]q + budget_type = "USAGE" + limit_amount = "1.0" + limit_unit = "dollars" + time_period_start = "2006-01-02_15:04" + time_unit = "MONTHLY" +} + +resource "aws_iam_policy" "test" { + name = %[1]q + description = "My test policy" + + policy = < 0 { + return tags + } + } + + return nil +} + +// setTagsOut sets budgets service tags in Context. +func setTagsOut(ctx context.Context, tags []awstypes.ResourceTag) { + if inContext, ok := tftags.FromContext(ctx); ok { + inContext.TagsOut = option.Some(KeyValueTags(ctx, tags)) + } +} + +// updateTags updates budgets service tags. +// The identifier is typically the Amazon Resource Name (ARN), although +// it may also be a different identifier depending on the service. +func updateTags(ctx context.Context, conn *budgets.Client, identifier string, oldTagsMap, newTagsMap any, optFns ...func(*budgets.Options)) error { + oldTags := tftags.New(ctx, oldTagsMap) + newTags := tftags.New(ctx, newTagsMap) + + ctx = tflog.SetField(ctx, logging.KeyResourceId, identifier) + + removedTags := oldTags.Removed(newTags) + removedTags = removedTags.IgnoreSystem(names.Budgets) + if len(removedTags) > 0 { + input := &budgets.UntagResourceInput{ + ResourceARN: aws.String(identifier), + ResourceTagKeys: removedTags.Keys(), + } + + _, err := conn.UntagResource(ctx, input, optFns...) + + if err != nil { + return fmt.Errorf("untagging resource (%s): %w", identifier, err) + } + } + + updatedTags := oldTags.Updated(newTags) + updatedTags = updatedTags.IgnoreSystem(names.Budgets) + if len(updatedTags) > 0 { + input := &budgets.TagResourceInput{ + ResourceARN: aws.String(identifier), + ResourceTags: Tags(updatedTags), + } + + _, err := conn.TagResource(ctx, input, optFns...) + + if err != nil { + return fmt.Errorf("tagging resource (%s): %w", identifier, err) + } + } + + return nil +} + +// UpdateTags updates budgets service tags. +// It is called from outside this package. +func (p *servicePackage) UpdateTags(ctx context.Context, meta any, identifier string, oldTags, newTags any) error { + return updateTags(ctx, meta.(*conns.AWSClient).BudgetsClient(ctx), identifier, oldTags, newTags) +} diff --git a/website/docs/d/budgets_budget.html.markdown b/website/docs/d/budgets_budget.html.markdown index 9c949509cfb..239f1aa32c7 100644 --- a/website/docs/d/budgets_budget.html.markdown +++ b/website/docs/d/budgets_budget.html.markdown @@ -44,6 +44,7 @@ This data source exports the following attributes in addition to the arguments a * `cost_types` - Object containing [CostTypes](#cost-types) The types of cost included in a budget, such as tax and subscriptions. * `notification` - Object containing [Budget Notifications](#budget-notification). Can be used multiple times to define more than one budget notification. * `planned_limit` - Object containing [Planned Budget Limits](#planned-budget-limits). Can be used multiple times to plan more than one budget limit. See [PlannedBudgetLimits](https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_budgets_Budget.html#awscostmanagement-Type-budgets_Budget-PlannedBudgetLimits) documentation. +* `tags` - Map of tags assigned to the resource. * `time_period_end` - The end of the time period covered by the budget. There are no restrictions on the end date. Format: `2017-01-01_12:00`. * `time_period_start` - The start of the time period covered by the budget. If you don't specify a start date, AWS defaults to the start of your chosen time period. The start date must come before the end date. Format: `2017-01-01_12:00`. * `time_unit` - The length of time until a budget resets the actual and forecasted spend. Valid values: `MONTHLY`, `QUARTERLY`, `ANNUALLY`, and `DAILY`. diff --git a/website/docs/r/budgets_budget.html.markdown b/website/docs/r/budgets_budget.html.markdown index 332522a2cdd..faf77bc1f63 100644 --- a/website/docs/r/budgets_budget.html.markdown +++ b/website/docs/r/budgets_budget.html.markdown @@ -36,6 +36,11 @@ resource "aws_budgets_budget" "ec2" { notification_type = "FORECASTED" subscriber_email_addresses = ["test@example.com"] } + + tags = { + Tag1 = "Value1" + Tag2 = "Value2" + } } ``` @@ -171,42 +176,47 @@ resource "aws_budgets_budget" "cost" { For more detailed documentation about each argument, refer to the [AWS official documentation](http://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/data-type-budget.html). -This argument supports the following arguments: +The following arguments are required: -* `account_id` - (Optional) The ID of the target account for budget. Will use current user's account_id by default if omitted. -* `auto_adjust_data` - (Optional) Object containing [AutoAdjustData] which determines the budget amount for an auto-adjusting budget. -* `name` - (Optional) The name of a budget. Unique within accounts. -* `name_prefix` - (Optional) The prefix of the name of a budget. Unique within accounts. * `budget_type` - (Required) Whether this budget tracks monetary cost or usage. -* `cost_filter` - (Optional) A list of [CostFilter](#cost-filter) name/values pair to apply to budget. -* `cost_types` - (Optional) Object containing [CostTypes](#cost-types) The types of cost included in a budget, such as tax and subscriptions. * `limit_amount` - (Required) The amount of cost or usage being measured for a budget. * `limit_unit` - (Required) The unit of measurement used for the budget forecast, actual spend, or budget threshold, such as dollars or GB. See [Spend](http://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/data-type-spend.html) documentation. -* `time_period_end` - (Optional) The end of the time period covered by the budget. There are no restrictions on the end date. Format: `2017-01-01_12:00`. -* `time_period_start` - (Optional) The start of the time period covered by the budget. If you don't specify a start date, AWS defaults to the start of your chosen time period. The start date must come before the end date. Format: `2017-01-01_12:00`. * `time_unit` - (Required) The length of time until a budget resets the actual and forecasted spend. Valid values: `MONTHLY`, `QUARTERLY`, `ANNUALLY`, and `DAILY`. + +The following arguments are optional: + +* `account_id` - (Optional) The ID of the target account for budget. Will use current user's account_id by default if omitted. +* `auto_adjust_data` - (Optional) Object containing [AutoAdjustData](#auto-adjust-data) which determines the budget amount for an auto-adjusting budget. +* `cost_filter` - (Optional) A list of [CostFilter](#cost-filter) name/values pair to apply to budget. +* `cost_types` - (Optional) Object containing [CostTypes](#cost-types) The types of cost included in a budget, such as tax and subscriptions. +* `name` - (Optional) The name of a budget. Unique within accounts. +* `name_prefix` - (Optional) The prefix of the name of a budget. Unique within accounts. * `notification` - (Optional) Object containing [Budget Notifications](#budget-notification). Can be used multiple times to define more than one budget notification. * `planned_limit` - (Optional) Object containing [Planned Budget Limits](#planned-budget-limits). Can be used multiple times to plan more than one budget limit. See [PlannedBudgetLimits](https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_budgets_Budget.html#awscostmanagement-Type-budgets_Budget-PlannedBudgetLimits) documentation. +* `tags` - (Optional) Map of tags assigned to the resource. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. +* `time_period_end` - (Optional) The end of the time period covered by the budget. There are no restrictions on the end date. Format: `2017-01-01_12:00`. +* `time_period_start` - (Optional) The start of the time period covered by the budget. If you don't specify a start date, AWS defaults to the start of your chosen time period. The start date must come before the end date. Format: `2017-01-01_12:00`. ## Attribute Reference This resource exports the following attributes in addition to the arguments above: -* `id` - id of resource. * `arn` - The ARN of the budget. +* `id` - id of resource. +* `tags_all` - Map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block). ### Auto Adjust Data The parameters that determine the budget amount for an auto-adjusting budget. -`auto_adjust_type` (Required) - The string that defines whether your budget auto-adjusts based on historical or forecasted data. Valid values: `FORECAST`,`HISTORICAL` -`historical_options` (Optional) - Configuration block of [Historical Options](#historical-options). Required for `auto_adjust_type` of `HISTORICAL` Configuration block that defines the historical data that your auto-adjusting budget is based on. -`last_auto_adjust_time` (Optional) - The last time that your budget was auto-adjusted. +* `auto_adjust_type` (Required) - The string that defines whether your budget auto-adjusts based on historical or forecasted data. Valid values: `FORECAST`,`HISTORICAL` +* `historical_options` (Optional) - Configuration block of [Historical Options](#historical-options). Required for `auto_adjust_type` of `HISTORICAL` Configuration block that defines the historical data that your auto-adjusting budget is based on. +* `last_auto_adjust_time` (Optional) - The last time that your budget was auto-adjusted. ### Historical Options -`budget_adjustment_period` (Required) - The number of budget periods included in the moving-average calculation that determines your auto-adjusted budget amount. -`lookback_available_periods` (Optional) - The integer that describes how many budget periods in your BudgetAdjustmentPeriod are included in the calculation of your current budget limit. If the first budget period in your BudgetAdjustmentPeriod has no cost data, then that budget period isn’t included in the average that determines your budget limit. You can’t set your own LookBackAvailablePeriods. The value is automatically calculated from the `budget_adjustment_period` and your historical cost data. +* `budget_adjustment_period` (Required) - The number of budget periods included in the moving-average calculation that determines your auto-adjusted budget amount. +* `lookback_available_periods` (Optional) - The integer that describes how many budget periods in your BudgetAdjustmentPeriod are included in the calculation of your current budget limit. If the first budget period in your BudgetAdjustmentPeriod has no cost data, then that budget period isn’t included in the average that determines your budget limit. You can’t set your own LookBackAvailablePeriods. The value is automatically calculated from the `budget_adjustment_period` and your historical cost data. ### Cost Types diff --git a/website/docs/r/budgets_budget_action.html.markdown b/website/docs/r/budgets_budget_action.html.markdown index 93c07598a42..ba895e59129 100644 --- a/website/docs/r/budgets_budget_action.html.markdown +++ b/website/docs/r/budgets_budget_action.html.markdown @@ -36,6 +36,11 @@ resource "aws_budgets_budget_action" "example" { address = "example@example.example" subscription_type = "EMAIL" } + + tags = { + Tag1 = "Value1" + Tag2 = "Value2" + } } data "aws_iam_policy_document" "example" { @@ -95,6 +100,7 @@ This resource supports the following arguments: * `execution_role_arn` - (Required) The role passed for action execution and reversion. Roles and actions must be in the same account. * `notification_type` - (Required) The type of a notification. Valid values are `ACTUAL` or `FORECASTED`. * `subscriber` - (Required) A list of subscribers. See [Subscriber](#subscriber). +* `tags` - (Optional) Map of tags assigned to the resource. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. ### Action Threshold @@ -138,6 +144,7 @@ This resource exports the following attributes in addition to the arguments abov * `id` - ID of resource. * `arn` - The ARN of the budget action. * `status` - The status of the budget action. +* `tags_all` - Map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block). ## Timeouts