Skip to content

Commit 4748c4f

Browse files
authored
Merge pull request #44375 from hashicorp/f-resource-identity-intercept-fix
Update identity interceptor
2 parents fa67f60 + 6f25ce2 commit 4748c4f

File tree

9 files changed

+291
-17
lines changed

9 files changed

+291
-17
lines changed

.changelog/44375.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
```release-note:note
2+
provider: This release contains both internal provider fixes and a Terraform Plugin SDK V2 update related to a [regression](https://github.com/hashicorp/terraform-provider-aws/issues/44366) which may impact resources that support resource identity
3+
```
4+
5+
```release-note:bug
6+
provider: Fix `Missing Resource Identity After Update` errors for non-refreshed and failed updates
7+
```
8+
```release-note:bug
9+
provider: Fix `Unexpected Identity Change` errors when fully-null identity values in state are updated to valid values
10+
```

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ require (
299299
github.com/hashicorp/terraform-plugin-go v0.29.0
300300
github.com/hashicorp/terraform-plugin-log v0.9.0
301301
github.com/hashicorp/terraform-plugin-mux v0.21.0
302-
github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.0
302+
github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1
303303
github.com/hashicorp/terraform-plugin-testing v1.14.0-beta.1
304304
github.com/jaswdr/faker/v2 v2.8.0
305305
github.com/jmespath/go-jmespath v0.4.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -683,8 +683,8 @@ github.com/hashicorp/terraform-plugin-go v0.29.0 h1:1nXKl/nSpaYIUBU1IG/EsDOX0vv+
683683
github.com/hashicorp/terraform-plugin-go v0.29.0/go.mod h1:vYZbIyvxyy0FWSmDHChCqKvI40cFTDGSb3D8D70i9GM=
684684
github.com/hashicorp/terraform-plugin-mux v0.21.0 h1:QsEYnzSD2c3zT8zUrUGqaFGhV/Z8zRUlU7FY3ZPJFfw=
685685
github.com/hashicorp/terraform-plugin-mux v0.21.0/go.mod h1:Qpt8+6AD7NmL0DS7ASkN0EXpDQ2J/FnnIgeUr1tzr5A=
686-
github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.0 h1:PQP7Crrc7t/ozj+P9x0/lsTzGNy3lVppH8zAJylofaE=
687-
github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.0/go.mod h1:GQhpKVvvuwzD79e8/NZ+xzj+ZpWovdPAe8nfV/skwNU=
686+
github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1 h1:mlAq/OrMlg04IuJT7NpefI1wwtdpWudnEmjuQs04t/4=
687+
github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.1/go.mod h1:GQhpKVvvuwzD79e8/NZ+xzj+ZpWovdPAe8nfV/skwNU=
688688
github.com/hashicorp/terraform-plugin-testing v1.14.0-beta.1 h1:caWmY2Fv/KgDAXU7IVjcBDfIdmr/n6VRYhCLxNmlaXs=
689689
github.com/hashicorp/terraform-plugin-testing v1.14.0-beta.1/go.mod h1:jVm3pD9uQAT0X2RSEdcqjju2bCGv5f73DGZFU4v7EAU=
690690
github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTGZmCNzuv7/GDTDX99E9gTk=

internal/provider/sdkv2/identity_interceptor.go

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func (r identityInterceptor) run(ctx context.Context, opts crudInterceptorOption
2929
case After:
3030
switch why {
3131
case Create, Read, Update:
32-
if why == Update && !(r.identitySpec.IsMutable && r.identitySpec.IsSetOnUpdate) {
32+
if why == Update && !(r.identitySpec.IsMutable && r.identitySpec.IsSetOnUpdate) && !identityIsFullyNull(d, r.identitySpec) {
3333
break
3434
}
3535
if d.Id() == "" {
@@ -63,11 +63,65 @@ func (r identityInterceptor) run(ctx context.Context, opts crudInterceptorOption
6363
}
6464
}
6565
}
66+
case OnError:
67+
switch why {
68+
case Update:
69+
if identityIsFullyNull(d, r.identitySpec) {
70+
if d.Id() == "" {
71+
break
72+
}
73+
identity, err := d.Identity()
74+
if err != nil {
75+
return sdkdiag.AppendFromErr(diags, err)
76+
}
77+
78+
for _, attr := range r.identitySpec.Attributes {
79+
switch attr.Name() {
80+
case names.AttrAccountID:
81+
if err := identity.Set(attr.Name(), awsClient.AccountID(ctx)); err != nil {
82+
return sdkdiag.AppendFromErr(diags, err)
83+
}
84+
85+
case names.AttrRegion:
86+
if err := identity.Set(attr.Name(), awsClient.Region(ctx)); err != nil {
87+
return sdkdiag.AppendFromErr(diags, err)
88+
}
89+
90+
default:
91+
val, ok := getAttributeOk(d, attr.ResourceAttributeName())
92+
if !ok {
93+
continue
94+
}
95+
if err := identity.Set(attr.Name(), val); err != nil {
96+
return sdkdiag.AppendFromErr(diags, err)
97+
}
98+
}
99+
}
100+
}
101+
}
66102
}
67103

68104
return diags
69105
}
70106

107+
// identityIsFullyNull returns true if a resource supports identity and
108+
// all attributes are set to null values
109+
func identityIsFullyNull(d schemaResourceData, identitySpec *inttypes.Identity) bool {
110+
identity, err := d.Identity()
111+
if err != nil {
112+
return false
113+
}
114+
115+
for _, attr := range identitySpec.Attributes {
116+
value := identity.Get(attr.Name())
117+
if value != "" {
118+
return false
119+
}
120+
}
121+
122+
return true
123+
}
124+
71125
func getAttributeOk(d schemaResourceData, name string) (string, bool) {
72126
if name == "id" {
73127
return d.Id(), true
@@ -81,17 +135,13 @@ func getAttributeOk(d schemaResourceData, name string) (string, bool) {
81135

82136
func newIdentityInterceptor(identitySpec *inttypes.Identity) interceptorInvocation {
83137
interceptor := interceptorInvocation{
84-
when: After,
85-
why: Create | Read,
138+
when: After | OnError,
139+
why: Create | Read | Update,
86140
interceptor: identityInterceptor{
87141
identitySpec: identitySpec,
88142
},
89143
}
90144

91-
if identitySpec.IsMutable && identitySpec.IsSetOnUpdate {
92-
interceptor.why |= Update
93-
}
94-
95145
return interceptor
96146
}
97147

internal/provider/sdkv2/identity_interceptor_test.go

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,32 +189,37 @@ func TestIdentityInterceptor_Update(t *testing.T) {
189189
attrName string
190190
identitySpec inttypes.Identity
191191
ExpectIdentity bool
192+
Description string
192193
}{
193-
"not mutable": {
194+
"not mutable - fresh resource": {
194195
attrName: "name",
195196
identitySpec: regionalSingleParameterizedIdentitySpec("name"),
196-
ExpectIdentity: false,
197+
ExpectIdentity: true,
198+
Description: "Immutable identity with all null attributes should get populated (bug fix scenario)",
197199
},
198200
"v6.0 SDK fix": {
199201
attrName: "name",
200202
identitySpec: regionalSingleParameterizedIdentitySpec("name",
201203
inttypes.WithV6_0SDKv2Fix(),
202204
),
203-
ExpectIdentity: false,
205+
ExpectIdentity: true,
206+
Description: "Mutable identity (v6.0 SDK fix) should always get populated on Update",
204207
},
205208
"identity fix": {
206209
attrName: "name",
207210
identitySpec: regionalSingleParameterizedIdentitySpec("name",
208211
inttypes.WithIdentityFix(),
209212
),
210-
ExpectIdentity: false,
213+
ExpectIdentity: true,
214+
Description: "Mutable identity (identity fix) should always get populated on Update",
211215
},
212216
"mutable": {
213217
attrName: "name",
214218
identitySpec: regionalSingleParameterizedIdentitySpec("name",
215219
inttypes.WithMutableIdentity(),
216220
),
217221
ExpectIdentity: true,
222+
Description: "Explicitly mutable identity should always get populated on Update",
218223
},
219224
}
220225

@@ -317,3 +322,82 @@ func (c mockClient) ValidateInContextRegionInPartition(ctx context.Context) erro
317322
func (c mockClient) AwsConfig(context.Context) aws.Config { // nosemgrep:ci.aws-in-func-name
318323
panic("not implemented") //lintignore:R009
319324
}
325+
326+
func TestIdentityIsFullyNull(t *testing.T) {
327+
t.Parallel()
328+
329+
identitySpec := &inttypes.Identity{
330+
Attributes: []inttypes.IdentityAttribute{
331+
inttypes.StringIdentityAttribute(names.AttrAccountID, false),
332+
inttypes.StringIdentityAttribute(names.AttrRegion, false),
333+
inttypes.StringIdentityAttribute(names.AttrBucket, true),
334+
},
335+
}
336+
337+
testCases := map[string]struct {
338+
identityValues map[string]string
339+
expectNull bool
340+
description string
341+
}{
342+
"all_null": {
343+
identityValues: map[string]string{},
344+
expectNull: true,
345+
description: "All attributes null should return true",
346+
},
347+
"some_null": {
348+
identityValues: map[string]string{
349+
names.AttrAccountID: "123456789012",
350+
// region and bucket remain null
351+
},
352+
expectNull: false,
353+
description: "Some attributes set should return false",
354+
},
355+
"all_set": {
356+
identityValues: map[string]string{
357+
names.AttrAccountID: "123456789012",
358+
names.AttrRegion: "us-west-2", // lintignore:AWSAT003
359+
names.AttrBucket: "test-bucket",
360+
},
361+
expectNull: false,
362+
description: "All attributes set should return false",
363+
},
364+
"empty_string_values": {
365+
identityValues: map[string]string{
366+
names.AttrAccountID: "",
367+
names.AttrRegion: "",
368+
names.AttrBucket: "",
369+
},
370+
expectNull: true,
371+
description: "Empty string values should be treated as null",
372+
},
373+
}
374+
375+
for name, tc := range testCases {
376+
t.Run(name, func(t *testing.T) {
377+
t.Parallel()
378+
379+
resourceSchema := map[string]*schema.Schema{
380+
names.AttrBucket: {Type: schema.TypeString, Required: true},
381+
}
382+
identitySchema := identity.NewIdentitySchema(*identitySpec)
383+
d := schema.TestResourceDataWithIdentityRaw(t, resourceSchema, identitySchema, nil)
384+
d.SetId("test-id")
385+
386+
identity, err := d.Identity()
387+
if err != nil {
388+
t.Fatalf("unexpected error getting identity: %v", err)
389+
}
390+
for attrName, value := range tc.identityValues {
391+
if err := identity.Set(attrName, value); err != nil {
392+
t.Fatalf("unexpected error setting %s in identity: %v", attrName, err)
393+
}
394+
}
395+
396+
result := identityIsFullyNull(d, identitySpec)
397+
if result != tc.expectNull {
398+
t.Errorf("%s: expected identityIsFullyNull to return %v, got %v",
399+
tc.description, tc.expectNull, result)
400+
}
401+
})
402+
}
403+
}

internal/service/iam/role_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,82 @@ func TestAccIAMRole_ManagedPolicy_outOfBandAdditionRemovedEmpty(t *testing.T) {
972972
})
973973
}
974974

975+
func TestAccIAMRole_Identity_ExistingResource_NoRefresh(t *testing.T) {
976+
ctx := acctest.Context(t)
977+
var conf awstypes.Role
978+
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
979+
resourceName := "aws_iam_role.test"
980+
981+
resource.ParallelTest(t, resource.TestCase{
982+
PreCheck: func() { acctest.PreCheck(ctx, t) },
983+
ErrorCheck: acctest.ErrorCheck(t, names.IAMServiceID),
984+
CheckDestroy: testAccCheckRoleDestroy(ctx),
985+
AdditionalCLIOptions: &resource.AdditionalCLIOptions{
986+
Plan: resource.PlanOptions{
987+
NoRefresh: true,
988+
},
989+
},
990+
Steps: []resource.TestStep{
991+
{
992+
ExternalProviders: map[string]resource.ExternalProvider{
993+
"aws": {
994+
Source: "hashicorp/aws",
995+
VersionConstraint: "5.100.0",
996+
},
997+
},
998+
Config: testAccRoleConfig_basic(rName),
999+
Check: resource.ComposeAggregateTestCheckFunc(
1000+
testAccCheckRoleExists(ctx, resourceName, &conf),
1001+
),
1002+
},
1003+
{
1004+
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
1005+
Config: testAccRoleConfig_description(rName),
1006+
Check: resource.ComposeAggregateTestCheckFunc(
1007+
testAccCheckRoleExists(ctx, resourceName, &conf),
1008+
),
1009+
},
1010+
},
1011+
})
1012+
}
1013+
1014+
func TestAccIAMRole_Identity_ExistingResource_NoRefreshFailure(t *testing.T) {
1015+
ctx := acctest.Context(t)
1016+
var conf awstypes.Role
1017+
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
1018+
resourceName := "aws_iam_role.test"
1019+
1020+
resource.ParallelTest(t, resource.TestCase{
1021+
PreCheck: func() { acctest.PreCheck(ctx, t) },
1022+
ErrorCheck: acctest.ErrorCheck(t, names.IAMServiceID),
1023+
CheckDestroy: testAccCheckRoleDestroy(ctx),
1024+
AdditionalCLIOptions: &resource.AdditionalCLIOptions{
1025+
Plan: resource.PlanOptions{
1026+
NoRefresh: true,
1027+
},
1028+
},
1029+
Steps: []resource.TestStep{
1030+
{
1031+
ExternalProviders: map[string]resource.ExternalProvider{
1032+
"aws": {
1033+
Source: "hashicorp/aws",
1034+
VersionConstraint: "5.100.0",
1035+
},
1036+
},
1037+
Config: testAccRoleConfig_basic(rName),
1038+
Check: resource.ComposeAggregateTestCheckFunc(
1039+
testAccCheckRoleExists(ctx, resourceName, &conf),
1040+
),
1041+
},
1042+
{
1043+
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
1044+
Config: testAccRoleConfig_invalidAssumeRolePolicy(rName),
1045+
ExpectError: regexache.MustCompile(`MalformedPolicyDocument: Unknown field invalid`),
1046+
},
1047+
},
1048+
})
1049+
}
1050+
9751051
func testAccCheckRoleDestroy(ctx context.Context) resource.TestCheckFunc {
9761052
return func(s *terraform.State) error {
9771053
conn := acctest.Provider.Meta().(*conns.AWSClient).IAMClient(ctx)
@@ -1260,6 +1336,21 @@ resource "aws_iam_role" "test" {
12601336
`, rName)
12611337
}
12621338

1339+
func testAccRoleConfig_invalidAssumeRolePolicy(rName string) string {
1340+
return fmt.Sprintf(`
1341+
data "aws_service_principal" "ec2" {
1342+
service_name = "ec2"
1343+
}
1344+
1345+
resource "aws_iam_role" "test" {
1346+
name = %[1]q
1347+
path = "/"
1348+
1349+
assume_role_policy = "{\"invalid\":true}"
1350+
}
1351+
`, rName)
1352+
}
1353+
12631354
func testAccRoleConfig_diffs(rName, tags string) string {
12641355
return fmt.Sprintf(`
12651356
data "aws_partition" "current" {}

0 commit comments

Comments
 (0)