Skip to content

Commit 432fd5e

Browse files
authored
Merge pull request hashicorp#45201 from hashicorp/b-tag-policy-interceptor
Provider: Fix required tag validation regressions
2 parents f84529a + f38d744 commit 432fd5e

File tree

4 files changed

+233
-17
lines changed

4 files changed

+233
-17
lines changed

.changelog/45201.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
```release-note:bug
2+
provider: Fix early return logic in the required tag validation interceptor. This addresses a performance regression introduced in [v6.22.0](https://github.com/hashicorp/terraform-provider-aws/blob/main/CHANGELOG.md#6220-november-20-2025).
3+
```
4+
```release-note:bug
5+
provider: Fix crash in required tag validation interceptor when tag values are unknown. This addresses a regression introduced in [v6.22.0](https://github.com/hashicorp/terraform-provider-aws/blob/main/CHANGELOG.md#6220-november-20-2025).
6+
```

internal/provider/framework/tags_interceptor.go

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,15 @@ func (r resourceValidateRequiredTagsInterceptor) modifyPlan(ctx context.Context,
278278
return
279279
}
280280

281+
policy := c.TagPolicyConfig(ctx)
282+
if policy == nil {
283+
return
284+
}
285+
reqTags, ok := policy.RequiredTags[typeName]
286+
if !ok {
287+
return
288+
}
289+
281290
switch request, _, when := opts.request, opts.response, opts.when; when {
282291
case Before:
283292
// If the entire plan is null, the resource is planned for destruction.
@@ -292,6 +301,10 @@ func (r resourceValidateRequiredTagsInterceptor) modifyPlan(ctx context.Context,
292301
return
293302
}
294303

304+
if !planTags.IsWhollyKnown() {
305+
return
306+
}
307+
295308
allPlanTags := c.DefaultTagsConfig(ctx).MergeTags(tftags.New(ctx, planTags))
296309
allStateTags := c.DefaultTagsConfig(ctx).MergeTags(tftags.New(ctx, stateTags))
297310

@@ -302,16 +315,6 @@ func (r resourceValidateRequiredTagsInterceptor) modifyPlan(ctx context.Context,
302315
return
303316
}
304317

305-
policy := c.TagPolicyConfig(ctx)
306-
if policy == nil {
307-
return
308-
}
309-
310-
reqTags, ok := policy.RequiredTags[typeName]
311-
if !ok {
312-
return
313-
}
314-
315318
if allPlanTags.ContainsAllKeys(reqTags) {
316319
return
317320
}

internal/provider/framework/tags_interceptor_test.go

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,42 @@ func Test_resourceValidateRequiredTagsInterceptor(t *testing.T) {
9191
},
9292
}
9393

94+
// Null tags
9495
attrs := map[string]tftypes.Value{
9596
"name": tftypes.NewValue(tftypes.String, "test"),
9697
"tags": tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, nil),
9798
}
9899
rawVal := tftypes.NewValue(resourceSchema.Type().TerraformType(ctx), attrs)
99100

101+
// Partial required tags
102+
attrsPartial := map[string]tftypes.Value{
103+
"name": tftypes.NewValue(tftypes.String, "test"),
104+
"tags": tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, map[string]tftypes.Value{
105+
"bar": tftypes.NewValue(tftypes.String, nil),
106+
}),
107+
}
108+
rawValPartial := tftypes.NewValue(resourceSchema.Type().TerraformType(ctx), attrsPartial)
109+
110+
// All required tags
111+
attrsRequired := map[string]tftypes.Value{
112+
"name": tftypes.NewValue(tftypes.String, "test"),
113+
"tags": tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, map[string]tftypes.Value{
114+
"foo": tftypes.NewValue(tftypes.String, nil),
115+
"bar": tftypes.NewValue(tftypes.String, nil),
116+
}),
117+
}
118+
rawValRequired := tftypes.NewValue(resourceSchema.Type().TerraformType(ctx), attrsRequired)
119+
120+
// Unknown tag values
121+
attrsUnknown := map[string]tftypes.Value{
122+
"name": tftypes.NewValue(tftypes.String, "test"),
123+
"tags": tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, map[string]tftypes.Value{
124+
"foo": tftypes.NewValue(tftypes.String, tftypes.UnknownValue),
125+
"bar": tftypes.NewValue(tftypes.String, tftypes.UnknownValue),
126+
}),
127+
}
128+
rawValUnknown := tftypes.NewValue(resourceSchema.Type().TerraformType(ctx), attrsUnknown)
129+
100130
tests := []struct {
101131
name string
102132
opts interceptorOptions[resource.ModifyPlanRequest, resource.ModifyPlanResponse]
@@ -135,6 +165,93 @@ func Test_resourceValidateRequiredTagsInterceptor(t *testing.T) {
135165
),
136166
},
137167
},
168+
{
169+
name: "create, partial tags",
170+
opts: interceptorOptions[resource.ModifyPlanRequest, resource.ModifyPlanResponse]{
171+
c: mockRequiredTagsClient{},
172+
request: &resource.ModifyPlanRequest{
173+
Config: tfsdk.Config{
174+
Raw: rawValPartial,
175+
Schema: resourceSchema,
176+
},
177+
State: tfsdk.State{
178+
Raw: tftypes.NewValue(resourceSchema.Type().TerraformType(ctx), nil), // Raw state is null on creation
179+
Schema: resourceSchema,
180+
},
181+
Plan: tfsdk.Plan{
182+
Raw: rawValPartial,
183+
Schema: resourceSchema,
184+
},
185+
},
186+
response: &resource.ModifyPlanResponse{
187+
Plan: tfsdk.Plan{
188+
Raw: rawValPartial,
189+
Schema: resourceSchema,
190+
},
191+
},
192+
when: Before,
193+
},
194+
wantDiags: diag.Diagnostics{diag.NewAttributeErrorDiagnostic(
195+
path.Root(names.AttrTags),
196+
"Missing Required Tags",
197+
"An organizational tag policy requires the following tags for aws_test: [foo]",
198+
),
199+
},
200+
},
201+
{
202+
name: "create, required tags",
203+
opts: interceptorOptions[resource.ModifyPlanRequest, resource.ModifyPlanResponse]{
204+
c: mockRequiredTagsClient{},
205+
request: &resource.ModifyPlanRequest{
206+
Config: tfsdk.Config{
207+
Raw: rawValRequired,
208+
Schema: resourceSchema,
209+
},
210+
State: tfsdk.State{
211+
Raw: tftypes.NewValue(resourceSchema.Type().TerraformType(ctx), nil), // Raw state is null on creation
212+
Schema: resourceSchema,
213+
},
214+
Plan: tfsdk.Plan{
215+
Raw: rawValRequired,
216+
Schema: resourceSchema,
217+
},
218+
},
219+
response: &resource.ModifyPlanResponse{
220+
Plan: tfsdk.Plan{
221+
Raw: rawValRequired,
222+
Schema: resourceSchema,
223+
},
224+
},
225+
when: Before,
226+
},
227+
},
228+
{
229+
name: "create, unknown tag values",
230+
opts: interceptorOptions[resource.ModifyPlanRequest, resource.ModifyPlanResponse]{
231+
c: mockRequiredTagsClient{},
232+
request: &resource.ModifyPlanRequest{
233+
Config: tfsdk.Config{
234+
Raw: rawValUnknown,
235+
Schema: resourceSchema,
236+
},
237+
State: tfsdk.State{
238+
Raw: tftypes.NewValue(resourceSchema.Type().TerraformType(ctx), nil), // Raw state is null on creation
239+
Schema: resourceSchema,
240+
},
241+
Plan: tfsdk.Plan{
242+
Raw: rawValUnknown,
243+
Schema: resourceSchema,
244+
},
245+
},
246+
response: &resource.ModifyPlanResponse{
247+
Plan: tfsdk.Plan{
248+
Raw: rawValUnknown,
249+
Schema: resourceSchema,
250+
},
251+
},
252+
when: Before,
253+
},
254+
},
138255
{
139256
name: "update, no tags change",
140257
opts: interceptorOptions[resource.ModifyPlanRequest, resource.ModifyPlanResponse]{
@@ -162,6 +279,93 @@ func Test_resourceValidateRequiredTagsInterceptor(t *testing.T) {
162279
when: Before,
163280
},
164281
},
282+
{
283+
name: "update, add required",
284+
opts: interceptorOptions[resource.ModifyPlanRequest, resource.ModifyPlanResponse]{
285+
c: mockRequiredTagsClient{},
286+
request: &resource.ModifyPlanRequest{
287+
Config: tfsdk.Config{
288+
Raw: rawValRequired,
289+
Schema: resourceSchema,
290+
},
291+
State: tfsdk.State{
292+
Raw: rawValPartial,
293+
Schema: resourceSchema,
294+
},
295+
Plan: tfsdk.Plan{
296+
Raw: rawValRequired,
297+
Schema: resourceSchema,
298+
},
299+
},
300+
response: &resource.ModifyPlanResponse{
301+
Plan: tfsdk.Plan{
302+
Raw: rawValRequired,
303+
Schema: resourceSchema,
304+
},
305+
},
306+
when: Before,
307+
},
308+
},
309+
{
310+
name: "update, remove required",
311+
opts: interceptorOptions[resource.ModifyPlanRequest, resource.ModifyPlanResponse]{
312+
c: mockRequiredTagsClient{},
313+
request: &resource.ModifyPlanRequest{
314+
Config: tfsdk.Config{
315+
Raw: rawValPartial,
316+
Schema: resourceSchema,
317+
},
318+
State: tfsdk.State{
319+
Raw: rawValRequired,
320+
Schema: resourceSchema,
321+
},
322+
Plan: tfsdk.Plan{
323+
Raw: rawValPartial,
324+
Schema: resourceSchema,
325+
},
326+
},
327+
response: &resource.ModifyPlanResponse{
328+
Plan: tfsdk.Plan{
329+
Raw: rawValRequired,
330+
Schema: resourceSchema,
331+
},
332+
},
333+
when: Before,
334+
},
335+
wantDiags: diag.Diagnostics{diag.NewAttributeErrorDiagnostic(
336+
path.Root(names.AttrTags),
337+
"Missing Required Tags",
338+
"An organizational tag policy requires the following tags for aws_test: [foo]",
339+
),
340+
},
341+
},
342+
{
343+
name: "update, unknown tag values",
344+
opts: interceptorOptions[resource.ModifyPlanRequest, resource.ModifyPlanResponse]{
345+
c: mockRequiredTagsClient{},
346+
request: &resource.ModifyPlanRequest{
347+
Config: tfsdk.Config{
348+
Raw: rawValUnknown,
349+
Schema: resourceSchema,
350+
},
351+
State: tfsdk.State{
352+
Raw: rawValPartial,
353+
Schema: resourceSchema,
354+
},
355+
Plan: tfsdk.Plan{
356+
Raw: rawValUnknown,
357+
Schema: resourceSchema,
358+
},
359+
},
360+
response: &resource.ModifyPlanResponse{
361+
Plan: tfsdk.Plan{
362+
Raw: rawValUnknown,
363+
Schema: resourceSchema,
364+
},
365+
},
366+
when: Before,
367+
},
368+
},
165369
{
166370
name: "destroy",
167371
opts: interceptorOptions[resource.ModifyPlanRequest, resource.ModifyPlanResponse]{

internal/provider/sdkv2/tags_interceptor.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,15 @@ func validateRequiredTags() customizeDiffInterceptor {
309309
return nil
310310
}
311311

312+
policy := c.TagPolicyConfig(ctx)
313+
if policy == nil {
314+
return nil
315+
}
316+
reqTags, ok := policy.RequiredTags[typeName]
317+
if !ok {
318+
return nil
319+
}
320+
312321
switch d, when, why := opts.d, opts.when, opts.why; when {
313322
case Before:
314323
switch why {
@@ -320,13 +329,7 @@ func validateRequiredTags() customizeDiffInterceptor {
320329
return nil
321330
}
322331

323-
policy := c.TagPolicyConfig(ctx)
324-
if policy == nil {
325-
return nil
326-
}
327-
328-
reqTags, ok := policy.RequiredTags[typeName]
329-
if !ok {
332+
if !d.GetRawPlan().GetAttr(names.AttrTags).IsWhollyKnown() {
330333
return nil
331334
}
332335

0 commit comments

Comments
 (0)