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

Set google_service_account IAM-related fields during plan stage #20510

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
3 changes: 3 additions & 0 deletions .changelog/11929.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resourcemanager: made `google_service_account` `email` and `member` fields available during plan
```
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
package resourcemanager

import (
"context"
"fmt"
"log"
"strings"
"time"

Expand Down Expand Up @@ -32,6 +34,7 @@ func ResourceGoogleServiceAccount() *schema.Resource {
},
CustomizeDiff: customdiff.All(
tpgresource.DefaultProviderProject,
resourceServiceAccountCustomDiff,
),
Schema: map[string]*schema.Schema{
"email": {
Expand Down Expand Up @@ -324,3 +327,34 @@ func resourceGoogleServiceAccountImport(d *schema.ResourceData, meta interface{}

return []*schema.ResourceData{d}, nil
}

func ResourceServiceAccountCustomDiffFunc(diff tpgresource.TerraformResourceDiff) error {
if !tpgresource.IsNewResource(diff) && !diff.HasChange("account_id") {
return nil
}

aid := diff.Get("account_id").(string)
proj := diff.Get("project").(string)
if aid == "" || proj == "" {
return nil
}

email := fmt.Sprintf("%s@%s.iam.gserviceaccount.com", aid, proj)
if err := diff.SetNew("email", email); err != nil {
return fmt.Errorf("error setting email: %s", err)
}
if err := diff.SetNew("member", "serviceAccount:"+email); err != nil {
return fmt.Errorf("error setting member: %s", err)
}

return nil
}
func resourceServiceAccountCustomDiff(_ context.Context, diff *schema.ResourceDiff, meta interface{}) error {
if ud := transport_tpg.GetUniverseDomainFromMeta(meta); ud != "googleapis.com" {
log.Printf("[WARN] The UniverseDomain is set to %q. Skipping resourceServiceAccountCustomDiff", ud)
return nil
}

// separate func to allow unit testing
return ResourceServiceAccountCustomDiffFunc(diff)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ package resourcemanager_test

import (
"fmt"
"maps"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
"github.com/hashicorp/terraform-provider-google/google/acctest"
"github.com/hashicorp/terraform-provider-google/google/envvar"
tpgresourcemanager "github.com/hashicorp/terraform-provider-google/google/services/resourcemanager"
"github.com/hashicorp/terraform-provider-google/google/tpgresource"
)

// Test that a service account resource can be created, updated, and destroyed
Expand Down Expand Up @@ -301,3 +305,107 @@ resource "google_service_account" "acceptance" {
}
`, account, name, desc, disabled)
}

func TestResourceServiceAccountCustomDiff(t *testing.T) {
t.Parallel()

accountId := "a" + acctest.RandString(t, 10)
project := envvar.GetTestProjectFromEnv()
if project == "" {
project = "test-project"
}
expectedEmail := fmt.Sprintf("%s@%s.iam.gserviceaccount.com", accountId, project)
expectedMember := "serviceAccount:" + expectedEmail

cases := []struct {
name string
before map[string]interface{}
after map[string]interface{}
wantEmail string
wantMember string
}{
{
name: "normal (new)",
before: map[string]interface{}{},
after: map[string]interface{}{
"account_id": accountId,
"name": "", // Empty name indicates a new resource
"project": project,
},
wantEmail: expectedEmail,
wantMember: expectedMember,
},
{
name: "no change",
before: map[string]interface{}{
"account_id": accountId,
"email": "dontchange",
"member": "dontchange",
"project": project,
},
after: map[string]interface{}{
"account_id": accountId,
"name": "unimportant",
"project": project,
},
wantEmail: "",
wantMember: "",
},
{
name: "recreate (new)",
before: map[string]interface{}{
"account_id": "recreate-account",
"email": "recreate-email",
"member": "recreate-member",
"project": project,
},
after: map[string]interface{}{
"account_id": accountId,
"name": "",
"project": project,
},
wantEmail: expectedEmail,
wantMember: expectedMember,
},
{
name: "missing account_id (new)",
before: map[string]interface{}{},
after: map[string]interface{}{
"account_id": "",
"name": "",
"project": project,
},
wantEmail: "",
wantMember: "",
},
{
name: "missing project (new)",
before: map[string]interface{}{},
after: map[string]interface{}{
"account_id": accountId,
"name": "",
"project": "",
},
wantEmail: "",
wantMember: "",
},
}
for _, tc := range cases {
result := maps.Clone(tc.after)
if tc.wantEmail != "" || tc.wantMember != "" {
result["email"] = tc.wantEmail
result["member"] = tc.wantMember
}
t.Run(tc.name, func(t *testing.T) {
diff := &tpgresource.ResourceDiffMock{
Before: tc.before,
After: tc.after,
Schema: tpgresourcemanager.ResourceGoogleServiceAccount().Schema,
}
tpgresourcemanager.ResourceServiceAccountCustomDiffFunc(diff)
if d := cmp.Diff(result, diff.After); d != "" {
t.Fatalf("got unexpected change: %v expected: %v", diff.After, result)
}
})
}
}
27 changes: 27 additions & 0 deletions google/tpgresource/resource_test_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ type ResourceDiffMock struct {
Before map[string]interface{}
After map[string]interface{}
Cleared map[string]interface{}
Schema map[string]*schema.Schema
IsForceNew bool
}

Expand Down Expand Up @@ -115,6 +116,32 @@ func (d *ResourceDiffMock) ForceNew(key string) error {
return nil
}

func (d *ResourceDiffMock) SetNew(key string, value interface{}) error {
if len(d.Schema) > 0 {
if err := d.checkKey(key, "SetNew"); err != nil {
return err
}
}

d.After[key] = value
return nil
}

func (d *ResourceDiffMock) checkKey(key, caller string) error {
var schema *schema.Schema
s, ok := d.Schema[key]
if ok {
schema = s
}
if schema == nil {
return fmt.Errorf("%s: invalid key: %s", caller, key)
}
if !schema.Computed {
return fmt.Errorf("%s only operates on computed keys - %s is not one", caller, key)
}
return nil
}

// This function isn't a test of transport.go; instead, it is used as an alternative
// to ReplaceVars inside tests.
func ReplaceVarsForTest(config *transport_tpg.Config, rs *terraform.ResourceState, linkTmpl string) (string, error) {
Expand Down
1 change: 1 addition & 0 deletions google/tpgresource/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ type TerraformResourceDiff interface {
GetOk(string) (interface{}, bool)
Clear(string) error
ForceNew(string) error
SetNew(string, interface{}) error
}

// Contains functions that don't really belong anywhere else.
Expand Down
8 changes: 8 additions & 0 deletions google/transport/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2449,3 +2449,11 @@ func GetRegionFromRegionSelfLink(selfLink string) string {
}
return selfLink
}

func GetUniverseDomainFromMeta(meta interface{}) string {
config := meta.(*Config)
if config.UniverseDomain == "" {
return "googleapis.com"
}
return config.UniverseDomain
}