From bf10e5519df1d6a9d7cf70170c2ec57b587b2380 Mon Sep 17 00:00:00 2001 From: Evan Brown Date: Mon, 20 Feb 2017 09:32:24 -0800 Subject: [PATCH] providers/google: google_project supports billing account (#11653) * Vendor google.golang.org/api/cloudbilling/v1 * providers/google: Add cloudbilling client * providers/google: google_project supports billing account This change allows a Terraform user to set and update the billing account associated with their project. * providers/google: Testing project billing account This change adds optional acceptance tests for project billing accounts. GOOGLE_PROJECT_BILLING_ACCOUNT and GOOGLE_PROJECT_BILLING_ACCOUNT_2 must be set in the environment for the tests to run; otherwise, they will be skipped. Also includes a few code cleanups per review. * providers/google: Improve project billing error message --- config.go | 9 ++ resource_google_project.go | 58 ++++++++++++ resource_google_project_iam_policy_test.go | 10 ++ resource_google_project_test.go | 105 +++++++++++++++++++++ 4 files changed, 182 insertions(+) diff --git a/config.go b/config.go index 9f9eb075..37ac2db8 100644 --- a/config.go +++ b/config.go @@ -13,6 +13,7 @@ import ( "golang.org/x/oauth2" "golang.org/x/oauth2/google" "golang.org/x/oauth2/jwt" + "google.golang.org/api/cloudbilling/v1" "google.golang.org/api/cloudresourcemanager/v1" "google.golang.org/api/compute/v1" "google.golang.org/api/container/v1" @@ -31,6 +32,7 @@ type Config struct { Project string Region string + clientBilling *cloudbilling.Service clientCompute *compute.Service clientContainer *container.Service clientDns *dns.Service @@ -160,6 +162,13 @@ func (c *Config) loadAndValidate() error { } c.clientServiceMan.UserAgent = userAgent + log.Printf("[INFO] Instantiating Google Cloud Billing Client...") + c.clientBilling, err = cloudbilling.New(client) + if err != nil { + return err + } + c.clientBilling.UserAgent = userAgent + return nil } diff --git a/resource_google_project.go b/resource_google_project.go index 24dc56b8..b4bcb9c4 100644 --- a/resource_google_project.go +++ b/resource_google_project.go @@ -6,8 +6,10 @@ import ( "log" "net/http" "strconv" + "strings" "github.com/hashicorp/terraform/helper/schema" + "google.golang.org/api/cloudbilling/v1" "google.golang.org/api/cloudresourcemanager/v1" "google.golang.org/api/googleapi" ) @@ -86,6 +88,10 @@ func resourceGoogleProject() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "billing_account": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, }, } } @@ -172,6 +178,22 @@ func resourceGoogleProjectCreate(d *schema.ResourceData, meta interface{}) error } } + // Set the billing account + if v, ok := d.GetOk("billing_account"); ok { + name := v.(string) + ba := cloudbilling.ProjectBillingInfo{ + BillingAccountName: "billingAccounts/" + name, + } + _, err = config.clientBilling.Projects.UpdateBillingInfo(prefixedProject(pid), &ba).Do() + if err != nil { + d.Set("billing_account", "") + if _err, ok := err.(*googleapi.Error); ok { + return fmt.Errorf("Error setting billing account %q for project %q: %v", name, prefixedProject(pid), _err) + } + return fmt.Errorf("Error setting billing account %q for project %q: %v", name, prefixedProject(pid), err) + } + } + return resourceGoogleProjectRead(d, meta) } @@ -196,9 +218,30 @@ func resourceGoogleProjectRead(d *schema.ResourceData, meta interface{}) error { d.Set("org_id", p.Parent.Id) } + // Read the billing account + ba, err := config.clientBilling.Projects.GetBillingInfo(prefixedProject(pid)).Do() + if err != nil { + return fmt.Errorf("Error reading billing account for project %q: %v", prefixedProject(pid), err) + } + if ba.BillingAccountName != "" { + // BillingAccountName is contains the resource name of the billing account + // associated with the project, if any. For example, + // `billingAccounts/012345-567890-ABCDEF`. We care about the ID and not + // the `billingAccounts/` prefix, so we need to remove that. If the + // prefix ever changes, we'll validate to make sure it's something we + // recognize. + _ba := strings.TrimPrefix(ba.BillingAccountName, "billingAccounts/") + if ba.BillingAccountName == _ba { + return fmt.Errorf("Error parsing billing account for project %q. Expected value to begin with 'billingAccounts/' but got %s", prefixedProject(pid), ba.BillingAccountName) + } + d.Set("billing_account", _ba) + } return nil } +func prefixedProject(pid string) string { + return "projects/" + pid +} func resourceGoogleProjectUpdate(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) pid := d.Id() @@ -224,6 +267,21 @@ func resourceGoogleProjectUpdate(d *schema.ResourceData, meta interface{}) error } } + // Billing account has changed + if ok := d.HasChange("billing_account"); ok { + name := d.Get("billing_account").(string) + ba := cloudbilling.ProjectBillingInfo{ + BillingAccountName: "billingAccounts/" + name, + } + _, err = config.clientBilling.Projects.UpdateBillingInfo(prefixedProject(pid), &ba).Do() + if err != nil { + d.Set("billing_account", "") + if _err, ok := err.(*googleapi.Error); ok { + return fmt.Errorf("Error updating billing account %q for project %q: %v", name, prefixedProject(pid), _err) + } + return fmt.Errorf("Error updating billing account %q for project %q: %v", name, prefixedProject(pid), err) + } + } return updateProjectIamPolicy(d, config, pid) } diff --git a/resource_google_project_iam_policy_test.go b/resource_google_project_iam_policy_test.go index 57e9a296..59903ca8 100644 --- a/resource_google_project_iam_policy_test.go +++ b/resource_google_project_iam_policy_test.go @@ -624,3 +624,13 @@ resource "google_project" "acceptance" { org_id = "%s" }`, pid, name, org) } + +func testAccGoogleProject_createBilling(pid, name, org, billing string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" + billing_account = "%s" +}`, pid, name, org, billing) +} diff --git a/resource_google_project_test.go b/resource_google_project_test.go index 03bdeee0..8381cb33 100644 --- a/resource_google_project_test.go +++ b/resource_google_project_test.go @@ -3,6 +3,7 @@ package google import ( "fmt" "os" + "strings" "testing" "github.com/hashicorp/terraform/helper/acctest" @@ -48,6 +49,76 @@ func TestAccGoogleProject_create(t *testing.T) { }) } +// Test that a Project resource can be created with an associated +// billing account +func TestAccGoogleProject_createBilling(t *testing.T) { + skipIfEnvNotSet(t, + []string{ + "GOOGLE_ORG", + "GOOGLE_BILLING_ACCOUNT", + }..., + ) + + billingId := os.Getenv("GOOGLE_BILLING_ACCOUNT") + pid := "terraform-" + acctest.RandString(10) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // This step creates a new project with a billing account + resource.TestStep{ + Config: testAccGoogleProject_createBilling(pid, pname, org, billingId), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectHasBillingAccount("google_project.acceptance", pid, billingId), + ), + }, + }, + }) +} + +// Test that a Project resource can be created and updated +// with billing account information +func TestAccGoogleProject_updateBilling(t *testing.T) { + skipIfEnvNotSet(t, + []string{ + "GOOGLE_ORG", + "GOOGLE_BILLING_ACCOUNT", + "GOOGLE_BILLING_ACCOUNT_2", + }..., + ) + + billingId := os.Getenv("GOOGLE_BILLING_ACCOUNT") + billingId2 := os.Getenv("GOOGLE_BILLING_ACCOUNT_2") + pid := "terraform-" + acctest.RandString(10) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // This step creates a new project without a billing account + resource.TestStep{ + Config: testAccGoogleProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectExists("google_project.acceptance", pid), + ), + }, + // Update to include a billing account + resource.TestStep{ + Config: testAccGoogleProject_createBilling(pid, pname, org, billingId), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectHasBillingAccount("google_project.acceptance", pid, billingId), + ), + }, + // Update to a different billing account + resource.TestStep{ + Config: testAccGoogleProject_createBilling(pid, pname, org, billingId2), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectHasBillingAccount("google_project.acceptance", pid, billingId2), + ), + }, + }, + }) +} + // Test that a Project resource merges the IAM policies that already // exist, and won't lock people out. func TestAccGoogleProject_merge(t *testing.T) { @@ -95,6 +166,32 @@ func testAccCheckGoogleProjectExists(r, pid string) resource.TestCheckFunc { } } +func testAccCheckGoogleProjectHasBillingAccount(r, pid, billingId string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[r] + if !ok { + return fmt.Errorf("Not found: %s", r) + } + + // State should match expected + if rs.Primary.Attributes["billing_account"] != billingId { + return fmt.Errorf("Billing ID in state (%s) does not match expected value (%s)", rs.Primary.Attributes["billing_account"], billingId) + } + + // Actual value in API should match state and expected + // Read the billing account + config := testAccProvider.Meta().(*Config) + ba, err := config.clientBilling.Projects.GetBillingInfo(prefixedProject(pid)).Do() + if err != nil { + return fmt.Errorf("Error reading billing account for project %q: %v", prefixedProject(pid), err) + } + if billingId != strings.TrimPrefix(ba.BillingAccountName, "billingAccounts/") { + return fmt.Errorf("Billing ID returned by API (%s) did not match expected value (%s)", ba.BillingAccountName, billingId) + } + return nil + } +} + func testAccCheckGoogleProjectHasMoreBindingsThan(pid string, count int) resource.TestCheckFunc { return func(s *terraform.State) error { policy, err := getProjectIamPolicy(pid, testAccProvider.Meta().(*Config)) @@ -167,3 +264,11 @@ resource "google_project" "acceptance" { org_id = "%s" }`, pid, name, org) } + +func skipIfEnvNotSet(t *testing.T, envs ...string) { + for _, k := range envs { + if os.Getenv(k) == "" { + t.Skipf("Environment variable %s is not set", k) + } + } +}