Skip to content

Commit a6c07c2

Browse files
authored
resource: Prevent private state data loss on Delete method error (#864)
Reference: #863 The previous implementation assumed that Terraform core would preserve private state similar to "regular" state when a provider returned an error diagnostic while destroying a resource. The core implementation instead always uses the response data, which the framework was previously always discarding. This change corrects the errant design while also enabling provider logic to potentially update the private state in this situation.
1 parent cd4d1ec commit a6c07c2

File tree

8 files changed

+484
-2
lines changed

8 files changed

+484
-2
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: BUG FIXES
2+
body: 'resource: Add `Private` field to `DeleteResource` type, which was missing to
3+
allow provider logic to update private state on errors'
4+
time: 2023-10-23T16:57:12.447739-04:00
5+
custom:
6+
Issue: "863"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: BUG FIXES
2+
body: 'resource: Prevented private state data loss if resource destruction returned
3+
an error'
4+
time: 2023-10-23T16:58:14.585227-04:00
5+
custom:
6+
Issue: "863"

internal/fwserver/server_applyresourcechange_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,90 @@ func TestServerApplyResourceChange(t *testing.T) {
638638
}),
639639
Schema: testSchema,
640640
},
641+
Private: testEmptyPrivate,
642+
},
643+
},
644+
"delete-response-private": {
645+
server: &fwserver.Server{
646+
Provider: &testprovider.Provider{},
647+
},
648+
request: &fwserver.ApplyResourceChangeRequest{
649+
PriorState: &tfsdk.State{
650+
Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{
651+
"test_computed": tftypes.NewValue(tftypes.String, nil),
652+
"test_required": tftypes.NewValue(tftypes.String, "test-priorstate-value"),
653+
}),
654+
Schema: testSchema,
655+
},
656+
ResourceSchema: testSchema,
657+
Resource: &testprovider.Resource{
658+
DeleteMethod: func(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
659+
diags := resp.Private.SetKey(ctx, "providerKeyOne", []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`))
660+
661+
resp.Diagnostics.Append(diags...)
662+
663+
// Must return error to prevent automatic private state clearing
664+
resp.Diagnostics.AddError("error summary", "error detail")
665+
},
666+
},
667+
},
668+
expectedResponse: &fwserver.ApplyResourceChangeResponse{
669+
Diagnostics: diag.Diagnostics{
670+
diag.NewErrorDiagnostic(
671+
"error summary",
672+
"error detail",
673+
),
674+
},
675+
NewState: &tfsdk.State{
676+
Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{
677+
"test_computed": tftypes.NewValue(tftypes.String, nil),
678+
"test_required": tftypes.NewValue(tftypes.String, "test-priorstate-value"),
679+
}),
680+
Schema: testSchema,
681+
},
682+
Private: testPrivateProvider,
683+
},
684+
},
685+
"delete-response-private-updated": {
686+
server: &fwserver.Server{
687+
Provider: &testprovider.Provider{},
688+
},
689+
request: &fwserver.ApplyResourceChangeRequest{
690+
PlannedPrivate: testPrivateFramework,
691+
PriorState: &tfsdk.State{
692+
Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{
693+
"test_computed": tftypes.NewValue(tftypes.String, nil),
694+
"test_required": tftypes.NewValue(tftypes.String, "test-priorstate-value"),
695+
}),
696+
Schema: testSchema,
697+
},
698+
ResourceSchema: testSchema,
699+
Resource: &testprovider.Resource{
700+
DeleteMethod: func(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
701+
diags := resp.Private.SetKey(ctx, "providerKeyOne", []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`))
702+
703+
resp.Diagnostics.Append(diags...)
704+
705+
// Must return error to prevent automatic private state clearing
706+
resp.Diagnostics.AddError("error summary", "error detail")
707+
},
708+
},
709+
},
710+
expectedResponse: &fwserver.ApplyResourceChangeResponse{
711+
Diagnostics: diag.Diagnostics{
712+
diag.NewErrorDiagnostic(
713+
"error summary",
714+
"error detail",
715+
),
716+
},
717+
NewState: &tfsdk.State{
718+
Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{
719+
"test_computed": tftypes.NewValue(tftypes.String, nil),
720+
"test_required": tftypes.NewValue(tftypes.String, "test-priorstate-value"),
721+
}),
722+
Schema: testSchema,
723+
},
724+
Private: testPrivate,
641725
},
642726
},
643727
"delete-response-newstate": {

internal/fwserver/server_deleteresource.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,19 +82,42 @@ func (s *Server) DeleteResource(ctx context.Context, req *DeleteResourceRequest,
8282
deleteReq.ProviderMeta = *req.ProviderMeta
8383
}
8484

85+
privateProviderData := privatestate.EmptyProviderData(ctx)
86+
87+
deleteReq.Private = privateProviderData
88+
deleteResp.Private = privateProviderData
89+
8590
if req.PlannedPrivate != nil {
86-
deleteReq.Private = req.PlannedPrivate.Provider
91+
if req.PlannedPrivate.Provider != nil {
92+
deleteReq.Private = req.PlannedPrivate.Provider
93+
deleteResp.Private = req.PlannedPrivate.Provider
94+
}
95+
96+
resp.Private = req.PlannedPrivate
8797
}
8898

8999
logging.FrameworkTrace(ctx, "Calling provider defined Resource Delete")
90100
req.Resource.Delete(ctx, deleteReq, &deleteResp)
91101
logging.FrameworkTrace(ctx, "Called provider defined Resource Delete")
92102

93103
if !deleteResp.Diagnostics.HasError() {
94-
logging.FrameworkTrace(ctx, "No provider defined Delete errors detected, ensuring State is cleared")
104+
logging.FrameworkTrace(ctx, "No provider defined Delete errors detected, ensuring State and Priavate are cleared")
95105
deleteResp.State.RemoveResource(ctx)
106+
107+
// Preserve prior behavior of always returning nil.
108+
// Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/863
109+
deleteResp.Private = nil
110+
resp.Private = nil
96111
}
97112

98113
resp.Diagnostics = deleteResp.Diagnostics
99114
resp.NewState = &deleteResp.State
115+
116+
if deleteResp.Private != nil {
117+
if resp.Private == nil {
118+
resp.Private = &privatestate.Data{}
119+
}
120+
121+
resp.Private.Provider = deleteResp.Private
122+
}
100123
}

internal/fwserver/server_deleteresource_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,35 @@ func TestServerDeleteResource(t *testing.T) {
8181
TestProviderMetaAttribute types.String `tfsdk:"test_provider_meta_attribute"`
8282
}
8383

84+
testPrivateFrameworkMap := map[string][]byte{
85+
".frameworkKey": []byte(`{"fk": "framework value"}`),
86+
}
87+
8488
testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{
8589
"providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`),
8690
})
8791

8892
testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue)
8993

94+
testPrivate := &privatestate.Data{
95+
Framework: testPrivateFrameworkMap,
96+
Provider: testProviderData,
97+
}
98+
99+
testPrivateFramework := &privatestate.Data{
100+
Framework: testPrivateFrameworkMap,
101+
}
102+
103+
testPrivateProvider := &privatestate.Data{
104+
Provider: testProviderData,
105+
}
106+
107+
testEmptyProviderData := privatestate.EmptyProviderData(context.Background())
108+
109+
testEmptyPrivate := &privatestate.Data{
110+
Provider: testEmptyProviderData,
111+
}
112+
90113
testCases := map[string]struct {
91114
server *fwserver.Server
92115
request *fwserver.DeleteResourceRequest
@@ -245,6 +268,7 @@ func TestServerDeleteResource(t *testing.T) {
245268
}),
246269
Schema: testSchema,
247270
},
271+
Private: testEmptyPrivate,
248272
},
249273
},
250274
"resource-configure-data": {
@@ -310,6 +334,89 @@ func TestServerDeleteResource(t *testing.T) {
310334
NewState: testEmptyState,
311335
},
312336
},
337+
"response-private": {
338+
server: &fwserver.Server{
339+
Provider: &testprovider.Provider{},
340+
},
341+
request: &fwserver.DeleteResourceRequest{
342+
PriorState: &tfsdk.State{
343+
Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{
344+
"test_computed": tftypes.NewValue(tftypes.String, nil),
345+
"test_required": tftypes.NewValue(tftypes.String, "test-priorstate-value"),
346+
}),
347+
Schema: testSchema,
348+
},
349+
ResourceSchema: testSchema,
350+
Resource: &testprovider.Resource{
351+
DeleteMethod: func(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
352+
diags := resp.Private.SetKey(ctx, "providerKeyOne", []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`))
353+
354+
resp.Diagnostics.Append(diags...)
355+
356+
// Must return error to prevent automatic private state clearing
357+
resp.Diagnostics.AddError("error summary", "error detail")
358+
},
359+
},
360+
},
361+
expectedResponse: &fwserver.DeleteResourceResponse{
362+
Diagnostics: diag.Diagnostics{
363+
diag.NewErrorDiagnostic(
364+
"error summary",
365+
"error detail",
366+
),
367+
},
368+
NewState: &tfsdk.State{
369+
Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{
370+
"test_computed": tftypes.NewValue(tftypes.String, nil),
371+
"test_required": tftypes.NewValue(tftypes.String, "test-priorstate-value"),
372+
}),
373+
Schema: testSchema,
374+
},
375+
Private: testPrivateProvider,
376+
},
377+
},
378+
"response-private-updated": {
379+
server: &fwserver.Server{
380+
Provider: &testprovider.Provider{},
381+
},
382+
request: &fwserver.DeleteResourceRequest{
383+
PlannedPrivate: testPrivateFramework,
384+
PriorState: &tfsdk.State{
385+
Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{
386+
"test_computed": tftypes.NewValue(tftypes.String, nil),
387+
"test_required": tftypes.NewValue(tftypes.String, "test-priorstate-value"),
388+
}),
389+
Schema: testSchema,
390+
},
391+
ResourceSchema: testSchema,
392+
Resource: &testprovider.Resource{
393+
DeleteMethod: func(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
394+
diags := resp.Private.SetKey(ctx, "providerKeyOne", []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`))
395+
396+
resp.Diagnostics.Append(diags...)
397+
398+
// Must return error to prevent automatic private state clearing
399+
resp.Diagnostics.AddError("error summary", "error detail")
400+
},
401+
},
402+
},
403+
expectedResponse: &fwserver.DeleteResourceResponse{
404+
Diagnostics: diag.Diagnostics{
405+
diag.NewErrorDiagnostic(
406+
"error summary",
407+
"error detail",
408+
),
409+
},
410+
NewState: &tfsdk.State{
411+
Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{
412+
"test_computed": tftypes.NewValue(tftypes.String, nil),
413+
"test_required": tftypes.NewValue(tftypes.String, "test-priorstate-value"),
414+
}),
415+
Schema: testSchema,
416+
},
417+
Private: testPrivate,
418+
},
419+
},
313420
}
314421

315422
for name, testCase := range testCases {

0 commit comments

Comments
 (0)