Skip to content

Commit 7d6a083

Browse files
Set google_service_account IAM-related fields during plan stage (#11929) (#20510)
[upstream:1e21e33c6db05b12a0fe0f44e92bea2e3c9a998f] Signed-off-by: Modular Magician <[email protected]>
1 parent 00a1eb3 commit 7d6a083

File tree

6 files changed

+181
-0
lines changed

6 files changed

+181
-0
lines changed

.changelog/11929.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:enhancement
2+
resourcemanager: made `google_service_account` `email` and `member` fields available during plan
3+
```

google/services/resourcemanager/resource_google_service_account.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
package resourcemanager
44

55
import (
6+
"context"
67
"fmt"
8+
"log"
79
"strings"
810
"time"
911

@@ -32,6 +34,7 @@ func ResourceGoogleServiceAccount() *schema.Resource {
3234
},
3335
CustomizeDiff: customdiff.All(
3436
tpgresource.DefaultProviderProject,
37+
resourceServiceAccountCustomDiff,
3538
),
3639
Schema: map[string]*schema.Schema{
3740
"email": {
@@ -324,3 +327,34 @@ func resourceGoogleServiceAccountImport(d *schema.ResourceData, meta interface{}
324327

325328
return []*schema.ResourceData{d}, nil
326329
}
330+
331+
func ResourceServiceAccountCustomDiffFunc(diff tpgresource.TerraformResourceDiff) error {
332+
if !tpgresource.IsNewResource(diff) && !diff.HasChange("account_id") {
333+
return nil
334+
}
335+
336+
aid := diff.Get("account_id").(string)
337+
proj := diff.Get("project").(string)
338+
if aid == "" || proj == "" {
339+
return nil
340+
}
341+
342+
email := fmt.Sprintf("%s@%s.iam.gserviceaccount.com", aid, proj)
343+
if err := diff.SetNew("email", email); err != nil {
344+
return fmt.Errorf("error setting email: %s", err)
345+
}
346+
if err := diff.SetNew("member", "serviceAccount:"+email); err != nil {
347+
return fmt.Errorf("error setting member: %s", err)
348+
}
349+
350+
return nil
351+
}
352+
func resourceServiceAccountCustomDiff(_ context.Context, diff *schema.ResourceDiff, meta interface{}) error {
353+
if ud := transport_tpg.GetUniverseDomainFromMeta(meta); ud != "googleapis.com" {
354+
log.Printf("[WARN] The UniverseDomain is set to %q. Skipping resourceServiceAccountCustomDiff", ud)
355+
return nil
356+
}
357+
358+
// separate func to allow unit testing
359+
return ResourceServiceAccountCustomDiffFunc(diff)
360+
}

google/services/resourcemanager/resource_google_service_account_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@ package resourcemanager_test
44

55
import (
66
"fmt"
7+
"maps"
78
"testing"
89

10+
"github.com/google/go-cmp/cmp"
911
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
1012
"github.com/hashicorp/terraform-plugin-testing/terraform"
1113
"github.com/hashicorp/terraform-provider-google/google/acctest"
1214
"github.com/hashicorp/terraform-provider-google/google/envvar"
15+
tpgresourcemanager "github.com/hashicorp/terraform-provider-google/google/services/resourcemanager"
16+
"github.com/hashicorp/terraform-provider-google/google/tpgresource"
1317
)
1418

1519
// Test that a service account resource can be created, updated, and destroyed
@@ -301,3 +305,107 @@ resource "google_service_account" "acceptance" {
301305
}
302306
`, account, name, desc, disabled)
303307
}
308+
309+
func TestResourceServiceAccountCustomDiff(t *testing.T) {
310+
t.Parallel()
311+
312+
accountId := "a" + acctest.RandString(t, 10)
313+
project := envvar.GetTestProjectFromEnv()
314+
if project == "" {
315+
project = "test-project"
316+
}
317+
expectedEmail := fmt.Sprintf("%s@%s.iam.gserviceaccount.com", accountId, project)
318+
expectedMember := "serviceAccount:" + expectedEmail
319+
320+
cases := []struct {
321+
name string
322+
before map[string]interface{}
323+
after map[string]interface{}
324+
wantEmail string
325+
wantMember string
326+
}{
327+
{
328+
name: "normal (new)",
329+
before: map[string]interface{}{},
330+
after: map[string]interface{}{
331+
"account_id": accountId,
332+
"name": "", // Empty name indicates a new resource
333+
"project": project,
334+
},
335+
wantEmail: expectedEmail,
336+
wantMember: expectedMember,
337+
},
338+
{
339+
name: "no change",
340+
before: map[string]interface{}{
341+
"account_id": accountId,
342+
"email": "dontchange",
343+
"member": "dontchange",
344+
"project": project,
345+
},
346+
after: map[string]interface{}{
347+
"account_id": accountId,
348+
"name": "unimportant",
349+
"project": project,
350+
},
351+
wantEmail: "",
352+
wantMember: "",
353+
},
354+
{
355+
name: "recreate (new)",
356+
before: map[string]interface{}{
357+
"account_id": "recreate-account",
358+
"email": "recreate-email",
359+
"member": "recreate-member",
360+
"project": project,
361+
},
362+
after: map[string]interface{}{
363+
"account_id": accountId,
364+
"name": "",
365+
"project": project,
366+
},
367+
wantEmail: expectedEmail,
368+
wantMember: expectedMember,
369+
},
370+
{
371+
name: "missing account_id (new)",
372+
before: map[string]interface{}{},
373+
after: map[string]interface{}{
374+
"account_id": "",
375+
"name": "",
376+
"project": project,
377+
},
378+
wantEmail: "",
379+
wantMember: "",
380+
},
381+
{
382+
name: "missing project (new)",
383+
before: map[string]interface{}{},
384+
after: map[string]interface{}{
385+
"account_id": accountId,
386+
"name": "",
387+
"project": "",
388+
},
389+
wantEmail: "",
390+
wantMember: "",
391+
},
392+
}
393+
for _, tc := range cases {
394+
result := maps.Clone(tc.after)
395+
if tc.wantEmail != "" || tc.wantMember != "" {
396+
result["email"] = tc.wantEmail
397+
result["member"] = tc.wantMember
398+
}
399+
t.Run(tc.name, func(t *testing.T) {
400+
diff := &tpgresource.ResourceDiffMock{
401+
Before: tc.before,
402+
After: tc.after,
403+
Schema: tpgresourcemanager.ResourceGoogleServiceAccount().Schema,
404+
}
405+
tpgresourcemanager.ResourceServiceAccountCustomDiffFunc(diff)
406+
if d := cmp.Diff(result, diff.After); d != "" {
407+
t.Fatalf("got unexpected change: %v expected: %v", diff.After, result)
408+
}
409+
})
410+
}
411+
}

google/tpgresource/resource_test_utils.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ type ResourceDiffMock struct {
8181
Before map[string]interface{}
8282
After map[string]interface{}
8383
Cleared map[string]interface{}
84+
Schema map[string]*schema.Schema
8485
IsForceNew bool
8586
}
8687

@@ -115,6 +116,32 @@ func (d *ResourceDiffMock) ForceNew(key string) error {
115116
return nil
116117
}
117118

119+
func (d *ResourceDiffMock) SetNew(key string, value interface{}) error {
120+
if len(d.Schema) > 0 {
121+
if err := d.checkKey(key, "SetNew"); err != nil {
122+
return err
123+
}
124+
}
125+
126+
d.After[key] = value
127+
return nil
128+
}
129+
130+
func (d *ResourceDiffMock) checkKey(key, caller string) error {
131+
var schema *schema.Schema
132+
s, ok := d.Schema[key]
133+
if ok {
134+
schema = s
135+
}
136+
if schema == nil {
137+
return fmt.Errorf("%s: invalid key: %s", caller, key)
138+
}
139+
if !schema.Computed {
140+
return fmt.Errorf("%s only operates on computed keys - %s is not one", caller, key)
141+
}
142+
return nil
143+
}
144+
118145
// This function isn't a test of transport.go; instead, it is used as an alternative
119146
// to ReplaceVars inside tests.
120147
func ReplaceVarsForTest(config *transport_tpg.Config, rs *terraform.ResourceState, linkTmpl string) (string, error) {

google/tpgresource/utils.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ type TerraformResourceDiff interface {
5656
GetOk(string) (interface{}, bool)
5757
Clear(string) error
5858
ForceNew(string) error
59+
SetNew(string, interface{}) error
5960
}
6061

6162
// Contains functions that don't really belong anywhere else.

google/transport/config.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2449,3 +2449,11 @@ func GetRegionFromRegionSelfLink(selfLink string) string {
24492449
}
24502450
return selfLink
24512451
}
2452+
2453+
func GetUniverseDomainFromMeta(meta interface{}) string {
2454+
config := meta.(*Config)
2455+
if config.UniverseDomain == "" {
2456+
return "googleapis.com"
2457+
}
2458+
return config.UniverseDomain
2459+
}

0 commit comments

Comments
 (0)