diff --git a/.changelog/2640.txt b/.changelog/2640.txt new file mode 100644 index 00000000000..413b6708ffe --- /dev/null +++ b/.changelog/2640.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +`google_billing_subaccount` +``` diff --git a/google/provider.go b/google/provider.go index 6e1954622e5..d2d38e531cd 100644 --- a/google/provider.go +++ b/google/provider.go @@ -968,6 +968,7 @@ func ResourceMapWithErrors() (map[string]*schema.Resource, error) { "google_billing_account_iam_binding": ResourceIamBinding(IamBillingAccountSchema, NewBillingAccountIamUpdater, BillingAccountIdParseFunc), "google_billing_account_iam_member": ResourceIamMember(IamBillingAccountSchema, NewBillingAccountIamUpdater, BillingAccountIdParseFunc), "google_billing_account_iam_policy": ResourceIamPolicy(IamBillingAccountSchema, NewBillingAccountIamUpdater, BillingAccountIdParseFunc), + "google_billing_subaccount": resourceBillingSubaccount(), "google_cloudfunctions_function": resourceCloudFunctionsFunction(), "google_composer_environment": resourceComposerEnvironment(), "google_compute_attached_disk": resourceComputeAttachedDisk(), diff --git a/google/provider_test.go b/google/provider_test.go index dfa3dcb2724..864ee468325 100644 --- a/google/provider_test.go +++ b/google/provider_test.go @@ -98,6 +98,10 @@ type VcrSource struct { var sources map[string]VcrSource +var masterBillingAccountEnvVars = []string{ + "GOOGLE_MASTER_BILLING_ACCOUNT", +} + func init() { configs = make(map[string]*Config) sources = make(map[string]VcrSource) @@ -904,6 +908,11 @@ func getTestBillingAccountFromEnv(t *testing.T) string { return multiEnvSearch(billingAccountEnvVars) } +func getTestMasterBillingAccountFromEnv(t *testing.T) string { + skipIfEnvNotSet(t, masterBillingAccountEnvVars...) + return multiEnvSearch(masterBillingAccountEnvVars) +} + func getTestServiceAccountFromEnv(t *testing.T) string { skipIfEnvNotSet(t, serviceAccountEnvVars...) return multiEnvSearch(serviceAccountEnvVars) diff --git a/google/resource_google_billing_subaccount.go b/google/resource_google_billing_subaccount.go new file mode 100644 index 00000000000..a19461f534b --- /dev/null +++ b/google/resource_google_billing_subaccount.go @@ -0,0 +1,157 @@ +package google + +import ( + "fmt" + "strings" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "google.golang.org/api/cloudbilling/v1" +) + +func resourceBillingSubaccount() *schema.Resource { + return &schema.Resource{ + Create: resourceBillingSubaccountCreate, + Read: resourceBillingSubaccountRead, + Delete: resourceBillingSubaccountDelete, + Update: resourceBillingSubaccountUpdate, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "display_name": { + Type: schema.TypeString, + Required: true, + }, + "master_billing_account": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + DiffSuppressFunc: compareSelfLinkOrResourceName, + }, + "deletion_policy": { + Type: schema.TypeString, + Optional: true, + Default: "", + ValidateFunc: validation.StringInSlice([]string{"RENAME_ON_DESTROY", ""}, false), + }, + "billing_account_id": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "open": { + Type: schema.TypeBool, + Computed: true, + }, + }, + } +} + +func resourceBillingSubaccountCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + displayName := d.Get("display_name").(string) + masterBillingAccount := d.Get("master_billing_account").(string) + + billingAccount := &cloudbilling.BillingAccount{ + DisplayName: displayName, + MasterBillingAccount: canonicalBillingAccountName(masterBillingAccount), + } + + res, err := config.NewBillingClient(userAgent).BillingAccounts.Create(billingAccount).Do() + if err != nil { + return fmt.Errorf("Error creating billing subaccount '%s' in master account '%s': %s", displayName, masterBillingAccount, err) + } + + d.SetId(res.Name) + + return resourceBillingSubaccountRead(d, meta) +} + +func resourceBillingSubaccountRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + id := d.Id() + + billingAccount, err := config.NewBillingClient(userAgent).BillingAccounts.Get(d.Id()).Do() + if err != nil { + return handleNotFoundError(err, d, fmt.Sprintf("Billing Subaccount Not Found : %s", id)) + } + + if err := d.Set("name", billingAccount.Name); err != nil { + return fmt.Errorf("Error setting name: %s", err) + } + if err := d.Set("display_name", billingAccount.DisplayName); err != nil { + return fmt.Errorf("Error setting display_na,e: %s", err) + } + if err := d.Set("open", billingAccount.Open); err != nil { + return fmt.Errorf("Error setting open: %s", err) + } + if err := d.Set("master_billing_account", billingAccount.MasterBillingAccount); err != nil { + return fmt.Errorf("Error setting master_billing_account: %s", err) + } + if err := d.Set("billing_account_id", strings.TrimPrefix(d.Get("name").(string), "billingAccounts/")); err != nil { + return fmt.Errorf("Error setting billing_account_id: %s", err) + } + + return nil +} + +func resourceBillingSubaccountUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + if ok := d.HasChange("display_name"); ok { + billingAccount := &cloudbilling.BillingAccount{ + DisplayName: d.Get("display_name").(string), + } + _, err := config.NewBillingClient(userAgent).BillingAccounts.Patch(d.Id(), billingAccount).UpdateMask("display_name").Do() + if err != nil { + return handleNotFoundError(err, d, fmt.Sprintf("Error updating billing account : %s", d.Id())) + } + } + return resourceBillingSubaccountRead(d, meta) +} + +func resourceBillingSubaccountDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + deletionPolicy := d.Get("deletion_policy").(string) + + if deletionPolicy == "RENAME_ON_DESTROY" { + t := time.Now() + billingAccount := &cloudbilling.BillingAccount{ + DisplayName: "Terraform Destroyed " + t.Format("20060102150405"), + } + _, err := config.NewBillingClient(userAgent).BillingAccounts.Patch(d.Id(), billingAccount).UpdateMask("display_name").Do() + if err != nil { + return handleNotFoundError(err, d, fmt.Sprintf("Error updating billing account : %s", d.Id())) + } + } + + d.SetId("") + + return nil +} diff --git a/google/resource_google_billing_subaccount_test.go b/google/resource_google_billing_subaccount_test.go new file mode 100644 index 00000000000..dfb71006bd6 --- /dev/null +++ b/google/resource_google_billing_subaccount_test.go @@ -0,0 +1,131 @@ +package google + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccBillingSubaccount_renameOnDestroy(t *testing.T) { + t.Parallel() + + masterBilling := getTestMasterBillingAccountFromEnv(t) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGoogleBillingSubaccountRenameOnDestroy, + Steps: []resource.TestStep{ + { + // Test Billing Subaccount creation + Config: testAccBillingSubccount_renameOnDestroy(masterBilling), + Check: testAccCheckGoogleBillingSubaccountExists("subaccount_with_rename_on_destroy"), + }, + }, + }) +} + +func TestAccBillingSubaccount_basic(t *testing.T) { + t.Parallel() + + masterBilling := getTestMasterBillingAccountFromEnv(t) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + // Test Billing Subaccount creation + Config: testAccBillingSubccount_basic(masterBilling), + Check: testAccCheckGoogleBillingSubaccountExists("subaccount"), + }, + { + ResourceName: "google_billing_subaccount.subaccount", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_policy"}, + }, + { + // Test Billing Subaccount update + Config: testAccBillingSubccount_update(masterBilling), + Check: testAccCheckGoogleBillingSubaccountExists("subaccount"), + }, + { + ResourceName: "google_billing_subaccount.subaccount", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_policy"}, + }, + }, + }) +} + +func testAccBillingSubccount_basic(masterBillingAccountId string) string { + return fmt.Sprintf(` +resource "google_billing_subaccount" "subaccount" { + display_name = "Test Billing Subaccount" + master_billing_account = "%s" +} +`, masterBillingAccountId) +} + +func testAccBillingSubccount_update(masterBillingAccountId string) string { + return fmt.Sprintf(` +resource "google_billing_subaccount" "subaccount" { + display_name = "Rename Test Billing Subaccount" + master_billing_account = "%s" +} +`, masterBillingAccountId) +} + +func testAccBillingSubccount_renameOnDestroy(masterBillingAccountId string) string { + return fmt.Sprintf(` +resource "google_billing_subaccount" "subaccount_with_rename_on_destroy" { + display_name = "Test Billing Subaccount (Rename on Destroy)" + master_billing_account = "%s" + deletion_policy = "RENAME_ON_DESTROY" +} +`, masterBillingAccountId) +} + +func testAccCheckGoogleBillingSubaccountExists(bindingResourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + subaccount, ok := s.RootModule().Resources["google_billing_subaccount."+bindingResourceName] + if !ok { + return fmt.Errorf("Not found: %s", bindingResourceName) + } + + config := testAccProvider.Meta().(*Config) + _, err := config.NewBillingClient(config.userAgent).BillingAccounts.Get(subaccount.Primary.ID).Do() + if err != nil { + return err + } + + return nil + } +} + +func testAccCheckGoogleBillingSubaccountRenameOnDestroy(s *terraform.State) error { + for name, rs := range s.RootModule().Resources { + if rs.Type != "google_billing_subaccount" { + continue + } + if strings.HasPrefix(name, "data.") { + continue + } + + config := testAccProvider.Meta().(*Config) + + res, err := config.NewBillingClient(config.userAgent).BillingAccounts.Get(rs.Primary.ID).Do() + if err != nil { + return err + } + + if !strings.HasPrefix(res.DisplayName, "Terraform Destroyed") { + return fmt.Errorf("Billing account %s was not renamed on destroy", rs.Primary.ID) + } + } + + return nil +} diff --git a/website/docs/r/google_billing_subaccount.html.markdown b/website/docs/r/google_billing_subaccount.html.markdown new file mode 100644 index 00000000000..960b76582dd --- /dev/null +++ b/website/docs/r/google_billing_subaccount.html.markdown @@ -0,0 +1,50 @@ +--- +subcategory: "Cloud Platform" +layout: "google" +page_title: "Google: google_billing_subaccount" +sidebar_current: "docs-google-billing-subaccount" +description: |- + Allows management of a Google Cloud Billing Subaccount. +--- + +# google\_billing\_subaccount + +Allows creation and management of a Google Cloud Billing Subaccount. + +!> **WARNING:** Deleting this Terraform resource will not delete or close the billing subaccount. + +```hcl +resource "google_billing_subaccount" "subaccount" { + display_name = "My Billing Account" + master_billing_account = "012345-567890-ABCDEF" +} +``` + +## Argument Reference + +* `display_name` (Required) - The display name of the billing account. + +* `master_billing_account` (Required) - The name of the master billing account that the subaccount + will be created under in the form `{billing_account_id}` or `billingAccounts/{billing_account_id}`. + +* `deletion_policy` (Optional) - If set to "RENAME_ON_DESTROY" the billing account display_name + will be changed to "Terraform Destroyed" along with a timestamp. If set to "" this will not occur. + Default is "". + +## Attributes Reference + +The following additional attributes are exported: + +* `open` - `true` if the billing account is open, `false` if the billing account is closed. + +* `name` - The resource name of the billing account in the form `billingAccounts/{billing_account_id}`. + +* `billing_account_id` - The billing account id. + +## Import + +Billing Subaccounts can be imported using any of these accepted formats: + +``` +$ terraform import google_billing_subaccount.default billingAccounts/{billing_account_id} +``` diff --git a/website/google.erb b/website/google.erb index 8e3ed10d548..339866ec78a 100644 --- a/website/google.erb +++ b/website/google.erb @@ -1036,6 +1036,10 @@ Resources