diff --git a/google-beta/config.go b/google-beta/config.go index 26140c844d..5f84531e65 100644 --- a/google-beta/config.go +++ b/google-beta/config.go @@ -77,6 +77,7 @@ type Config struct { BigQueryBasePath string BigqueryDataTransferBasePath string BigtableBasePath string + BillingBasePath string BinaryAuthorizationBasePath string CloudBuildBasePath string CloudFunctionsBasePath string @@ -215,6 +216,7 @@ var AppEngineDefaultBasePath = "https://appengine.googleapis.com/v1/" var BigQueryDefaultBasePath = "https://www.googleapis.com/bigquery/v2/" var BigqueryDataTransferDefaultBasePath = "https://bigquerydatatransfer.googleapis.com/v1/" var BigtableDefaultBasePath = "https://bigtableadmin.googleapis.com/v2/" +var BillingDefaultBasePath = "https://billingbudgets.googleapis.com/v1beta1/" var BinaryAuthorizationDefaultBasePath = "https://binaryauthorization.googleapis.com/v1beta1/" var CloudBuildDefaultBasePath = "https://cloudbuild.googleapis.com/v1/" var CloudFunctionsDefaultBasePath = "https://cloudfunctions.googleapis.com/v1/" @@ -706,6 +708,7 @@ func ConfigureBasePaths(c *Config) { c.BigQueryBasePath = BigQueryDefaultBasePath c.BigqueryDataTransferBasePath = BigqueryDataTransferDefaultBasePath c.BigtableBasePath = BigtableDefaultBasePath + c.BillingBasePath = BillingDefaultBasePath c.BinaryAuthorizationBasePath = BinaryAuthorizationDefaultBasePath c.CloudBuildBasePath = CloudBuildDefaultBasePath c.CloudFunctionsBasePath = CloudFunctionsDefaultBasePath diff --git a/google-beta/provider.go b/google-beta/provider.go index f870c64060..54ceacf36b 100644 --- a/google-beta/provider.go +++ b/google-beta/provider.go @@ -143,6 +143,14 @@ func Provider() terraform.ResourceProvider { "GOOGLE_BIGTABLE_CUSTOM_ENDPOINT", }, BigtableDefaultBasePath), }, + "billing_custom_endpoint": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateCustomEndpoint, + DefaultFunc: schema.MultiEnvDefaultFunc([]string{ + "GOOGLE_BILLING_CUSTOM_ENDPOINT", + }, BillingDefaultBasePath), + }, "binary_authorization_custom_endpoint": { Type: schema.TypeString, Optional: true, @@ -489,9 +497,9 @@ func Provider() terraform.ResourceProvider { return provider } -// Generated resources: 102 +// Generated resources: 103 // Generated IAM resources: 39 -// Total generated resources: 141 +// Total generated resources: 142 func ResourceMap() map[string]*schema.Resource { resourceMap, _ := ResourceMapWithErrors() return resourceMap @@ -510,6 +518,7 @@ func ResourceMapWithErrors() (map[string]*schema.Resource, error) { "google_bigquery_dataset": resourceBigQueryDataset(), "google_bigquery_data_transfer_config": resourceBigqueryDataTransferConfig(), "google_bigtable_app_profile": resourceBigtableAppProfile(), + "google_billing_budget": resourceBillingBudget(), "google_binary_authorization_attestor": resourceBinaryAuthorizationAttestor(), "google_binary_authorization_attestor_iam_binding": ResourceIamBinding(BinaryAuthorizationAttestorIamSchema, BinaryAuthorizationAttestorIamUpdaterProducer, BinaryAuthorizationAttestorIdParseFunc), "google_binary_authorization_attestor_iam_member": ResourceIamMember(BinaryAuthorizationAttestorIamSchema, BinaryAuthorizationAttestorIamUpdaterProducer, BinaryAuthorizationAttestorIdParseFunc), @@ -807,6 +816,7 @@ func providerConfigure(d *schema.ResourceData, terraformVersion string) (interfa config.BigQueryBasePath = d.Get("big_query_custom_endpoint").(string) config.BigqueryDataTransferBasePath = d.Get("bigquery_data_transfer_custom_endpoint").(string) config.BigtableBasePath = d.Get("bigtable_custom_endpoint").(string) + config.BillingBasePath = d.Get("billing_custom_endpoint").(string) config.BinaryAuthorizationBasePath = d.Get("binary_authorization_custom_endpoint").(string) config.CloudBuildBasePath = d.Get("cloud_build_custom_endpoint").(string) config.CloudFunctionsBasePath = d.Get("cloud_functions_custom_endpoint").(string) diff --git a/google-beta/resource_billing_budget.go b/google-beta/resource_billing_budget.go new file mode 100644 index 0000000000..021527badb --- /dev/null +++ b/google-beta/resource_billing_budget.go @@ -0,0 +1,720 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package google + +import ( + "fmt" + "log" + "reflect" + "strconv" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" +) + +func resourceBillingBudget() *schema.Resource { + return &schema.Resource{ + Create: resourceBillingBudgetCreate, + Read: resourceBillingBudgetRead, + Update: resourceBillingBudgetUpdate, + Delete: resourceBillingBudgetDelete, + + Importer: &schema.ResourceImporter{ + State: resourceBillingBudgetImport, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(4 * time.Minute), + Update: schema.DefaultTimeout(4 * time.Minute), + Delete: schema.DefaultTimeout(4 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "billing_account": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `ID of the billing account to set a budget on.`, + }, + "amount": { + Type: schema.TypeList, + Required: true, + Description: `The budgeted amount for each usage period.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "specified_amount": { + Type: schema.TypeList, + Required: 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, + 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.`, + }, + }, + }, + }, + }, + }, + }, + "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: "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{ + "pubsub_topic": { + Type: schema.TypeString, + Required: 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.`, + }, + "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, + 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_treatment": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"INCLUDE_ALL_CREDITS", "EXCLUDE_ALL_CREDITS", ""}, false), + Description: `Specifies how credits should be treated when determining spend +for threshold calculations.`, + Default: "INCLUDE_ALL_CREDITS", + }, + "projects": { + Type: schema.TypeList, + Optional: true, + Description: `A set of projects of the form projects/{project_id}, +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. Only zero or one project can be +specified currently.`, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "services": { + Type: schema.TypeList, + 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{}, + }, + "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 resourceBillingBudgetCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + obj := make(map[string]interface{}) + budgetProp, err := expandBillingBudgetBudget(nil, d, config) + if err != nil { + return err + } else if !isEmptyValue(reflect.ValueOf(budgetProp)) { + obj["budget"] = budgetProp + } + + url, err := replaceVars(d, config, "{{BillingBasePath}}billingAccounts/{{billing_account}}/budgets") + if err != nil { + return err + } + + log.Printf("[DEBUG] Creating new Budget: %#v", obj) + res, err := sendRequestWithTimeout(config, "POST", "", url, obj, d.Timeout(schema.TimeoutCreate)) + if err != nil { + return fmt.Errorf("Error creating Budget: %s", err) + } + + // Store the ID now + id, err := replaceVars(d, config, "{{name}}") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + log.Printf("[DEBUG] Finished creating Budget %q: %#v", d.Id(), res) + + // `name` is autogenerated from the api so needs to be set post-create + name, ok := res["name"] + if !ok { + return fmt.Errorf("Create response didn't contain critical fields. Create may not have succeeded.") + } + d.Set("name", name.(string)) + d.SetId(name.(string)) + + return resourceBillingBudgetRead(d, meta) +} + +func resourceBillingBudgetRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + url, err := replaceVars(d, config, "{{BillingBasePath}}{{name}}") + if err != nil { + return err + } + + res, err := sendRequest(config, "GET", "", url, nil) + if err != nil { + return handleNotFoundError(err, d, fmt.Sprintf("BillingBudget %q", d.Id())) + } + + if err := d.Set("name", flattenBillingBudgetName(res["name"], d)); err != nil { + return fmt.Errorf("Error reading Budget: %s", err) + } + // Terraform must set the top level schema field, but since this object contains collapsed properties + // it's difficult to know what the top level should be. Instead we just loop over the map returned from flatten. + if flattenedProp := flattenBillingBudgetBudget(res["budget"], d); flattenedProp != nil { + casted := flattenedProp.([]interface{})[0] + if casted != nil { + for k, v := range casted.(map[string]interface{}) { + d.Set(k, v) + } + } + } + + return nil +} + +func resourceBillingBudgetUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + obj := make(map[string]interface{}) + budgetProp, err := expandBillingBudgetBudget(nil, d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("budget"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, budgetProp)) { + obj["budget"] = budgetProp + } + + url, err := replaceVars(d, config, "{{BillingBasePath}}{{name}}") + if err != nil { + return err + } + + log.Printf("[DEBUG] Updating Budget %q: %#v", d.Id(), obj) + _, err = sendRequestWithTimeout(config, "PATCH", "", url, obj, d.Timeout(schema.TimeoutUpdate)) + + if err != nil { + return fmt.Errorf("Error updating Budget %q: %s", d.Id(), err) + } + + return resourceBillingBudgetRead(d, meta) +} + +func resourceBillingBudgetDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + url, err := replaceVars(d, config, "{{BillingBasePath}}{{name}}") + if err != nil { + return err + } + + var obj map[string]interface{} + log.Printf("[DEBUG] Deleting Budget %q", d.Id()) + + res, err := sendRequestWithTimeout(config, "DELETE", "", url, obj, d.Timeout(schema.TimeoutDelete)) + if err != nil { + return handleNotFoundError(err, d, "Budget") + } + + log.Printf("[DEBUG] Finished deleting Budget %q: %#v", d.Id(), res) + return nil +} + +func resourceBillingBudgetImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + + config := meta.(*Config) + + // current import_formats can't import fields with forward slashes in their value + if err := parseImportId([]string{"(?P.+)"}, d, config); err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil +} + +func flattenBillingBudgetName(v interface{}, d *schema.ResourceData) interface{} { + return v +} + +func flattenBillingBudgetBudget(v interface{}, d *schema.ResourceData) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["display_name"] = + flattenBillingBudgetBudgetDisplayName(original["displayName"], d) + transformed["budget_filter"] = + flattenBillingBudgetBudgetBudgetFilter(original["budgetFilter"], d) + transformed["amount"] = + flattenBillingBudgetBudgetAmount(original["amount"], d) + transformed["threshold_rules"] = + flattenBillingBudgetBudgetThresholdRules(original["thresholdRules"], d) + transformed["all_updates_rule"] = + flattenBillingBudgetBudgetAllUpdatesRule(original["allUpdatesRule"], d) + return []interface{}{transformed} +} +func flattenBillingBudgetBudgetDisplayName(v interface{}, d *schema.ResourceData) interface{} { + return v +} + +func flattenBillingBudgetBudgetBudgetFilter(v interface{}, d *schema.ResourceData) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["projects"] = + flattenBillingBudgetBudgetBudgetFilterProjects(original["projects"], d) + transformed["credit_types_treatment"] = + flattenBillingBudgetBudgetBudgetFilterCreditTypesTreatment(original["creditTypesTreatment"], d) + transformed["services"] = + flattenBillingBudgetBudgetBudgetFilterServices(original["services"], d) + return []interface{}{transformed} +} +func flattenBillingBudgetBudgetBudgetFilterProjects(v interface{}, d *schema.ResourceData) interface{} { + return v +} + +func flattenBillingBudgetBudgetBudgetFilterCreditTypesTreatment(v interface{}, d *schema.ResourceData) interface{} { + return v +} + +func flattenBillingBudgetBudgetBudgetFilterServices(v interface{}, d *schema.ResourceData) interface{} { + return v +} + +func flattenBillingBudgetBudgetAmount(v interface{}, d *schema.ResourceData) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["specified_amount"] = + flattenBillingBudgetBudgetAmountSpecifiedAmount(original["specifiedAmount"], d) + return []interface{}{transformed} +} +func flattenBillingBudgetBudgetAmountSpecifiedAmount(v interface{}, d *schema.ResourceData) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["currency_code"] = + flattenBillingBudgetBudgetAmountSpecifiedAmountCurrencyCode(original["currencyCode"], d) + transformed["units"] = + flattenBillingBudgetBudgetAmountSpecifiedAmountUnits(original["units"], d) + transformed["nanos"] = + flattenBillingBudgetBudgetAmountSpecifiedAmountNanos(original["nanos"], d) + return []interface{}{transformed} +} +func flattenBillingBudgetBudgetAmountSpecifiedAmountCurrencyCode(v interface{}, d *schema.ResourceData) interface{} { + return v +} + +func flattenBillingBudgetBudgetAmountSpecifiedAmountUnits(v interface{}, d *schema.ResourceData) interface{} { + return v +} + +func flattenBillingBudgetBudgetAmountSpecifiedAmountNanos(v interface{}, d *schema.ResourceData) interface{} { + // Handles the string fixed64 format + if strVal, ok := v.(string); ok { + if intVal, err := strconv.ParseInt(strVal, 10, 64); err == nil { + return intVal + } // let terraform core handle it if we can't convert the string to an int. + } + return v +} + +func flattenBillingBudgetBudgetThresholdRules(v interface{}, d *schema.ResourceData) interface{} { + if v == nil { + return v + } + l := v.([]interface{}) + transformed := make([]interface{}, 0, len(l)) + for _, raw := range l { + original := raw.(map[string]interface{}) + if len(original) < 1 { + // Do not include empty json objects coming back from the api + continue + } + transformed = append(transformed, map[string]interface{}{ + "threshold_percent": flattenBillingBudgetBudgetThresholdRulesThresholdPercent(original["thresholdPercent"], d), + "spend_basis": flattenBillingBudgetBudgetThresholdRulesSpendBasis(original["spendBasis"], d), + }) + } + return transformed +} +func flattenBillingBudgetBudgetThresholdRulesThresholdPercent(v interface{}, d *schema.ResourceData) interface{} { + return v +} + +func flattenBillingBudgetBudgetThresholdRulesSpendBasis(v interface{}, d *schema.ResourceData) interface{} { + return v +} + +func flattenBillingBudgetBudgetAllUpdatesRule(v interface{}, d *schema.ResourceData) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["pubsub_topic"] = + flattenBillingBudgetBudgetAllUpdatesRulePubsubTopic(original["pubsubTopic"], d) + transformed["schema_version"] = + flattenBillingBudgetBudgetAllUpdatesRuleSchemaVersion(original["schemaVersion"], d) + return []interface{}{transformed} +} +func flattenBillingBudgetBudgetAllUpdatesRulePubsubTopic(v interface{}, d *schema.ResourceData) interface{} { + return v +} + +func flattenBillingBudgetBudgetAllUpdatesRuleSchemaVersion(v interface{}, d *schema.ResourceData) interface{} { + return v +} + +func expandBillingBudgetBudget(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + transformed := make(map[string]interface{}) + transformedDisplayName, err := expandBillingBudgetBudgetDisplayName(d.Get("display_name"), d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedDisplayName); val.IsValid() && !isEmptyValue(val) { + transformed["displayName"] = transformedDisplayName + } + + transformedBudgetFilter, err := expandBillingBudgetBudgetBudgetFilter(d.Get("budget_filter"), d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedBudgetFilter); val.IsValid() && !isEmptyValue(val) { + transformed["budgetFilter"] = transformedBudgetFilter + } + + transformedAmount, err := expandBillingBudgetBudgetAmount(d.Get("amount"), d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedAmount); val.IsValid() && !isEmptyValue(val) { + transformed["amount"] = transformedAmount + } + + transformedThresholdRules, err := expandBillingBudgetBudgetThresholdRules(d.Get("threshold_rules"), d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedThresholdRules); val.IsValid() && !isEmptyValue(val) { + transformed["thresholdRules"] = transformedThresholdRules + } + + transformedAllUpdatesRule, err := expandBillingBudgetBudgetAllUpdatesRule(d.Get("all_updates_rule"), d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedAllUpdatesRule); val.IsValid() && !isEmptyValue(val) { + transformed["allUpdatesRule"] = transformedAllUpdatesRule + } + + return transformed, nil +} + +func expandBillingBudgetBudgetDisplayName(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandBillingBudgetBudgetBudgetFilter(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedProjects, err := expandBillingBudgetBudgetBudgetFilterProjects(original["projects"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedProjects); val.IsValid() && !isEmptyValue(val) { + transformed["projects"] = transformedProjects + } + + transformedCreditTypesTreatment, err := expandBillingBudgetBudgetBudgetFilterCreditTypesTreatment(original["credit_types_treatment"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedCreditTypesTreatment); val.IsValid() && !isEmptyValue(val) { + transformed["creditTypesTreatment"] = transformedCreditTypesTreatment + } + + transformedServices, err := expandBillingBudgetBudgetBudgetFilterServices(original["services"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedServices); val.IsValid() && !isEmptyValue(val) { + transformed["services"] = transformedServices + } + + return transformed, nil +} + +func expandBillingBudgetBudgetBudgetFilterProjects(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandBillingBudgetBudgetBudgetFilterCreditTypesTreatment(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandBillingBudgetBudgetBudgetFilterServices(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandBillingBudgetBudgetAmount(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedSpecifiedAmount, err := expandBillingBudgetBudgetAmountSpecifiedAmount(original["specified_amount"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedSpecifiedAmount); val.IsValid() && !isEmptyValue(val) { + transformed["specifiedAmount"] = transformedSpecifiedAmount + } + + return transformed, nil +} + +func expandBillingBudgetBudgetAmountSpecifiedAmount(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedCurrencyCode, err := expandBillingBudgetBudgetAmountSpecifiedAmountCurrencyCode(original["currency_code"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedCurrencyCode); val.IsValid() && !isEmptyValue(val) { + transformed["currencyCode"] = transformedCurrencyCode + } + + transformedUnits, err := expandBillingBudgetBudgetAmountSpecifiedAmountUnits(original["units"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedUnits); val.IsValid() && !isEmptyValue(val) { + transformed["units"] = transformedUnits + } + + transformedNanos, err := expandBillingBudgetBudgetAmountSpecifiedAmountNanos(original["nanos"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedNanos); val.IsValid() && !isEmptyValue(val) { + transformed["nanos"] = transformedNanos + } + + return transformed, nil +} + +func expandBillingBudgetBudgetAmountSpecifiedAmountCurrencyCode(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandBillingBudgetBudgetAmountSpecifiedAmountUnits(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandBillingBudgetBudgetAmountSpecifiedAmountNanos(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandBillingBudgetBudgetThresholdRules(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + req := make([]interface{}, 0, len(l)) + for _, raw := range l { + if raw == nil { + continue + } + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedThresholdPercent, err := expandBillingBudgetBudgetThresholdRulesThresholdPercent(original["threshold_percent"], d, config) + if err != nil { + return nil, err + } else { + transformed["thresholdPercent"] = transformedThresholdPercent + } + + transformedSpendBasis, err := expandBillingBudgetBudgetThresholdRulesSpendBasis(original["spend_basis"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedSpendBasis); val.IsValid() && !isEmptyValue(val) { + transformed["spendBasis"] = transformedSpendBasis + } + + req = append(req, transformed) + } + return req, nil +} + +func expandBillingBudgetBudgetThresholdRulesThresholdPercent(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandBillingBudgetBudgetThresholdRulesSpendBasis(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandBillingBudgetBudgetAllUpdatesRule(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedPubsubTopic, err := expandBillingBudgetBudgetAllUpdatesRulePubsubTopic(original["pubsub_topic"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedPubsubTopic); val.IsValid() && !isEmptyValue(val) { + transformed["pubsubTopic"] = transformedPubsubTopic + } + + transformedSchemaVersion, err := expandBillingBudgetBudgetAllUpdatesRuleSchemaVersion(original["schema_version"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedSchemaVersion); val.IsValid() && !isEmptyValue(val) { + transformed["schemaVersion"] = transformedSchemaVersion + } + + return transformed, nil +} + +func expandBillingBudgetBudgetAllUpdatesRulePubsubTopic(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandBillingBudgetBudgetAllUpdatesRuleSchemaVersion(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} diff --git a/google-beta/resource_billing_budget_generated_test.go b/google-beta/resource_billing_budget_generated_test.go new file mode 100644 index 0000000000..f4864e3208 --- /dev/null +++ b/google-beta/resource_billing_budget_generated_test.go @@ -0,0 +1,148 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package google + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" +) + +func TestAccBillingBudget_billingBudgetBasicExample(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "billing_acct": getTestBillingAccountFromEnv(t), + "random_suffix": acctest.RandString(10), + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProvidersOiCS, + CheckDestroy: testAccCheckBillingBudgetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBillingBudget_billingBudgetBasicExample(context), + }, + }, + }) +} + +func testAccBillingBudget_billingBudgetBasicExample(context map[string]interface{}) string { + return Nprintf(` +data "google_billing_account" "account" { + billing_account = "%{billing_acct}" +} + +resource "google_billing_budget" "budget" { + provider = google-beta + billing_account = data.google_billing_account.account.id + display_name = "Example Billing Budget%{random_suffix}" + amount { + specified_amount { + currency_code = "USD" + units = "10" + } + } + threshold_rules { + threshold_percent = 0.5 + } +} +`, context) +} + +func TestAccBillingBudget_billingBudgetFilterExample(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "billing_acct": getTestBillingAccountFromEnv(t), + "random_suffix": acctest.RandString(10), + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProvidersOiCS, + CheckDestroy: testAccCheckBillingBudgetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBillingBudget_billingBudgetFilterExample(context), + }, + }, + }) +} + +func testAccBillingBudget_billingBudgetFilterExample(context map[string]interface{}) string { + return Nprintf(` +data "google_billing_account" "account" { + billing_account = "%{billing_acct}" +} + +resource "google_billing_budget" "budget" { + provider = google-beta + billing_account = data.google_billing_account.account.id + display_name = "Example Billing Budget%{random_suffix}" + + budget_filter { + projects = ["projects/example-project"] + credit_types_treatment = "EXCLUDE_ALL_CREDITS" + services = ["services/24E6-581D-38E5"] # Bigquery + } + + amount { + specified_amount { + currency_code = "USD" + units = "1000" + } + } + + threshold_rules { + threshold_percent = 0.5 + } + threshold_rules { + threshold_percent = 0.9 + spend_basis = "FORECASTED_SPEND" + } +} +`, context) +} + +func testAccCheckBillingBudgetDestroy(s *terraform.State) error { + for name, rs := range s.RootModule().Resources { + if rs.Type != "google_billing_budget" { + continue + } + if strings.HasPrefix(name, "data.") { + continue + } + + config := testAccProvider.Meta().(*Config) + + url, err := replaceVarsForTest(config, rs, "{{BillingBasePath}}{{name}}") + if err != nil { + return err + } + + _, err = sendRequest(config, "GET", "", url, nil) + if err == nil { + return fmt.Errorf("BillingBudget still exists at %s", url) + } + } + + return nil +} diff --git a/website/docs/r/billing_budget.html.markdown b/website/docs/r/billing_budget.html.markdown new file mode 100644 index 0000000000..c598796c9b --- /dev/null +++ b/website/docs/r/billing_budget.html.markdown @@ -0,0 +1,256 @@ +--- +# ---------------------------------------------------------------------------- +# +# *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +# +# ---------------------------------------------------------------------------- +# +# This file is automatically generated by Magic Modules and manual +# changes will be clobbered when the file is regenerated. +# +# Please read more about how to change this file in +# .github/CONTRIBUTING.md. +# +# ---------------------------------------------------------------------------- +subcategory: "Billing Budget" +layout: "google" +page_title: "Google: google_billing_budget" +sidebar_current: "docs-google-billing-budget" +description: |- + Budget configuration for a billing account. +--- + +# google\_billing\_budget + +Budget configuration for a billing account. + +~> **Warning:** This resource is in beta, and should be used with the terraform-provider-google-beta provider. +See [Provider Versions](https://terraform.io/docs/providers/google/guides/provider_versions.html) for more details on beta resources. + +To get more information about Budget, see: + +* [API documentation](https://cloud.google.com/billing/docs/reference/budget/rest/v1beta1/billingAccounts.budgets) +* How-to Guides + * [Creating a budget](https://cloud.google.com/billing/docs/how-to/budgets) + +
+ + Open in Cloud Shell + +
+## Example Usage - Billing Budget Basic + + +```hcl +data "google_billing_account" "account" { + billing_account = "000000-0000000-0000000-000000" +} + +resource "google_billing_budget" "budget" { + provider = google-beta + billing_account = data.google_billing_account.account.id + display_name = "Example Billing Budget" + amount { + specified_amount { + currency_code = "USD" + units = "10" + } + } + threshold_rules { + threshold_percent = 0.5 + } +} +``` +
+ + Open in Cloud Shell + +
+## Example Usage - Billing Budget Filter + + +```hcl +data "google_billing_account" "account" { + billing_account = "000000-0000000-0000000-000000" +} + +resource "google_billing_budget" "budget" { + provider = google-beta + billing_account = data.google_billing_account.account.id + display_name = "Example Billing Budget" + + budget_filter { + projects = ["projects/example-project"] + credit_types_treatment = "EXCLUDE_ALL_CREDITS" + services = ["services/24E6-581D-38E5"] # Bigquery + } + + amount { + specified_amount { + currency_code = "USD" + units = "1000" + } + } + + threshold_rules { + threshold_percent = 0.5 + } + threshold_rules { + threshold_percent = 0.9 + spend_basis = "FORECASTED_SPEND" + } +} +``` + +## Argument Reference + +The following arguments are supported: + + +* `amount` - + (Required) + The budgeted amount for each usage period. Structure is documented below. + +* `threshold_rules` - + (Required) + Rules that trigger alerts (notifications of thresholds being + crossed) when spend exceeds the specified percentages of the + budget. Structure is documented below. + +* `billing_account` - + (Required) + ID of the billing account to set a budget on. + + + +The `budget_filter` block supports: + +* `projects` - + (Optional) + A set of projects of the form projects/{project_id}, + 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. Only zero or one project can be + specified currently. + +* `credit_types_treatment` - + (Optional) + Specifies how credits should be treated when determining spend + for threshold calculations. + +* `services` - + (Optional) + 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. + +The `amount` block supports: + +* `specified_amount` - + (Required) + 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. Structure is documented below. + + +The `specified_amount` block supports: + +* `currency_code` - + (Optional) + The 3-letter currency code defined in ISO 4217. + +* `units` - + (Optional) + The whole units of the amount. For example if currencyCode + is "USD", then 1 unit is one US dollar. + +* `nanos` - + (Optional) + 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. + +The `threshold_rules` block supports: + +* `threshold_percent` - + (Required) + Send an alert when this threshold is exceeded. This is a + 1.0-based percentage, so 0.5 = 50%. Must be >= 0. + +* `spend_basis` - + (Optional) + The type of basis used to determine if spend has passed + the threshold. + +The `all_updates_rule` block supports: + +* `pubsub_topic` - + (Required) + 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. + +* `schema_version` - + (Optional) + 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. + +- - - + + +* `display_name` - + (Optional) + User data for display name in UI. Must be <= 60 chars. + +* `budget_filter` - + (Optional) + Filters that define which resources are used to compute the actual + spend against the budget. Structure is documented below. + +* `all_updates_rule` - + (Optional) + Defines notifications that are sent on every update to the + billing account's spend, regardless of the thresholds defined + using threshold rules. Structure is documented below. + + +## Attributes Reference + +In addition to the arguments listed above, the following computed attributes are exported: + + +* `name` - + Resource name of the budget. The resource name + implies the scope of a budget. Values are of the form + billingAccounts/{billingAccountId}/budgets/{budgetId}. + + +## Timeouts + +This resource provides the following +[Timeouts](/docs/configuration/resources.html#timeouts) configuration options: + +- `create` - Default is 4 minutes. +- `update` - Default is 4 minutes. +- `delete` - Default is 4 minutes. + +## Import + +Budget can be imported using any of these accepted formats: + +``` +$ terraform import -provider=google-beta google_billing_budget.default {{name}} +``` + +-> If you're importing a resource with beta features, make sure to include `-provider=google-beta` +as an argument so that Terraform uses the correct provider to import your resource.