From 91cf4cb0aad96c5effb535b191fcccceba9d39bd Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Tue, 9 Aug 2022 18:28:30 +0100 Subject: [PATCH] Implementing private state management for ModifyAttribute and ModifyResource (#399) Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/399 --- internal/fromproto5/planresourcechange.go | 11 +- .../fromproto5/planresourcechange_test.go | 29 +- internal/fromproto6/planresourcechange.go | 11 +- .../fromproto6/planresourcechange_test.go | 29 +- .../fwserver/attribute_plan_modification.go | 19 + .../attribute_plan_modification_test.go | 219 ++++++- internal/fwserver/block_plan_modification.go | 19 + .../fwserver/block_plan_modification_test.go | 133 +++- internal/fwserver/schema_plan_modification.go | 9 + .../fwserver/server_planresourcechange.go | 48 +- .../server_planresourcechange_test.go | 598 +++++++++++++++++- .../testing/planmodifiers/planmodifiers.go | 39 ++ internal/toproto5/planresourcechange.go | 11 +- internal/toproto5/planresourcechange_test.go | 42 +- internal/toproto6/planresourcechange.go | 11 +- internal/toproto6/planresourcechange_test.go | 42 +- resource/modify_plan.go | 8 + tfsdk/attribute_plan_modification.go | 7 + 18 files changed, 1238 insertions(+), 47 deletions(-) diff --git a/internal/fromproto5/planresourcechange.go b/internal/fromproto5/planresourcechange.go index 1745a129a..84239f79c 100644 --- a/internal/fromproto5/planresourcechange.go +++ b/internal/fromproto5/planresourcechange.go @@ -3,11 +3,13 @@ package fromproto5 import ( "context" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/provider" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" ) // PlanResourceChangeRequest returns the *fwserver.PlanResourceChangeRequest @@ -34,7 +36,6 @@ func PlanResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.PlanResour } fw := &fwserver.PlanResourceChangeRequest{ - PriorPrivate: proto5.PriorPrivate, ResourceSchema: resourceSchema, ResourceType: resourceType, } @@ -63,5 +64,11 @@ func PlanResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.PlanResour fw.ProviderMeta = providerMeta + privateData, privateDataDiags := privatestate.NewData(ctx, proto5.PriorPrivate) + + diags.Append(privateDataDiags...) + + fw.PriorPrivate = privateData + return fw, diags } diff --git a/internal/fromproto5/planresourcechange_test.go b/internal/fromproto5/planresourcechange_test.go index a34d9f9af..ced0f3068 100644 --- a/internal/fromproto5/planresourcechange_test.go +++ b/internal/fromproto5/planresourcechange_test.go @@ -5,15 +5,17 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestPlanResourceChangeRequest(t *testing.T) { @@ -44,6 +46,15 @@ func TestPlanResourceChangeRequest(t *testing.T) { }, } + testProviderKeyValue := marshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData, diags := privatestate.NewProviderData(context.Background(), testProviderKeyValue) + if diags.HasError() { + panic("error creating new provider data") + } + testCases := map[string]struct { input *tfprotov5.PlanResourceChangeRequest resourceSchema fwschema.Schema @@ -99,11 +110,19 @@ func TestPlanResourceChangeRequest(t *testing.T) { }, "priorprivate": { input: &tfprotov5.PlanResourceChangeRequest{ - PriorPrivate: []byte("{}"), + PriorPrivate: marshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), }, resourceSchema: testFwSchema, expected: &fwserver.PlanResourceChangeRequest{ - PriorPrivate: []byte("{}"), + PriorPrivate: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + }, + Provider: testProviderData, + }, ResourceSchema: testFwSchema, }, }, @@ -209,7 +228,7 @@ func TestPlanResourceChangeRequest(t *testing.T) { got, diags := fromproto5.PlanResourceChangeRequest(context.Background(), testCase.input, testCase.resourceType, testCase.resourceSchema, testCase.providerMetaSchema) - if diff := cmp.Diff(got, testCase.expected); diff != "" { + if diff := cmp.Diff(got, testCase.expected, cmp.AllowUnexported(privatestate.ProviderData{})); diff != "" { t.Errorf("unexpected difference: %s", diff) } diff --git a/internal/fromproto6/planresourcechange.go b/internal/fromproto6/planresourcechange.go index 9bb33f901..bf7e18b60 100644 --- a/internal/fromproto6/planresourcechange.go +++ b/internal/fromproto6/planresourcechange.go @@ -3,11 +3,13 @@ package fromproto6 import ( "context" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/provider" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" ) // PlanResourceChangeRequest returns the *fwserver.PlanResourceChangeRequest @@ -34,7 +36,6 @@ func PlanResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.PlanResour } fw := &fwserver.PlanResourceChangeRequest{ - PriorPrivate: proto6.PriorPrivate, ResourceSchema: resourceSchema, ResourceType: resourceType, } @@ -63,5 +64,11 @@ func PlanResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.PlanResour fw.ProviderMeta = providerMeta + privateData, privateDataDiags := privatestate.NewData(ctx, proto6.PriorPrivate) + + diags.Append(privateDataDiags...) + + fw.PriorPrivate = privateData + return fw, diags } diff --git a/internal/fromproto6/planresourcechange_test.go b/internal/fromproto6/planresourcechange_test.go index c85598bad..aaa39ab27 100644 --- a/internal/fromproto6/planresourcechange_test.go +++ b/internal/fromproto6/planresourcechange_test.go @@ -5,15 +5,17 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestPlanResourceChangeRequest(t *testing.T) { @@ -44,6 +46,15 @@ func TestPlanResourceChangeRequest(t *testing.T) { }, } + testProviderKeyValue := marshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData, diags := privatestate.NewProviderData(context.Background(), testProviderKeyValue) + if diags.HasError() { + panic("error creating new provider data") + } + testCases := map[string]struct { input *tfprotov6.PlanResourceChangeRequest resourceSchema fwschema.Schema @@ -99,11 +110,19 @@ func TestPlanResourceChangeRequest(t *testing.T) { }, "priorprivate": { input: &tfprotov6.PlanResourceChangeRequest{ - PriorPrivate: []byte("{}"), + PriorPrivate: marshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), }, resourceSchema: testFwSchema, expected: &fwserver.PlanResourceChangeRequest{ - PriorPrivate: []byte("{}"), + PriorPrivate: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + }, + Provider: testProviderData, + }, ResourceSchema: testFwSchema, }, }, @@ -209,7 +228,7 @@ func TestPlanResourceChangeRequest(t *testing.T) { got, diags := fromproto6.PlanResourceChangeRequest(context.Background(), testCase.input, testCase.resourceType, testCase.resourceSchema, testCase.providerMetaSchema) - if diff := cmp.Diff(got, testCase.expected); diff != "" { + if diff := cmp.Diff(got, testCase.expected, cmp.AllowUnexported(privatestate.ProviderData{})); diff != "" { t.Errorf("unexpected difference: %s", diff) } diff --git a/internal/fwserver/attribute_plan_modification.go b/internal/fwserver/attribute_plan_modification.go index 7b8839952..ae494b3bb 100644 --- a/internal/fwserver/attribute_plan_modification.go +++ b/internal/fwserver/attribute_plan_modification.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -49,11 +50,24 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo var requiresReplace bool + privateProviderData, diags := privatestate.NewProviderData(ctx, nil) + + resp.Diagnostics.Append(diags...) + + if diags.HasError() { + return + } + + if req.Private != nil { + privateProviderData = req.Private + } + if attributeWithPlanModifiers, ok := a.(fwxschema.AttributeWithPlanModifiers); ok { for _, planModifier := range attributeWithPlanModifiers.GetPlanModifiers() { modifyResp := &tfsdk.ModifyAttributePlanResponse{ AttributePlan: req.AttributePlan, RequiresReplace: requiresReplace, + Private: privateProviderData, } logging.FrameworkDebug( @@ -75,6 +89,7 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo req.AttributePlan = modifyResp.AttributePlan resp.Diagnostics.Append(modifyResp.Diagnostics...) requiresReplace = modifyResp.RequiresReplace + resp.Private = modifyResp.Private // Only on new errors. if modifyResp.Diagnostics.HasError() { @@ -122,6 +137,7 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo Plan: resp.Plan, ProviderMeta: req.ProviderMeta, State: req.State, + Private: privateProviderData, } AttributeModifyPlan(ctx, attr, attrReq, resp) @@ -149,6 +165,7 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo Plan: resp.Plan, ProviderMeta: req.ProviderMeta, State: req.State, + Private: privateProviderData, } AttributeModifyPlan(ctx, attr, attrReq, resp) @@ -176,6 +193,7 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo Plan: resp.Plan, ProviderMeta: req.ProviderMeta, State: req.State, + Private: privateProviderData, } AttributeModifyPlan(ctx, attr, attrReq, resp) @@ -206,6 +224,7 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo Plan: resp.Plan, ProviderMeta: req.ProviderMeta, State: req.State, + Private: privateProviderData, } AttributeModifyPlan(ctx, attr, attrReq, resp) diff --git a/internal/fwserver/attribute_plan_modification_test.go b/internal/fwserver/attribute_plan_modification_test.go index aeda70b3a..294970ce5 100644 --- a/internal/fwserver/attribute_plan_modification_test.go +++ b/internal/fwserver/attribute_plan_modification_test.go @@ -2,22 +2,39 @@ package fwserver import ( "context" + "encoding/json" "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/testing/planmodifiers" "github.com/hashicorp/terraform-plugin-framework/internal/totftypes" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestAttributeModifyPlan(t *testing.T) { t.Parallel() + testProviderKeyValue := marshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData, diags := privatestate.NewProviderData(context.Background(), testProviderKeyValue) + if diags.HasError() { + panic("error creating new provider data") + } + + testEmptyProviderData, diags := privatestate.NewProviderData(context.Background(), nil) + if diags.HasError() { + panic("error creating new empty provider data") + } + testCases := map[string]struct { req tfsdk.ModifyAttributePlanRequest resp ModifySchemaPlanResponse // Plan automatically copied from req @@ -726,6 +743,186 @@ func TestAttributeModifyPlan(t *testing.T) { }, }, }, + Private: testEmptyProviderData, + }, + }, + "attribute-request-private": { + req: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "TESTATTRONE"), + }), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test": { + Type: types.StringType, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.TestAttrPlanPrivateModifierGet{}, + }, + }, + }, + }, + }, + Plan: tfsdk.Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "TESTATTRONE"), + }), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test": { + Type: types.StringType, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.TestAttrPlanPrivateModifierGet{}, + }, + }, + }, + }, + }, + State: tfsdk.State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "TESTATTRONE"), + }), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test": { + Type: types.StringType, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.TestAttrPlanPrivateModifierGet{}, + }, + }, + }, + }, + }, + Private: testProviderData, + }, + resp: ModifySchemaPlanResponse{}, + expectedResp: ModifySchemaPlanResponse{ + Plan: tfsdk.Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "TESTATTRONE"), + }), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test": { + Type: types.StringType, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.TestAttrPlanPrivateModifierGet{}, + }, + }, + }, + }, + }, + Private: testProviderData, + }, + }, + "attribute-response-private": { + req: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "TESTATTRONE"), + }), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test": { + Type: types.StringType, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.TestAttrPlanPrivateModifierSet{}, + }, + }, + }, + }, + }, + Plan: tfsdk.Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "TESTATTRONE"), + }), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test": { + Type: types.StringType, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.TestAttrPlanPrivateModifierSet{}, + }, + }, + }, + }, + }, + State: tfsdk.State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "TESTATTRONE"), + }), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test": { + Type: types.StringType, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.TestAttrPlanPrivateModifierSet{}, + }, + }, + }, + }, + }, + }, + resp: ModifySchemaPlanResponse{}, + expectedResp: ModifySchemaPlanResponse{ + Plan: tfsdk.Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "TESTATTRONE"), + }), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test": { + Type: types.StringType, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.TestAttrPlanPrivateModifierSet{}, + }, + }, + }, + }, + }, + Private: testProviderData, }, }, "attribute-plan-previous-error": { @@ -831,6 +1028,7 @@ func TestAttributeModifyPlan(t *testing.T) { }, }, }, + Private: testEmptyProviderData, }, }, "requires-replacement": { @@ -919,6 +1117,7 @@ func TestAttributeModifyPlan(t *testing.T) { RequiresReplace: path.Paths{ path.Root("test"), }, + Private: testEmptyProviderData, }, }, "requires-replacement-previous-error": { @@ -1020,6 +1219,7 @@ func TestAttributeModifyPlan(t *testing.T) { RequiresReplace: path.Paths{ path.Root("test"), }, + Private: testEmptyProviderData, }, }, "requires-replacement-passthrough": { @@ -1111,6 +1311,7 @@ func TestAttributeModifyPlan(t *testing.T) { RequiresReplace: path.Paths{ path.Root("test"), }, + Private: testEmptyProviderData, }, }, "requires-replacement-unset": { @@ -1199,6 +1400,7 @@ func TestAttributeModifyPlan(t *testing.T) { }, }, }, + Private: testEmptyProviderData, }, }, "warnings": { @@ -1300,6 +1502,7 @@ func TestAttributeModifyPlan(t *testing.T) { }, }, }, + Private: testEmptyProviderData, }, }, "warnings-previous-error": { @@ -1412,6 +1615,7 @@ func TestAttributeModifyPlan(t *testing.T) { }, }, }, + Private: testEmptyProviderData, }, }, "error": { @@ -1510,6 +1714,7 @@ func TestAttributeModifyPlan(t *testing.T) { }, }, }, + Private: testEmptyProviderData, }, }, "error-previous-error": { @@ -1619,6 +1824,7 @@ func TestAttributeModifyPlan(t *testing.T) { }, }, }, + Private: testEmptyProviderData, }, }, } @@ -1652,9 +1858,18 @@ func TestAttributeModifyPlan(t *testing.T) { AttributeModifyPlan(context.Background(), attribute, tc.req, &tc.resp) - if diff := cmp.Diff(tc.expectedResp, tc.resp); diff != "" { + if diff := cmp.Diff(tc.expectedResp, tc.resp, cmp.AllowUnexported(privatestate.ProviderData{})); diff != "" { t.Errorf("Unexpected response (-wanted, +got): %s", diff) } }) } } + +func marshalToJson(input map[string][]byte) []byte { + output, err := json.Marshal(input) + if err != nil { + panic(err) + } + + return output +} diff --git a/internal/fwserver/block_plan_modification.go b/internal/fwserver/block_plan_modification.go index 7cdfde66f..544be4d38 100644 --- a/internal/fwserver/block_plan_modification.go +++ b/internal/fwserver/block_plan_modification.go @@ -6,6 +6,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -46,11 +47,24 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr var requiresReplace bool + privateProviderData, diags := privatestate.NewProviderData(ctx, nil) + + resp.Diagnostics.Append(diags...) + + if diags.HasError() { + return + } + + if req.Private != nil { + privateProviderData = req.Private + } + if blockWithPlanModifiers, ok := b.(fwxschema.BlockWithPlanModifiers); ok { for _, planModifier := range blockWithPlanModifiers.GetPlanModifiers() { modifyResp := &tfsdk.ModifyAttributePlanResponse{ AttributePlan: req.AttributePlan, RequiresReplace: requiresReplace, + Private: privateProviderData, } planModifier.Modify(ctx, req, modifyResp) @@ -58,6 +72,7 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr req.AttributePlan = modifyResp.AttributePlan resp.Diagnostics.Append(modifyResp.Diagnostics...) requiresReplace = modifyResp.RequiresReplace + resp.Private = modifyResp.Private // Only on new errors. if modifyResp.Diagnostics.HasError() { @@ -101,6 +116,7 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr Plan: resp.Plan, ProviderMeta: req.ProviderMeta, State: req.State, + Private: privateProviderData, } AttributeModifyPlan(ctx, attr, attrReq, resp) @@ -113,6 +129,7 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr Plan: resp.Plan, ProviderMeta: req.ProviderMeta, State: req.State, + Private: privateProviderData, } BlockModifyPlan(ctx, block, blockReq, resp) @@ -140,6 +157,7 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr Plan: resp.Plan, ProviderMeta: req.ProviderMeta, State: req.State, + Private: privateProviderData, } AttributeModifyPlan(ctx, attr, attrReq, resp) @@ -152,6 +170,7 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr Plan: resp.Plan, ProviderMeta: req.ProviderMeta, State: req.State, + Private: privateProviderData, } BlockModifyPlan(ctx, block, blockReq, resp) diff --git a/internal/fwserver/block_plan_modification_test.go b/internal/fwserver/block_plan_modification_test.go index 550e1ebe5..a88b743f6 100644 --- a/internal/fwserver/block_plan_modification_test.go +++ b/internal/fwserver/block_plan_modification_test.go @@ -5,14 +5,16 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/testing/planmodifiers" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestBlockModifyPlan(t *testing.T) { @@ -125,6 +127,27 @@ func TestBlockModifyPlan(t *testing.T) { } } + modifyAttributePlanWithPrivateRequest := func(attrPath path.Path, schema tfsdk.Schema, values modifyAttributePlanValues, privateProviderData *privatestate.ProviderData) tfsdk.ModifyAttributePlanRequest { + req := modifyAttributePlanRequest(attrPath, schema, values) + req.Private = privateProviderData + + return req + } + + providerKeyValue := marshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData, diags := privatestate.NewProviderData(context.Background(), providerKeyValue) + if diags.HasError() { + panic("error creating new provider data") + } + + testEmptyProviderData, diags := privatestate.NewProviderData(context.Background(), nil) + if diags.HasError() { + panic("error creating new empty provider data") + } + testCases := map[string]struct { req tfsdk.ModifyAttributePlanRequest resp ModifySchemaPlanResponse // Plan automatically copied from req @@ -168,6 +191,54 @@ func TestBlockModifyPlan(t *testing.T) { testBlockPlanModifierNullList{}, }, nil), }, + Private: testEmptyProviderData, + }, + }, + "block-request-private": { + req: modifyAttributePlanWithPrivateRequest( + path.Root("test"), + schema([]tfsdk.AttributePlanModifier{ + testBlockPlanModifierPrivateGet{}, + }, nil), + modifyAttributePlanValues{ + config: "TESTATTRONE", + plan: "TESTATTRONE", + state: "TESTATTRONE", + }, + testProviderData, + ), + resp: ModifySchemaPlanResponse{}, + expectedResp: ModifySchemaPlanResponse{ + Plan: tfsdk.Plan{ + Raw: schemaTfValue("TESTATTRONE"), + Schema: schema([]tfsdk.AttributePlanModifier{ + testBlockPlanModifierPrivateGet{}, + }, nil), + }, + Private: testProviderData, + }, + }, + "block-response-private": { + req: modifyAttributePlanRequest( + path.Root("test"), + schema([]tfsdk.AttributePlanModifier{ + testBlockPlanModifierPrivateSet{}, + }, nil), + modifyAttributePlanValues{ + config: "TESTATTRONE", + plan: "TESTATTRONE", + state: "TESTATTRONE", + }, + ), + resp: ModifySchemaPlanResponse{}, + expectedResp: ModifySchemaPlanResponse{ + Plan: tfsdk.Plan{ + Raw: schemaTfValue("TESTATTRONE"), + Schema: schema([]tfsdk.AttributePlanModifier{ + testBlockPlanModifierPrivateSet{}, + }, nil), + }, + Private: testProviderData, }, }, "block-modified-previous-error": { @@ -203,6 +274,7 @@ func TestBlockModifyPlan(t *testing.T) { testBlockPlanModifierNullList{}, }, nil), }, + Private: testEmptyProviderData, }, }, "block-requires-replacement": { @@ -228,6 +300,7 @@ func TestBlockModifyPlan(t *testing.T) { RequiresReplace: path.Paths{ path.Root("test"), }, + Private: testEmptyProviderData, }, }, "block-requires-replacement-previous-error": { @@ -266,6 +339,7 @@ func TestBlockModifyPlan(t *testing.T) { RequiresReplace: path.Paths{ path.Root("test"), }, + Private: testEmptyProviderData, }, }, "block-requires-replacement-passthrough": { @@ -293,6 +367,7 @@ func TestBlockModifyPlan(t *testing.T) { RequiresReplace: path.Paths{ path.Root("test"), }, + Private: testEmptyProviderData, }, }, "block-requires-replacement-unset": { @@ -317,6 +392,7 @@ func TestBlockModifyPlan(t *testing.T) { planmodifiers.TestRequiresReplaceFalseModifier{}, }, nil), }, + Private: testEmptyProviderData, }, }, "block-warnings": { @@ -350,6 +426,7 @@ func TestBlockModifyPlan(t *testing.T) { planmodifiers.TestWarningDiagModifier{}, }, nil), }, + Private: testEmptyProviderData, }, }, "block-warnings-previous-error": { @@ -394,6 +471,7 @@ func TestBlockModifyPlan(t *testing.T) { planmodifiers.TestWarningDiagModifier{}, }, nil), }, + Private: testEmptyProviderData, }, }, "block-error": { @@ -424,6 +502,7 @@ func TestBlockModifyPlan(t *testing.T) { planmodifiers.TestErrorDiagModifier{}, }, nil), }, + Private: testEmptyProviderData, }, }, "block-error-previous-error": { @@ -465,6 +544,7 @@ func TestBlockModifyPlan(t *testing.T) { planmodifiers.TestErrorDiagModifier{}, }, nil), }, + Private: testEmptyProviderData, }, }, "nested-attribute-modified": { @@ -489,6 +569,7 @@ func TestBlockModifyPlan(t *testing.T) { planmodifiers.TestAttrPlanValueModifierTwo{}, }), }, + Private: testEmptyProviderData, }, }, "nested-attribute-modified-previous-error": { @@ -526,6 +607,7 @@ func TestBlockModifyPlan(t *testing.T) { planmodifiers.TestAttrPlanValueModifierTwo{}, }), }, + Private: testEmptyProviderData, }, }, "nested-attribute-requires-replacement": { @@ -551,6 +633,7 @@ func TestBlockModifyPlan(t *testing.T) { RequiresReplace: path.Paths{ path.Root("test").AtListIndex(0).AtName("nested_attr"), }, + Private: testEmptyProviderData, }, }, "nested-attribute-requires-replacement-previous-error": { @@ -589,6 +672,7 @@ func TestBlockModifyPlan(t *testing.T) { RequiresReplace: path.Paths{ path.Root("test").AtListIndex(0).AtName("nested_attr"), }, + Private: testEmptyProviderData, }, }, "nested-attribute-requires-replacement-passthrough": { @@ -616,6 +700,7 @@ func TestBlockModifyPlan(t *testing.T) { RequiresReplace: path.Paths{ path.Root("test").AtListIndex(0).AtName("nested_attr"), }, + Private: testEmptyProviderData, }, }, "nested-attribute-requires-replacement-unset": { @@ -640,6 +725,7 @@ func TestBlockModifyPlan(t *testing.T) { planmodifiers.TestRequiresReplaceFalseModifier{}, }), }, + Private: testEmptyProviderData, }, }, "nested-attribute-warnings": { @@ -673,6 +759,7 @@ func TestBlockModifyPlan(t *testing.T) { planmodifiers.TestWarningDiagModifier{}, }), }, + Private: testEmptyProviderData, }, }, "nested-attribute-warnings-previous-error": { @@ -717,6 +804,7 @@ func TestBlockModifyPlan(t *testing.T) { planmodifiers.TestWarningDiagModifier{}, }), }, + Private: testEmptyProviderData, }, }, "nested-attribute-error": { @@ -747,6 +835,7 @@ func TestBlockModifyPlan(t *testing.T) { planmodifiers.TestErrorDiagModifier{}, }), }, + Private: testEmptyProviderData, }, }, "nested-attribute-error-previous-error": { @@ -788,6 +877,7 @@ func TestBlockModifyPlan(t *testing.T) { planmodifiers.TestErrorDiagModifier{}, }), }, + Private: testEmptyProviderData, }, }, } @@ -807,7 +897,7 @@ func TestBlockModifyPlan(t *testing.T) { BlockModifyPlan(context.Background(), block, tc.req, &tc.resp) - if diff := cmp.Diff(tc.expectedResp, tc.resp); diff != "" { + if diff := cmp.Diff(tc.expectedResp, tc.resp, cmp.AllowUnexported(privatestate.ProviderData{})); diff != "" { t.Errorf("Unexpected response (+wanted, -got): %s", diff) } }) @@ -839,3 +929,42 @@ func (t testBlockPlanModifierNullList) Description(ctx context.Context) string { func (t testBlockPlanModifierNullList) MarkdownDescription(ctx context.Context) string { return "This plan modifier is for use during testing only" } + +type testBlockPlanModifierPrivateGet struct{} + +func (t testBlockPlanModifierPrivateGet) Modify(ctx context.Context, req tfsdk.ModifyAttributePlanRequest, resp *tfsdk.ModifyAttributePlanResponse) { + expected := `{"pKeyOne": {"k0": "zero", "k1": 1}}` + + key := "providerKeyOne" + got, diags := req.Private.GetKey(ctx, key) + + resp.Diagnostics.Append(diags...) + + if string(got) != expected { + resp.Diagnostics.AddError("unexpected req.Private.Provider value: %s", string(got)) + } +} + +func (t testBlockPlanModifierPrivateGet) Description(ctx context.Context) string { + return "This plan modifier is for use during testing only" +} + +func (t testBlockPlanModifierPrivateGet) MarkdownDescription(ctx context.Context) string { + return "This plan modifier is for use during testing only" +} + +type testBlockPlanModifierPrivateSet struct{} + +func (t testBlockPlanModifierPrivateSet) Modify(ctx context.Context, req tfsdk.ModifyAttributePlanRequest, resp *tfsdk.ModifyAttributePlanResponse) { + diags := resp.Private.SetKey(ctx, "providerKeyOne", []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`)) + + resp.Diagnostics.Append(diags...) +} + +func (t testBlockPlanModifierPrivateSet) Description(ctx context.Context) string { + return "This plan modifier is for use during testing only" +} + +func (t testBlockPlanModifierPrivateSet) MarkdownDescription(ctx context.Context) string { + return "This plan modifier is for use during testing only" +} diff --git a/internal/fwserver/schema_plan_modification.go b/internal/fwserver/schema_plan_modification.go index 9138f6eca..2c8fb9993 100644 --- a/internal/fwserver/schema_plan_modification.go +++ b/internal/fwserver/schema_plan_modification.go @@ -5,6 +5,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -23,6 +24,9 @@ type ModifySchemaPlanRequest struct { // ProviderMeta is metadata from the provider_meta block of the module. ProviderMeta tfsdk.Config + + // Private is provider private state data. + Private *privatestate.ProviderData } // ModifySchemaPlanResponse represents a response to a ModifySchemaPlanRequest. @@ -36,6 +40,9 @@ type ModifySchemaPlanResponse struct { // recreated. RequiresReplace path.Paths + // Private is provider private state data following potential modifications. + Private *privatestate.ProviderData + // Diagnostics report errors or warnings related to running all attribute // plan modifiers. Returning an empty slice indicates a successful // plan modification with no warnings or errors generated. @@ -57,6 +64,7 @@ func SchemaModifyPlan(ctx context.Context, s fwschema.Schema, req ModifySchemaPl State: req.State, Plan: req.Plan, ProviderMeta: req.ProviderMeta, + Private: req.Private, } AttributeModifyPlan(ctx, attribute, attrReq, resp) @@ -69,6 +77,7 @@ func SchemaModifyPlan(ctx context.Context, s fwschema.Schema, req ModifySchemaPl State: req.State, Plan: req.Plan, ProviderMeta: req.ProviderMeta, + Private: req.Private, } BlockModifyPlan(ctx, block, blockReq, resp) diff --git a/internal/fwserver/server_planresourcechange.go b/internal/fwserver/server_planresourcechange.go index c6a86dbb2..ec1d1209f 100644 --- a/internal/fwserver/server_planresourcechange.go +++ b/internal/fwserver/server_planresourcechange.go @@ -6,21 +6,23 @@ import ( "fmt" "sort" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // PlanResourceChangeRequest is the framework server request for the // PlanResourceChange RPC. type PlanResourceChangeRequest struct { Config *tfsdk.Config - PriorPrivate []byte + PriorPrivate *privatestate.Data PriorState *tfsdk.State ProposedNewState *tfsdk.Plan ProviderMeta *tfsdk.Config @@ -32,7 +34,7 @@ type PlanResourceChangeRequest struct { // PlanResourceChange RPC. type PlanResourceChangeResponse struct { Diagnostics diag.Diagnostics - PlannedPrivate []byte + PlannedPrivate *privatestate.Data PlannedState *tfsdk.State RequiresReplace path.Paths } @@ -79,6 +81,28 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange } } + privateData := &privatestate.Data{} + privateProviderData, diags := privatestate.NewProviderData(ctx, nil) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + // Ensure that resp.PlannedPrivate is never nil. + resp.PlannedPrivate = privateData + + if req.PriorPrivate != nil { + if req.PriorPrivate.Provider != nil { + privateProviderData = req.PriorPrivate.Provider + } + + // Overwrite resp.PlannedPrivate with req.PriorPrivate providing + // it is not nil. + resp.PlannedPrivate = req.PriorPrivate + } + resp.PlannedState = planToState(*req.ProposedNewState) // Execute any AttributePlanModifiers. @@ -159,9 +183,10 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange // represents a resource being deleted and there's no point. if !resp.PlannedState.Raw.IsNull() { modifySchemaPlanReq := ModifySchemaPlanRequest{ - Config: *req.Config, - Plan: stateToPlan(*resp.PlannedState), - State: *req.PriorState, + Config: *req.Config, + Plan: stateToPlan(*resp.PlannedState), + State: *req.PriorState, + Private: privateProviderData, } if req.ProviderMeta != nil { @@ -171,6 +196,7 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange modifySchemaPlanResp := ModifySchemaPlanResponse{ Diagnostics: resp.Diagnostics, Plan: modifySchemaPlanReq.Plan, + Private: privateProviderData, } SchemaModifyPlan(ctx, req.ResourceSchema, modifySchemaPlanReq, &modifySchemaPlanResp) @@ -178,6 +204,7 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange resp.Diagnostics = modifySchemaPlanResp.Diagnostics resp.PlannedState = planToState(modifySchemaPlanResp.Plan) resp.RequiresReplace = append(resp.RequiresReplace, modifySchemaPlanResp.RequiresReplace...) + resp.PlannedPrivate.Provider = modifySchemaPlanResp.Private if resp.Diagnostics.HasError() { return @@ -196,9 +223,10 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange logging.FrameworkTrace(ctx, "Resource implements ResourceWithModifyPlan") modifyPlanReq := resource.ModifyPlanRequest{ - Config: *req.Config, - Plan: stateToPlan(*resp.PlannedState), - State: *req.PriorState, + Config: *req.Config, + Plan: stateToPlan(*resp.PlannedState), + State: *req.PriorState, + Private: privateProviderData, } if req.ProviderMeta != nil { @@ -209,6 +237,7 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange Diagnostics: resp.Diagnostics, Plan: modifyPlanReq.Plan, RequiresReplace: path.Paths{}, + Private: privateProviderData, } logging.FrameworkDebug(ctx, "Calling provider defined Resource ModifyPlan") @@ -218,6 +247,7 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange resp.Diagnostics = modifyPlanResp.Diagnostics resp.PlannedState = planToState(modifyPlanResp.Plan) resp.RequiresReplace = append(resp.RequiresReplace, modifyPlanResp.RequiresReplace...) + resp.PlannedPrivate.Provider = modifyPlanResp.Private } // Ensure deterministic RequiresReplace by sorting and deduplicating diff --git a/internal/fwserver/server_planresourcechange_test.go b/internal/fwserver/server_planresourcechange_test.go index 94f0950b1..7b7cb0417 100644 --- a/internal/fwserver/server_planresourcechange_test.go +++ b/internal/fwserver/server_planresourcechange_test.go @@ -6,16 +6,18 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestMarkComputedNilsAsUnknown(t *testing.T) { @@ -320,6 +322,49 @@ func TestServerPlanResourceChange(t *testing.T) { }, } + testSchemaAttributePlanModifierPrivatePlanRequest := tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test_computed": { + Computed: true, + Type: types.StringType, + PlanModifiers: tfsdk.AttributePlanModifiers{ + &testprovider.AttributePlanModifier{ + ModifyMethod: func(ctx context.Context, req tfsdk.ModifyAttributePlanRequest, resp *tfsdk.ModifyAttributePlanResponse) { + expected := `{"pKeyOne": {"k0": "zero", "k1": 1}}` + + key := "providerKeyOne" + got, diags := req.Private.GetKey(ctx, key) + + resp.Diagnostics.Append(diags...) + + if string(got) != expected { + resp.Diagnostics.AddError("unexpected req.Private.Provider value: %s", string(got)) + } + }, + }, + }, + }, + }, + } + + testSchemaAttributePlanModifierPrivatePlanResponse := tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test_computed": { + Computed: true, + Type: types.StringType, + PlanModifiers: tfsdk.AttributePlanModifiers{ + &testprovider.AttributePlanModifier{ + ModifyMethod: func(ctx context.Context, req tfsdk.ModifyAttributePlanRequest, resp *tfsdk.ModifyAttributePlanResponse) { + diags := resp.Private.SetKey(ctx, "providerKeyOne", []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`)) + + resp.Diagnostics.Append(diags...) + }, + }, + }, + }, + }, + } + testSchemaAttributePlanModifierDiagnosticsError := tfsdk.Schema{ Attributes: map[string]tfsdk.Attribute{ "test_computed": { @@ -388,6 +433,37 @@ func TestServerPlanResourceChange(t *testing.T) { TestProviderMetaAttribute types.String `tfsdk:"test_provider_meta_attribute"` } + testPrivateFrameworkMap := map[string][]byte{ + ".frameworkKey": []byte(`{"fk": "framework value"}`), + } + + providerKeyValue := marshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData, diags := privatestate.NewProviderData(context.Background(), providerKeyValue) + if diags.HasError() { + panic("error creating new provider data") + } + + testPrivateProvider := &privatestate.Data{ + Provider: testProviderData, + } + + testPrivate := &privatestate.Data{ + Framework: testPrivateFrameworkMap, + Provider: testProviderData, + } + + emptyProviderData, diags := privatestate.NewProviderData(context.Background(), nil) + if diags.HasError() { + panic("error creating new empty provider data") + } + + emptyPrivate := &privatestate.Data{ + Provider: emptyProviderData, + } + testCases := map[string]struct { server *fwserver.Server request *fwserver.PlanResourceChangeRequest @@ -431,6 +507,49 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchema, }, + PlannedPrivate: emptyPrivate, + }, + }, + "create-attributeplanmodifier-request-privateplan": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchemaAttributePlanModifierPrivatePlanRequest, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchemaAttributePlanModifierPrivatePlanRequest, + }, + PriorState: testEmptyState, + ResourceSchema: testSchemaAttributePlanModifierPrivatePlanRequest, + ResourceType: &testprovider.ResourceType{ + GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { + return testSchemaAttributePlanModifierPrivatePlanRequest, nil + }, + NewResourceMethod: func(_ context.Context, _ provider.Provider) (resource.Resource, diag.Diagnostics) { + return &testprovider.Resource{}, nil + }, + }, + PriorPrivate: testPrivate, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchemaAttributePlanModifierPrivatePlanRequest, + }, + PlannedPrivate: testPrivate, }, }, "create-attributeplanmodifier-response-attributeplan": { @@ -471,6 +590,48 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchemaAttributePlanModifierAttributePlan, }, + PlannedPrivate: emptyPrivate, + }, + }, + "create-attributeplanmodifier-response-privateplan": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchemaAttributePlanModifierPrivatePlanResponse, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchemaAttributePlanModifierPrivatePlanResponse, + }, + PriorState: testEmptyState, + ResourceSchema: testSchemaAttributePlanModifierPrivatePlanResponse, + ResourceType: &testprovider.ResourceType{ + GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { + return testSchemaAttributePlanModifierPrivatePlanResponse, nil + }, + NewResourceMethod: func(_ context.Context, _ provider.Provider) (resource.Resource, diag.Diagnostics) { + return &testprovider.Resource{}, nil + }, + }, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchemaAttributePlanModifierPrivatePlanResponse, + }, + PlannedPrivate: testPrivateProvider, }, }, "create-attributeplanmodifier-response-diagnostics": { @@ -517,6 +678,7 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchemaAttributePlanModifierDiagnosticsError, }, + PlannedPrivate: emptyPrivate, }, }, "create-attributeplanmodifier-response-requiresreplace": { @@ -565,6 +727,7 @@ func TestServerPlanResourceChange(t *testing.T) { RequiresReplace: path.Paths{ path.Root("test_required"), }, + PlannedPrivate: emptyPrivate, }, }, "create-resourcewithmodifyplan-request-config": { @@ -615,6 +778,62 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchema, }, + PlannedPrivate: emptyPrivate, + }, + }, + "create-resourcewithmodifyplan-request-private": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PriorState: testEmptyState, + ResourceSchema: testSchema, + ResourceType: &testprovider.ResourceType{ + GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { + return testSchema, nil + }, + NewResourceMethod: func(_ context.Context, _ provider.Provider) (resource.Resource, diag.Diagnostics) { + return &testprovider.ResourceWithModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + expected := `{"pKeyOne": {"k0": "zero", "k1": 1}}` + + key := "providerKeyOne" + got, diags := req.Private.GetKey(ctx, key) + + resp.Diagnostics.Append(diags...) + + if string(got) != expected { + resp.Diagnostics.AddError("unexpected req.Private.Provider value: %s", string(got)) + } + }, + }, nil + }, + }, + PriorPrivate: testPrivate, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PlannedPrivate: testPrivate, }, }, "create-resourcewithmodifyplan-request-proposednewstate": { @@ -665,6 +884,7 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchema, }, + PlannedPrivate: emptyPrivate, }, }, "create-resourcewithmodifyplan-request-providermeta": { @@ -716,6 +936,7 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchema, }, + PlannedPrivate: emptyPrivate, }, }, "create-resourcewithmodifyplan-response-diagnostics": { @@ -765,6 +986,7 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchema, }, + PlannedPrivate: emptyPrivate, }, }, "create-resourcewithmodifyplan-response-plannedstate": { @@ -815,6 +1037,54 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchema, }, + PlannedPrivate: emptyPrivate, + }, + }, + "create-resourcewithmodifyplan-response-private": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PriorState: testEmptyState, + ResourceSchema: testSchema, + ResourceType: &testprovider.ResourceType{ + GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { + return testSchema, nil + }, + NewResourceMethod: func(_ context.Context, _ provider.Provider) (resource.Resource, diag.Diagnostics) { + return &testprovider.ResourceWithModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + diags := resp.Private.SetKey(ctx, "providerKeyOne", []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`)) + + resp.Diagnostics.Append(diags...) + }, + }, nil + }, + }, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PlannedPrivate: testPrivateProvider, }, }, "create-resourcewithmodifyplan-response-requiresreplace": { @@ -870,6 +1140,7 @@ func TestServerPlanResourceChange(t *testing.T) { RequiresReplace: path.Paths{ path.Root("test_required"), }, + PlannedPrivate: emptyPrivate, }, }, "delete-resourcewithmodifyplan-request-config": { @@ -913,7 +1184,57 @@ func TestServerPlanResourceChange(t *testing.T) { }, }, expectedResponse: &fwserver.PlanResourceChangeResponse{ - PlannedState: testEmptyState, + PlannedState: testEmptyState, + PlannedPrivate: emptyPrivate, + }, + }, + "delete-resourcewithmodifyplan-request-private": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + ProposedNewState: testEmptyPlan, + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-state-value"), + }), + Schema: testSchema, + }, + ResourceSchema: testSchema, + ResourceType: &testprovider.ResourceType{ + GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { + return testSchema, nil + }, + NewResourceMethod: func(_ context.Context, _ provider.Provider) (resource.Resource, diag.Diagnostics) { + return &testprovider.ResourceWithModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + expected := `{"pKeyOne": {"k0": "zero", "k1": 1}}` + + key := "providerKeyOne" + got, diags := req.Private.GetKey(ctx, key) + + resp.Diagnostics.Append(diags...) + + if string(got) != expected { + resp.Diagnostics.AddError("unexpected req.Private.Provider value: %s", string(got)) + } + }, + }, nil + }, + }, + PriorPrivate: testPrivateProvider, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + PlannedState: testEmptyState, + PlannedPrivate: testPrivateProvider, }, }, "delete-resourcewithmodifyplan-request-priorstate": { @@ -957,7 +1278,8 @@ func TestServerPlanResourceChange(t *testing.T) { }, }, expectedResponse: &fwserver.PlanResourceChangeResponse{ - PlannedState: testEmptyState, + PlannedState: testEmptyState, + PlannedPrivate: emptyPrivate, }, }, "delete-resourcewithmodifyplan-request-providermeta": { @@ -1002,7 +1324,8 @@ func TestServerPlanResourceChange(t *testing.T) { }, }, expectedResponse: &fwserver.PlanResourceChangeResponse{ - PlannedState: testEmptyState, + PlannedState: testEmptyState, + PlannedPrivate: emptyPrivate, }, }, "delete-resourcewithmodifyplan-response-diagnostics": { @@ -1045,7 +1368,8 @@ func TestServerPlanResourceChange(t *testing.T) { diag.NewWarningDiagnostic("warning summary", "warning detail"), diag.NewErrorDiagnostic("error summary", "error detail"), }, - PlannedState: testEmptyState, + PlannedState: testEmptyState, + PlannedPrivate: emptyPrivate, }, }, "delete-resourcewithmodifyplan-response-plannedstate": { @@ -1099,6 +1423,7 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchema, }, + PlannedPrivate: emptyPrivate, }, }, "delete-resourcewithmodifyplan-response-requiresreplace": { @@ -1148,6 +1473,48 @@ func TestServerPlanResourceChange(t *testing.T) { RequiresReplace: path.Paths{ path.Root("test_required"), }, + PlannedPrivate: emptyPrivate, + }, + }, + "delete-resourcewithmodifyplan-response-private": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + ProposedNewState: testEmptyPlan, + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-state-value"), + }), + Schema: testSchema, + }, + ResourceSchema: testSchema, + ResourceType: &testprovider.ResourceType{ + GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { + return testSchema, nil + }, + NewResourceMethod: func(_ context.Context, _ provider.Provider) (resource.Resource, diag.Diagnostics) { + return &testprovider.ResourceWithModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + diags := resp.Private.SetKey(ctx, "providerKeyOne", []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`)) + + resp.Diagnostics.Append(diags...) + }, + }, nil + }, + }, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + PlannedState: testEmptyState, + PlannedPrivate: testPrivateProvider, }, }, "update-mark-computed-config-nils-as-unknown": { @@ -1194,6 +1561,55 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchema, }, + PlannedPrivate: emptyPrivate, + }, + }, + "update-attributeplanmodifier-request-private": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchemaAttributePlanModifierPrivatePlanRequest, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchemaAttributePlanModifierPrivatePlanRequest, + }, + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + Schema: testSchemaAttributePlanModifierPrivatePlanRequest, + }, + ResourceSchema: testSchemaAttributePlanModifierPrivatePlanRequest, + ResourceType: &testprovider.ResourceType{ + GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { + return testSchemaAttributePlanModifierPrivatePlanRequest, nil + }, + NewResourceMethod: func(_ context.Context, _ provider.Provider) (resource.Resource, diag.Diagnostics) { + return &testprovider.Resource{}, nil + }, + }, + PriorPrivate: testPrivateProvider, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchemaAttributePlanModifierPrivatePlanRequest, + }, + PlannedPrivate: testPrivateProvider, }, }, "update-attributeplanmodifier-response-attributeplan": { @@ -1240,6 +1656,54 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchemaAttributePlanModifierAttributePlan, }, + PlannedPrivate: emptyPrivate, + }, + }, + "update-attributeplanmodifier-response-private": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchemaAttributePlanModifierPrivatePlanResponse, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchemaAttributePlanModifierPrivatePlanResponse, + }, + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + Schema: testSchemaAttributePlanModifierPrivatePlanResponse, + }, + ResourceSchema: testSchemaAttributePlanModifierPrivatePlanResponse, + ResourceType: &testprovider.ResourceType{ + GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { + return testSchemaAttributePlanModifierPrivatePlanResponse, nil + }, + NewResourceMethod: func(_ context.Context, _ provider.Provider) (resource.Resource, diag.Diagnostics) { + return &testprovider.Resource{}, nil + }, + }, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchemaAttributePlanModifierPrivatePlanResponse, + }, + PlannedPrivate: testPrivateProvider, }, }, "update-attributeplanmodifier-response-diagnostics": { @@ -1292,6 +1756,7 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchemaAttributePlanModifierDiagnosticsError, }, + PlannedPrivate: emptyPrivate, }, }, "update-attributeplanmodifier-response-requiresreplace": { @@ -1341,6 +1806,7 @@ func TestServerPlanResourceChange(t *testing.T) { RequiresReplace: path.Paths{ path.Root("test_required"), }, + PlannedPrivate: emptyPrivate, }, }, "update-resourcewithmodifyplan-request-config": { @@ -1397,6 +1863,7 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchema, }, + PlannedPrivate: emptyPrivate, }, }, "update-resourcewithmodifyplan-request-proposednewstate": { @@ -1453,6 +1920,7 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchema, }, + PlannedPrivate: emptyPrivate, }, }, "update-resourcewithmodifyplan-request-providermeta": { @@ -1510,6 +1978,68 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchema, }, + PlannedPrivate: emptyPrivate, + }, + }, + "update-resourcewithmodifyplan-request-private": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchema, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchema, + }, + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + Schema: testSchema, + }, + ResourceSchema: testSchema, + ResourceType: &testprovider.ResourceType{ + GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { + return testSchema, nil + }, + NewResourceMethod: func(_ context.Context, _ provider.Provider) (resource.Resource, diag.Diagnostics) { + return &testprovider.ResourceWithModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + expected := `{"pKeyOne": {"k0": "zero", "k1": 1}}` + + key := "providerKeyOne" + got, diags := req.Private.GetKey(ctx, key) + + resp.Diagnostics.Append(diags...) + + if string(got) != expected { + resp.Diagnostics.AddError("unexpected req.Private.Provider value: %s", string(got)) + } + }, + }, nil + }, + }, + PriorPrivate: testPrivate, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchema, + }, + PlannedPrivate: testPrivate, }, }, "update-resourcewithmodifyplan-response-diagnostics": { @@ -1565,6 +2095,7 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchema, }, + PlannedPrivate: emptyPrivate, }, }, "update-resourcewithmodifyplan-response-plannedstate": { @@ -1621,6 +2152,7 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchema, }, + PlannedPrivate: emptyPrivate, }, }, "update-resourcewithmodifyplan-response-requiresreplace": { @@ -1682,6 +2214,60 @@ func TestServerPlanResourceChange(t *testing.T) { RequiresReplace: path.Paths{ path.Root("test_required"), }, + PlannedPrivate: emptyPrivate, + }, + }, + "update-resourcewithmodifyplan-response-private": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchema, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchema, + }, + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + Schema: testSchema, + }, + ResourceSchema: testSchema, + ResourceType: &testprovider.ResourceType{ + GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { + return testSchema, nil + }, + NewResourceMethod: func(_ context.Context, _ provider.Provider) (resource.Resource, diag.Diagnostics) { + return &testprovider.ResourceWithModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + diags := resp.Private.SetKey(ctx, "providerKeyOne", []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`)) + + resp.Diagnostics.Append(diags...) + }, + }, nil + }, + }, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchema, + }, + PlannedPrivate: testPrivateProvider, }, }, } @@ -1695,7 +2281,7 @@ func TestServerPlanResourceChange(t *testing.T) { response := &fwserver.PlanResourceChangeResponse{} testCase.server.PlanResourceChange(context.Background(), testCase.request, response) - if diff := cmp.Diff(response, testCase.expectedResponse); diff != "" { + if diff := cmp.Diff(response, testCase.expectedResponse, cmp.AllowUnexported(privatestate.ProviderData{})); diff != "" { t.Errorf("unexpected difference: %s", diff) } }) diff --git a/internal/testing/planmodifiers/planmodifiers.go b/internal/testing/planmodifiers/planmodifiers.go index 228d4d5c9..827df2c9a 100644 --- a/internal/testing/planmodifiers/planmodifiers.go +++ b/internal/testing/planmodifiers/planmodifiers.go @@ -127,3 +127,42 @@ func (m TestRequiresReplaceFalseModifier) Description(ctx context.Context) strin func (m TestRequiresReplaceFalseModifier) MarkdownDescription(ctx context.Context) string { return "Always unsets requires replace." } + +type TestAttrPlanPrivateModifierGet struct{} + +func (t TestAttrPlanPrivateModifierGet) Modify(ctx context.Context, req tfsdk.ModifyAttributePlanRequest, resp *tfsdk.ModifyAttributePlanResponse) { + expected := `{"pKeyOne": {"k0": "zero", "k1": 1}}` + + key := "providerKeyOne" + got, diags := req.Private.GetKey(ctx, key) + + resp.Diagnostics.Append(diags...) + + if string(got) != expected { + resp.Diagnostics.AddError("unexpected req.Private.Provider value: %s", string(got)) + } +} + +func (t TestAttrPlanPrivateModifierGet) Description(ctx context.Context) string { + return "This plan modifier is for use during testing only" +} + +func (t TestAttrPlanPrivateModifierGet) MarkdownDescription(ctx context.Context) string { + return "This plan modifier is for use during testing only" +} + +type TestAttrPlanPrivateModifierSet struct{} + +func (t TestAttrPlanPrivateModifierSet) Modify(ctx context.Context, req tfsdk.ModifyAttributePlanRequest, resp *tfsdk.ModifyAttributePlanResponse) { + diags := resp.Private.SetKey(ctx, "providerKeyOne", []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`)) + + resp.Diagnostics.Append(diags...) +} + +func (t TestAttrPlanPrivateModifierSet) Description(ctx context.Context) string { + return "This plan modifier is for use during testing only" +} + +func (t TestAttrPlanPrivateModifierSet) MarkdownDescription(ctx context.Context) string { + return "This plan modifier is for use during testing only" +} diff --git a/internal/toproto5/planresourcechange.go b/internal/toproto5/planresourcechange.go index 69a831797..20a116980 100644 --- a/internal/toproto5/planresourcechange.go +++ b/internal/toproto5/planresourcechange.go @@ -3,9 +3,10 @@ package toproto5 import ( "context" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/totftypes" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" ) // PlanResourceChangeResponse returns the *tfprotov5.PlanResourceChangeResponse @@ -16,8 +17,7 @@ func PlanResourceChangeResponse(ctx context.Context, fw *fwserver.PlanResourceCh } proto5 := &tfprotov5.PlanResourceChangeResponse{ - Diagnostics: Diagnostics(ctx, fw.Diagnostics), - PlannedPrivate: fw.PlannedPrivate, + Diagnostics: Diagnostics(ctx, fw.Diagnostics), } plannedState, diags := State(ctx, fw.PlannedState) @@ -30,5 +30,10 @@ func PlanResourceChangeResponse(ctx context.Context, fw *fwserver.PlanResourceCh proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) proto5.RequiresReplace = requiresReplace + plannedPrivate, diags := fw.PlannedPrivate.Bytes(ctx) + + proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) + proto5.PlannedPrivate = plannedPrivate + return proto5 } diff --git a/internal/toproto5/planresourcechange_test.go b/internal/toproto5/planresourcechange_test.go index a342f9192..5c47c69af 100644 --- a/internal/toproto5/planresourcechange_test.go +++ b/internal/toproto5/planresourcechange_test.go @@ -5,14 +5,16 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestPlanResourceChangeResponse(t *testing.T) { @@ -58,6 +60,20 @@ func TestPlanResourceChangeResponse(t *testing.T) { }, } + testProviderKeyValue := marshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData, diags := privatestate.NewProviderData(context.Background(), testProviderKeyValue) + if diags.HasError() { + panic("error creating new provider data") + } + + testEmptyProviderData, diags := privatestate.NewProviderData(context.Background(), nil) + if diags.HasError() { + panic("error creating new empty provider data") + } + testCases := map[string]struct { input *fwserver.PlanResourceChangeResponse expected *tfprotov5.PlanResourceChangeResponse @@ -123,12 +139,30 @@ func TestPlanResourceChangeResponse(t *testing.T) { }, }, }, + "plannedprivate-empty": { + input: &fwserver.PlanResourceChangeResponse{ + PlannedPrivate: &privatestate.Data{ + Framework: map[string][]byte{}, + Provider: testEmptyProviderData, + }, + }, + expected: &tfprotov5.PlanResourceChangeResponse{ + PlannedPrivate: nil, + }, + }, "plannedprivate": { input: &fwserver.PlanResourceChangeResponse{ - PlannedPrivate: []byte("{}"), + PlannedPrivate: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`)}, + Provider: testProviderData, + }, }, expected: &tfprotov5.PlanResourceChangeResponse{ - PlannedPrivate: []byte("{}"), + PlannedPrivate: marshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), }, }, "plannedstate": { diff --git a/internal/toproto6/planresourcechange.go b/internal/toproto6/planresourcechange.go index 2590d681d..908abccf0 100644 --- a/internal/toproto6/planresourcechange.go +++ b/internal/toproto6/planresourcechange.go @@ -3,9 +3,10 @@ package toproto6 import ( "context" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/totftypes" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" ) // PlanResourceChangeResponse returns the *tfprotov6.PlanResourceChangeResponse @@ -16,8 +17,7 @@ func PlanResourceChangeResponse(ctx context.Context, fw *fwserver.PlanResourceCh } proto6 := &tfprotov6.PlanResourceChangeResponse{ - Diagnostics: Diagnostics(ctx, fw.Diagnostics), - PlannedPrivate: fw.PlannedPrivate, + Diagnostics: Diagnostics(ctx, fw.Diagnostics), } plannedState, diags := State(ctx, fw.PlannedState) @@ -30,5 +30,10 @@ func PlanResourceChangeResponse(ctx context.Context, fw *fwserver.PlanResourceCh proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) proto6.RequiresReplace = requiresReplace + plannedPrivate, diags := fw.PlannedPrivate.Bytes(ctx) + + proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) + proto6.PlannedPrivate = plannedPrivate + return proto6 } diff --git a/internal/toproto6/planresourcechange_test.go b/internal/toproto6/planresourcechange_test.go index 848dfb9ee..6232d8179 100644 --- a/internal/toproto6/planresourcechange_test.go +++ b/internal/toproto6/planresourcechange_test.go @@ -5,14 +5,16 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestPlanResourceChangeResponse(t *testing.T) { @@ -58,6 +60,20 @@ func TestPlanResourceChangeResponse(t *testing.T) { }, } + testProviderKeyValue := marshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData, diags := privatestate.NewProviderData(context.Background(), testProviderKeyValue) + if diags.HasError() { + panic("error creating new provider data") + } + + testEmptyProviderData, diags := privatestate.NewProviderData(context.Background(), nil) + if diags.HasError() { + panic("error creating new empty provider data") + } + testCases := map[string]struct { input *fwserver.PlanResourceChangeResponse expected *tfprotov6.PlanResourceChangeResponse @@ -123,12 +139,30 @@ func TestPlanResourceChangeResponse(t *testing.T) { }, }, }, + "plannedprivate-empty": { + input: &fwserver.PlanResourceChangeResponse{ + PlannedPrivate: &privatestate.Data{ + Framework: map[string][]byte{}, + Provider: testEmptyProviderData, + }, + }, + expected: &tfprotov6.PlanResourceChangeResponse{ + PlannedPrivate: nil, + }, + }, "plannedprivate": { input: &fwserver.PlanResourceChangeResponse{ - PlannedPrivate: []byte("{}"), + PlannedPrivate: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`)}, + Provider: testProviderData, + }, }, expected: &tfprotov6.PlanResourceChangeResponse{ - PlannedPrivate: []byte("{}"), + PlannedPrivate: marshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), }, }, "plannedstate": { diff --git a/resource/modify_plan.go b/resource/modify_plan.go index 0ead46623..7098474ab 100644 --- a/resource/modify_plan.go +++ b/resource/modify_plan.go @@ -2,6 +2,7 @@ package resource import ( "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -26,6 +27,9 @@ type ModifyPlanRequest struct { // ProviderMeta is metadata from the provider_meta block of the module. ProviderMeta tfsdk.Config + + // Private is provider private state data. + Private *privatestate.ProviderData } // ModifyPlanResponse represents a response to a @@ -42,6 +46,10 @@ type ModifyPlanResponse struct { // recreated. RequiresReplace path.Paths + // Private is provider private state data following potential modifications + // by provider developer. + Private *privatestate.ProviderData + // Diagnostics report errors or warnings related to determining the // planned state of the requested resource. Returning an empty slice // indicates a successful plan modification with no warnings or errors diff --git a/tfsdk/attribute_plan_modification.go b/tfsdk/attribute_plan_modification.go index 5dad778e7..1410b6481 100644 --- a/tfsdk/attribute_plan_modification.go +++ b/tfsdk/attribute_plan_modification.go @@ -5,6 +5,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/path" ) @@ -84,6 +85,9 @@ type ModifyAttributePlanRequest struct { // ProviderMeta is metadata from the provider_meta block of the module. ProviderMeta Config + + // Private is provider private state data. + Private *privatestate.ProviderData } // ModifyAttributePlanResponse represents a response to a @@ -97,6 +101,9 @@ type ModifyAttributePlanResponse struct { // requires replacement of the whole resource. RequiresReplace bool + // Private is provider private state data following potential modification. + Private *privatestate.ProviderData + // Diagnostics report errors or warnings related to determining the // planned state of the requested resource. Returning an empty slice // indicates a successful validation with no warnings or errors