diff --git a/.changes/unreleased/BUG FIXES-20250922-175341.yaml b/.changes/unreleased/BUG FIXES-20250922-175341.yaml new file mode 100644 index 000000000..0fea8ecbf --- /dev/null +++ b/.changes/unreleased/BUG FIXES-20250922-175341.yaml @@ -0,0 +1,5 @@ +kind: BUG FIXES +body: 'all: Added an additional validation check to ensure the resource identity object is not null.' +time: 2025-09-22T17:53:41.039656-04:00 +custom: + Issue: "1193" diff --git a/internal/fwserver/server_createresource.go b/internal/fwserver/server_createresource.go index 00a13d68e..6fa6eb794 100644 --- a/internal/fwserver/server_createresource.go +++ b/internal/fwserver/server_createresource.go @@ -169,6 +169,17 @@ func (s *Server) CreateResource(ctx context.Context, req *CreateResourceRequest, return } + if req.IdentitySchema != nil { + if resp.NewIdentity.Raw.IsFullyNull() { + resp.Diagnostics.AddError( + "Missing Resource Identity After Create", + "The Terraform Provider unexpectedly returned no resource identity data after having no errors in the resource create. "+ + "This is always an issue in the Terraform Provider and should be reported to the provider developers.", + ) + return + } + } + semanticEqualityReq := SchemaSemanticEqualityRequest{ PriorData: fwschemadata.Data{ Description: fwschemadata.DataDescriptionPlan, diff --git a/internal/fwserver/server_createresource_test.go b/internal/fwserver/server_createresource_test.go index 3095638ff..71912fe2f 100644 --- a/internal/fwserver/server_createresource_test.go +++ b/internal/fwserver/server_createresource_test.go @@ -580,6 +580,106 @@ func TestServerCreateResource(t *testing.T) { Private: testEmptyPrivate, }, }, + "response-invalid-nil-identity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.CreateResourceRequest{ + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + resp.Identity.Raw = tftypes.NewValue(testIdentitySchema.Type().TerraformType(ctx), nil) + // Prevent missing resource state error diagnostic + var data testSchemaData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + }, + }, + }, + expectedResponse: &fwserver.CreateResourceResponse{ + Diagnostics: []diag.Diagnostic{ + diag.NewErrorDiagnostic( + "Missing Resource Identity After Create", + "The Terraform Provider unexpectedly returned no resource identity data after having no errors in the resource create. "+ + "This is always an issue in the Terraform Provider and should be reported to the provider developers.", + ), + }, + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, nil), + Schema: testIdentitySchema, + }, + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate, + }, + }, + "response-invalid-null-identity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.CreateResourceRequest{ + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.Append(resp.Identity.Set(ctx, testIdentitySchemaData{})...) + // Prevent missing resource state error diagnostic + var data testSchemaData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + }, + }, + }, + expectedResponse: &fwserver.CreateResourceResponse{ + Diagnostics: []diag.Diagnostic{ + diag.NewErrorDiagnostic( + "Missing Resource Identity After Create", + "The Terraform Provider unexpectedly returned no resource identity data after having no errors in the resource create. "+ + "This is always an issue in the Terraform Provider and should be reported to the provider developers.", + ), + }, + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testIdentitySchema, + }, + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate, + }, + }, "response-invalid-newidentity": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, diff --git a/internal/fwserver/server_readresource.go b/internal/fwserver/server_readresource.go index 7f0a00423..d8c5edbe1 100644 --- a/internal/fwserver/server_readresource.go +++ b/internal/fwserver/server_readresource.go @@ -198,6 +198,17 @@ func (s *Server) ReadResource(ctx context.Context, req *ReadResourceRequest, res } } + if req.IdentitySchema != nil { + if resp.NewIdentity.Raw.IsFullyNull() { + resp.Diagnostics.AddError( + "Missing Resource Identity After Read", + "The Terraform Provider unexpectedly returned no resource identity data after having no errors in the resource read. "+ + "This is always an issue in the Terraform Provider and should be reported to the provider developers.", + ) + return + } + } + semanticEqualityReq := SchemaSemanticEqualityRequest{ PriorData: fwschemadata.Data{ Description: fwschemadata.DataDescriptionState, diff --git a/internal/fwserver/server_readresource_test.go b/internal/fwserver/server_readresource_test.go index b0b0ab127..1fd3dcfea 100644 --- a/internal/fwserver/server_readresource_test.go +++ b/internal/fwserver/server_readresource_test.go @@ -177,6 +177,11 @@ func TestServerReadResource(t *testing.T) { Schema: testIdentitySchema, } + testEmptyIdentity := &tfsdk.ResourceIdentity{ + Schema: testIdentitySchema, + Raw: tftypes.NewValue(testIdentitySchema.Type().TerraformType(context.Background()), nil), + } + testNewStateRemoved := &tfsdk.State{ Raw: tftypes.NewValue(testType, nil), Schema: testSchema, @@ -649,6 +654,35 @@ func TestServerReadResource(t *testing.T) { Private: testEmptyPrivate, }, }, + "response-invalid-nil-identity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ReadResourceRequest{ + CurrentState: testCurrentState, + CurrentIdentity: nil, + IdentitySchema: testIdentitySchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + resp.Identity = req.Identity + }, + }, + }, + }, + expectedResponse: &fwserver.ReadResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing Resource Identity After Read", + "The Terraform Provider unexpectedly returned no resource identity data after having no errors in the resource read. "+ + "This is always an issue in the Terraform Provider and should be reported to the provider developers.", + ), + }, + NewState: testCurrentState, + NewIdentity: testEmptyIdentity, + Private: testEmptyPrivate, + }, + }, "response-identity-valid-update-null-currentidentity": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, diff --git a/internal/fwserver/server_updateresource.go b/internal/fwserver/server_updateresource.go index 0521aa80c..8943703a9 100644 --- a/internal/fwserver/server_updateresource.go +++ b/internal/fwserver/server_updateresource.go @@ -199,6 +199,17 @@ func (s *Server) UpdateResource(ctx context.Context, req *UpdateResourceRequest, } } + if req.IdentitySchema != nil { + if resp.NewIdentity.Raw.IsFullyNull() { + resp.Diagnostics.AddError( + "Missing Resource Identity After Update", + "The Terraform Provider unexpectedly returned no resource identity data after having no errors in the resource update. "+ + "This is always an issue in the Terraform Provider and should be reported to the provider developers.", + ) + return + } + } + semanticEqualityReq := SchemaSemanticEqualityRequest{ PriorData: fwschemadata.Data{ Description: fwschemadata.DataDescriptionPlan, diff --git a/internal/fwserver/server_updateresource_test.go b/internal/fwserver/server_updateresource_test.go index f17616703..cb132addf 100644 --- a/internal/fwserver/server_updateresource_test.go +++ b/internal/fwserver/server_updateresource_test.go @@ -927,6 +927,116 @@ func TestServerUpdateResource(t *testing.T) { Private: testEmptyPrivate, }, }, + "response-new-identity-nil": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpdateResourceRequest{ + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, nil), + Schema: testIdentitySchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data testSchemaData + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + + resp.Identity.Raw = tftypes.NewValue(testIdentityType, nil) + + }, + }, + }, + }, + expectedResponse: &fwserver.UpdateResourceResponse{ + Diagnostics: []diag.Diagnostic{ + diag.NewErrorDiagnostic( + "Missing Resource Identity After Update", + "The Terraform Provider unexpectedly returned no resource identity data after having no errors in the resource update. This is always an issue in the Terraform Provider and should be reported to the provider developers.", + ), + }, + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, nil), + Schema: testIdentitySchema, + }, + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate, + }, + }, + "response-new-identity-null": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpdateResourceRequest{ + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testIdentitySchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data testSchemaData + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + + var identityData testIdentitySchemaData + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + resp.Diagnostics.Append(resp.Identity.Set(ctx, &identityData)...) + + }, + }, + }, + }, + expectedResponse: &fwserver.UpdateResourceResponse{ + Diagnostics: []diag.Diagnostic{ + diag.NewErrorDiagnostic( + "Missing Resource Identity After Update", + "The Terraform Provider unexpectedly returned no resource identity data after having no errors in the resource update. This is always an issue in the Terraform Provider and should be reported to the provider developers.", + ), + }, + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testIdentitySchema, + }, + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate, + }, + }, "response-newstate-semantic-equality": { server: &fwserver.Server{ Provider: &testprovider.Provider{},