Skip to content

Commit 14d0e52

Browse files
modular-magiciannat-henderson
authored andcommitted
Add billing account IAM (#2413)
1 parent d6d8085 commit 14d0e52

7 files changed

+455
-0
lines changed

google/iam_billing_account.go

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package google
2+
3+
import (
4+
"fmt"
5+
"github.com/hashicorp/errwrap"
6+
"github.com/hashicorp/terraform/helper/schema"
7+
"google.golang.org/api/cloudbilling/v1"
8+
"google.golang.org/api/cloudresourcemanager/v1"
9+
)
10+
11+
var IamBillingAccountSchema = map[string]*schema.Schema{
12+
"billing_account_id": {
13+
Type: schema.TypeString,
14+
Required: true,
15+
ForceNew: true,
16+
},
17+
}
18+
19+
type BillingAccountIamUpdater struct {
20+
billingAccountId string
21+
Config *Config
22+
}
23+
24+
func NewBillingAccountIamUpdater(d *schema.ResourceData, config *Config) (ResourceIamUpdater, error) {
25+
return &BillingAccountIamUpdater{
26+
billingAccountId: canonicalBillingAccountId(d.Get("billing_account_id").(string)),
27+
Config: config,
28+
}, nil
29+
}
30+
31+
func BillingAccountIdParseFunc(d *schema.ResourceData, _ *Config) error {
32+
d.Set("billing_account_id", d.Id())
33+
return nil
34+
}
35+
36+
func (u *BillingAccountIamUpdater) GetResourceIamPolicy() (*cloudresourcemanager.Policy, error) {
37+
return getBillingAccountIamPolicyByBillingAccountName(u.billingAccountId, u.Config)
38+
}
39+
40+
func (u *BillingAccountIamUpdater) SetResourceIamPolicy(policy *cloudresourcemanager.Policy) error {
41+
billingPolicy, err := resourceManagerToBillingPolicy(policy)
42+
if err != nil {
43+
return err
44+
}
45+
46+
_, err = u.Config.clientBilling.BillingAccounts.SetIamPolicy("billingAccounts/"+u.billingAccountId, &cloudbilling.SetIamPolicyRequest{
47+
Policy: billingPolicy,
48+
}).Do()
49+
50+
if err != nil {
51+
return errwrap.Wrapf(fmt.Sprintf("Error setting IAM policy for %s: {{err}}", u.DescribeResource()), err)
52+
}
53+
54+
return nil
55+
}
56+
57+
func (u *BillingAccountIamUpdater) GetResourceId() string {
58+
return u.billingAccountId
59+
}
60+
61+
func (u *BillingAccountIamUpdater) GetMutexKey() string {
62+
return fmt.Sprintf("iam-billing-account-%s", u.billingAccountId)
63+
}
64+
65+
func (u *BillingAccountIamUpdater) DescribeResource() string {
66+
return fmt.Sprintf("billingAccount %q", u.billingAccountId)
67+
}
68+
69+
func canonicalBillingAccountId(resource string) string {
70+
return resource
71+
}
72+
73+
func resourceManagerToBillingPolicy(p *cloudresourcemanager.Policy) (*cloudbilling.Policy, error) {
74+
out := &cloudbilling.Policy{}
75+
err := Convert(p, out)
76+
if err != nil {
77+
return nil, errwrap.Wrapf("Cannot convert a v1 policy to a billing policy: {{err}}", err)
78+
}
79+
return out, nil
80+
}
81+
82+
func billingToResourceManagerPolicy(p *cloudbilling.Policy) (*cloudresourcemanager.Policy, error) {
83+
out := &cloudresourcemanager.Policy{}
84+
err := Convert(p, out)
85+
if err != nil {
86+
return nil, errwrap.Wrapf("Cannot convert a billing policy to a v1 policy: {{err}}", err)
87+
}
88+
return out, nil
89+
}
90+
91+
// Retrieve the existing IAM Policy for a billing account
92+
func getBillingAccountIamPolicyByBillingAccountName(resource string, config *Config) (*cloudresourcemanager.Policy, error) {
93+
p, err := config.clientBilling.BillingAccounts.GetIamPolicy("billingAccounts/" + resource).Do()
94+
95+
if err != nil {
96+
return nil, errwrap.Wrapf(fmt.Sprintf("Error retrieving IAM policy for billing account %q: {{err}}", resource), err)
97+
}
98+
99+
v1Policy, err := billingToResourceManagerPolicy(p)
100+
if err != nil {
101+
return nil, err
102+
}
103+
104+
return v1Policy, nil
105+
}

google/provider.go

+3
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ func Provider() terraform.ResourceProvider {
114114
"google_bigquery_table": resourceBigQueryTable(),
115115
"google_bigtable_instance": resourceBigtableInstance(),
116116
"google_bigtable_table": resourceBigtableTable(),
117+
"google_billing_account_iam_binding": ResourceIamBindingWithImport(IamBillingAccountSchema, NewBillingAccountIamUpdater, BillingAccountIdParseFunc),
118+
"google_billing_account_iam_member": ResourceIamMemberWithImport(IamBillingAccountSchema, NewBillingAccountIamUpdater, BillingAccountIdParseFunc),
119+
"google_billing_account_iam_policy": ResourceIamPolicyWithImport(IamBillingAccountSchema, NewBillingAccountIamUpdater, BillingAccountIdParseFunc),
117120
"google_cloudbuild_trigger": resourceCloudBuildTrigger(),
118121
"google_cloudfunctions_function": resourceCloudFunctionsFunction(),
119122
"google_cloudiot_registry": resourceCloudIoTRegistry(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package google
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
"sort"
7+
"testing"
8+
9+
"github.com/hashicorp/terraform/helper/acctest"
10+
"github.com/hashicorp/terraform/helper/resource"
11+
"github.com/hashicorp/terraform/terraform"
12+
)
13+
14+
func TestAccBillingAccountIam(t *testing.T) {
15+
t.Parallel()
16+
17+
billing := getTestBillingAccountFromEnv(t)
18+
account := acctest.RandomWithPrefix("tf-test")
19+
role := "roles/billing.viewer"
20+
resource.Test(t, resource.TestCase{
21+
PreCheck: func() { testAccPreCheck(t) },
22+
Providers: testAccProviders,
23+
Steps: []resource.TestStep{
24+
{
25+
// Test Iam Binding creation
26+
Config: testAccBillingAccountIamBinding_basic(account, billing, role),
27+
Check: testAccCheckGoogleBillingAccountIamBindingExists("foo", role, []string{
28+
fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", account, getTestProjectFromEnv()),
29+
}),
30+
},
31+
{
32+
ResourceName: "google_billing_account_iam_binding.foo",
33+
ImportStateId: fmt.Sprintf("%s roles/billing.viewer", billing),
34+
ImportState: true,
35+
ImportStateVerify: true,
36+
},
37+
{
38+
// Test Iam Binding update
39+
Config: testAccBillingAccountIamBinding_update(account, billing, role),
40+
Check: testAccCheckGoogleBillingAccountIamBindingExists("foo", role, []string{
41+
fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", account, getTestProjectFromEnv()),
42+
fmt.Sprintf("serviceAccount:%s-2@%s.iam.gserviceaccount.com", account, getTestProjectFromEnv()),
43+
}),
44+
},
45+
{
46+
ResourceName: "google_billing_account_iam_binding.foo",
47+
ImportStateId: fmt.Sprintf("%s roles/billing.viewer", billing),
48+
ImportState: true,
49+
ImportStateVerify: true,
50+
},
51+
{
52+
// Test Iam Member creation (no update for member, no need to test)
53+
Config: testAccBillingAccountIamMember_basic(account, billing, role),
54+
Check: testAccCheckGoogleBillingAccountIamMemberExists("foo", "roles/billing.viewer",
55+
fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", account, getTestProjectFromEnv()),
56+
),
57+
},
58+
{
59+
ResourceName: "google_billing_account_iam_member.foo",
60+
ImportStateId: fmt.Sprintf("%s roles/billing.viewer serviceAccount:%s@%s.iam.gserviceaccount.com", billing, account, getTestProjectFromEnv()),
61+
ImportState: true,
62+
ImportStateVerify: true,
63+
},
64+
},
65+
})
66+
}
67+
68+
func testAccCheckGoogleBillingAccountIamBindingExists(bindingResourceName, role string, members []string) resource.TestCheckFunc {
69+
return func(s *terraform.State) error {
70+
bindingRs, ok := s.RootModule().Resources["google_billing_account_iam_binding."+bindingResourceName]
71+
if !ok {
72+
return fmt.Errorf("Not found: %s", bindingResourceName)
73+
}
74+
75+
config := testAccProvider.Meta().(*Config)
76+
p, err := config.clientBilling.BillingAccounts.GetIamPolicy("billingAccounts/" + bindingRs.Primary.Attributes["billing_account_id"]).Do()
77+
if err != nil {
78+
return err
79+
}
80+
81+
for _, binding := range p.Bindings {
82+
if binding.Role == role {
83+
sort.Strings(members)
84+
sort.Strings(binding.Members)
85+
86+
if reflect.DeepEqual(members, binding.Members) {
87+
return nil
88+
}
89+
90+
return fmt.Errorf("Binding found but expected members is %v, got %v", members, binding.Members)
91+
}
92+
}
93+
94+
return fmt.Errorf("No binding for role %q", role)
95+
}
96+
}
97+
98+
func testAccCheckGoogleBillingAccountIamMemberExists(n, role, member string) resource.TestCheckFunc {
99+
return func(s *terraform.State) error {
100+
rs, ok := s.RootModule().Resources["google_billing_account_iam_member."+n]
101+
if !ok {
102+
return fmt.Errorf("Not found: %s", n)
103+
}
104+
105+
config := testAccProvider.Meta().(*Config)
106+
p, err := config.clientBilling.BillingAccounts.GetIamPolicy("billingAccounts/" + rs.Primary.Attributes["billing_account_id"]).Do()
107+
if err != nil {
108+
return err
109+
}
110+
111+
for _, binding := range p.Bindings {
112+
if binding.Role == role {
113+
for _, m := range binding.Members {
114+
if m == member {
115+
return nil
116+
}
117+
}
118+
119+
return fmt.Errorf("Missing member %q, got %v", member, binding.Members)
120+
}
121+
}
122+
123+
return fmt.Errorf("No binding for role %q", role)
124+
}
125+
}
126+
127+
func testAccBillingAccountIamBinding_basic(account, billingAccountId, role string) string {
128+
return fmt.Sprintf(`
129+
resource "google_service_account" "test-account" {
130+
account_id = "%s"
131+
display_name = "Iam Testing Account"
132+
}
133+
134+
resource "google_billing_account_iam_binding" "foo" {
135+
billing_account_id = "%s"
136+
role = "%s"
137+
members = ["serviceAccount:${google_service_account.test-account.email}"]
138+
}
139+
`, account, billingAccountId, role)
140+
}
141+
142+
func testAccBillingAccountIamBinding_update(account, billingAccountId, role string) string {
143+
return fmt.Sprintf(`
144+
resource "google_service_account" "test-account" {
145+
account_id = "%s"
146+
display_name = "Iam Testing Account"
147+
}
148+
149+
resource "google_service_account" "test-account-2" {
150+
account_id = "%s-2"
151+
display_name = "Iam Testing Account"
152+
}
153+
154+
resource "google_billing_account_iam_binding" "foo" {
155+
billing_account_id = "%s"
156+
role = "%s"
157+
members = [
158+
"serviceAccount:${google_service_account.test-account.email}",
159+
"serviceAccount:${google_service_account.test-account-2.email}"
160+
]
161+
}
162+
`, account, account, billingAccountId, role)
163+
}
164+
165+
func testAccBillingAccountIamMember_basic(account, billingAccountId, role string) string {
166+
return fmt.Sprintf(`
167+
resource "google_service_account" "test-account" {
168+
account_id = "%s"
169+
display_name = "Iam Testing Account"
170+
}
171+
172+
resource "google_billing_account_iam_member" "foo" {
173+
billing_account_id = "%s"
174+
role = "%s"
175+
member = "serviceAccount:${google_service_account.test-account.email}"
176+
}
177+
`, account, billingAccountId, role)
178+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
layout: "google"
3+
page_title: "Google: google_billing_account_iam_binding"
4+
sidebar_current: "docs-google-billing-account-iam-binding"
5+
description: |-
6+
Allows management of a single binding with an IAM policy for a Google Cloud Platform Billing Account.
7+
---
8+
9+
# google\_billing_account_\_iam\_binding
10+
11+
Allows creation and management of a single binding within IAM policy for
12+
an existing Google Cloud Platform Billing Account.
13+
14+
~> **Note:** This resource __must not__ be used in conjunction with
15+
`google_billing_account_iam_member` for the __same role__ or they will fight over
16+
what your policy should be.
17+
18+
## Example Usage
19+
20+
```hcl
21+
resource "google_billing_account_iam_binding" "binding" {
22+
billing_account_id = "00AA00-000AAA-00AA0A"
23+
role = "roles/billing.viewer"
24+
25+
members = [
26+
27+
]
28+
}
29+
```
30+
31+
## Argument Reference
32+
33+
The following arguments are supported:
34+
35+
* `billing_account_id` - (Required) The billing account id.
36+
37+
* `role` - (Required) The role that should be applied.
38+
39+
* `members` - (Required) A list of users that the role should apply to.
40+
41+
## Attributes Reference
42+
43+
In addition to the arguments listed above, the following computed attributes are
44+
exported:
45+
46+
* `etag` - (Computed) The etag of the billing account's IAM policy.
47+
48+
## Import
49+
50+
IAM binding imports use space-delimited identifiers; first the resource in question and then the role. These bindings can be imported using the `billing_account_id` and role, e.g.
51+
52+
```
53+
$ terraform import google_billing_account_iam_binding.binding "your-billing-account-id roles/viewer"
54+
```

0 commit comments

Comments
 (0)