Skip to content

Commit b4e37fb

Browse files
authored
Merge branch 'main' into main
2 parents 5e09c69 + 6d2ebde commit b4e37fb

11 files changed

+526
-3
lines changed

.changes/1.16.1.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
## 1.16.1 (September 29, 2025)
2+
3+
BUG FIXES:
4+
5+
* all: Prevent identity change validation from raising an error when prior identity is empty (all attributes are null) ([#1229](https://github.com/hashicorp/terraform-plugin-framework/issues/1229))
6+
* all: Added an additional validation check to ensure the resource identity object is not null. ([#1193](https://github.com/hashicorp/terraform-plugin-framework/issues/1193))
7+

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 1.16.1 (September 29, 2025)
2+
3+
BUG FIXES:
4+
5+
* all: Prevent identity change validation from raising an error when prior identity is empty (all attributes are null) ([#1229](https://github.com/hashicorp/terraform-plugin-framework/issues/1229))
6+
* all: Added an additional validation check to ensure the resource identity object is not null. ([#1193](https://github.com/hashicorp/terraform-plugin-framework/issues/1193))
7+
18
## 1.16.0 (September 17, 2025)
29

310
NOTES:

internal/fwserver/server_applyresourcechange_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,24 @@ func TestServerApplyResourceChange(t *testing.T) {
5959
},
6060
}
6161

62+
type testMultiIdentitySchemaData struct {
63+
TestAttrA types.String `tfsdk:"test_attr_a"`
64+
TestAttrB types.Int64 `tfsdk:"test_attr_b"`
65+
}
66+
67+
testMultiAttrIdentitySchema := identityschema.Schema{
68+
Attributes: map[string]identityschema.Attribute{
69+
"test_attr_a": identityschema.StringAttribute{
70+
RequiredForImport: true,
71+
},
72+
"test_attr_b": identityschema.Int64Attribute{
73+
OptionalForImport: true,
74+
},
75+
},
76+
}
77+
78+
testMultiAttrIdentityType := testMultiAttrIdentitySchema.Type().TerraformType(context.Background())
79+
6280
testEmptyPlan := &tfsdk.Plan{
6381
Raw: tftypes.NewValue(testSchemaType, nil),
6482
Schema: testSchema,
@@ -1719,6 +1737,76 @@ func TestServerApplyResourceChange(t *testing.T) {
17191737
Private: testEmptyPrivate,
17201738
},
17211739
},
1740+
"update-response-newidentity-empty-plannedidentity": {
1741+
server: &fwserver.Server{
1742+
Provider: &testprovider.Provider{},
1743+
},
1744+
request: &fwserver.ApplyResourceChangeRequest{
1745+
Config: &tfsdk.Config{
1746+
Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{
1747+
"test_computed": tftypes.NewValue(tftypes.String, nil),
1748+
"test_required": tftypes.NewValue(tftypes.String, "test-new-value"),
1749+
}),
1750+
Schema: testSchema,
1751+
},
1752+
PlannedState: &tfsdk.Plan{
1753+
Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{
1754+
"test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"),
1755+
"test_required": tftypes.NewValue(tftypes.String, "test-new-value"),
1756+
}),
1757+
Schema: testSchema,
1758+
},
1759+
PriorState: &tfsdk.State{
1760+
Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{
1761+
"test_computed": tftypes.NewValue(tftypes.String, nil),
1762+
"test_required": tftypes.NewValue(tftypes.String, "test-old-value"),
1763+
}),
1764+
Schema: testSchema,
1765+
},
1766+
PlannedIdentity: &tfsdk.ResourceIdentity{
1767+
Raw: tftypes.NewValue(testMultiAttrIdentityType, map[string]tftypes.Value{
1768+
"test_attr_a": tftypes.NewValue(tftypes.String, nil),
1769+
"test_attr_b": tftypes.NewValue(tftypes.Number, nil),
1770+
}),
1771+
Schema: testMultiAttrIdentitySchema,
1772+
},
1773+
IdentitySchema: testMultiAttrIdentitySchema,
1774+
ResourceSchema: testSchema,
1775+
Resource: &testprovider.ResourceWithIdentity{
1776+
Resource: &testprovider.Resource{
1777+
CreateMethod: func(_ context.Context, _ resource.CreateRequest, resp *resource.CreateResponse) {
1778+
resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Create")
1779+
},
1780+
DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) {
1781+
resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Delete")
1782+
},
1783+
UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
1784+
resp.Diagnostics.Append(resp.Identity.Set(ctx, testMultiIdentitySchemaData{
1785+
TestAttrA: types.StringValue("new value"),
1786+
TestAttrB: types.Int64Value(20),
1787+
})...)
1788+
},
1789+
},
1790+
},
1791+
},
1792+
expectedResponse: &fwserver.ApplyResourceChangeResponse{
1793+
NewIdentity: &tfsdk.ResourceIdentity{
1794+
Raw: tftypes.NewValue(testMultiAttrIdentityType, map[string]tftypes.Value{
1795+
"test_attr_a": tftypes.NewValue(tftypes.String, "new value"),
1796+
"test_attr_b": tftypes.NewValue(tftypes.Number, 20),
1797+
}),
1798+
Schema: testMultiAttrIdentitySchema,
1799+
},
1800+
NewState: &tfsdk.State{
1801+
Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{
1802+
"test_computed": tftypes.NewValue(tftypes.String, nil),
1803+
"test_required": tftypes.NewValue(tftypes.String, "test-old-value"),
1804+
}),
1805+
Schema: testSchema,
1806+
},
1807+
Private: testEmptyPrivate,
1808+
},
1809+
},
17221810
"update-response-newidentity-with-plannedidentity": {
17231811
server: &fwserver.Server{
17241812
Provider: &testprovider.Provider{},

internal/fwserver/server_createresource.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,17 @@ func (s *Server) CreateResource(ctx context.Context, req *CreateResourceRequest,
169169
return
170170
}
171171

172+
if req.IdentitySchema != nil {
173+
if resp.NewIdentity.Raw.IsFullyNull() {
174+
resp.Diagnostics.AddError(
175+
"Missing Resource Identity After Create",
176+
"The Terraform Provider unexpectedly returned no resource identity data after having no errors in the resource create. "+
177+
"This is always an issue in the Terraform Provider and should be reported to the provider developers.",
178+
)
179+
return
180+
}
181+
}
182+
172183
semanticEqualityReq := SchemaSemanticEqualityRequest{
173184
PriorData: fwschemadata.Data{
174185
Description: fwschemadata.DataDescriptionPlan,

internal/fwserver/server_createresource_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,106 @@ func TestServerCreateResource(t *testing.T) {
580580
Private: testEmptyPrivate,
581581
},
582582
},
583+
"response-invalid-nil-identity": {
584+
server: &fwserver.Server{
585+
Provider: &testprovider.Provider{},
586+
},
587+
request: &fwserver.CreateResourceRequest{
588+
PlannedState: &tfsdk.Plan{
589+
Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{
590+
"test_computed": tftypes.NewValue(tftypes.String, nil),
591+
"test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"),
592+
}),
593+
Schema: testSchema,
594+
},
595+
IdentitySchema: testIdentitySchema,
596+
ResourceSchema: testSchema,
597+
Resource: &testprovider.ResourceWithIdentity{
598+
Resource: &testprovider.Resource{
599+
CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
600+
resp.Identity.Raw = tftypes.NewValue(testIdentitySchema.Type().TerraformType(ctx), nil)
601+
// Prevent missing resource state error diagnostic
602+
var data testSchemaData
603+
604+
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
605+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
606+
},
607+
},
608+
},
609+
},
610+
expectedResponse: &fwserver.CreateResourceResponse{
611+
Diagnostics: []diag.Diagnostic{
612+
diag.NewErrorDiagnostic(
613+
"Missing Resource Identity After Create",
614+
"The Terraform Provider unexpectedly returned no resource identity data after having no errors in the resource create. "+
615+
"This is always an issue in the Terraform Provider and should be reported to the provider developers.",
616+
),
617+
},
618+
NewIdentity: &tfsdk.ResourceIdentity{
619+
Raw: tftypes.NewValue(testIdentityType, nil),
620+
Schema: testIdentitySchema,
621+
},
622+
NewState: &tfsdk.State{
623+
Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{
624+
"test_computed": tftypes.NewValue(tftypes.String, nil),
625+
"test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"),
626+
}),
627+
Schema: testSchema,
628+
},
629+
Private: testEmptyPrivate,
630+
},
631+
},
632+
"response-invalid-null-identity": {
633+
server: &fwserver.Server{
634+
Provider: &testprovider.Provider{},
635+
},
636+
request: &fwserver.CreateResourceRequest{
637+
PlannedState: &tfsdk.Plan{
638+
Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{
639+
"test_computed": tftypes.NewValue(tftypes.String, nil),
640+
"test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"),
641+
}),
642+
Schema: testSchema,
643+
},
644+
IdentitySchema: testIdentitySchema,
645+
ResourceSchema: testSchema,
646+
Resource: &testprovider.ResourceWithIdentity{
647+
Resource: &testprovider.Resource{
648+
CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
649+
resp.Diagnostics.Append(resp.Identity.Set(ctx, testIdentitySchemaData{})...)
650+
// Prevent missing resource state error diagnostic
651+
var data testSchemaData
652+
653+
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
654+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
655+
},
656+
},
657+
},
658+
},
659+
expectedResponse: &fwserver.CreateResourceResponse{
660+
Diagnostics: []diag.Diagnostic{
661+
diag.NewErrorDiagnostic(
662+
"Missing Resource Identity After Create",
663+
"The Terraform Provider unexpectedly returned no resource identity data after having no errors in the resource create. "+
664+
"This is always an issue in the Terraform Provider and should be reported to the provider developers.",
665+
),
666+
},
667+
NewIdentity: &tfsdk.ResourceIdentity{
668+
Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{
669+
"test_id": tftypes.NewValue(tftypes.String, nil),
670+
}),
671+
Schema: testIdentitySchema,
672+
},
673+
NewState: &tfsdk.State{
674+
Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{
675+
"test_computed": tftypes.NewValue(tftypes.String, nil),
676+
"test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"),
677+
}),
678+
Schema: testSchema,
679+
},
680+
Private: testEmptyPrivate,
681+
},
682+
},
583683
"response-invalid-newidentity": {
584684
server: &fwserver.Server{
585685
Provider: &testprovider.Provider{},

internal/fwserver/server_planresourcechange.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange
383383
}
384384

385385
// If we're updating or deleting and we already have an identity stored, validate that the planned identity isn't changing
386-
if !req.ResourceBehavior.MutableIdentity && !req.PriorState.Raw.IsNull() && !req.PriorIdentity.Raw.IsNull() && !req.PriorIdentity.Raw.Equal(resp.PlannedIdentity.Raw) {
386+
if !req.ResourceBehavior.MutableIdentity && !req.PriorState.Raw.IsNull() && !req.PriorIdentity.Raw.IsFullyNull() && !req.PriorIdentity.Raw.Equal(resp.PlannedIdentity.Raw) {
387387
resp.Diagnostics.AddError(
388388
"Unexpected Identity Change",
389389
"During the planning operation, the Terraform Provider unexpectedly returned a different identity than the previously stored one.\n\n"+

internal/fwserver/server_planresourcechange_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,24 @@ func TestServerPlanResourceChange(t *testing.T) {
589589
},
590590
}
591591

592+
type testMultiIdentitySchemaData struct {
593+
TestAttrA types.String `tfsdk:"test_attr_a"`
594+
TestAttrB types.Int64 `tfsdk:"test_attr_b"`
595+
}
596+
597+
testMultiAttrIdentitySchema := identityschema.Schema{
598+
Attributes: map[string]identityschema.Attribute{
599+
"test_attr_a": identityschema.StringAttribute{
600+
RequiredForImport: true,
601+
},
602+
"test_attr_b": identityschema.Int64Attribute{
603+
OptionalForImport: true,
604+
},
605+
},
606+
}
607+
608+
testMultiAttrIdentityType := testMultiAttrIdentitySchema.Type().TerraformType(context.Background())
609+
592610
testSchemaWriteOnly := schema.Schema{
593611
Attributes: map[string]schema.Attribute{
594612
"test_computed": schema.StringAttribute{
@@ -6722,6 +6740,77 @@ func TestServerPlanResourceChange(t *testing.T) {
67226740
PlannedPrivate: testEmptyPrivate,
67236741
},
67246742
},
6743+
"update-resourcewithmodifyplan-empty-prioridentity-plannedidentity-changed": {
6744+
server: &fwserver.Server{
6745+
Provider: &testprovider.Provider{},
6746+
},
6747+
request: &fwserver.PlanResourceChangeRequest{
6748+
Config: &tfsdk.Config{
6749+
Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{
6750+
"test_computed": tftypes.NewValue(tftypes.String, nil),
6751+
"test_required": tftypes.NewValue(tftypes.String, "test-new-value"),
6752+
}),
6753+
Schema: testSchema,
6754+
},
6755+
ProposedNewState: &tfsdk.Plan{
6756+
Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{
6757+
"test_computed": tftypes.NewValue(tftypes.String, nil),
6758+
"test_required": tftypes.NewValue(tftypes.String, "test-new-value"),
6759+
}),
6760+
Schema: testSchema,
6761+
},
6762+
PriorState: &tfsdk.State{
6763+
Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{
6764+
"test_computed": tftypes.NewValue(tftypes.String, nil),
6765+
"test_required": tftypes.NewValue(tftypes.String, "test-old-value"),
6766+
}),
6767+
Schema: testSchema,
6768+
},
6769+
PriorIdentity: &tfsdk.ResourceIdentity{
6770+
Raw: tftypes.NewValue(testMultiAttrIdentityType, map[string]tftypes.Value{
6771+
"test_attr_a": tftypes.NewValue(tftypes.String, nil),
6772+
"test_attr_b": tftypes.NewValue(tftypes.Number, nil),
6773+
}),
6774+
Schema: testMultiAttrIdentitySchema,
6775+
},
6776+
IdentitySchema: testMultiAttrIdentitySchema,
6777+
ResourceSchema: testSchema,
6778+
Resource: &testprovider.ResourceWithIdentityAndModifyPlan{
6779+
ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
6780+
var data testSchemaData
6781+
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
6782+
data.TestComputed = types.StringValue("test-plannedstate-value")
6783+
resp.Diagnostics.Append(resp.Plan.Set(ctx, &data)...)
6784+
6785+
var identityData testMultiIdentitySchemaData
6786+
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
6787+
identityData.TestAttrA = types.StringValue("new value")
6788+
identityData.TestAttrB = types.Int64Value(20)
6789+
resp.Diagnostics.Append(resp.Identity.Set(ctx, &identityData)...)
6790+
},
6791+
IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) {
6792+
resp.IdentitySchema = testMultiAttrIdentitySchema
6793+
},
6794+
},
6795+
},
6796+
expectedResponse: &fwserver.PlanResourceChangeResponse{
6797+
PlannedIdentity: &tfsdk.ResourceIdentity{
6798+
Raw: tftypes.NewValue(testMultiAttrIdentityType, map[string]tftypes.Value{
6799+
"test_attr_a": tftypes.NewValue(tftypes.String, "new value"),
6800+
"test_attr_b": tftypes.NewValue(tftypes.Number, 20),
6801+
}),
6802+
Schema: testMultiAttrIdentitySchema,
6803+
},
6804+
PlannedState: &tfsdk.State{
6805+
Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{
6806+
"test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"),
6807+
"test_required": tftypes.NewValue(tftypes.String, "test-new-value"),
6808+
}),
6809+
Schema: testSchema,
6810+
},
6811+
PlannedPrivate: testEmptyPrivate,
6812+
},
6813+
},
67256814
"update-resourcewithmodifyplan-invalid-response-plannedidentity-changed": {
67266815
server: &fwserver.Server{
67276816
Provider: &testprovider.Provider{},

internal/fwserver/server_readresource.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ func (s *Server) ReadResource(ctx context.Context, req *ReadResourceRequest, res
185185
}
186186

187187
// If we're refreshing the resource state (excluding a recently imported resource), validate that the new identity isn't changing
188-
if !req.ResourceBehavior.MutableIdentity && !readFollowingImport && !req.CurrentIdentity.Raw.IsNull() && !req.CurrentIdentity.Raw.Equal(resp.NewIdentity.Raw) {
188+
if !req.ResourceBehavior.MutableIdentity && !readFollowingImport && !req.CurrentIdentity.Raw.IsFullyNull() && !req.CurrentIdentity.Raw.Equal(resp.NewIdentity.Raw) {
189189
resp.Diagnostics.AddError(
190190
"Unexpected Identity Change",
191191
"During the read operation, the Terraform Provider unexpectedly returned a different identity than the previously stored one.\n\n"+
@@ -198,6 +198,17 @@ func (s *Server) ReadResource(ctx context.Context, req *ReadResourceRequest, res
198198
}
199199
}
200200

201+
if req.IdentitySchema != nil {
202+
if resp.NewIdentity.Raw.IsFullyNull() {
203+
resp.Diagnostics.AddError(
204+
"Missing Resource Identity After Read",
205+
"The Terraform Provider unexpectedly returned no resource identity data after having no errors in the resource read. "+
206+
"This is always an issue in the Terraform Provider and should be reported to the provider developers.",
207+
)
208+
return
209+
}
210+
}
211+
201212
semanticEqualityReq := SchemaSemanticEqualityRequest{
202213
PriorData: fwschemadata.Data{
203214
Description: fwschemadata.DataDescriptionState,

0 commit comments

Comments
 (0)