Skip to content

Commit 3463a5b

Browse files
fix: google_compute_instance with user-managed service account and empty scopes results in no service account assignment (#10358) (#18521)
[upstream:a00db19f538bab3f8f8d42b35d033a7703e19e18] Signed-off-by: Modular Magician <magic-modules@google.com>
1 parent 1e7fb46 commit 3463a5b

File tree

3 files changed

+191
-1
lines changed

3 files changed

+191
-1
lines changed

.changelog/10358.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:bug
2+
compute: fixed google_compute_instance with `service_account.email` but no `service_account.scopes`
3+
```

google/services/compute/resource_compute_instance.go

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2252,7 +2252,7 @@ func resourceComputeInstanceUpdate(d *schema.ResourceData, meta interface{}) err
22522252
if d.HasChange("service_account.0.email") || scopesChange {
22532253
sa := d.Get("service_account").([]interface{})
22542254
req := &compute.InstancesSetServiceAccountRequest{ForceSendFields: []string{"email"}}
2255-
if len(sa) > 0 && sa[0] != nil {
2255+
if !isEmptyServiceAccountBlock(d) && len(sa) > 0 && sa[0] != nil {
22562256
saMap := sa[0].(map[string]interface{})
22572257
req.Email = saMap["email"].(string)
22582258
req.Scopes = tpgresource.CanonicalizeServiceScopes(tpgresource.ConvertStringSet(saMap["scopes"].(*schema.Set)))
@@ -2862,6 +2862,11 @@ func serviceAccountDiffSuppress(k, old, new string, d *schema.ResourceData) bool
28622862
// suppress changes between { } and {scopes:[]}
28632863
if l[0] != nil {
28642864
contents := l[0].(map[string]interface{})
2865+
email := contents["email"]
2866+
if email != "" {
2867+
// if email is non empty, don't suppress the diff
2868+
return false
2869+
}
28652870
if scopes, ok := contents["scopes"]; ok {
28662871
a := scopes.(*schema.Set).List()
28672872
if a != nil && len(a) > 0 {
@@ -2871,3 +2876,42 @@ func serviceAccountDiffSuppress(k, old, new string, d *schema.ResourceData) bool
28712876
}
28722877
return true
28732878
}
2879+
2880+
// isEmptyServiceAccountBlock is used to work around an issue when updating
2881+
// service accounts. Creating the instance with some scopes but without
2882+
// specifying a service account email, assigns default compute service account
2883+
// to the instance:
2884+
//
2885+
// service_account {
2886+
// scopes = ["some-scope"]
2887+
// }
2888+
//
2889+
// Then when updating the instance with empty service account:
2890+
//
2891+
// service_account {
2892+
// scopes = []
2893+
// }
2894+
//
2895+
// the default Terraform behavior is to clear scopes without clearing the
2896+
// email. The email was previously computed to be the default service account
2897+
// and has not been modified, so the default plan is to leave it unchanged.
2898+
// However, when creating a new instance:
2899+
//
2900+
// service_account {
2901+
// scopes = []
2902+
// }
2903+
//
2904+
// indicates an instance without any service account set.
2905+
// isEmptyServiceAccountBlock is used to detect empty service_account block
2906+
// and if it is, it is interpreted as no service account and no scopes.
2907+
func isEmptyServiceAccountBlock(d *schema.ResourceData) bool {
2908+
serviceAccountsConfig := d.GetRawConfig().GetAttr("service_account")
2909+
if serviceAccountsConfig.IsNull() || len(serviceAccountsConfig.AsValueSlice()) == 0 {
2910+
return true
2911+
}
2912+
serviceAccount := serviceAccountsConfig.AsValueSlice()[0]
2913+
if serviceAccount.GetAttr("email").IsNull() && len(serviceAccount.GetAttr("scopes").AsValueSlice()) == 0 {
2914+
return true
2915+
}
2916+
return false
2917+
}

google/services/compute/resource_compute_instance_test.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,6 +1101,54 @@ func TestAccComputeInstance_serviceAccount(t *testing.T) {
11011101
})
11021102
}
11031103

1104+
func TestAccComputeInstance_noServiceAccount(t *testing.T) {
1105+
t.Parallel()
1106+
1107+
var instance compute.Instance
1108+
var instanceName = fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10))
1109+
1110+
acctest.VcrTest(t, resource.TestCase{
1111+
PreCheck: func() { acctest.AccTestPreCheck(t) },
1112+
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
1113+
CheckDestroy: testAccCheckComputeInstanceDestroyProducer(t),
1114+
Steps: []resource.TestStep{
1115+
{
1116+
Config: testAccComputeInstance_noServiceAccount(instanceName),
1117+
Check: resource.ComposeTestCheckFunc(
1118+
testAccCheckComputeInstanceExists(
1119+
t, "google_compute_instance.foobar", &instance),
1120+
testAccCheckComputeInstanceNoServiceAccount(&instance),
1121+
),
1122+
},
1123+
computeInstanceImportStep("us-central1-a", instanceName, []string{}),
1124+
},
1125+
})
1126+
}
1127+
1128+
func TestAccComputeInstance_serviceAccountEmail_0scopes(t *testing.T) {
1129+
t.Parallel()
1130+
1131+
var instance compute.Instance
1132+
var instanceName = fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10))
1133+
1134+
acctest.VcrTest(t, resource.TestCase{
1135+
PreCheck: func() { acctest.AccTestPreCheck(t) },
1136+
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
1137+
CheckDestroy: testAccCheckComputeInstanceDestroyProducer(t),
1138+
Steps: []resource.TestStep{
1139+
{
1140+
Config: testAccComputeInstance_serviceAccountEmail_0scopes(instanceName),
1141+
Check: resource.ComposeTestCheckFunc(
1142+
testAccCheckComputeInstanceExists(
1143+
t, "google_compute_instance.foobar", &instance),
1144+
testAccCheckComputeInstanceMatchServiceAccount(&instance, "\\d+-compute@developer.gserviceaccount.com"),
1145+
),
1146+
},
1147+
computeInstanceImportStep("us-central1-a", instanceName, []string{}),
1148+
},
1149+
})
1150+
}
1151+
11041152
func TestAccComputeInstance_serviceAccount_updated(t *testing.T) {
11051153
t.Parallel()
11061154

@@ -1117,6 +1165,7 @@ func TestAccComputeInstance_serviceAccount_updated(t *testing.T) {
11171165
Check: resource.ComposeTestCheckFunc(
11181166
testAccCheckComputeInstanceExists(
11191167
t, "google_compute_instance.foobar", &instance),
1168+
testAccCheckComputeInstanceNoServiceAccount(&instance),
11201169
testAccCheckComputeInstanceScopes(&instance, 0),
11211170
),
11221171
},
@@ -1126,6 +1175,7 @@ func TestAccComputeInstance_serviceAccount_updated(t *testing.T) {
11261175
Check: resource.ComposeTestCheckFunc(
11271176
testAccCheckComputeInstanceExists(
11281177
t, "google_compute_instance.foobar", &instance),
1178+
testAccCheckComputeInstanceNoServiceAccount(&instance),
11291179
testAccCheckComputeInstanceScopes(&instance, 0),
11301180
),
11311181
},
@@ -1135,6 +1185,7 @@ func TestAccComputeInstance_serviceAccount_updated(t *testing.T) {
11351185
Check: resource.ComposeTestCheckFunc(
11361186
testAccCheckComputeInstanceExists(
11371187
t, "google_compute_instance.foobar", &instance),
1188+
testAccCheckComputeInstanceMatchServiceAccount(&instance, "\\d+-compute@developer.gserviceaccount.com"),
11381189
testAccCheckComputeInstanceScopes(&instance, 0),
11391190
),
11401191
},
@@ -1144,6 +1195,7 @@ func TestAccComputeInstance_serviceAccount_updated(t *testing.T) {
11441195
Check: resource.ComposeTestCheckFunc(
11451196
testAccCheckComputeInstanceExists(
11461197
t, "google_compute_instance.foobar", &instance),
1198+
testAccCheckComputeInstanceMatchServiceAccount(&instance, "\\d+-compute@developer.gserviceaccount.com"),
11471199
testAccCheckComputeInstanceScopes(&instance, 3),
11481200
),
11491201
},
@@ -1168,6 +1220,7 @@ func TestAccComputeInstance_serviceAccount_updated0to1to0scopes(t *testing.T) {
11681220
Check: resource.ComposeTestCheckFunc(
11691221
testAccCheckComputeInstanceExists(
11701222
t, "google_compute_instance.foobar", &instance),
1223+
testAccCheckComputeInstanceNoServiceAccount(&instance),
11711224
testAccCheckComputeInstanceScopes(&instance, 0),
11721225
),
11731226
},
@@ -1177,6 +1230,7 @@ func TestAccComputeInstance_serviceAccount_updated0to1to0scopes(t *testing.T) {
11771230
Check: resource.ComposeTestCheckFunc(
11781231
testAccCheckComputeInstanceExists(
11791232
t, "google_compute_instance.foobar", &instance),
1233+
testAccCheckComputeInstanceMatchServiceAccount(&instance, "\\d+-compute@developer.gserviceaccount.com"),
11801234
testAccCheckComputeInstanceScopes(&instance, 1),
11811235
),
11821236
},
@@ -1186,6 +1240,7 @@ func TestAccComputeInstance_serviceAccount_updated0to1to0scopes(t *testing.T) {
11861240
Check: resource.ComposeTestCheckFunc(
11871241
testAccCheckComputeInstanceExists(
11881242
t, "google_compute_instance.foobar", &instance),
1243+
testAccCheckComputeInstanceNoServiceAccount(&instance),
11891244
testAccCheckComputeInstanceScopes(&instance, 0),
11901245
),
11911246
},
@@ -3306,6 +3361,30 @@ func testAccCheckComputeInstanceServiceAccount(instance *compute.Instance, scope
33063361
}
33073362
}
33083363

3364+
func testAccCheckComputeInstanceNoServiceAccount(instance *compute.Instance) resource.TestCheckFunc {
3365+
return func(s *terraform.State) error {
3366+
if count := len(instance.ServiceAccounts); count != 0 {
3367+
return fmt.Errorf("Wrong number of ServiceAccounts: expected 0, got %d", count)
3368+
}
3369+
return nil
3370+
}
3371+
}
3372+
3373+
func testAccCheckComputeInstanceMatchServiceAccount(instance *compute.Instance, serviceAcctRegexp string) resource.TestCheckFunc {
3374+
return func(s *terraform.State) error {
3375+
if count := len(instance.ServiceAccounts); count != 1 {
3376+
return fmt.Errorf("Wrong number of ServiceAccounts: expected 1, got %d", count)
3377+
}
3378+
3379+
email := instance.ServiceAccounts[0].Email
3380+
if !regexp.MustCompile(serviceAcctRegexp).MatchString(email) {
3381+
return fmt.Errorf("ServiceAccount email didn't match:\"%s\", got \"%s\"", serviceAcctRegexp, email)
3382+
}
3383+
3384+
return nil
3385+
}
3386+
}
3387+
33093388
func testAccCheckComputeInstanceScopes(instance *compute.Instance, scopeCount int) resource.TestCheckFunc {
33103389
return func(s *terraform.State) error {
33113390

@@ -5277,6 +5356,70 @@ resource "google_compute_instance" "foobar" {
52775356
`, instance)
52785357
}
52795358

5359+
func testAccComputeInstance_noServiceAccount(instance string) string {
5360+
return fmt.Sprintf(`
5361+
data "google_compute_image" "my_image" {
5362+
family = "debian-11"
5363+
project = "debian-cloud"
5364+
}
5365+
5366+
resource "google_compute_instance" "foobar" {
5367+
name = "%s"
5368+
machine_type = "e2-medium"
5369+
zone = "us-central1-a"
5370+
5371+
boot_disk {
5372+
initialize_params {
5373+
image = data.google_compute_image.my_image.self_link
5374+
}
5375+
}
5376+
5377+
network_interface {
5378+
network = "default"
5379+
}
5380+
5381+
service_account {
5382+
scopes = []
5383+
}
5384+
}
5385+
`, instance)
5386+
}
5387+
5388+
func testAccComputeInstance_serviceAccountEmail_0scopes(instance string) string {
5389+
return fmt.Sprintf(`
5390+
data "google_project" "project" {}
5391+
5392+
data "google_compute_image" "my_image" {
5393+
family = "debian-11"
5394+
project = "debian-cloud"
5395+
}
5396+
5397+
resource "google_compute_instance" "foobar" {
5398+
name = "%s"
5399+
machine_type = "e2-medium"
5400+
zone = "us-central1-a"
5401+
5402+
boot_disk {
5403+
initialize_params {
5404+
image = data.google_compute_image.my_image.self_link
5405+
}
5406+
}
5407+
5408+
network_interface {
5409+
network = "default"
5410+
}
5411+
5412+
service_account {
5413+
email = data.google_compute_default_service_account.default.email
5414+
scopes = []
5415+
}
5416+
}
5417+
5418+
data "google_compute_default_service_account" "default" {
5419+
}
5420+
`, instance)
5421+
}
5422+
52805423
func testAccComputeInstance_serviceAccount_update0(instance string) string {
52815424
return fmt.Sprintf(`
52825425
data "google_compute_image" "my_image" {

0 commit comments

Comments
 (0)