Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

providers/google: google_project supports billing account #11653

Merged
merged 5 commits into from
Feb 20, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions builtin/providers/google/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -31,6 +32,7 @@ type Config struct {
Project string
Region string

clientBilling *cloudbilling.Service
clientCompute *compute.Service
clientContainer *container.Service
clientDns *dns.Service
Expand Down Expand Up @@ -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
}

Expand Down
58 changes: 58 additions & 0 deletions builtin/providers/google/resource_google_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -86,6 +88,10 @@ func resourceGoogleProject() *schema.Resource {
Type: schema.TypeString,
Computed: true,
},
"billing_account": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
},
}
}
Expand Down Expand Up @@ -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)
}

Expand All @@ -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()
Expand All @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like tabs vs spaces happened here

name = "%s"
org_id = "%s"
billing_account = "%s"
}`, pid, name, org, billing)
}
105 changes: 105 additions & 0 deletions builtin/providers/google/resource_google_project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package google
import (
"fmt"
"os"
"strings"
"testing"

"github.com/hashicorp/terraform/helper/acctest"
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)
}
}
}
Loading