From 6c80af08874b902f621b0e7554d3d17bcb6c9ef5 Mon Sep 17 00:00:00 2001 From: Stephen Lewis Date: Fri, 23 Apr 2021 13:30:58 -0700 Subject: [PATCH 1/3] Enabled imports for billing budget --- mmv1/products/billingbudget/api.yaml | 2 +- mmv1/products/billingbudget/terraform.yaml | 13 ++++--------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/mmv1/products/billingbudget/api.yaml b/mmv1/products/billingbudget/api.yaml index 0d2679859aec..49b4bdbd4324 100644 --- a/mmv1/products/billingbudget/api.yaml +++ b/mmv1/products/billingbudget/api.yaml @@ -29,7 +29,7 @@ objects: - !ruby/object:Api::Resource name: Budget base_url: billingAccounts/{{billing_account}}/budgets - self_link: '{{name}}' + self_link: 'billingAccounts/{{billing_account}}/budgets/{{name}}' update_verb: :PATCH # TODO: investigate why updates are not being handled by the API when this # is enabled. diff --git a/mmv1/products/billingbudget/terraform.yaml b/mmv1/products/billingbudget/terraform.yaml index 3100040ad027..f50ef8076c25 100644 --- a/mmv1/products/billingbudget/terraform.yaml +++ b/mmv1/products/billingbudget/terraform.yaml @@ -21,8 +21,8 @@ overrides: !ruby/object:Overrides::ResourceOverrides in the provider configuration. Otherwise the Billing Budgets API will return a 403 error. Your account must have the `serviceusage.services.use` permission on the `billing_project` you defined. - id_format: '{{name}}' - exclude_import: true + id_format: 'billingAccounts/{{billing_account}}/budgets/{{name}}' + import_format: ["billingAccounts/{{billing_account}}/budgets/{{name}}", "{{name}}"] examples: - !ruby/object:Provider::Terraform::Examples name: 'billing_budget_basic' @@ -31,7 +31,6 @@ overrides: !ruby/object:Overrides::ResourceOverrides display_name: 'Example Billing Budget' test_env_vars: billing_acct: :BILLING_ACCT - skip_import_test: true - !ruby/object:Provider::Terraform::Examples name: 'billing_budget_lastperiod' primary_resource_id: 'budget' @@ -39,7 +38,6 @@ overrides: !ruby/object:Overrides::ResourceOverrides display_name: 'Example Billing Budget' test_env_vars: billing_acct: :BILLING_ACCT - skip_import_test: true - !ruby/object:Provider::Terraform::Examples name: 'billing_budget_filter' primary_resource_id: 'budget' @@ -47,7 +45,6 @@ overrides: !ruby/object:Overrides::ResourceOverrides display_name: 'Example Billing Budget' test_env_vars: billing_acct: :BILLING_ACCT - skip_import_test: true - !ruby/object:Provider::Terraform::Examples name: 'billing_budget_notify' primary_resource_id: 'budget' @@ -56,11 +53,9 @@ overrides: !ruby/object:Overrides::ResourceOverrides channel_name: 'Example Notification Channel' test_env_vars: billing_acct: :BILLING_ACCT - skip_import_test: true - custom_code: !ruby/object:Provider::Terraform::CustomCode - custom_import: templates/terraform/custom_import/self_link_as_name.erb - post_create: templates/terraform/post_create/set_computed_name.erb properties: + name: !ruby/object:Overrides::Terraform::PropertyOverride + custom_flatten: 'templates/terraform/custom_flatten/name_from_self_link.erb' allUpdatesRule.schemaVersion: !ruby/object:Overrides::Terraform::PropertyOverride custom_flatten: templates/terraform/custom_flatten/default_if_empty.erb budgetFilter.creditTypes: !ruby/object:Overrides::Terraform::PropertyOverride From e1e76ce7ce5674418c41746c4671658bbe9e7883 Mon Sep 17 00:00:00 2001 From: Stephen Lewis Date: Fri, 23 Apr 2021 15:13:47 -0700 Subject: [PATCH 2/3] Made updates of filter projects get recognized during updates --- mmv1/products/billingbudget/api.yaml | 4 +- mmv1/products/billingbudget/terraform.yaml | 2 + .../tests/resource_billing_budget_test.go | 103 ++++++++++++++++++ 3 files changed, 106 insertions(+), 3 deletions(-) diff --git a/mmv1/products/billingbudget/api.yaml b/mmv1/products/billingbudget/api.yaml index 49b4bdbd4324..21586547e26b 100644 --- a/mmv1/products/billingbudget/api.yaml +++ b/mmv1/products/billingbudget/api.yaml @@ -31,9 +31,7 @@ objects: base_url: billingAccounts/{{billing_account}}/budgets self_link: 'billingAccounts/{{billing_account}}/budgets/{{name}}' update_verb: :PATCH - # TODO: investigate why updates are not being handled by the API when this - # is enabled. - # update_mask: true + update_mask: true description: | Budget configuration for a billing account. references: !ruby/object:Api::Resource::ReferenceLinks diff --git a/mmv1/products/billingbudget/terraform.yaml b/mmv1/products/billingbudget/terraform.yaml index f50ef8076c25..dbca55deb7b1 100644 --- a/mmv1/products/billingbudget/terraform.yaml +++ b/mmv1/products/billingbudget/terraform.yaml @@ -62,6 +62,8 @@ overrides: !ruby/object:Overrides::ResourceOverrides default_from_api: true budgetFilter: !ruby/object:Overrides::Terraform::PropertyOverride default_from_api: true + update_mask_fields: + - "budgetFilter.projects" budgetFilter.services: !ruby/object:Overrides::Terraform::PropertyOverride default_from_api: true budgetFilter.subaccounts: !ruby/object:Overrides::Terraform::PropertyOverride diff --git a/mmv1/third_party/terraform/tests/resource_billing_budget_test.go b/mmv1/third_party/terraform/tests/resource_billing_budget_test.go index aea6d52fce63..055d8df77aa4 100644 --- a/mmv1/third_party/terraform/tests/resource_billing_budget_test.go +++ b/mmv1/third_party/terraform/tests/resource_billing_budget_test.go @@ -60,3 +60,106 @@ resource "google_billing_budget" "budget" { } `, context) } + +func TestAccBillingBudget_billingBudgetUpdateRemoveFilter(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "billing_acct": getTestBillingAccountFromEnv(t), + "random_suffix": randString(t, 10), + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckBillingBudgetDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccBillingBudget_billingBudgetUpdateRemoveFilterStart(context), + }, + { + ResourceName: "google_billing_budget.budget", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccBillingBudget_billingBudgetUpdateRemoveFilterEnd(context), + }, + { + ResourceName: "google_billing_budget.budget", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccBillingBudget_billingBudgetUpdateRemoveFilterStart(context map[string]interface{}) string { + return Nprintf(` +data "google_billing_account" "account" { + billing_account = "%{billing_acct}" +} + +data "google_project" "project" { +} + +resource "google_billing_budget" "budget" { + billing_account = data.google_billing_account.account.id + display_name = "Example Billing Budget%{random_suffix}" + + budget_filter { + projects = ["projects/${data.google_project.project.number}"] + } + + amount { + specified_amount { + currency_code = "USD" + units = "100000" + } + } + + threshold_rules { + threshold_percent = 0.5 + } + threshold_rules { + threshold_percent = 0.9 + spend_basis = "FORECASTED_SPEND" + } +} +`, context) +} + +func testAccBillingBudget_billingBudgetUpdateRemoveFilterEnd(context map[string]interface{}) string { + return Nprintf(` +data "google_billing_account" "account" { + billing_account = "%{billing_acct}" +} + +data "google_project" "project" { +} + +resource "google_billing_budget" "budget" { + billing_account = data.google_billing_account.account.id + display_name = "Example Billing Budget%{random_suffix}" + + budget_filter { + projects = [] + } + + amount { + specified_amount { + currency_code = "USD" + units = "100000" + } + } + + threshold_rules { + threshold_percent = 0.5 + } + threshold_rules { + threshold_percent = 0.9 + spend_basis = "FORECASTED_SPEND" + } +} +`, context) +} From ef2f5c95339e591b38571f470bb8883e3953e8d3 Mon Sep 17 00:00:00 2001 From: Stephen Lewis Date: Fri, 23 Apr 2021 15:35:28 -0700 Subject: [PATCH 3/3] Added state migration function --- mmv1/products/billingbudget/terraform.yaml | 1 + .../state_migrations/billing_budget.go.erb | 251 ++++++++++++++++++ .../tests/resource_billing_budget_test.go | 46 ++++ 3 files changed, 298 insertions(+) create mode 100644 mmv1/templates/terraform/state_migrations/billing_budget.go.erb diff --git a/mmv1/products/billingbudget/terraform.yaml b/mmv1/products/billingbudget/terraform.yaml index dbca55deb7b1..33a04cd988d5 100644 --- a/mmv1/products/billingbudget/terraform.yaml +++ b/mmv1/products/billingbudget/terraform.yaml @@ -23,6 +23,7 @@ overrides: !ruby/object:Overrides::ResourceOverrides `billing_project` you defined. id_format: 'billingAccounts/{{billing_account}}/budgets/{{name}}' import_format: ["billingAccounts/{{billing_account}}/budgets/{{name}}", "{{name}}"] + schema_version: 1 examples: - !ruby/object:Provider::Terraform::Examples name: 'billing_budget_basic' diff --git a/mmv1/templates/terraform/state_migrations/billing_budget.go.erb b/mmv1/templates/terraform/state_migrations/billing_budget.go.erb new file mode 100644 index 000000000000..7d1462c249cd --- /dev/null +++ b/mmv1/templates/terraform/state_migrations/billing_budget.go.erb @@ -0,0 +1,251 @@ +func resourceBillingBudgetResourceV0() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "amount": { + Type: schema.TypeList, + Required: true, + Description: `The budgeted amount for each usage period.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "last_period_amount": { + Type: schema.TypeBool, + Optional: true, + Description: `Configures a budget amount that is automatically set to 100% of +last period's spend. +Boolean. Set value to true to use. Do not set to false, instead +use the 'specified_amount' block.`, + ExactlyOneOf: []string{"amount.0.specified_amount", "amount.0.last_period_amount"}, + }, + "specified_amount": { + Type: schema.TypeList, + Optional: true, + Description: `A specified amount to use as the budget. currencyCode is +optional. If specified, it must match the currency of the +billing account. The currencyCode is provided on output.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "currency_code": { + Type: schema.TypeString, + Computed: true, + Optional: true, + Description: `The 3-letter currency code defined in ISO 4217.`, + }, + "nanos": { + Type: schema.TypeInt, + Optional: true, + Description: `Number of nano (10^-9) units of the amount. +The value must be between -999,999,999 and +999,999,999 +inclusive. If units is positive, nanos must be positive or +zero. If units is zero, nanos can be positive, zero, or +negative. If units is negative, nanos must be negative or +zero. For example $-1.75 is represented as units=-1 and +nanos=-750,000,000.`, + }, + "units": { + Type: schema.TypeString, + Optional: true, + Description: `The whole units of the amount. For example if currencyCode +is "USD", then 1 unit is one US dollar.`, + }, + }, + }, + ExactlyOneOf: []string{"amount.0.specified_amount", "amount.0.last_period_amount"}, + }, + }, + }, + }, + "billing_account": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `ID of the billing account to set a budget on.`, + }, + "threshold_rules": { + Type: schema.TypeList, + Required: true, + Description: `Rules that trigger alerts (notifications of thresholds being +crossed) when spend exceeds the specified percentages of the +budget.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "threshold_percent": { + Type: schema.TypeFloat, + Required: true, + Description: `Send an alert when this threshold is exceeded. This is a +1.0-based percentage, so 0.5 = 50%. Must be >= 0.`, + }, + "spend_basis": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"CURRENT_SPEND", "FORECASTED_SPEND", ""}, false), + Description: `The type of basis used to determine if spend has passed +the threshold. Default value: "CURRENT_SPEND" Possible values: ["CURRENT_SPEND", "FORECASTED_SPEND"]`, + Default: "CURRENT_SPEND", + }, + }, + }, + }, + "all_updates_rule": { + Type: schema.TypeList, + Optional: true, + Description: `Defines notifications that are sent on every update to the +billing account's spend, regardless of the thresholds defined +using threshold rules.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "disable_default_iam_recipients": { + Type: schema.TypeBool, + Optional: true, + Description: `Boolean. When set to true, disables default notifications sent +when a threshold is exceeded. Default recipients are +those with Billing Account Administrators and Billing +Account Users IAM roles for the target account.`, + Default: false, + }, + "monitoring_notification_channels": { + Type: schema.TypeList, + Optional: true, + Description: `The full resource name of a monitoring notification +channel in the form +projects/{project_id}/notificationChannels/{channel_id}. +A maximum of 5 channels are allowed.`, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + AtLeastOneOf: []string{"all_updates_rule.0.pubsub_topic", "all_updates_rule.0.monitoring_notification_channels"}, + }, + "pubsub_topic": { + Type: schema.TypeString, + Optional: true, + Description: `The name of the Cloud Pub/Sub topic where budget related +messages will be published, in the form +projects/{project_id}/topics/{topic_id}. Updates are sent +at regular intervals to the topic.`, + AtLeastOneOf: []string{"all_updates_rule.0.pubsub_topic", "all_updates_rule.0.monitoring_notification_channels"}, + }, + "schema_version": { + Type: schema.TypeString, + Optional: true, + Description: `The schema version of the notification. Only "1.0" is +accepted. It represents the JSON schema as defined in +https://cloud.google.com/billing/docs/how-to/budgets#notification_format.`, + Default: "1.0", + }, + }, + }, + }, + "budget_filter": { + Type: schema.TypeList, + Computed: true, + Optional: true, + Description: `Filters that define which resources are used to compute the actual +spend against the budget.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "credit_types": { + Type: schema.TypeList, + Computed: true, + Optional: true, + Description: `A set of subaccounts of the form billingAccounts/{account_id}, +specifying that usage from only this set of subaccounts should +be included in the budget. If a subaccount is set to the name of +the parent account, usage from the parent account will be included. +If the field is omitted, the report will include usage from the parent +account and all subaccounts, if they exist.`, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + AtLeastOneOf: []string{"budget_filter.0.projects", "budget_filter.0.credit_types_treatment", "budget_filter.0.services", "budget_filter.0.subaccounts", "budget_filter.0.labels"}, + }, + "credit_types_treatment": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"INCLUDE_ALL_CREDITS", "EXCLUDE_ALL_CREDITS", "INCLUDE_SPECIFIED_CREDITS", ""}, false), + Description: `Specifies how credits should be treated when determining spend +for threshold calculations. Default value: "INCLUDE_ALL_CREDITS" Possible values: ["INCLUDE_ALL_CREDITS", "EXCLUDE_ALL_CREDITS", "INCLUDE_SPECIFIED_CREDITS"]`, + Default: "INCLUDE_ALL_CREDITS", + AtLeastOneOf: []string{"budget_filter.0.projects", "budget_filter.0.credit_types_treatment", "budget_filter.0.services", "budget_filter.0.subaccounts", "budget_filter.0.labels"}, + }, + "labels": { + Type: schema.TypeMap, + Computed: true, + Optional: true, + Description: `A single label and value pair specifying that usage from only +this set of labeled resources should be included in the budget.`, + Elem: &schema.Schema{Type: schema.TypeString}, + AtLeastOneOf: []string{"budget_filter.0.projects", "budget_filter.0.credit_types_treatment", "budget_filter.0.services", "budget_filter.0.subaccounts", "budget_filter.0.labels"}, + }, + "projects": { + Type: schema.TypeList, + Optional: true, + Description: `A set of projects of the form projects/{project_number}, +specifying that usage from only this set of projects should be +included in the budget. If omitted, the report will include +all usage for the billing account, regardless of which project +the usage occurred on.`, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + AtLeastOneOf: []string{"budget_filter.0.projects", "budget_filter.0.credit_types_treatment", "budget_filter.0.services", "budget_filter.0.subaccounts", "budget_filter.0.labels"}, + }, + "services": { + Type: schema.TypeList, + Computed: true, + Optional: true, + Description: `A set of services of the form services/{service_id}, +specifying that usage from only this set of services should be +included in the budget. If omitted, the report will include +usage for all the services. The service names are available +through the Catalog API: +https://cloud.google.com/billing/v1/how-tos/catalog-api.`, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + AtLeastOneOf: []string{"budget_filter.0.projects", "budget_filter.0.credit_types_treatment", "budget_filter.0.services", "budget_filter.0.subaccounts", "budget_filter.0.labels"}, + }, + "subaccounts": { + Type: schema.TypeList, + Computed: true, + Optional: true, + Description: `A set of subaccounts of the form billingAccounts/{account_id}, +specifying that usage from only this set of subaccounts should +be included in the budget. If a subaccount is set to the name of +the parent account, usage from the parent account will be included. +If the field is omitted, the report will include usage from the parent +account and all subaccounts, if they exist.`, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + AtLeastOneOf: []string{"budget_filter.0.projects", "budget_filter.0.credit_types_treatment", "budget_filter.0.services", "budget_filter.0.subaccounts", "budget_filter.0.labels"}, + }, + }, + }, + }, + "display_name": { + Type: schema.TypeString, + Optional: true, + Description: `User data for display name in UI. Must be <= 60 chars.`, + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: `Resource name of the budget. The resource name +implies the scope of a budget. Values are of the form +billingAccounts/{billingAccountId}/budgets/{budgetId}.`, + }, + }, + } +} + +func resourceBillingBudgetUpgradeV0(_ context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { + log.Printf("[DEBUG] Attributes before migration: %#v", rawState) + + rawState["name"] = GetResourceNameFromSelfLink(rawState["name"].(string)) + + log.Printf("[DEBUG] Attributes after migration: %#v", rawState) + return rawState, nil +} diff --git a/mmv1/third_party/terraform/tests/resource_billing_budget_test.go b/mmv1/third_party/terraform/tests/resource_billing_budget_test.go index 055d8df77aa4..9b09bd928730 100644 --- a/mmv1/third_party/terraform/tests/resource_billing_budget_test.go +++ b/mmv1/third_party/terraform/tests/resource_billing_budget_test.go @@ -1,6 +1,7 @@ package google import ( + "context" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" @@ -163,3 +164,48 @@ resource "google_billing_budget" "budget" { } `, context) } + +func TestBillingBudgetStateUpgradeV0(t *testing.T) { + t.Parallel() + + cases := map[string]struct { + Attributes map[string]interface{} + Expected map[string]string + Meta interface{} + }{ + "shorten long name": { + Attributes: map[string]interface{}{ + "name": "billingAccounts/000000-111111-222222/budgets/9188612e-e4c0-4e69-9d14-9befebbcb87d", + }, + Expected: map[string]string{ + "name": "9188612e-e4c0-4e69-9d14-9befebbcb87d", + }, + Meta: &Config{}, + }, + "short name stays": { + Attributes: map[string]interface{}{ + "name": "9188612e-e4c0-4e69-9d14-9befebbcb87d", + }, + Expected: map[string]string{ + "name": "9188612e-e4c0-4e69-9d14-9befebbcb87d", + }, + Meta: &Config{}, + }, + } + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + actual, err := resourceBillingBudgetUpgradeV0(context.Background(), tc.Attributes, tc.Meta) + + if err != nil { + t.Error(err) + } + + for _, expectedName := range tc.Expected { + if actual["name"] != expectedName { + t.Errorf("expected: name -> %#v\n got: name -> %#v\n in: %#v", + expectedName, actual["name"], actual) + } + } + }) + } +}