diff --git a/.changelog/433.txt b/.changelog/433.txt new file mode 100644 index 000000000..75dd0b00f --- /dev/null +++ b/.changelog/433.txt @@ -0,0 +1,3 @@ +```release-note:note +A new internal package has been introduced which enables provider developers to read/write framework-managed private state data. +``` diff --git a/internal/fromproto5/applyresourcechange.go b/internal/fromproto5/applyresourcechange.go index f0392aa05..c658bc4f7 100644 --- a/internal/fromproto5/applyresourcechange.go +++ b/internal/fromproto5/applyresourcechange.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" ) // ApplyResourceChangeRequest returns the *fwserver.ApplyResourceChangeRequest @@ -34,7 +36,6 @@ func ApplyResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.ApplyReso } fw := &fwserver.ApplyResourceChangeRequest{ - PlannedPrivate: proto5.PlannedPrivate, ResourceSchema: resourceSchema, ResourceType: resourceType, } @@ -63,5 +64,11 @@ func ApplyResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.ApplyReso fw.ProviderMeta = providerMeta + privateData, privateDataDiags := privatestate.NewData(ctx, proto5.PlannedPrivate) + + diags.Append(privateDataDiags...) + + fw.PlannedPrivate = privateData + return fw, diags } diff --git a/internal/fromproto5/applyresourcechange_test.go b/internal/fromproto5/applyresourcechange_test.go index 947c963ab..ce66d840d 100644 --- a/internal/fromproto5/applyresourcechange_test.go +++ b/internal/fromproto5/applyresourcechange_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 TestApplyResourceChangeRequest(t *testing.T) { @@ -44,6 +46,14 @@ func TestApplyResourceChangeRequest(t *testing.T) { }, } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + testCases := map[string]struct { input *tfprotov5.ApplyResourceChangeRequest resourceSchema fwschema.Schema @@ -125,14 +135,50 @@ func TestApplyResourceChangeRequest(t *testing.T) { ResourceSchema: testFwSchema, }, }, - "plannedprivate": { + "plannedprivate-malformed-json": { input: &tfprotov5.ApplyResourceChangeRequest{ - PlannedPrivate: []byte("{}"), + PlannedPrivate: []byte(`{`), }, resourceSchema: testFwSchema, expected: &fwserver.ApplyResourceChangeRequest{ + ResourceSchema: testFwSchema, + }, expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Error Decoding Private State", + "An error was encountered when decoding private state: unexpected end of JSON input.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "plannedprivate-empty-json": { + input: &tfprotov5.ApplyResourceChangeRequest{ PlannedPrivate: []byte("{}"), + }, + resourceSchema: testFwSchema, + expected: &fwserver.ApplyResourceChangeRequest{ ResourceSchema: testFwSchema, + PlannedPrivate: &privatestate.Data{ + Framework: map[string][]byte{}, + Provider: testEmptyProviderData, + }, + }, + }, + "plannedprivate": { + input: &tfprotov5.ApplyResourceChangeRequest{ + PlannedPrivate: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), + }, + resourceSchema: testFwSchema, + expected: &fwserver.ApplyResourceChangeRequest{ + ResourceSchema: testFwSchema, + PlannedPrivate: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + }, + Provider: testProviderData, + }, }, }, "priorstate-missing-schema": { @@ -209,7 +255,7 @@ func TestApplyResourceChangeRequest(t *testing.T) { got, diags := fromproto5.ApplyResourceChangeRequest(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/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..d25c47ab7 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,12 @@ func TestPlanResourceChangeRequest(t *testing.T) { }, } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + testCases := map[string]struct { input *tfprotov5.PlanResourceChangeRequest resourceSchema fwschema.Schema @@ -99,11 +107,19 @@ func TestPlanResourceChangeRequest(t *testing.T) { }, "priorprivate": { input: &tfprotov5.PlanResourceChangeRequest{ - PriorPrivate: []byte("{}"), + PriorPrivate: privatestate.MustMarshalToJson(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 +225,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/fromproto5/readresource.go b/internal/fromproto5/readresource.go index 1c3f99bf2..ad20208ec 100644 --- a/internal/fromproto5/readresource.go +++ b/internal/fromproto5/readresource.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" ) // ReadResourceRequest returns the *fwserver.ReadResourceRequest @@ -20,7 +22,6 @@ func ReadResourceRequest(ctx context.Context, proto5 *tfprotov5.ReadResourceRequ var diags diag.Diagnostics fw := &fwserver.ReadResourceRequest{ - Private: proto5.Private, ResourceType: resourceType, } @@ -36,5 +37,11 @@ func ReadResourceRequest(ctx context.Context, proto5 *tfprotov5.ReadResourceRequ fw.ProviderMeta = providerMeta + privateData, privateDataDiags := privatestate.NewData(ctx, proto5.Private) + + diags.Append(privateDataDiags...) + + fw.Private = privateData + return fw, diags } diff --git a/internal/fromproto5/readresource_test.go b/internal/fromproto5/readresource_test.go index 302421f67..b5f262944 100644 --- a/internal/fromproto5/readresource_test.go +++ b/internal/fromproto5/readresource_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 TestReadResourceRequest(t *testing.T) { @@ -44,6 +46,14 @@ func TestReadResourceRequest(t *testing.T) { }, } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + testCases := map[string]struct { input *tfprotov5.ReadResourceRequest resourceSchema fwschema.Schema @@ -87,13 +97,47 @@ func TestReadResourceRequest(t *testing.T) { }, }, }, - "private": { + "private-malformed-json": { + input: &tfprotov5.ReadResourceRequest{ + Private: []byte(`{`), + }, + resourceSchema: testFwSchema, + expected: &fwserver.ReadResourceRequest{}, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Error Decoding Private State", + "An error was encountered when decoding private state: unexpected end of JSON input.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "private-empty-json": { input: &tfprotov5.ReadResourceRequest{ Private: []byte("{}"), }, resourceSchema: testFwSchema, expected: &fwserver.ReadResourceRequest{ - Private: []byte("{}"), + Private: &privatestate.Data{ + Framework: map[string][]byte{}, + Provider: testEmptyProviderData, + }, + }, + }, + "private": { + input: &tfprotov5.ReadResourceRequest{ + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), + }, + resourceSchema: testFwSchema, + expected: &fwserver.ReadResourceRequest{ + Private: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + }, + Provider: testProviderData, + }, }, }, "providermeta-missing-data": { @@ -136,7 +180,7 @@ func TestReadResourceRequest(t *testing.T) { got, diags := fromproto5.ReadResourceRequest(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/applyresourcechange.go b/internal/fromproto6/applyresourcechange.go index 22214253a..47f843e06 100644 --- a/internal/fromproto6/applyresourcechange.go +++ b/internal/fromproto6/applyresourcechange.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" ) // ApplyResourceChangeRequest returns the *fwserver.ApplyResourceChangeRequest @@ -34,7 +36,6 @@ func ApplyResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.ApplyReso } fw := &fwserver.ApplyResourceChangeRequest{ - PlannedPrivate: proto6.PlannedPrivate, ResourceSchema: resourceSchema, ResourceType: resourceType, } @@ -63,5 +64,11 @@ func ApplyResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.ApplyReso fw.ProviderMeta = providerMeta + privateData, privateDataDiags := privatestate.NewData(ctx, proto6.PlannedPrivate) + + diags.Append(privateDataDiags...) + + fw.PlannedPrivate = privateData + return fw, diags } diff --git a/internal/fromproto6/applyresourcechange_test.go b/internal/fromproto6/applyresourcechange_test.go index 76a930cf1..88af00265 100644 --- a/internal/fromproto6/applyresourcechange_test.go +++ b/internal/fromproto6/applyresourcechange_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 TestApplyResourceChangeRequest(t *testing.T) { @@ -44,6 +46,14 @@ func TestApplyResourceChangeRequest(t *testing.T) { }, } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + testCases := map[string]struct { input *tfprotov6.ApplyResourceChangeRequest resourceSchema fwschema.Schema @@ -125,14 +135,50 @@ func TestApplyResourceChangeRequest(t *testing.T) { ResourceSchema: testFwSchema, }, }, - "plannedprivate": { + "plannedprivate-malformed-json": { input: &tfprotov6.ApplyResourceChangeRequest{ - PlannedPrivate: []byte("{}"), + PlannedPrivate: []byte(`{`), }, resourceSchema: testFwSchema, expected: &fwserver.ApplyResourceChangeRequest{ + ResourceSchema: testFwSchema, + }, expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Error Decoding Private State", + "An error was encountered when decoding private state: unexpected end of JSON input.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "plannedprivate-empty-json": { + input: &tfprotov6.ApplyResourceChangeRequest{ PlannedPrivate: []byte("{}"), + }, + resourceSchema: testFwSchema, + expected: &fwserver.ApplyResourceChangeRequest{ ResourceSchema: testFwSchema, + PlannedPrivate: &privatestate.Data{ + Framework: map[string][]byte{}, + Provider: testEmptyProviderData, + }, + }, + }, + "plannedprivate": { + input: &tfprotov6.ApplyResourceChangeRequest{ + PlannedPrivate: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), + }, + resourceSchema: testFwSchema, + expected: &fwserver.ApplyResourceChangeRequest{ + ResourceSchema: testFwSchema, + PlannedPrivate: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + }, + Provider: testProviderData, + }, }, }, "priorstate-missing-schema": { @@ -209,7 +255,7 @@ func TestApplyResourceChangeRequest(t *testing.T) { got, diags := fromproto6.ApplyResourceChangeRequest(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..f15d680a5 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,12 @@ func TestPlanResourceChangeRequest(t *testing.T) { }, } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + testCases := map[string]struct { input *tfprotov6.PlanResourceChangeRequest resourceSchema fwschema.Schema @@ -99,11 +107,19 @@ func TestPlanResourceChangeRequest(t *testing.T) { }, "priorprivate": { input: &tfprotov6.PlanResourceChangeRequest{ - PriorPrivate: []byte("{}"), + PriorPrivate: privatestate.MustMarshalToJson(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 +225,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/fromproto6/readresource.go b/internal/fromproto6/readresource.go index f9d66a11e..6f6067371 100644 --- a/internal/fromproto6/readresource.go +++ b/internal/fromproto6/readresource.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" ) // ReadResourceRequest returns the *fwserver.ReadResourceRequest @@ -20,7 +22,6 @@ func ReadResourceRequest(ctx context.Context, proto6 *tfprotov6.ReadResourceRequ var diags diag.Diagnostics fw := &fwserver.ReadResourceRequest{ - Private: proto6.Private, ResourceType: resourceType, } @@ -36,5 +37,11 @@ func ReadResourceRequest(ctx context.Context, proto6 *tfprotov6.ReadResourceRequ fw.ProviderMeta = providerMeta + privateData, privateDataDiags := privatestate.NewData(ctx, proto6.Private) + + diags.Append(privateDataDiags...) + + fw.Private = privateData + return fw, diags } diff --git a/internal/fromproto6/readresource_test.go b/internal/fromproto6/readresource_test.go index 431e6f893..6c9311f97 100644 --- a/internal/fromproto6/readresource_test.go +++ b/internal/fromproto6/readresource_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 TestReadResourceRequest(t *testing.T) { @@ -44,6 +46,14 @@ func TestReadResourceRequest(t *testing.T) { }, } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + testCases := map[string]struct { input *tfprotov6.ReadResourceRequest resourceSchema fwschema.Schema @@ -87,13 +97,47 @@ func TestReadResourceRequest(t *testing.T) { }, }, }, - "private": { + "private-malformed-json": { + input: &tfprotov6.ReadResourceRequest{ + Private: []byte(`{`), + }, + resourceSchema: testFwSchema, + expected: &fwserver.ReadResourceRequest{}, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Error Decoding Private State", + "An error was encountered when decoding private state: unexpected end of JSON input.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "private-empty-json": { input: &tfprotov6.ReadResourceRequest{ Private: []byte("{}"), }, resourceSchema: testFwSchema, expected: &fwserver.ReadResourceRequest{ - Private: []byte("{}"), + Private: &privatestate.Data{ + Framework: map[string][]byte{}, + Provider: testEmptyProviderData, + }, + }, + }, + "private": { + input: &tfprotov6.ReadResourceRequest{ + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), + }, + resourceSchema: testFwSchema, + expected: &fwserver.ReadResourceRequest{ + Private: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + }, + Provider: testProviderData, + }, }, }, "providermeta-missing-data": { @@ -136,7 +180,7 @@ func TestReadResourceRequest(t *testing.T) { got, diags := fromproto6.ReadResourceRequest(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..23ffd5149 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,19 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo var requiresReplace bool + privateProviderData := privatestate.EmptyProviderData(ctx) + + if req.Private != nil { + resp.Private = req.Private + 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 +84,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 +132,7 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo Plan: resp.Plan, ProviderMeta: req.ProviderMeta, State: req.State, + Private: resp.Private, } AttributeModifyPlan(ctx, attr, attrReq, resp) @@ -149,6 +160,7 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo Plan: resp.Plan, ProviderMeta: req.ProviderMeta, State: req.State, + Private: resp.Private, } AttributeModifyPlan(ctx, attr, attrReq, resp) @@ -176,6 +188,7 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo Plan: resp.Plan, ProviderMeta: req.ProviderMeta, State: req.State, + Private: resp.Private, } AttributeModifyPlan(ctx, attr, attrReq, resp) @@ -206,6 +219,7 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo Plan: resp.Plan, ProviderMeta: req.ProviderMeta, State: req.State, + Private: resp.Private, } 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..d8d498f43 100644 --- a/internal/fwserver/attribute_plan_modification_test.go +++ b/internal/fwserver/attribute_plan_modification_test.go @@ -5,19 +5,29 @@ import ( "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 := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + testCases := map[string]struct { req tfsdk.ModifyAttributePlanRequest resp ModifySchemaPlanResponse // Plan automatically copied from req @@ -726,6 +736,1078 @@ 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-list-nested-private": { + req: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test": { + Attributes: tfsdk.ListNestedAttributes(map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierGet{}, + }, + }, + }), + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierSet{}, + }, + }, + }, + }, + }, + Plan: tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test": { + Attributes: tfsdk.ListNestedAttributes(map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierGet{}, + }, + }, + }), + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierSet{}, + }, + }, + }, + }, + }, + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test": { + Attributes: tfsdk.ListNestedAttributes(map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierGet{}, + }, + }, + }), + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierSet{}, + }, + }, + }, + }, + }, + }, + resp: ModifySchemaPlanResponse{}, + expectedResp: ModifySchemaPlanResponse{ + Plan: tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test": { + Attributes: tfsdk.ListNestedAttributes(map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierGet{}, + }, + }, + }), + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierSet{}, + }, + }, + }, + }, + }, + Private: testProviderData, + }, + }, + "attribute-set-nested-private": { + req: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test": { + Attributes: tfsdk.SetNestedAttributes(map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierGet{}, + }, + }, + }), + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierSet{}, + }, + }, + }, + }, + }, + Plan: tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test": { + Attributes: tfsdk.SetNestedAttributes(map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierGet{}, + }, + }, + }), + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierSet{}, + }, + }, + }, + }, + }, + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test": { + Attributes: tfsdk.SetNestedAttributes(map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierGet{}, + }, + }, + }), + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierSet{}, + }, + }, + }, + }, + }, + }, + resp: ModifySchemaPlanResponse{}, + expectedResp: ModifySchemaPlanResponse{ + Plan: tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test": { + Attributes: tfsdk.SetNestedAttributes(map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierGet{}, + }, + }, + }), + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierSet{}, + }, + }, + }, + }, + }, + Private: testProviderData, + }, + }, + "attribute-map-nested-private": { + req: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "testkey": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test": { + Attributes: tfsdk.MapNestedAttributes(map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierGet{}, + }, + }, + }), + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierSet{}, + }, + }, + }, + }, + }, + Plan: tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "testkey": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test": { + Attributes: tfsdk.MapNestedAttributes(map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierGet{}, + }, + }, + }), + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierSet{}, + }, + }, + }, + }, + }, + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "testkey": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test": { + Attributes: tfsdk.MapNestedAttributes(map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierGet{}, + }, + }, + }), + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierSet{}, + }, + }, + }, + }, + }, + }, + resp: ModifySchemaPlanResponse{}, + expectedResp: ModifySchemaPlanResponse{ + Plan: tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "testkey": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test": { + Attributes: tfsdk.MapNestedAttributes(map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierGet{}, + }, + }, + }), + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierSet{}, + }, + }, + }, + }, + }, + Private: testProviderData, + }, + }, + "attribute-single-nested-private": { + req: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testing": tftypes.String, + }, + }, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testing": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "testing": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test": { + Attributes: tfsdk.SingleNestedAttributes(map[string]tfsdk.Attribute{ + "testing": { + Type: types.StringType, + Optional: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierGet{}, + }, + }, + }), + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.TestAttrPlanPrivateModifierSet{}, + }, + }, + }, + }, + }, + Plan: tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testing": tftypes.String, + }, + }, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testing": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "testing": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test": { + Attributes: tfsdk.SingleNestedAttributes(map[string]tfsdk.Attribute{ + "testing": { + Type: types.StringType, + Optional: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierGet{}, + }, + }, + }), + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.TestAttrPlanPrivateModifierSet{}, + }, + }, + }, + }, + }, + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testing": tftypes.String, + }, + }, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testing": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "testing": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test": { + Attributes: tfsdk.SingleNestedAttributes(map[string]tfsdk.Attribute{ + "testing": { + Type: types.StringType, + Optional: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierGet{}, + }, + }, + }), + 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.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testing": tftypes.String, + }, + }, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "testing": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "testing": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test": { + Attributes: tfsdk.SingleNestedAttributes(map[string]tfsdk.Attribute{ + "testing": { + Type: types.StringType, + Optional: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierGet{}, + }, + }, + }), + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.TestAttrPlanPrivateModifierSet{}, + }, + }, + }, + }, + }, + Private: testProviderData, }, }, "attribute-plan-previous-error": { @@ -831,6 +1913,7 @@ func TestAttributeModifyPlan(t *testing.T) { }, }, }, + Private: testEmptyProviderData, }, }, "requires-replacement": { @@ -919,6 +2002,7 @@ func TestAttributeModifyPlan(t *testing.T) { RequiresReplace: path.Paths{ path.Root("test"), }, + Private: testEmptyProviderData, }, }, "requires-replacement-previous-error": { @@ -1020,6 +2104,7 @@ func TestAttributeModifyPlan(t *testing.T) { RequiresReplace: path.Paths{ path.Root("test"), }, + Private: testEmptyProviderData, }, }, "requires-replacement-passthrough": { @@ -1111,6 +2196,7 @@ func TestAttributeModifyPlan(t *testing.T) { RequiresReplace: path.Paths{ path.Root("test"), }, + Private: testEmptyProviderData, }, }, "requires-replacement-unset": { @@ -1199,6 +2285,7 @@ func TestAttributeModifyPlan(t *testing.T) { }, }, }, + Private: testEmptyProviderData, }, }, "warnings": { @@ -1300,6 +2387,7 @@ func TestAttributeModifyPlan(t *testing.T) { }, }, }, + Private: testEmptyProviderData, }, }, "warnings-previous-error": { @@ -1412,6 +2500,7 @@ func TestAttributeModifyPlan(t *testing.T) { }, }, }, + Private: testEmptyProviderData, }, }, "error": { @@ -1510,6 +2599,7 @@ func TestAttributeModifyPlan(t *testing.T) { }, }, }, + Private: testEmptyProviderData, }, }, "error-previous-error": { @@ -1619,6 +2709,7 @@ func TestAttributeModifyPlan(t *testing.T) { }, }, }, + Private: testEmptyProviderData, }, }, } @@ -1652,7 +2743,7 @@ 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) } }) diff --git a/internal/fwserver/attribute_validation_test.go b/internal/fwserver/attribute_validation_test.go index 7a4f77292..808a05b63 100644 --- a/internal/fwserver/attribute_validation_test.go +++ b/internal/fwserver/attribute_validation_test.go @@ -5,13 +5,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" testtypes "github.com/hashicorp/terraform-plugin-framework/internal/testing/types" "github.com/hashicorp/terraform-plugin-framework/internal/totftypes" "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/tftypes" ) func TestAttributeValidate(t *testing.T) { diff --git a/internal/fwserver/block_plan_modification.go b/internal/fwserver/block_plan_modification.go index 7cdfde66f..7e575dcb1 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,19 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr var requiresReplace bool + privateProviderData := privatestate.EmptyProviderData(ctx) + + if req.Private != nil { + resp.Private = req.Private + 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 +67,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 +111,7 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr Plan: resp.Plan, ProviderMeta: req.ProviderMeta, State: req.State, + Private: resp.Private, } AttributeModifyPlan(ctx, attr, attrReq, resp) @@ -113,6 +124,7 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr Plan: resp.Plan, ProviderMeta: req.ProviderMeta, State: req.State, + Private: resp.Private, } BlockModifyPlan(ctx, block, blockReq, resp) @@ -140,6 +152,7 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr Plan: resp.Plan, ProviderMeta: req.ProviderMeta, State: req.State, + Private: resp.Private, } AttributeModifyPlan(ctx, attr, attrReq, resp) @@ -152,6 +165,7 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req tfsdk.ModifyAttr Plan: resp.Plan, ProviderMeta: req.ProviderMeta, State: req.State, + Private: resp.Private, } 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..74ec393bc 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) { @@ -36,6 +38,24 @@ func TestBlockModifyPlan(t *testing.T) { } } + schemaSet := func(blockPlanModifiers tfsdk.AttributePlanModifiers, nestedAttrPlanModifiers tfsdk.AttributePlanModifiers) tfsdk.Schema { + return tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "test": { + Attributes: map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + PlanModifiers: nestedAttrPlanModifiers, + }, + }, + NestingMode: tfsdk.BlockNestingModeSet, + PlanModifiers: blockPlanModifiers, + }, + }, + } + } + schemaTfValue := func(nestedAttrValue string) tftypes.Value { return tftypes.NewValue( tftypes.Object{ @@ -75,6 +95,45 @@ func TestBlockModifyPlan(t *testing.T) { ) } + schemaTfValueSet := func(nestedAttrValue string) tftypes.Value { + return tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, nestedAttrValue), + }, + ), + }, + ), + }, + ) + } + var schemaNullTfValue tftypes.Value = tftypes.NewValue( tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ @@ -125,6 +184,21 @@ 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 + } + + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + testCases := map[string]struct { req tfsdk.ModifyAttributePlanRequest resp ModifySchemaPlanResponse // Plan automatically copied from req @@ -168,6 +242,429 @@ 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-list-nested-private": { + req: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "test": { + Attributes: map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierGet{}, + }, + }, + }, + NestingMode: tfsdk.BlockNestingModeList, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierSet{}, + }, + }, + }, + }, + }, + Plan: tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "test": { + Attributes: map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierGet{}, + }, + }, + }, + NestingMode: tfsdk.BlockNestingModeList, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierSet{}, + }, + }, + }, + }, + }, + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "test": { + Attributes: map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierGet{}, + }, + }, + }, + NestingMode: tfsdk.BlockNestingModeList, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierSet{}, + }, + }, + }, + }, + }, + }, + resp: ModifySchemaPlanResponse{}, + expectedResp: ModifySchemaPlanResponse{ + Plan: tfsdk.Plan{ + Raw: schemaTfValue("testvalue"), + Schema: schema([]tfsdk.AttributePlanModifier{ + testBlockPlanModifierPrivateSet{}, + }, []tfsdk.AttributePlanModifier{ + testBlockPlanModifierPrivateGet{}, + }), + }, + Private: testProviderData, + }, + }, + "block-set-nested-private": { + req: tfsdk.ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "test": { + Attributes: map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierGet{}, + }, + }, + }, + NestingMode: tfsdk.BlockNestingModeSet, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierSet{}, + }, + }, + }, + }, + }, + Plan: tfsdk.Plan{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "test": { + Attributes: map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierGet{}, + }, + }, + }, + NestingMode: tfsdk.BlockNestingModeSet, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierSet{}, + }, + }, + }, + }, + }, + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: tfsdk.Schema{ + Blocks: map[string]tfsdk.Block{ + "test": { + Attributes: map[string]tfsdk.Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierGet{}, + }, + }, + }, + NestingMode: tfsdk.BlockNestingModeSet, + PlanModifiers: tfsdk.AttributePlanModifiers{ + planmodifiers.TestAttrPlanPrivateModifierSet{}, + }, + }, + }, + }, + }, + }, + resp: ModifySchemaPlanResponse{}, + expectedResp: ModifySchemaPlanResponse{ + Plan: tfsdk.Plan{ + Raw: schemaTfValueSet("testvalue"), + Schema: schemaSet([]tfsdk.AttributePlanModifier{ + testBlockPlanModifierPrivateSet{}, + }, []tfsdk.AttributePlanModifier{ + testBlockPlanModifierPrivateGet{}, + }), + }, + Private: testProviderData, }, }, "block-modified-previous-error": { @@ -203,6 +700,7 @@ func TestBlockModifyPlan(t *testing.T) { testBlockPlanModifierNullList{}, }, nil), }, + Private: testEmptyProviderData, }, }, "block-requires-replacement": { @@ -228,6 +726,7 @@ func TestBlockModifyPlan(t *testing.T) { RequiresReplace: path.Paths{ path.Root("test"), }, + Private: testEmptyProviderData, }, }, "block-requires-replacement-previous-error": { @@ -266,6 +765,7 @@ func TestBlockModifyPlan(t *testing.T) { RequiresReplace: path.Paths{ path.Root("test"), }, + Private: testEmptyProviderData, }, }, "block-requires-replacement-passthrough": { @@ -293,6 +793,7 @@ func TestBlockModifyPlan(t *testing.T) { RequiresReplace: path.Paths{ path.Root("test"), }, + Private: testEmptyProviderData, }, }, "block-requires-replacement-unset": { @@ -317,6 +818,7 @@ func TestBlockModifyPlan(t *testing.T) { planmodifiers.TestRequiresReplaceFalseModifier{}, }, nil), }, + Private: testEmptyProviderData, }, }, "block-warnings": { @@ -350,6 +852,7 @@ func TestBlockModifyPlan(t *testing.T) { planmodifiers.TestWarningDiagModifier{}, }, nil), }, + Private: testEmptyProviderData, }, }, "block-warnings-previous-error": { @@ -394,6 +897,7 @@ func TestBlockModifyPlan(t *testing.T) { planmodifiers.TestWarningDiagModifier{}, }, nil), }, + Private: testEmptyProviderData, }, }, "block-error": { @@ -424,6 +928,7 @@ func TestBlockModifyPlan(t *testing.T) { planmodifiers.TestErrorDiagModifier{}, }, nil), }, + Private: testEmptyProviderData, }, }, "block-error-previous-error": { @@ -465,6 +970,7 @@ func TestBlockModifyPlan(t *testing.T) { planmodifiers.TestErrorDiagModifier{}, }, nil), }, + Private: testEmptyProviderData, }, }, "nested-attribute-modified": { @@ -489,6 +995,7 @@ func TestBlockModifyPlan(t *testing.T) { planmodifiers.TestAttrPlanValueModifierTwo{}, }), }, + Private: testEmptyProviderData, }, }, "nested-attribute-modified-previous-error": { @@ -526,6 +1033,7 @@ func TestBlockModifyPlan(t *testing.T) { planmodifiers.TestAttrPlanValueModifierTwo{}, }), }, + Private: testEmptyProviderData, }, }, "nested-attribute-requires-replacement": { @@ -551,6 +1059,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 +1098,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 +1126,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 +1151,7 @@ func TestBlockModifyPlan(t *testing.T) { planmodifiers.TestRequiresReplaceFalseModifier{}, }), }, + Private: testEmptyProviderData, }, }, "nested-attribute-warnings": { @@ -673,6 +1185,7 @@ func TestBlockModifyPlan(t *testing.T) { planmodifiers.TestWarningDiagModifier{}, }), }, + Private: testEmptyProviderData, }, }, "nested-attribute-warnings-previous-error": { @@ -717,6 +1230,7 @@ func TestBlockModifyPlan(t *testing.T) { planmodifiers.TestWarningDiagModifier{}, }), }, + Private: testEmptyProviderData, }, }, "nested-attribute-error": { @@ -747,6 +1261,7 @@ func TestBlockModifyPlan(t *testing.T) { planmodifiers.TestErrorDiagModifier{}, }), }, + Private: testEmptyProviderData, }, }, "nested-attribute-error-previous-error": { @@ -788,6 +1303,7 @@ func TestBlockModifyPlan(t *testing.T) { planmodifiers.TestErrorDiagModifier{}, }), }, + Private: testEmptyProviderData, }, }, } @@ -807,7 +1323,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 +1355,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_applyresourcechange.go b/internal/fwserver/server_applyresourcechange.go index 975574509..8f2069049 100644 --- a/internal/fwserver/server_applyresourcechange.go +++ b/internal/fwserver/server_applyresourcechange.go @@ -6,6 +6,7 @@ import ( "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/provider" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -14,7 +15,7 @@ import ( // ApplyResourceChange RPC. type ApplyResourceChangeRequest struct { Config *tfsdk.Config - PlannedPrivate []byte + PlannedPrivate *privatestate.Data PlannedState *tfsdk.Plan PriorState *tfsdk.State ProviderMeta *tfsdk.Config @@ -27,7 +28,7 @@ type ApplyResourceChangeRequest struct { type ApplyResourceChangeResponse struct { Diagnostics diag.Diagnostics NewState *tfsdk.State - Private []byte + Private *privatestate.Data } // ApplyResourceChange implements the framework server ApplyResourceChange RPC. diff --git a/internal/fwserver/server_applyresourcechange_test.go b/internal/fwserver/server_applyresourcechange_test.go index 1f3e5a02f..1095d3451 100644 --- a/internal/fwserver/server_applyresourcechange_test.go +++ b/internal/fwserver/server_applyresourcechange_test.go @@ -1,18 +1,22 @@ package fwserver_test import ( + "bytes" "context" + "fmt" "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/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "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 TestServerApplyResourceChange(t *testing.T) { @@ -81,6 +85,35 @@ func TestServerApplyResourceChange(t *testing.T) { TestProviderMetaAttribute types.String `tfsdk:"test_provider_meta_attribute"` } + testPrivateFrameworkMap := map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + } + + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testPrivate := &privatestate.Data{ + Framework: testPrivateFrameworkMap, + Provider: testProviderData, + } + + testPrivateFramework := &privatestate.Data{ + Framework: testPrivateFrameworkMap, + } + + testPrivateProvider := &privatestate.Data{ + Provider: testProviderData, + } + + testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + + testEmptyPrivate := &privatestate.Data{ + Provider: testEmptyProviderData, + } + testCases := map[string]struct { server *fwserver.Server request *fwserver.ApplyResourceChangeRequest @@ -136,6 +169,7 @@ func TestServerApplyResourceChange(t *testing.T) { }), Schema: testSchema, }, + Private: testEmptyPrivate, }, }, "create-request-plannedstate": { @@ -188,7 +222,7 @@ func TestServerApplyResourceChange(t *testing.T) { }), Schema: testSchema, }, - }, + Private: testEmptyPrivate}, }, "create-request-providermeta": { server: &fwserver.Server{ @@ -244,6 +278,7 @@ func TestServerApplyResourceChange(t *testing.T) { }), Schema: testSchema, }, + Private: testEmptyPrivate, }, }, "create-response-diagnostics": { @@ -286,6 +321,7 @@ func TestServerApplyResourceChange(t *testing.T) { }, // Intentionally empty, Create implementation does not call resp.State.Set() NewState: testEmptyState, + Private: testEmptyPrivate, }, }, "create-response-newstate": { @@ -339,6 +375,7 @@ func TestServerApplyResourceChange(t *testing.T) { }), Schema: testSchema, }, + Private: testEmptyPrivate, }, }, "create-response-newstate-null": { @@ -392,6 +429,53 @@ func TestServerApplyResourceChange(t *testing.T) { ), }, NewState: testEmptyState, + Private: testEmptyPrivate, + }, + }, + "create-response-private": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ApplyResourceChangeRequest{ + 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.Resource{ + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data testSchemaData + + // Prevent missing resource state error diagnostic + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + + diags := resp.Private.SetKey(ctx, "providerKeyOne", []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`)) + + resp.Diagnostics.Append(diags...) + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Delete") + }, + UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Update") + }, + }, nil + }, + }, + }, + expectedResponse: &fwserver.ApplyResourceChangeResponse{ + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, ""), + "test_required": tftypes.NewValue(tftypes.String, ""), + }), + Schema: testSchema, + }, + Private: &privatestate.Data{ + Provider: testProviderData, + }, }, }, "delete-request-priorstate": { @@ -481,6 +565,102 @@ func TestServerApplyResourceChange(t *testing.T) { NewState: testEmptyState, }, }, + "delete-request-private": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ApplyResourceChangeRequest{ + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, nil), + }), + 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.Resource{ + CreateMethod: func(_ context.Context, _ resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Delete, Got: Create") + }, + DeleteMethod: func(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + expected := `{"pKeyOne": {"k0": "zero", "k1": 1}}` + got, diags := req.Private.GetKey(ctx, "providerKeyOne") + + resp.Diagnostics.Append(diags...) + + if string(got) != expected { + resp.Diagnostics.AddError( + "Unexpected req.Private Value", + fmt.Sprintf("expected %q, got %q", expected, got), + ) + } + }, + UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Delete, Got: Update") + }, + }, nil + }, + }, + PlannedPrivate: &privatestate.Data{ + Provider: testProviderData, + }, + }, + expectedResponse: &fwserver.ApplyResourceChangeResponse{ + NewState: testEmptyState, + }, + }, + "delete-request-private-planned-private-nil": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ApplyResourceChangeRequest{ + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, nil), + }), + 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.Resource{ + CreateMethod: func(_ context.Context, _ resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Delete, Got: Create") + }, + DeleteMethod: func(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var expected []byte + + got, diags := req.Private.GetKey(ctx, "providerKeyOne") + + resp.Diagnostics.Append(diags...) + + if !bytes.Equal(got, expected) { + resp.Diagnostics.AddError( + "Unexpected req.Private Value", + fmt.Sprintf("expected %q, got %q", expected, got), + ) + } + }, + UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Delete, Got: Update") + }, + }, nil + }, + }, + }, + expectedResponse: &fwserver.ApplyResourceChangeResponse{ + NewState: testEmptyState, + }, + }, "delete-response-diagnostics": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -633,6 +813,7 @@ func TestServerApplyResourceChange(t *testing.T) { }), Schema: testSchema, }, + Private: testEmptyPrivate, }, }, "update-request-plannedstate": { @@ -696,6 +877,7 @@ func TestServerApplyResourceChange(t *testing.T) { }), Schema: testSchema, }, + Private: testEmptyPrivate, }, }, "update-request-priorstate": { @@ -759,6 +941,7 @@ func TestServerApplyResourceChange(t *testing.T) { }), Schema: testSchema, }, + Private: testEmptyPrivate, }, }, "update-request-providermeta": { @@ -823,6 +1006,132 @@ func TestServerApplyResourceChange(t *testing.T) { }), Schema: testSchema, }, + Private: testEmptyPrivate, + }, + }, + "update-request-private": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ApplyResourceChangeRequest{ + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, nil), + }), + 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, nil), + }), + 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.Resource{ + CreateMethod: func(_ context.Context, _ resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Create") + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Delete") + }, + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + expected := `{"pKeyOne": {"k0": "zero", "k1": 1}}` + got, diags := req.Private.GetKey(ctx, "providerKeyOne") + + resp.Diagnostics.Append(diags...) + + if string(got) != expected { + resp.Diagnostics.AddError( + "Unexpected req.Private Value", + fmt.Sprintf("expected %q, got %q", expected, got), + ) + } + }, + }, nil + }, + }, + PlannedPrivate: testPrivateProvider, + }, + expectedResponse: &fwserver.ApplyResourceChangeResponse{ + // Intentionally old, Update implementation does not call resp.State.Set() + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchema, + }, + Private: &privatestate.Data{ + Provider: testProviderData, + }, + }, + }, + "update-request-private-nil": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ApplyResourceChangeRequest{ + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, nil), + }), + 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, nil), + }), + 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.Resource{ + CreateMethod: func(_ context.Context, _ resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Create") + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Delete") + }, + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var expected []byte + got, diags := req.Private.GetKey(ctx, "providerKeyOne") + + resp.Diagnostics.Append(diags...) + + if !bytes.Equal(got, expected) { + resp.Diagnostics.AddError( + "Unexpected req.Private Value", + fmt.Sprintf("expected %q, got %q", expected, got), + ) + } + }, + }, nil + }, + }, + }, + expectedResponse: &fwserver.ApplyResourceChangeResponse{ + // Intentionally old, Update implementation does not call resp.State.Set() + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate, }, }, "update-response-diagnostics": { @@ -890,6 +1199,7 @@ func TestServerApplyResourceChange(t *testing.T) { }), Schema: testSchema, }, + Private: testEmptyPrivate, }, }, "update-response-newstate": { @@ -949,6 +1259,7 @@ func TestServerApplyResourceChange(t *testing.T) { }), Schema: testSchema, }, + Private: testEmptyPrivate, }, }, "update-response-newstate-null": { @@ -1006,6 +1317,112 @@ func TestServerApplyResourceChange(t *testing.T) { ), }, NewState: testEmptyState, + Private: testEmptyPrivate, + }, + }, + "update-response-private": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ApplyResourceChangeRequest{ + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, nil), + }), + 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, nil), + }), + 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.Resource{ + CreateMethod: func(_ context.Context, _ resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Create") + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Delete") + }, + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + diags := resp.Private.SetKey(ctx, "providerKeyOne", []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`)) + + resp.Diagnostics.Append(diags...) + }, + }, nil + }, + }, + }, + expectedResponse: &fwserver.ApplyResourceChangeResponse{ + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchema, + }, + Private: testPrivateProvider, + }, + }, + "update-response-private-updated": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ApplyResourceChangeRequest{ + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, nil), + }), + 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, nil), + }), + 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.Resource{ + CreateMethod: func(_ context.Context, _ resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Create") + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Delete") + }, + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + diags := resp.Private.SetKey(ctx, "providerKeyOne", []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`)) + + resp.Diagnostics.Append(diags...) + }, + }, nil + }, + }, + PlannedPrivate: testPrivateFramework, + }, + expectedResponse: &fwserver.ApplyResourceChangeResponse{ + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchema, + }, + Private: testPrivate, }, }, } @@ -1019,7 +1436,7 @@ func TestServerApplyResourceChange(t *testing.T) { response := &fwserver.ApplyResourceChangeResponse{} testCase.server.ApplyResourceChange(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/fwserver/server_createresource.go b/internal/fwserver/server_createresource.go index 6c24c408f..30838e1bb 100644 --- a/internal/fwserver/server_createresource.go +++ b/internal/fwserver/server_createresource.go @@ -3,20 +3,22 @@ package fwserver import ( "context" + "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/provider" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // CreateResourceRequest is the framework server request for a create request // with the ApplyResourceChange RPC. type CreateResourceRequest struct { Config *tfsdk.Config - PlannedPrivate []byte + PlannedPrivate *privatestate.Data PlannedState *tfsdk.Plan ProviderMeta *tfsdk.Config ResourceSchema fwschema.Schema @@ -28,7 +30,7 @@ type CreateResourceRequest struct { type CreateResourceResponse struct { Diagnostics diag.Diagnostics NewState *tfsdk.State - Private []byte + Private *privatestate.Data } // CreateResource implements the framework server create request logic for the @@ -61,11 +63,15 @@ func (s *Server) CreateResource(ctx context.Context, req *CreateResourceRequest, Raw: nullSchemaData, }, } + + privateProviderData := privatestate.EmptyProviderData(ctx) + createResp := resource.CreateResponse{ State: tfsdk.State{ Schema: schema(req.ResourceSchema), Raw: nullSchemaData, }, + Private: privateProviderData, } if req.Config != nil { @@ -102,4 +108,12 @@ func (s *Server) CreateResource(ctx context.Context, req *CreateResourceRequest, detail, ) } + + if createResp.Private != nil { + if resp.Private == nil { + resp.Private = &privatestate.Data{} + } + + resp.Private.Provider = createResp.Private + } } diff --git a/internal/fwserver/server_createresource_test.go b/internal/fwserver/server_createresource_test.go index 62c48125f..70e1f2c19 100644 --- a/internal/fwserver/server_createresource_test.go +++ b/internal/fwserver/server_createresource_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/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/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 TestServerCreateResource(t *testing.T) { @@ -76,6 +78,18 @@ func TestServerCreateResource(t *testing.T) { TestProviderMetaAttribute types.String `tfsdk:"test_provider_meta_attribute"` } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + + testEmptyPrivate := &privatestate.Data{ + Provider: testEmptyProviderData, + } + testCases := map[string]struct { server *fwserver.Server request *fwserver.CreateResourceRequest @@ -124,6 +138,7 @@ func TestServerCreateResource(t *testing.T) { }), Schema: testSchema, }, + Private: testEmptyPrivate, }, }, "request-plannedstate": { @@ -169,6 +184,7 @@ func TestServerCreateResource(t *testing.T) { }), Schema: testSchema, }, + Private: testEmptyPrivate, }, }, "request-providermeta": { @@ -218,6 +234,7 @@ func TestServerCreateResource(t *testing.T) { }), Schema: testSchema, }, + Private: testEmptyPrivate, }, }, "response-diagnostics": { @@ -253,6 +270,7 @@ func TestServerCreateResource(t *testing.T) { }, // Intentionally empty, Create implementation does not call resp.State.Set() NewState: testEmptyState, + Private: testEmptyPrivate, }, }, "response-newstate": { @@ -292,6 +310,7 @@ func TestServerCreateResource(t *testing.T) { }), Schema: testSchema, }, + Private: testEmptyPrivate, }, }, "response-newstate-null": { @@ -331,6 +350,46 @@ func TestServerCreateResource(t *testing.T) { ), }, NewState: testEmptyState, + Private: testEmptyPrivate, + }, + }, + "response-private": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.CreateResourceRequest{ + 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.Resource{ + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data testSchemaData + + // Prevent missing resource state error diagnostic + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + + diags := resp.Private.SetKey(ctx, "providerKeyOne", []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`)) + + resp.Diagnostics.Append(diags...) + }, + }, nil + }, + }, + }, + expectedResponse: &fwserver.CreateResourceResponse{ + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, ""), + "test_required": tftypes.NewValue(tftypes.String, ""), + }), + Schema: testSchema, + }, + Private: &privatestate.Data{ + Provider: testProviderData, + }, }, }, } @@ -344,7 +403,7 @@ func TestServerCreateResource(t *testing.T) { response := &fwserver.CreateResourceResponse{} testCase.server.CreateResource(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/fwserver/server_deleteresource.go b/internal/fwserver/server_deleteresource.go index 22a64dfb5..cb2768275 100644 --- a/internal/fwserver/server_deleteresource.go +++ b/internal/fwserver/server_deleteresource.go @@ -3,19 +3,21 @@ package fwserver import ( "context" + "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/provider" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // DeleteResourceRequest is the framework server request for a delete request // with the ApplyResourceChange RPC. type DeleteResourceRequest struct { - PlannedPrivate []byte + PlannedPrivate *privatestate.Data PriorState *tfsdk.State ProviderMeta *tfsdk.Config ResourceSchema fwschema.Schema @@ -27,7 +29,7 @@ type DeleteResourceRequest struct { type DeleteResourceResponse struct { Diagnostics diag.Diagnostics NewState *tfsdk.State - Private []byte + Private *privatestate.Data } // DeleteResource implements the framework server delete request logic for the @@ -70,6 +72,10 @@ func (s *Server) DeleteResource(ctx context.Context, req *DeleteResourceRequest, deleteReq.ProviderMeta = *req.ProviderMeta } + if req.PlannedPrivate != nil { + deleteReq.Private = req.PlannedPrivate.Provider + } + logging.FrameworkDebug(ctx, "Calling provider defined Resource Delete") resourceImpl.Delete(ctx, deleteReq, &deleteResp) logging.FrameworkDebug(ctx, "Called provider defined Resource Delete") diff --git a/internal/fwserver/server_deleteresource_test.go b/internal/fwserver/server_deleteresource_test.go index 7d4c5b4fa..b97a82567 100644 --- a/internal/fwserver/server_deleteresource_test.go +++ b/internal/fwserver/server_deleteresource_test.go @@ -1,18 +1,22 @@ package fwserver_test import ( + "bytes" "context" + "fmt" "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/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "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 TestServerDeleteResource(t *testing.T) { @@ -76,6 +80,12 @@ func TestServerDeleteResource(t *testing.T) { TestProviderMetaAttribute types.String `tfsdk:"test_provider_meta_attribute"` } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + testCases := map[string]struct { server *fwserver.Server request *fwserver.DeleteResourceRequest @@ -154,6 +164,76 @@ func TestServerDeleteResource(t *testing.T) { NewState: testEmptyState, }, }, + "request-private": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.DeleteResourceRequest{ + 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.Resource{ + DeleteMethod: func(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + expected := `{"pKeyOne": {"k0": "zero", "k1": 1}}` + got, diags := req.Private.GetKey(ctx, "providerKeyOne") + + resp.Diagnostics.Append(diags...) + + if string(got) != expected { + resp.Diagnostics.AddError( + "Unexpected req.Private Value", + fmt.Sprintf("expected %q, got %q", expected, got), + ) + } + }, + }, nil + }, + }, + PlannedPrivate: &privatestate.Data{ + Provider: testProviderData, + }, + }, + expectedResponse: &fwserver.DeleteResourceResponse{ + NewState: testEmptyState, + }, + }, + "request-private-planned-private-nil": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.DeleteResourceRequest{ + 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.Resource{ + DeleteMethod: func(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var expected []byte + + got, diags := req.Private.GetKey(ctx, "providerKeyOne") + + resp.Diagnostics.Append(diags...) + + if !bytes.Equal(got, expected) { + resp.Diagnostics.AddError( + "Unexpected req.Private Value", + fmt.Sprintf("expected %q, got %q", expected, got), + ) + } + }, + }, nil + }, + }, + }, + expectedResponse: &fwserver.DeleteResourceResponse{ + NewState: testEmptyState, + }, + }, "response-diagnostics": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, diff --git a/internal/fwserver/server_importresourcestate.go b/internal/fwserver/server_importresourcestate.go index 6b6b2e8be..fd8ae410e 100644 --- a/internal/fwserver/server_importresourcestate.go +++ b/internal/fwserver/server_importresourcestate.go @@ -5,6 +5,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/tfsdk" @@ -12,7 +13,7 @@ import ( // ImportedResource represents a resource that was imported. type ImportedResource struct { - Private []byte + Private *privatestate.Data State tfsdk.State TypeName string } @@ -81,11 +82,15 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta importReq := resource.ImportStateRequest{ ID: req.ID, } + + privateProviderData := privatestate.EmptyProviderData(ctx) + importResp := resource.ImportStateResponse{ State: tfsdk.State{ Raw: req.EmptyState.Raw.Copy(), Schema: req.EmptyState.Schema, }, + Private: privateProviderData, } logging.FrameworkDebug(ctx, "Calling provider defined Resource ImportState") @@ -107,10 +112,17 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta return } + private := &privatestate.Data{} + + if importResp.Private != nil { + private.Provider = importResp.Private + } + resp.ImportedResources = []ImportedResource{ { State: importResp.State, TypeName: req.TypeName, + Private: private, }, } } diff --git a/internal/fwserver/server_importresourcestate_test.go b/internal/fwserver/server_importresourcestate_test.go index fa2c87779..5cd09d97a 100644 --- a/internal/fwserver/server_importresourcestate_test.go +++ b/internal/fwserver/server_importresourcestate_test.go @@ -5,15 +5,17 @@ import ( "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/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 TestServerImportResourceState(t *testing.T) { @@ -66,6 +68,22 @@ func TestServerImportResourceState(t *testing.T) { Schema: testSchema, } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testPrivate := &privatestate.Data{ + Provider: testProviderData, + } + + testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + + testEmptyPrivate := &privatestate.Data{ + Provider: testEmptyProviderData, + } + testCases := map[string]struct { server *fwserver.Server request *fwserver.ImportResourceStateRequest @@ -108,6 +126,7 @@ func TestServerImportResourceState(t *testing.T) { { State: *testState, TypeName: "test_resource", + Private: testEmptyPrivate, }, }, }, @@ -200,6 +219,43 @@ func TestServerImportResourceState(t *testing.T) { { State: *testState, TypeName: "test_resource", + Private: testEmptyPrivate, + }, + }, + }, + }, + "response-importedresources-private": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ImportResourceStateRequest{ + EmptyState: *testEmptyState, + ID: "test-id", + 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.ResourceWithImportState{ + Resource: &testprovider.Resource{}, + ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + diags := resp.Private.SetKey(ctx, "providerKeyOne", []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`)) + + resp.Diagnostics.Append(diags...) + + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + }, + }, nil + }, + }, + TypeName: "test_resource", + }, + expectedResponse: &fwserver.ImportResourceStateResponse{ + ImportedResources: []fwserver.ImportedResource{ + { + State: *testState, + TypeName: "test_resource", + Private: testPrivate, }, }, }, @@ -247,7 +303,7 @@ func TestServerImportResourceState(t *testing.T) { response := &fwserver.ImportResourceStateResponse{} testCase.server.ImportResourceState(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/fwserver/server_planresourcechange.go b/internal/fwserver/server_planresourcechange.go index c6a86dbb2..1ee43276f 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,20 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange } } + // Ensure that resp.PlannedPrivate is never nil. + resp.PlannedPrivate = privatestate.EmptyData(ctx) + + if req.PriorPrivate != nil { + // Overwrite resp.PlannedPrivate with req.PriorPrivate providing + // it is not nil. + resp.PlannedPrivate = req.PriorPrivate + + // Ensure that resp.PlannedPrivate.Provider is never nil. + if resp.PlannedPrivate.Provider == nil { + resp.PlannedPrivate.Provider = privatestate.EmptyProviderData(ctx) + } + } + resp.PlannedState = planToState(*req.ProposedNewState) // Execute any AttributePlanModifiers. @@ -159,9 +175,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: resp.PlannedPrivate.Provider, } if req.ProviderMeta != nil { @@ -171,6 +188,7 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange modifySchemaPlanResp := ModifySchemaPlanResponse{ Diagnostics: resp.Diagnostics, Plan: modifySchemaPlanReq.Plan, + Private: modifySchemaPlanReq.Private, } SchemaModifyPlan(ctx, req.ResourceSchema, modifySchemaPlanReq, &modifySchemaPlanResp) @@ -178,6 +196,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 +215,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: resp.PlannedPrivate.Provider, } if req.ProviderMeta != nil { @@ -209,6 +229,7 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange Diagnostics: resp.Diagnostics, Plan: modifyPlanReq.Plan, RequiresReplace: path.Paths{}, + Private: modifyPlanReq.Private, } logging.FrameworkDebug(ctx, "Calling provider defined Resource ModifyPlan") @@ -218,6 +239,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..3f602bd30 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) { @@ -272,6 +274,12 @@ func TestServerPlanResourceChange(t *testing.T) { }, } + testSchemaTypeComputed := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_computed": tftypes.String, + }, + } + testSchema := tfsdk.Schema{ Attributes: map[string]tfsdk.Attribute{ "test_computed": { @@ -320,6 +328,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 +439,31 @@ func TestServerPlanResourceChange(t *testing.T) { TestProviderMetaAttribute types.String `tfsdk:"test_provider_meta_attribute"` } + testPrivateFrameworkMap := map[string][]byte{ + ".frameworkKey": []byte(`{"fk": "framework value"}`), + } + + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testPrivateProvider := &privatestate.Data{ + Provider: testProviderData, + } + + testPrivate := &privatestate.Data{ + Framework: testPrivateFrameworkMap, + Provider: testProviderData, + } + + testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + + testEmptyPrivate := &privatestate.Data{ + Provider: testEmptyProviderData, + } + testCases := map[string]struct { server *fwserver.Server request *fwserver.PlanResourceChangeRequest @@ -431,6 +507,49 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchema, }, + PlannedPrivate: testEmptyPrivate, + }, + }, + "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: testEmptyPrivate, + }, + }, + "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: testEmptyPrivate, }, }, "create-attributeplanmodifier-response-requiresreplace": { @@ -565,6 +727,7 @@ func TestServerPlanResourceChange(t *testing.T) { RequiresReplace: path.Paths{ path.Root("test_required"), }, + PlannedPrivate: testEmptyPrivate, }, }, "create-resourcewithmodifyplan-request-config": { @@ -615,6 +778,62 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchema, }, + PlannedPrivate: testEmptyPrivate, + }, + }, + "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: testEmptyPrivate, }, }, "create-resourcewithmodifyplan-request-providermeta": { @@ -716,6 +936,7 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchema, }, + PlannedPrivate: testEmptyPrivate, }, }, "create-resourcewithmodifyplan-response-diagnostics": { @@ -765,6 +986,7 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchema, }, + PlannedPrivate: testEmptyPrivate, }, }, "create-resourcewithmodifyplan-response-plannedstate": { @@ -815,6 +1037,54 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchema, }, + PlannedPrivate: testEmptyPrivate, + }, + }, + "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,42 +1140,44 @@ func TestServerPlanResourceChange(t *testing.T) { RequiresReplace: path.Paths{ path.Root("test_required"), }, + PlannedPrivate: testEmptyPrivate, }, }, - "delete-resourcewithmodifyplan-request-config": { + "create-resourcewithmodifyplan-attributeplanmodifier-private": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, }, request: &fwserver.PlanResourceChangeRequest{ Config: &tfsdk.Config{ - Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + Raw: tftypes.NewValue(testSchemaTypeComputed, map[string]tftypes.Value{ "test_computed": tftypes.NewValue(tftypes.String, nil), - "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), }), - Schema: testSchema, + Schema: testSchemaAttributePlanModifierPrivatePlanResponse, }, - ProposedNewState: testEmptyPlan, - PriorState: &tfsdk.State{ - Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaTypeComputed, map[string]tftypes.Value{ "test_computed": tftypes.NewValue(tftypes.String, nil), - "test_required": tftypes.NewValue(tftypes.String, "test-state-value"), }), - Schema: testSchema, + Schema: testSchemaAttributePlanModifierPrivatePlanResponse, }, - ResourceSchema: testSchema, + PriorState: testEmptyState, + ResourceSchema: testSchemaAttributePlanModifierPrivatePlanResponse, ResourceType: &testprovider.ResourceType{ GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { - return testSchema, nil + return testSchemaAttributePlanModifierPrivatePlanResponse, 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) { - var data testSchemaData + expected := `{"pKeyOne": {"k0": "zero", "k1": 1}}` - resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + key := "providerKeyOne" + got, diags := req.Private.GetKey(ctx, key) - if data.TestRequired.Value != "test-config-value" { - resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.Value) + resp.Diagnostics.Append(diags...) + + if string(got) != expected { + resp.Diagnostics.AddError("unexpected req.Private.Provider value: %s", string(got)) } }, }, nil @@ -913,10 +1185,16 @@ func TestServerPlanResourceChange(t *testing.T) { }, }, expectedResponse: &fwserver.PlanResourceChangeResponse{ - PlannedState: testEmptyState, + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaTypeComputed, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + }), + Schema: testSchemaAttributePlanModifierPrivatePlanResponse, + }, + PlannedPrivate: testPrivateProvider, }, }, - "delete-resourcewithmodifyplan-request-priorstate": { + "delete-resourcewithmodifyplan-request-config": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, }, @@ -946,10 +1224,10 @@ func TestServerPlanResourceChange(t *testing.T) { ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { var data testSchemaData - resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) - if data.TestRequired.Value != "test-state-value" { - resp.Diagnostics.AddError("Unexpected req.State Value", "Got: "+data.TestRequired.Value) + if data.TestRequired.Value != "test-config-value" { + resp.Diagnostics.AddError("Unexpected req.Config Value", "Got: "+data.TestRequired.Value) } }, }, nil @@ -957,10 +1235,11 @@ func TestServerPlanResourceChange(t *testing.T) { }, }, expectedResponse: &fwserver.PlanResourceChangeResponse{ - PlannedState: testEmptyState, + PlannedState: testEmptyState, + PlannedPrivate: testEmptyPrivate, }, }, - "delete-resourcewithmodifyplan-request-providermeta": { + "delete-resourcewithmodifyplan-request-private": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, }, @@ -980,7 +1259,6 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchema, }, - ProviderMeta: testProviderMetaConfig, ResourceSchema: testSchema, ResourceType: &testprovider.ResourceType{ GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { @@ -989,23 +1267,28 @@ func TestServerPlanResourceChange(t *testing.T) { NewResourceMethod: func(_ context.Context, _ provider.Provider) (resource.Resource, diag.Diagnostics) { return &testprovider.ResourceWithModifyPlan{ ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { - var data testProviderMetaData + expected := `{"pKeyOne": {"k0": "zero", "k1": 1}}` - resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &data)...) + key := "providerKeyOne" + got, diags := req.Private.GetKey(ctx, key) - if data.TestProviderMetaAttribute.Value != "test-provider-meta-value" { - resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.Value) + 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, + PlannedState: testEmptyState, + PlannedPrivate: testPrivateProvider, }, }, - "delete-resourcewithmodifyplan-response-diagnostics": { + "delete-resourcewithmodifyplan-request-priorstate": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, }, @@ -1033,11 +1316,102 @@ func TestServerPlanResourceChange(t *testing.T) { NewResourceMethod: func(_ context.Context, _ provider.Provider) (resource.Resource, diag.Diagnostics) { return &testprovider.ResourceWithModifyPlan{ ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { - resp.Diagnostics.AddWarning("warning summary", "warning detail") - resp.Diagnostics.AddError("error summary", "error detail") - }, - }, nil - }, + var data testSchemaData + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if data.TestRequired.Value != "test-state-value" { + resp.Diagnostics.AddError("Unexpected req.State Value", "Got: "+data.TestRequired.Value) + } + }, + }, nil + }, + }, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + PlannedState: testEmptyState, + PlannedPrivate: testEmptyPrivate, + }, + }, + "delete-resourcewithmodifyplan-request-providermeta": { + 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, + }, + ProviderMeta: testProviderMetaConfig, + 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) { + var data testProviderMetaData + + resp.Diagnostics.Append(req.ProviderMeta.Get(ctx, &data)...) + + if data.TestProviderMetaAttribute.Value != "test-provider-meta-value" { + resp.Diagnostics.AddError("Unexpected req.ProviderMeta Value", "Got: "+data.TestProviderMetaAttribute.Value) + } + }, + }, nil + }, + }, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + PlannedState: testEmptyState, + PlannedPrivate: testEmptyPrivate, + }, + }, + "delete-resourcewithmodifyplan-response-diagnostics": { + 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) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + }, nil + }, }, }, expectedResponse: &fwserver.PlanResourceChangeResponse{ @@ -1045,7 +1419,8 @@ func TestServerPlanResourceChange(t *testing.T) { diag.NewWarningDiagnostic("warning summary", "warning detail"), diag.NewErrorDiagnostic("error summary", "error detail"), }, - PlannedState: testEmptyState, + PlannedState: testEmptyState, + PlannedPrivate: testEmptyPrivate, }, }, "delete-resourcewithmodifyplan-response-plannedstate": { @@ -1099,6 +1474,7 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchema, }, + PlannedPrivate: testEmptyPrivate, }, }, "delete-resourcewithmodifyplan-response-requiresreplace": { @@ -1148,6 +1524,87 @@ func TestServerPlanResourceChange(t *testing.T) { RequiresReplace: path.Paths{ path.Root("test_required"), }, + PlannedPrivate: testEmptyPrivate, + }, + }, + "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, + }, + }, + "delete-resourcewithmodifyplan-attributeplanmodifier-private": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaTypeComputed, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchemaAttributePlanModifierPrivatePlanResponse, + }, + ProposedNewState: testEmptyPlan, + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaTypeComputed, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchemaAttributePlanModifierPrivatePlanResponse, + }, + ResourceSchema: testSchema, + 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.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 +1651,55 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchema, }, + PlannedPrivate: testEmptyPrivate, + }, + }, + "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 +1746,54 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchemaAttributePlanModifierAttributePlan, }, + PlannedPrivate: testEmptyPrivate, + }, + }, + "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 +1846,7 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchemaAttributePlanModifierDiagnosticsError, }, + PlannedPrivate: testEmptyPrivate, }, }, "update-attributeplanmodifier-response-requiresreplace": { @@ -1341,6 +1896,7 @@ func TestServerPlanResourceChange(t *testing.T) { RequiresReplace: path.Paths{ path.Root("test_required"), }, + PlannedPrivate: testEmptyPrivate, }, }, "update-resourcewithmodifyplan-request-config": { @@ -1397,6 +1953,7 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchema, }, + PlannedPrivate: testEmptyPrivate, }, }, "update-resourcewithmodifyplan-request-proposednewstate": { @@ -1453,6 +2010,7 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchema, }, + PlannedPrivate: testEmptyPrivate, }, }, "update-resourcewithmodifyplan-request-providermeta": { @@ -1510,6 +2068,68 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchema, }, + PlannedPrivate: testEmptyPrivate, + }, + }, + "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 +2185,7 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchema, }, + PlannedPrivate: testEmptyPrivate, }, }, "update-resourcewithmodifyplan-response-plannedstate": { @@ -1621,6 +2242,7 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchema, }, + PlannedPrivate: testEmptyPrivate, }, }, "update-resourcewithmodifyplan-response-requiresreplace": { @@ -1682,6 +2304,109 @@ func TestServerPlanResourceChange(t *testing.T) { RequiresReplace: path.Paths{ path.Root("test_required"), }, + PlannedPrivate: testEmptyPrivate, + }, + }, + "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, + }, + }, + "update-resourcewithmodifyplan-attributeplanmodifier-private": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaTypeComputed, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchemaAttributePlanModifierPrivatePlanResponse, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaTypeComputed, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchemaAttributePlanModifierPrivatePlanResponse, + }, + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaTypeComputed, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + Schema: testSchema, + }, + 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.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(testSchemaTypeComputed, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchemaAttributePlanModifierPrivatePlanResponse, + }, + PlannedPrivate: testPrivateProvider, }, }, } @@ -1695,7 +2420,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/fwserver/server_readresource.go b/internal/fwserver/server_readresource.go index 06ad1c639..f716c045a 100644 --- a/internal/fwserver/server_readresource.go +++ b/internal/fwserver/server_readresource.go @@ -5,6 +5,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/tfsdk" @@ -15,7 +16,7 @@ import ( type ReadResourceRequest struct { CurrentState *tfsdk.State ResourceType provider.ResourceType - Private []byte + Private *privatestate.Data ProviderMeta *tfsdk.Config } @@ -24,7 +25,7 @@ type ReadResourceRequest struct { type ReadResourceResponse struct { Diagnostics diag.Diagnostics NewState *tfsdk.State - Private []byte + Private *privatestate.Data } // ReadResource implements the framework server ReadResource RPC. @@ -71,10 +72,32 @@ func (s *Server) ReadResource(ctx context.Context, req *ReadResourceRequest, res readReq.ProviderMeta = *req.ProviderMeta } + privateProviderData := privatestate.EmptyProviderData(ctx) + + readReq.Private = privateProviderData + readResp.Private = privateProviderData + + if req.Private != nil { + if req.Private.Provider != nil { + readReq.Private = req.Private.Provider + readResp.Private = req.Private.Provider + } + + resp.Private = req.Private + } + logging.FrameworkDebug(ctx, "Calling provider defined Resource Read") resourceImpl.Read(ctx, readReq, &readResp) logging.FrameworkDebug(ctx, "Called provider defined Resource Read") resp.Diagnostics = readResp.Diagnostics resp.NewState = &readResp.State + + if readResp.Private != nil { + if resp.Private == nil { + resp.Private = &privatestate.Data{} + } + + resp.Private.Provider = readResp.Private + } } diff --git a/internal/fwserver/server_readresource_test.go b/internal/fwserver/server_readresource_test.go index 31e381932..cab5de1ad 100644 --- a/internal/fwserver/server_readresource_test.go +++ b/internal/fwserver/server_readresource_test.go @@ -1,18 +1,21 @@ package fwserver_test import ( + "bytes" "context" "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/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "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 TestServerReadResource(t *testing.T) { @@ -68,6 +71,35 @@ func TestServerReadResource(t *testing.T) { Schema: testSchema, } + testPrivateFrameworkMap := map[string][]byte{ + ".frameworkKey": []byte(`{"fk": "framework value"}`), + } + + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testPrivate := &privatestate.Data{ + Framework: testPrivateFrameworkMap, + Provider: testProviderData, + } + + testPrivateFramework := &privatestate.Data{ + Framework: testPrivateFrameworkMap, + } + + testPrivateProvider := &privatestate.Data{ + Provider: testProviderData, + } + + testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + + testEmptyPrivate := &privatestate.Data{ + Provider: testEmptyProviderData, + } + testCases := map[string]struct { server *fwserver.Server request *fwserver.ReadResourceRequest @@ -124,6 +156,7 @@ func TestServerReadResource(t *testing.T) { }, expectedResponse: &fwserver.ReadResourceResponse{ NewState: testCurrentState, + Private: testEmptyPrivate, }, }, "request-providermeta": { @@ -157,6 +190,74 @@ func TestServerReadResource(t *testing.T) { }, expectedResponse: &fwserver.ReadResourceResponse{ NewState: testCurrentState, + Private: testEmptyPrivate, + }, + }, + "request-private": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ReadResourceRequest{ + CurrentState: testCurrentState, + 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.Resource{ + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + 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 + }, + }, + Private: testPrivate, + }, + expectedResponse: &fwserver.ReadResourceResponse{ + NewState: testCurrentState, + Private: testPrivate, + }, + }, + "request-private-nil": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ReadResourceRequest{ + CurrentState: testCurrentState, + 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.Resource{ + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var expected []byte + + key := "providerKeyOne" + got, diags := req.Private.GetKey(ctx, key) + + resp.Diagnostics.Append(diags...) + + if !bytes.Equal(got, expected) { + resp.Diagnostics.AddError("unexpected req.Private.Provider value: %s", string(got)) + } + }, + }, nil + }, + }, + }, + expectedResponse: &fwserver.ReadResourceResponse{ + NewState: testCurrentState, + Private: testEmptyPrivate, }, }, "response-diagnostics": { @@ -191,6 +292,7 @@ func TestServerReadResource(t *testing.T) { ), }, NewState: testCurrentState, + Private: testEmptyPrivate, }, }, "response-state": { @@ -223,6 +325,7 @@ func TestServerReadResource(t *testing.T) { }, expectedResponse: &fwserver.ReadResourceResponse{ NewState: testNewState, + Private: testEmptyPrivate, }, }, "response-state-removeresource": { @@ -246,6 +349,60 @@ func TestServerReadResource(t *testing.T) { }, expectedResponse: &fwserver.ReadResourceResponse{ NewState: testNewStateRemoved, + Private: testEmptyPrivate, + }, + }, + "response-private": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ReadResourceRequest{ + CurrentState: testCurrentState, + 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.Resource{ + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + diags := resp.Private.SetKey(ctx, "providerKeyOne", []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`)) + + resp.Diagnostics.Append(diags...) + }, + }, nil + }, + }, + }, + expectedResponse: &fwserver.ReadResourceResponse{ + NewState: testCurrentState, + Private: testPrivateProvider, + }, + }, + "response-private-updated": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ReadResourceRequest{ + CurrentState: testCurrentState, + 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.Resource{ + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + diags := resp.Private.SetKey(ctx, "providerKeyOne", []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`)) + + resp.Diagnostics.Append(diags...) + }, + }, nil + }, + }, + Private: testPrivateFramework, + }, + expectedResponse: &fwserver.ReadResourceResponse{ + NewState: testCurrentState, + Private: testPrivate, }, }, } @@ -259,7 +416,7 @@ func TestServerReadResource(t *testing.T) { response := &fwserver.ReadResourceResponse{} testCase.server.ReadResource(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/fwserver/server_updateresource.go b/internal/fwserver/server_updateresource.go index 46a965548..78030c06d 100644 --- a/internal/fwserver/server_updateresource.go +++ b/internal/fwserver/server_updateresource.go @@ -3,20 +3,22 @@ package fwserver import ( "context" + "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/provider" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // UpdateResourceRequest is the framework server request for an update request // with the ApplyResourceChange RPC. type UpdateResourceRequest struct { Config *tfsdk.Config - PlannedPrivate []byte + PlannedPrivate *privatestate.Data PlannedState *tfsdk.Plan PriorState *tfsdk.State ProviderMeta *tfsdk.Config @@ -29,7 +31,7 @@ type UpdateResourceRequest struct { type UpdateResourceResponse struct { Diagnostics diag.Diagnostics NewState *tfsdk.State - Private []byte + Private *privatestate.Data } // UpdateResource implements the framework server update request logic for the @@ -91,6 +93,20 @@ func (s *Server) UpdateResource(ctx context.Context, req *UpdateResourceRequest, updateReq.ProviderMeta = *req.ProviderMeta } + privateProviderData := privatestate.EmptyProviderData(ctx) + + updateReq.Private = privateProviderData + updateResp.Private = privateProviderData + + if req.PlannedPrivate != nil { + if req.PlannedPrivate.Provider != nil { + updateReq.Private = req.PlannedPrivate.Provider + updateResp.Private = req.PlannedPrivate.Provider + } + + resp.Private = req.PlannedPrivate + } + logging.FrameworkDebug(ctx, "Calling provider defined Resource Update") resourceImpl.Update(ctx, updateReq, &updateResp) logging.FrameworkDebug(ctx, "Called provider defined Resource Update") @@ -105,4 +121,12 @@ func (s *Server) UpdateResource(ctx context.Context, req *UpdateResourceRequest, "This is always an issue in the Terraform Provider and should be reported to the provider developers.", ) } + + if updateResp.Private != nil { + if resp.Private == nil { + resp.Private = &privatestate.Data{} + } + + resp.Private.Provider = updateResp.Private + } } diff --git a/internal/fwserver/server_updateresource_test.go b/internal/fwserver/server_updateresource_test.go index e486d6eb7..143d44453 100644 --- a/internal/fwserver/server_updateresource_test.go +++ b/internal/fwserver/server_updateresource_test.go @@ -1,18 +1,22 @@ package fwserver_test import ( + "bytes" "context" + "fmt" "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/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "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 TestServerUpdateResource(t *testing.T) { @@ -71,6 +75,35 @@ func TestServerUpdateResource(t *testing.T) { TestProviderMetaAttribute types.String `tfsdk:"test_provider_meta_attribute"` } + testPrivateFrameworkMap := map[string][]byte{ + ".frameworkKey": []byte(`{"fk": "framework value"}`), + } + + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testPrivate := &privatestate.Data{ + Framework: testPrivateFrameworkMap, + Provider: testProviderData, + } + + testPrivateFramework := &privatestate.Data{ + Framework: testPrivateFrameworkMap, + } + + testPrivateProvider := &privatestate.Data{ + Provider: testProviderData, + } + + testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + + testEmptyPrivate := &privatestate.Data{ + Provider: testEmptyProviderData, + } + testCases := map[string]struct { server *fwserver.Server request *fwserver.UpdateResourceRequest @@ -131,6 +164,7 @@ func TestServerUpdateResource(t *testing.T) { }), Schema: testSchema, }, + Private: testEmptyPrivate, }, }, "request-plannedstate": { @@ -188,6 +222,7 @@ func TestServerUpdateResource(t *testing.T) { }), Schema: testSchema, }, + Private: testEmptyPrivate, }, }, "request-priorstate": { @@ -245,6 +280,7 @@ func TestServerUpdateResource(t *testing.T) { }), Schema: testSchema, }, + Private: testEmptyPrivate, }, }, "request-providermeta": { @@ -303,6 +339,106 @@ func TestServerUpdateResource(t *testing.T) { }), Schema: testSchema, }, + Private: testEmptyPrivate, + }, + }, + "request-private": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpdateResourceRequest{ + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, nil), + }), + 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.Resource{ + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + expected := `{"pKeyOne": {"k0": "zero", "k1": 1}}` + got, diags := req.Private.GetKey(ctx, "providerKeyOne") + + resp.Diagnostics.Append(diags...) + + if string(got) != expected { + resp.Diagnostics.AddError( + "Unexpected req.Private Value", + fmt.Sprintf("expected %q, got %q", expected, got), + ) + } + }, + }, nil + }, + }, + PlannedPrivate: &privatestate.Data{ + Provider: testProviderData, + }, + }, + expectedResponse: &fwserver.UpdateResourceResponse{ + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchema, + }, + Private: &privatestate.Data{ + Provider: testProviderData, + }, + }, + }, + "request-private-nil": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpdateResourceRequest{ + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, nil), + }), + 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.Resource{ + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var expected []byte + got, diags := req.Private.GetKey(ctx, "providerKeyOne") + + resp.Diagnostics.Append(diags...) + + if !bytes.Equal(got, expected) { + resp.Diagnostics.AddError( + "Unexpected req.Private Value", + fmt.Sprintf("expected %q, got %q", expected, got), + ) + } + }, + }, nil + }, + }, + }, + expectedResponse: &fwserver.UpdateResourceResponse{ + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate, }, }, "response-diagnostics": { @@ -364,6 +500,7 @@ func TestServerUpdateResource(t *testing.T) { }), Schema: testSchema, }, + Private: testEmptyPrivate, }, }, "response-newstate": { @@ -417,6 +554,7 @@ func TestServerUpdateResource(t *testing.T) { }), Schema: testSchema, }, + Private: testEmptyPrivate, }, }, "response-newstate-null": { @@ -471,6 +609,86 @@ func TestServerUpdateResource(t *testing.T) { Raw: tftypes.NewValue(testSchemaType, nil), Schema: testSchema, }, + Private: testEmptyPrivate, + }, + }, + "response-private": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpdateResourceRequest{ + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, nil), + }), + 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.Resource{ + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + diags := resp.Private.SetKey(ctx, "providerKeyOne", []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`)) + + resp.Diagnostics.Append(diags...) + }, + }, nil + }, + }, + }, + expectedResponse: &fwserver.UpdateResourceResponse{ + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchema, + }, + Private: testPrivateProvider, + }, + }, + "response-private-updated": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpdateResourceRequest{ + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, nil), + }), + 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.Resource{ + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + diags := resp.Private.SetKey(ctx, "providerKeyOne", []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`)) + + resp.Diagnostics.Append(diags...) + }, + }, nil + }, + }, + PlannedPrivate: testPrivateFramework, + }, + expectedResponse: &fwserver.UpdateResourceResponse{ + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchema, + }, + Private: testPrivate, }, }, } @@ -484,7 +702,7 @@ func TestServerUpdateResource(t *testing.T) { response := &fwserver.UpdateResourceResponse{} testCase.server.UpdateResource(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/privatestate/data.go b/internal/privatestate/data.go new file mode 100644 index 000000000..89ca3e3c1 --- /dev/null +++ b/internal/privatestate/data.go @@ -0,0 +1,368 @@ +package privatestate + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "unicode/utf8" + + "github.com/hashicorp/terraform-plugin-log/tflog" + + "github.com/hashicorp/terraform-plugin-framework/diag" +) + +// Data contains private state data for the framework and providers. +type Data struct { + // Potential future usage: + // Framework contains private state data for framework usage. + Framework map[string][]byte + + // Provider contains private state data for provider usage. + Provider *ProviderData +} + +// Bytes returns a JSON encoded slice of bytes containing the merged +// framework and provider private state data. +func (d *Data) Bytes(ctx context.Context) ([]byte, diag.Diagnostics) { + var diags diag.Diagnostics + + if d == nil { + return nil, nil + } + + if (d.Provider == nil || len(d.Provider.data) == 0) && len(d.Framework) == 0 { + return nil, nil + } + + var providerData map[string][]byte + + if d.Provider != nil { + providerData = d.Provider.data + } + + mergedMap := make(map[string][]byte, len(d.Framework)+len(providerData)) + + for _, m := range []map[string][]byte{d.Framework, providerData} { + for k, v := range m { + if len(v) == 0 { + continue + } + + // Values in FrameworkData and ProviderData should never be invalid UTF-8, but let's make sure. + if !utf8.Valid(v) { + diags.AddError( + "Error Encoding Private State", + "An error was encountered when validating private state value."+ + fmt.Sprintf("The value associated with key %q is is not valid UTF-8.\n\n", k)+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ) + + tflog.Error(ctx, "error encoding private state: invalid UTF-8 value", map[string]interface{}{"key": k, "value": v}) + + continue + } + + // Values in FrameworkData and ProviderData should never be invalid JSON, but let's make sure. + if !json.Valid(v) { + diags.AddError( + "Error Encoding Private State", + fmt.Sprintf("An error was encountered when validating private state value."+ + fmt.Sprintf("The value associated with key %q is is not valid JSON.\n\n", k)+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer."), + ) + + tflog.Error(ctx, "error encoding private state: invalid JSON value", map[string]interface{}{"key": k, "value": v}) + + continue + } + + mergedMap[k] = v + } + } + + if diags.HasError() { + return nil, diags + } + + bytes, err := json.Marshal(mergedMap) + if err != nil { + diags.AddError( + "Error Encoding Private State", + fmt.Sprintf("An error was encountered when encoding private state: %s.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", err), + ) + + return nil, diags + } + + return bytes, diags +} + +// NewData creates a new Data based on the given slice of bytes. +// It must be a JSON encoded slice of bytes, that is map[string][]byte. +func NewData(ctx context.Context, data []byte) (*Data, diag.Diagnostics) { + var ( + dataMap map[string][]byte + diags diag.Diagnostics + ) + + if len(data) == 0 { + return nil, nil + } + + err := json.Unmarshal(data, &dataMap) + if err != nil { + diags.AddError( + "Error Decoding Private State", + fmt.Sprintf("An error was encountered when decoding private state: %s.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", err), + ) + + return nil, diags + } + + output := Data{ + Framework: make(map[string][]byte), + Provider: &ProviderData{ + make(map[string][]byte), + }, + } + + for k, v := range dataMap { + if !utf8.Valid(v) { + diags.AddError( + "Error Decoding Private State", + "An error was encountered when validating private state value.\n"+ + fmt.Sprintf("The value being supplied for key %q is is not valid UTF-8.\n\n", k)+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ) + + tflog.Error(ctx, "error decoding private state: invalid UTF-8 value", map[string]interface{}{"key": k, "value": v}) + + continue + } + + if !json.Valid(v) { + diags.AddError( + "Error Decoding Private State", + "An error was encountered when validating private state value.\n"+ + fmt.Sprintf("The value being supplied for key %q is is not valid JSON.\n\n", k)+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ) + + tflog.Error(ctx, "error decoding private state: invalid JSON value", map[string]interface{}{"key": k, "value": v}) + + continue + } + + if isInvalidProviderDataKey(ctx, k) { + output.Framework[k] = v + continue + } + + output.Provider.data[k] = v + } + + if diags.HasError() { + return nil, diags + } + + return &output, diags +} + +// EmptyData creates an initialised but empty Data. +func EmptyData(ctx context.Context) *Data { + return &Data{ + Provider: EmptyProviderData(ctx), + } +} + +// NewProviderData creates a new ProviderData based on the given slice of bytes. +// It must be a JSON encoded slice of bytes, that is map[string][]byte. +func NewProviderData(ctx context.Context, data []byte) (*ProviderData, diag.Diagnostics) { + providerData := EmptyProviderData(ctx) + + if len(data) == 0 { + return providerData, nil + } + + var ( + dataMap map[string][]byte + diags diag.Diagnostics + ) + + err := json.Unmarshal(data, &dataMap) + if err != nil { + diags.AddError( + "Error Decoding Provider Data", + fmt.Sprintf("An error was encountered when decoding provider data: %s.\n\n"+ + "Please check that the data you are supplying is a byte representation of valid JSON.", err), + ) + + return nil, diags + } + + for k, v := range dataMap { + diags.Append(providerData.SetKey(ctx, k, v)...) + } + + if diags.HasError() { + return nil, diags + } + + return providerData, diags +} + +// EmptyProviderData creates a ProviderData containing initialised but empty data. +func EmptyProviderData(ctx context.Context) *ProviderData { + return &ProviderData{ + data: make(map[string][]byte), + } +} + +// ProviderData contains private state data for provider usage. +type ProviderData struct { + data map[string][]byte +} + +// GetKey returns the private state data associated with the given key. +// +// If the key is reserved for framework usage, an error diagnostic +// is returned. If the key is valid, but private state data is not found, +// nil is returned. +// +// The naming of keys only matters in context of a single resource, +// however care should be taken that any historical keys are not reused +// without accounting for older resource instances that may still have +// older data at the key. +func (d *ProviderData) GetKey(ctx context.Context, key string) ([]byte, diag.Diagnostics) { + if d == nil || d.data == nil { + return nil, nil + } + + diags := ValidateProviderDataKey(ctx, key) + + if diags.HasError() { + return nil, diags + } + + value, ok := d.data[key] + if !ok { + return nil, nil + } + + return value, nil +} + +// SetKey sets the private state data at the given key. +// +// If the key is reserved for framework usage, an error diagnostic +// is returned. The data must be valid JSON and UTF-8 safe or an error +// diagnostic is returned. +// +// The naming of keys only matters in context of a single resource, +// however care should be taken that any historical keys are not reused +// without accounting for older resource instances that may still have +// older data at the key. +func (d *ProviderData) SetKey(ctx context.Context, key string, value []byte) diag.Diagnostics { + var diags diag.Diagnostics + + if d == nil { + tflog.Error(ctx, "error calling SetKey on uninitialized ProviderData") + + diags.AddError("Uninitialized ProviderData", + "ProviderData must be initialized before it is used.\n\n"+ + "Call privatestate.NewProviderData to obtain an initialized instance of ProviderData.", + ) + + return diags + } + + if d.data == nil { + d.data = make(map[string][]byte) + } + + diags.Append(ValidateProviderDataKey(ctx, key)...) + + if diags.HasError() { + return diags + } + + if !utf8.Valid(value) { + tflog.Error(ctx, "invalid UTF-8 value", map[string]interface{}{"key": key, "value": value}) + + diags.AddError("UTF-8 Invalid", + "Values stored in private state must be valid UTF-8.\n\n"+ + fmt.Sprintf("The value being supplied for key %q is invalid. Please verify that the value is valid UTF-8.", key), + ) + + return diags + } + + if !json.Valid(value) { + tflog.Error(ctx, "invalid JSON value", map[string]interface{}{"key": key, "value": value}) + + diags.AddError("JSON Invalid", + "Values stored in private state must be valid JSON.\n\n"+ + fmt.Sprintf("The value being supplied for key %q is invalid. Please verify that the value is valid JSON.", key), + ) + + return diags + } + + d.data[key] = value + + return nil +} + +// ValidateProviderDataKey determines whether the key supplied is allowed on the basis of any +// restrictions that are in place, such as key prefixes that are reserved for use with +// framework private state data. +func ValidateProviderDataKey(ctx context.Context, key string) diag.Diagnostics { + if isInvalidProviderDataKey(ctx, key) { + return diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Restricted Resource Private State Namespace", + "Using a period ('.') as a prefix for a key used in private state is not allowed.\n\n"+ + fmt.Sprintf("The key %q is invalid. Please check the key you are supplying does not use a a period ('.') as a prefix.", key), + ), + } + } + + return nil +} + +// isInvalidProviderDataKey determines whether the supplied key has a prefix that is reserved for +// keys in Data.Framework +func isInvalidProviderDataKey(_ context.Context, key string) bool { + return strings.HasPrefix(key, ".") +} + +// MustMarshalToJson is for use in tests and panics if input cannot be marshalled to JSON. +func MustMarshalToJson(input map[string][]byte) []byte { + output, err := json.Marshal(input) + if err != nil { + panic(err) + } + + return output +} + +// MustProviderData is for use in tests and panics if the underlying call to NewProviderData +// returns diag.Diagnostics that contains any errors. +func MustProviderData(ctx context.Context, data []byte) *ProviderData { + providerData, diags := NewProviderData(ctx, data) + + if diags.HasError() { + var diagMsgs []string + + for _, v := range diags { + diagMsgs = append(diagMsgs, fmt.Sprintf("%s: %s", v.Summary(), v.Detail())) + } + + panic(fmt.Sprintf("error creating new provider data: %s", strings.Join(diagMsgs, ", "))) + } + + return providerData +} diff --git a/internal/privatestate/data_test.go b/internal/privatestate/data_test.go new file mode 100644 index 000000000..3a7e2ba7b --- /dev/null +++ b/internal/privatestate/data_test.go @@ -0,0 +1,819 @@ +package privatestate + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-framework/diag" +) + +func TestData_Bytes(t *testing.T) { + // 1 x 1 transparent gif pixel. + const transPixel = "\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x21\xF9\x04\x01\x00\x00\x00\x00\x2C\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02\x44\x01\x00\x3B" + + t.Parallel() + + testCases := map[string]struct { + data *Data + expected []byte + expectedDiags diag.Diagnostics + }{ + "nil": { + data: nil, + }, + "empty": { + data: &Data{}, + }, + "uninitialized-provider-data-data": { + data: &Data{ + Provider: &ProviderData{}, + }, + }, + "empty-initialized-provider-data-data": { + data: &Data{ + Provider: &ProviderData{ + data: nil, + }, + }, + }, + "framework-data-value-invalid-utf-8": { + data: &Data{ + Framework: map[string][]byte{ + ".frameworkKeyOne": []byte(fmt.Sprintf(`{"fwKeyOne": "%s"}`, transPixel)), + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Error Encoding Private State", + "An error was encountered when validating private state value."+ + "The value associated with key \".frameworkKeyOne\" is is not valid UTF-8.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "framework-data-value-invalid-json": { + data: &Data{ + Framework: map[string][]byte{ + ".frameworkKeyOne": []byte(`}`), + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Error Encoding Private State", + "An error was encountered when validating private state value."+ + "The value associated with key \".frameworkKeyOne\" is is not valid JSON.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "framework-data": { + data: &Data{ + Framework: map[string][]byte{ + ".frameworkKeyOne": []byte(`{"fwKeyOne": {"k0": "zero", "k1": 1}}`), + ".frameworkKeyTwo": []byte(`{"fwKeyTwo": {"k2": "two", "k3": 3}}`), + }, + }, + expected: []byte(`{` + + `".frameworkKeyOne":"eyJmd0tleU9uZSI6IHsiazAiOiAiemVybyIsICJrMSI6IDF9fQ==",` + + `".frameworkKeyTwo":"eyJmd0tleVR3byI6IHsiazIiOiAidHdvIiwgImszIjogM319"` + + `}`), + }, + "framework-data-value-nil": { + data: &Data{ + Framework: map[string][]byte{ + ".frameworkKeyOne": []byte(`{"fwKeyOne": {"k0": "zero", "k1": 1}}`), + ".frameworkKeyTwo": nil, + }, + }, + expected: []byte(`{` + + `".frameworkKeyOne":"eyJmd0tleU9uZSI6IHsiazAiOiAiemVybyIsICJrMSI6IDF9fQ=="` + + `}`), + }, + "framework-data-value-zero-len": { + data: &Data{ + Framework: map[string][]byte{ + ".frameworkKeyOne": []byte(`{"fwKeyOne": {"k0": "zero", "k1": 1}}`), + ".frameworkKeyTwo": {}, + }, + }, + expected: []byte(`{` + + `".frameworkKeyOne":"eyJmd0tleU9uZSI6IHsiazAiOiAiemVybyIsICJrMSI6IDF9fQ=="` + + `}`), + }, + "provider-data-data-value-invalid-utf-8": { + data: &Data{ + Provider: &ProviderData{ + data: map[string][]byte{ + "providerKeyOne": []byte(fmt.Sprintf(`{"key": "%s"}`, transPixel)), + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Error Encoding Private State", + "An error was encountered when validating private state value."+ + "The value associated with key \"providerKeyOne\" is is not valid UTF-8.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "provider-data-data-value-invalid-json": { + data: &Data{ + Provider: &ProviderData{ + data: map[string][]byte{ + "providerKeyOne": []byte(`}`), + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Error Encoding Private State", + "An error was encountered when validating private state value."+ + "The value associated with key \"providerKeyOne\" is is not valid JSON.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "provider-data": { + data: &Data{ + Provider: &ProviderData{ + data: map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyTwo": []byte(`{"pKeyTwo": {"k2": "two", "k3": 3}}`), + }, + }, + }, + expected: []byte(`{` + + `"providerKeyOne":"eyJwS2V5T25lIjogeyJrMCI6ICJ6ZXJvIiwgImsxIjogMX19",` + + `"providerKeyTwo":"eyJwS2V5VHdvIjogeyJrMiI6ICJ0d28iLCAiazMiOiAzfX0="` + + `}`), + }, + "provider-data-data-value-nil": { + data: &Data{ + Provider: &ProviderData{ + data: map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyTwo": nil, + }, + }, + }, + expected: []byte(`{"providerKeyOne":"eyJwS2V5T25lIjogeyJrMCI6ICJ6ZXJvIiwgImsxIjogMX19"}`), + }, + "provider-data-data-value-zero-len": { + data: &Data{ + Provider: &ProviderData{ + data: map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyTwo": {}, + }, + }, + }, + expected: []byte(`{"providerKeyOne":"eyJwS2V5T25lIjogeyJrMCI6ICJ6ZXJvIiwgImsxIjogMX19"}`), + }, + "framework-provider-data": { + data: &Data{ + Framework: map[string][]byte{ + ".frameworkKeyOne": []byte(`{"fwKeyOne": {"k0": "zero", "k1": 1}}`), + ".frameworkKeyTwo": []byte(`{"fwKeyTwo": {"k2": "two", "k3": 3}}`), + }, + Provider: &ProviderData{ + data: map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyTwo": []byte(`{"pKeyTwo": {"k2": "two", "k3": 3}}`), + }, + }, + }, + expected: []byte(`{` + + `".frameworkKeyOne":"eyJmd0tleU9uZSI6IHsiazAiOiAiemVybyIsICJrMSI6IDF9fQ==",` + + `".frameworkKeyTwo":"eyJmd0tleVR3byI6IHsiazIiOiAidHdvIiwgImszIjogM319",` + + `"providerKeyOne":"eyJwS2V5T25lIjogeyJrMCI6ICJ6ZXJvIiwgImsxIjogMX19",` + + `"providerKeyTwo":"eyJwS2V5VHdvIjogeyJrMiI6ICJ0d28iLCAiazMiOiAzfX0="` + + `}`), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + actual, actualDiags := testCase.data.Bytes(context.Background()) + + if diff := cmp.Diff(actual, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(actualDiags, testCase.expectedDiags); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNewData(t *testing.T) { + // 1 x 1 transparent gif pixel. + const transPixel = "\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x21\xF9\x04\x01\x00\x00\x00\x00\x2C\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02\x44\x01\x00\x3B" + + frameworkInvalidUTF8 := MustMarshalToJson(map[string][]byte{ + ".frameworkKeyOne": []byte(transPixel), + ".frameworkKeyTwo": []byte(`{"fwKeyTwo": {"k2": "two", "k3": 3}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyTwo": []byte(`{"pKeyTwo": {"k2": "two", "k3": 3}}`), + }) + + frameworkValueInvalidUTF8 := MustMarshalToJson(map[string][]byte{ + ".frameworkKeyOne": []byte(fmt.Sprintf(`{"fwKeyOne": "%s"}`, transPixel)), + ".frameworkKeyTwo": []byte(`{"fwKeyTwo": {"k2": "two", "k3": 3}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyTwo": []byte(`{"pKeyTwo": {"k2": "two", "k3": 3}}`), + }) + + frameworkInvalidJSON := MustMarshalToJson(map[string][]byte{ + ".frameworkKeyOne": []byte(`{`), + ".frameworkKeyTwo": []byte(`{"fwKeyTwo": {"k2": "two", "k3": 3}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyTwo": []byte(`{"pKeyTwo": {"k2": "two", "k3": 3}}`), + }) + + frameworkValueInvalidJSON := MustMarshalToJson(map[string][]byte{ + ".frameworkKeyOne": []byte(`{"fwKeyOne": { }`), + ".frameworkKeyTwo": []byte(`{"fwKeyTwo": {"k2": "two", "k3": 3}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyTwo": []byte(`{"pKeyTwo": {"k2": "two", "k3": 3}}`), + }) + + providerInvalidUTF8 := MustMarshalToJson(map[string][]byte{ + ".frameworkKeyOne": []byte(`{"fwKeyOne": {"k0": "zero", "k1": 1}}`), + ".frameworkKeyTwo": []byte(`{"fwKeyTwo": {"k2": "two", "k3": 3}}`), + "providerKeyOne": []byte(transPixel), + "providerKeyTwo": []byte(`{"pKeyTwo": {"k2": "two", "k3": 3}}`), + }) + + providerValueInvalidUTF8 := MustMarshalToJson(map[string][]byte{ + ".frameworkKeyOne": []byte(`{"fwKeyOne": {"k0": "zero", "k1": 1}}`), + ".frameworkKeyTwo": []byte(`{"fwKeyTwo": {"k2": "two", "k3": 3}}`), + "providerKeyOne": []byte(fmt.Sprintf(`{"fwKeyOne": "%s"}`, transPixel)), + "providerKeyTwo": []byte(`{"pKeyTwo": {"k2": "two", "k3": 3}}`), + }) + + providerInvalidJSON := MustMarshalToJson(map[string][]byte{ + ".frameworkKeyOne": []byte(`{"fwKeyOne": {"k0": "zero", "k1": 1}}`), + ".frameworkKeyTwo": []byte(`{"fwKeyTwo": {"k2": "two", "k3": 3}}`), + "providerKeyOne": []byte(`{`), + "providerKeyTwo": []byte(`{"pKeyTwo": {"k2": "two", "k3": 3}}`), + }) + + providerValueInvalidJSON := MustMarshalToJson(map[string][]byte{ + ".frameworkKeyOne": []byte(`{"fwKeyOne": {"k0": "zero", "k1": 1}}`), + ".frameworkKeyTwo": []byte(`{"fwKeyTwo": {"k2": "two", "k3": 3}}`), + "providerKeyOne": []byte(`{"pKeyOne": { }`), + "providerKeyTwo": []byte(`{"pKeyTwo": {"k2": "two", "k3": 3}}`), + }) + + frameworkProviderData := MustMarshalToJson(map[string][]byte{ + ".frameworkKeyOne": []byte(`{"fwKeyOne": {"k0": "zero", "k1": 1}}`), + ".frameworkKeyTwo": []byte(`{"fwKeyTwo": {"k2": "two", "k3": 3}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyTwo": []byte(`{"pKeyTwo": {"k2": "two", "k3": 3}}`), + }) + + testCases := map[string]struct { + data []byte + expected *Data + expectedDiags diag.Diagnostics + }{ + "empty": { + data: []byte{}, + }, + "invalid-json": { + data: []byte(`{`), + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic("Error Decoding Private State", + "An error was encountered when decoding private state: unexpected end of JSON input.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "empty-json": { + data: []byte(`{}`), + expected: &Data{ + Framework: map[string][]byte{}, + Provider: &ProviderData{ + data: map[string][]byte{}, + }, + }, + }, + "framework-invalid-utf-8": { + data: frameworkInvalidUTF8, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Error Decoding Private State", + "An error was encountered when validating private state value.\n"+ + "The value being supplied for key \".frameworkKeyOne\" is is not valid UTF-8.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "framework-value-invalid-utf-8": { + data: frameworkValueInvalidUTF8, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Error Decoding Private State", + "An error was encountered when validating private state value.\n"+ + "The value being supplied for key \".frameworkKeyOne\" is is not valid UTF-8.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "framework-invalid-json": { + data: frameworkInvalidJSON, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Error Decoding Private State", + "An error was encountered when validating private state value.\n"+ + "The value being supplied for key \".frameworkKeyOne\" is is not valid JSON.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "framework-value-invalid-json": { + data: frameworkValueInvalidJSON, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Error Decoding Private State", + "An error was encountered when validating private state value.\n"+ + "The value being supplied for key \".frameworkKeyOne\" is is not valid JSON.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "provider-invalid-utf-8": { + data: providerInvalidUTF8, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Error Decoding Private State", + "An error was encountered when validating private state value.\n"+ + "The value being supplied for key \"providerKeyOne\" is is not valid UTF-8.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "provider-value-invalid-utf-8": { + data: providerValueInvalidUTF8, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Error Decoding Private State", + "An error was encountered when validating private state value.\n"+ + "The value being supplied for key \"providerKeyOne\" is is not valid UTF-8.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "provider-invalid-json": { + data: providerInvalidJSON, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Error Decoding Private State", + "An error was encountered when validating private state value.\n"+ + "The value being supplied for key \"providerKeyOne\" is is not valid JSON.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "provider-value-invalid-json": { + data: providerValueInvalidJSON, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Error Decoding Private State", + "An error was encountered when validating private state value.\n"+ + "The value being supplied for key \"providerKeyOne\" is is not valid JSON.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "framework-provider-data": { + data: frameworkProviderData, + expected: &Data{ + Framework: map[string][]byte{ + ".frameworkKeyOne": []byte(`{"fwKeyOne": {"k0": "zero", "k1": 1}}`), + ".frameworkKeyTwo": []byte(`{"fwKeyTwo": {"k2": "two", "k3": 3}}`), + }, + Provider: &ProviderData{ + data: map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyTwo": []byte(`{"pKeyTwo": {"k2": "two", "k3": 3}}`), + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + actual, actualDiags := NewData(context.Background(), testCase.data) + + if diff := cmp.Diff(actual, testCase.expected, cmp.AllowUnexported(ProviderData{})); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(actualDiags, testCase.expectedDiags); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNewProviderData(t *testing.T) { + // 1 x 1 transparent gif pixel. + const transPixel = "\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x21\xF9\x04\x01\x00\x00\x00\x00\x2C\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02\x44\x01\x00\x3B" + + invalidKey := MustMarshalToJson(map[string][]byte{ + ".providerKeyOne": {}, + }) + + invalidUTF8Value := MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(transPixel), + }) + + invalidJSONValue := MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{`), + }) + + validKeyValue := MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testCases := map[string]struct { + data []byte + expected *ProviderData + expectedDiags diag.Diagnostics + }{ + "empty": { + data: []byte{}, + expected: &ProviderData{ + data: map[string][]byte{}, + }, + }, + "invalid-json": { + data: []byte(`{`), + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic("Error Decoding Provider Data", + "An error was encountered when decoding provider data: unexpected end of JSON input.\n\n"+ + "Please check that the data you are supplying is a byte representation of valid JSON.", + ), + }, + }, + "empty-json": { + data: []byte(`{}`), + expected: &ProviderData{ + data: map[string][]byte{}, + }, + }, + "invalid-key": { + data: invalidKey, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Restricted Resource Private State Namespace", + "Using a period ('.') as a prefix for a key used in private state is not allowed.\n\n"+ + "The key \".providerKeyOne\" is invalid. Please check the key you are supplying does not use a a period ('.') as a prefix.", + ), + }, + }, + "invalid-utf-8-value": { + data: invalidUTF8Value, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "UTF-8 Invalid", + "Values stored in private state must be valid UTF-8.\n\n"+ + "The value being supplied for key \"providerKeyOne\" is invalid. Please verify that the value is valid UTF-8.", + ), + }, + }, + "invalid-json-value": { + data: invalidJSONValue, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "JSON Invalid", + "Values stored in private state must be valid JSON.\n\n"+ + "The value being supplied for key \"providerKeyOne\" is invalid. Please verify that the value is valid JSON.", + ), + }, + }, + "valid-key-value": { + data: validKeyValue, + expected: &ProviderData{ + data: map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + actual, actualDiags := NewProviderData(context.Background(), testCase.data) + + if diff := cmp.Diff(actual, testCase.expected, cmp.AllowUnexported(ProviderData{})); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(actualDiags, testCase.expectedDiags); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestProviderData_GetKey(t *testing.T) { + testCases := map[string]struct { + providerData *ProviderData + key string + expected []byte + expectedDiags diag.Diagnostics + }{ + "nil": { + providerData: &ProviderData{}, + key: "key", + }, + "key-invalid": { + providerData: &ProviderData{ + data: map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": "provider value one"}`), + }, + }, + key: ".key", + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Restricted Resource Private State Namespace", + "Using a period ('.') as a prefix for a key used in private state is not allowed.\n\n"+ + `The key ".key" is invalid. Please check the key you are supplying does not use a a period ('.') as a prefix.`, + ), + }, + }, + "key-not-found": { + providerData: &ProviderData{ + data: map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": "provider value one"}`), + }, + }, + key: "key-not-found", + }, + "key-found": { + providerData: &ProviderData{ + data: map[string][]byte{ + "key": []byte("value"), + }, + }, + key: "key", + expected: []byte("value"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + actual, actualDiags := testCase.providerData.GetKey(context.Background(), testCase.key) + + if diff := cmp.Diff(actual, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(actualDiags, testCase.expectedDiags); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestProviderData_SetKey(t *testing.T) { + // 1 x 1 transparent gif pixel. + const transPixel = "\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x21\xF9\x04\x01\x00\x00\x00\x00\x2C\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02\x44\x01\x00\x3B" + + testCases := map[string]struct { + providerData *ProviderData + key string + value []byte + expected *ProviderData + expectedDiags diag.Diagnostics + }{ + "nil": { + providerData: nil, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic("Uninitialized ProviderData", + "ProviderData must be initialized before it is used.\n\n"+ + "Call privatestate.NewProviderData to obtain an initialized instance of ProviderData."), + }, + }, + "key-invalid-data-uninitialized": { + providerData: &ProviderData{}, + key: ".key", + expected: &ProviderData{ + data: map[string][]byte{}, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Restricted Resource Private State Namespace", + "Using a period ('.') as a prefix for a key used in private state is not allowed.\n\n"+ + `The key ".key" is invalid. Please check the key you are supplying does not use a a period ('.') as a prefix.`, + ), + }, + }, + "key-invalid-data-initialized": { + providerData: &ProviderData{ + data: map[string][]byte{}, + }, + key: ".key", + expected: &ProviderData{ + data: map[string][]byte{}, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Restricted Resource Private State Namespace", + "Using a period ('.') as a prefix for a key used in private state is not allowed.\n\n"+ + `The key ".key" is invalid. Please check the key you are supplying does not use a a period ('.') as a prefix.`, + ), + }, + }, + "value-utf8-invalid-data-uninitialized": { + providerData: &ProviderData{}, + key: "key", + value: []byte(fmt.Sprintf(`{"key": "%s"}`, transPixel)), + expected: &ProviderData{ + data: map[string][]byte{}, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "UTF-8 Invalid", + "Values stored in private state must be valid UTF-8.\n\n"+ + `The value being supplied for key "key" is invalid. Please verify that the value is valid UTF-8.`, + ), + }, + }, + "value-utf8-invalid-data-initialized": { + providerData: &ProviderData{ + data: map[string][]byte{}, + }, + key: "key", + value: []byte(fmt.Sprintf(`{"key": "%s"}`, transPixel)), + expected: &ProviderData{ + data: map[string][]byte{}, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "UTF-8 Invalid", + "Values stored in private state must be valid UTF-8.\n\n"+ + `The value being supplied for key "key" is invalid. Please verify that the value is valid UTF-8.`, + ), + }, + }, + "value-json-invalid-data-uninitialized": { + providerData: &ProviderData{}, + key: "key", + value: []byte("{"), + expected: &ProviderData{ + data: map[string][]byte{}, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "JSON Invalid", + "Values stored in private state must be valid JSON.\n\n"+ + `The value being supplied for key "key" is invalid. Please verify that the value is valid JSON.`, + ), + }, + }, + "value-json-invalid-data-initialized": { + providerData: &ProviderData{ + data: map[string][]byte{}, + }, + key: "key", + value: []byte("{"), + expected: &ProviderData{ + data: map[string][]byte{}, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "JSON Invalid", + "Values stored in private state must be valid JSON.\n\n"+ + `The value being supplied for key "key" is invalid. Please verify that the value is valid JSON.`, + ), + }, + }, + "key-value-ok-data-uninitialized": { + providerData: &ProviderData{}, + key: "key", + value: []byte(`{"key": {"k0": "zero", "k1": 1}}`), + expected: &ProviderData{ + data: map[string][]byte{ + "key": []byte(`{"key": {"k0": "zero", "k1": 1}}`), + }, + }, + }, + "key-value-ok-data-initialized": { + providerData: &ProviderData{ + data: map[string][]byte{}, + }, + key: "key", + value: []byte(`{"key": {"k0": "zero", "k1": 1}}`), + expected: &ProviderData{ + data: map[string][]byte{ + "key": []byte(`{"key": {"k0": "zero", "k1": 1}}`), + }, + }, + }, + "key-value-added": { + providerData: &ProviderData{ + data: map[string][]byte{ + "keyOne": []byte(`{"foo": "bar"}`), + }, + }, + key: "keyTwo", + value: []byte(`{"buzz": "bazz"}`), + expected: &ProviderData{ + data: map[string][]byte{ + "keyOne": []byte(`{"foo": "bar"}`), + "keyTwo": []byte(`{"buzz": "bazz"}`), + }, + }, + }, + "key-value-updated": { + providerData: &ProviderData{ + data: map[string][]byte{ + "keyOne": []byte(`{"foo": "bar"}`), + }, + }, + key: "keyOne", + value: []byte(`{"buzz": "bazz"}`), + expected: &ProviderData{ + data: map[string][]byte{ + "keyOne": []byte(`{"buzz": "bazz"}`), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + actual := testCase.providerData.SetKey(context.Background(), testCase.key, testCase.value) + + if diff := cmp.Diff(testCase.expected, testCase.providerData, cmp.AllowUnexported(ProviderData{})); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(actual, testCase.expectedDiags); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestValidateProviderDataKey(t *testing.T) { + testCases := map[string]struct { + key string + expectedDiags diag.Diagnostics + }{ + "namespace-restricted": { + key: ".restricted", + expectedDiags: diag.Diagnostics{diag.NewErrorDiagnostic( + "Restricted Resource Private State Namespace", + "Using a period ('.') as a prefix for a key used in private state is not allowed.\n\n"+ + `The key ".restricted" is invalid. Please check the key you are supplying does not use a a period ('.') as a prefix.`, + )}, + }, + "namespace-ok": { + key: "unrestricted", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + actual := ValidateProviderDataKey(context.Background(), testCase.key) + + if diff := cmp.Diff(actual, testCase.expectedDiags); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/privatestate/doc.go b/internal/privatestate/doc.go new file mode 100644 index 000000000..db2c56cc9 --- /dev/null +++ b/internal/privatestate/doc.go @@ -0,0 +1,3 @@ +// Package privatestate contains the type used for handling private resource +// state data. +package privatestate diff --git a/internal/proto5server/server_applyresourcechange_test.go b/internal/proto5server/server_applyresourcechange_test.go index 4e34a9d86..c91b0cb7b 100644 --- a/internal/proto5server/server_applyresourcechange_test.go +++ b/internal/proto5server/server_applyresourcechange_test.go @@ -2,18 +2,21 @@ package proto5server import ( "context" + "fmt" "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/testing/testprovider" "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/tfprotov5" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestServerApplyResourceChange(t *testing.T) { @@ -419,6 +422,55 @@ func TestServerApplyResourceChange(t *testing.T) { NewState: &testEmptyDynamicValue, }, }, + "create-response-private": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + GetResourcesMethod: func(_ context.Context) (map[string]provider.ResourceType, diag.Diagnostics) { + return map[string]provider.ResourceType{ + "test_resource": &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.Resource{ + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data testSchemaData + + // Prevent missing resource state error diagnostic + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + + diags := resp.Private.SetKey(ctx, "providerKey", []byte(`{"key": "value"}`)) + + resp.Diagnostics.Append(diags...) + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Delete") + }, + UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Update") + }, + }, nil + }, + }, + }, nil + }, + }, + }, + }, + request: &tfprotov5.ApplyResourceChangeRequest{ + TypeName: "test_resource", + }, + expectedResponse: &tfprotov5.ApplyResourceChangeResponse{ + NewState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, ""), + "test_required": tftypes.NewValue(tftypes.String, ""), + }), + Private: privatestate.MustMarshalToJson(map[string][]byte{ + "providerKey": []byte(`{"key": "value"}`), + }), + }, + }, "delete-request-priorstate": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -519,6 +571,59 @@ func TestServerApplyResourceChange(t *testing.T) { NewState: &testEmptyDynamicValue, }, }, + "delete-request-private": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + GetResourcesMethod: func(_ context.Context) (map[string]provider.ResourceType, diag.Diagnostics) { + return map[string]provider.ResourceType{ + "test_resource": &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.Resource{ + CreateMethod: func(_ context.Context, _ resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Delete, Got: Create") + }, + DeleteMethod: func(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + expected := `{"key": "value"}` + got, diags := req.Private.GetKey(ctx, "providerKey") + + resp.Diagnostics.Append(diags...) + + if string(got) != expected { + resp.Diagnostics.AddError( + "Unexpected req.Private Value", + fmt.Sprintf("expected %q, got %q", expected, got), + ) + } + }, + UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Delete, Got: Update") + }, + }, nil + }, + }, + }, nil + }, + }, + }, + }, + request: &tfprotov5.ApplyResourceChangeRequest{ + PriorState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-priorstate-value"), + }), + TypeName: "test_resource", + PlannedPrivate: privatestate.MustMarshalToJson(map[string][]byte{ + "providerKey": []byte(`{"key": "value"}`), + }), + }, + expectedResponse: &tfprotov5.ApplyResourceChangeResponse{ + NewState: &testEmptyDynamicValue, + }, + }, "delete-response-diagnostics": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -857,6 +962,78 @@ func TestServerApplyResourceChange(t *testing.T) { }), }, }, + "update-request-private": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithMetaSchema{ + Provider: &testprovider.Provider{ + GetResourcesMethod: func(_ context.Context) (map[string]provider.ResourceType, diag.Diagnostics) { + return map[string]provider.ResourceType{ + "test_resource": &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.Resource{ + CreateMethod: func(_ context.Context, _ resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Create") + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Delete") + }, + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + expected := `{"providerKey": "provider value"}` + got, diags := req.Private.GetKey(ctx, "providerKey") + + resp.Diagnostics.Append(diags...) + + if string(got) != expected { + resp.Diagnostics.AddError( + "Unexpected req.Private Value", + fmt.Sprintf("expected %q, got %q", expected, got), + ) + } + }, + }, nil + }, + }, + }, nil + }, + }, + GetMetaSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { + return testProviderMetaSchema, nil + }, + }, + }, + }, + request: &tfprotov5.ApplyResourceChangeRequest{ + PlannedState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + PriorState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + ProviderMeta: testProviderMetaValue, + TypeName: "test_resource", + PlannedPrivate: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"frameworkKey": "framework value"}`), + "providerKey": []byte(`{"providerKey": "provider value"}`), + }), + }, + expectedResponse: &tfprotov5.ApplyResourceChangeResponse{ + // Intentionally old, Update implementation does not call resp.State.Set() + NewState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"frameworkKey": "framework value"}`), + "providerKey": []byte(`{"providerKey": "provider value"}`), + }), + }, + }, "update-response-diagnostics": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -1031,6 +1208,62 @@ func TestServerApplyResourceChange(t *testing.T) { NewState: &testEmptyDynamicValue, }, }, + "update-response-private": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + GetResourcesMethod: func(_ context.Context) (map[string]provider.ResourceType, diag.Diagnostics) { + return map[string]provider.ResourceType{ + "test_resource": &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.Resource{ + CreateMethod: func(_ context.Context, _ resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Create") + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Delete") + }, + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + diags := resp.Private.SetKey(ctx, "providerKey", []byte(`{"providerKey": "provider value"}`)) + + resp.Diagnostics.Append(diags...) + }, + }, nil + }, + }, + }, nil + }, + }, + }, + }, + request: &tfprotov5.ApplyResourceChangeRequest{ + PlannedState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + PriorState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + TypeName: "test_resource", + PlannedPrivate: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"frameworkKey": "framework value"}`), + }), + }, + expectedResponse: &tfprotov5.ApplyResourceChangeResponse{ + NewState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"frameworkKey": "framework value"}`), + "providerKey": []byte(`{"providerKey": "provider value"}`), + }), + }, + }, } for name, testCase := range testCases { diff --git a/internal/proto5server/server_importresourcestate_test.go b/internal/proto5server/server_importresourcestate_test.go index 80db18bcc..454f1d39d 100644 --- a/internal/proto5server/server_importresourcestate_test.go +++ b/internal/proto5server/server_importresourcestate_test.go @@ -5,16 +5,18 @@ 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/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/tfprotov5" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestServerImportResourceState(t *testing.T) { @@ -179,6 +181,50 @@ func TestServerImportResourceState(t *testing.T) { }, }, }, + "response-importedresources-private": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + GetResourcesMethod: func(_ context.Context) (map[string]provider.ResourceType, diag.Diagnostics) { + return map[string]provider.ResourceType{ + "test_resource": &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.ResourceWithImportState{ + Resource: &testprovider.Resource{}, + ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + diags := resp.Private.SetKey(ctx, "providerKey", []byte(`{"key": "value"}`)) + + resp.Diagnostics.Append(diags...) + + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + }, + }, nil + }, + }, + }, nil + }, + }, + }, + }, + request: &tfprotov5.ImportResourceStateRequest{ + ID: "test-id", + TypeName: "test_resource", + }, + expectedResponse: &tfprotov5.ImportResourceStateResponse{ + ImportedResources: []*tfprotov5.ImportedResource{ + { + State: testStateDynamicValue, + TypeName: "test_resource", + Private: privatestate.MustMarshalToJson(map[string][]byte{ + "providerKey": []byte(`{"key": "value"}`), + }), + }, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/proto5server/server_readresource.go b/internal/proto5server/server_readresource.go index 631dea4b3..fb9b45b87 100644 --- a/internal/proto5server/server_readresource.go +++ b/internal/proto5server/server_readresource.go @@ -3,11 +3,12 @@ package proto5server import ( "context" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/logging" "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" ) // ReadResource satisfies the tfprotov5.ProviderServer interface. diff --git a/internal/proto5server/server_readresource_test.go b/internal/proto5server/server_readresource_test.go index f8d355c6a..fdda2072a 100644 --- a/internal/proto5server/server_readresource_test.go +++ b/internal/proto5server/server_readresource_test.go @@ -2,18 +2,21 @@ package proto5server import ( "context" + "fmt" "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/testing/testprovider" "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/tfprotov5" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestServerReadResource(t *testing.T) { @@ -172,6 +175,55 @@ func TestServerReadResource(t *testing.T) { NewState: testEmptyDynamicValue, }, }, + "request-private": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + GetResourcesMethod: func(_ context.Context) (map[string]provider.ResourceType, diag.Diagnostics) { + return map[string]provider.ResourceType{ + "test_resource": &testprovider.ResourceType{ + GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { + return tfsdk.Schema{}, nil + }, + NewResourceMethod: func(_ context.Context, _ provider.Provider) (resource.Resource, diag.Diagnostics) { + return &testprovider.Resource{ + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + expected := `{"pKeyOne": {"k0": "zero", "k1": 1}}` + got, diags := req.Private.GetKey(ctx, "providerKey") + + resp.Diagnostics.Append(diags...) + + if string(got) != expected { + resp.Diagnostics.AddError( + "Unexpected req.Private Value", + fmt.Sprintf("expected %q, got %q", expected, got), + ) + } + }, + }, nil + }, + }, + }, nil + }, + }, + }, + }, + request: &tfprotov5.ReadResourceRequest{ + CurrentState: testEmptyDynamicValue, + TypeName: "test_resource", + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKey": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), + }, + expectedResponse: &tfprotov5.ReadResourceResponse{ + NewState: testEmptyDynamicValue, + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKey": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), + }, + }, "response-diagnostics": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -287,6 +339,42 @@ func TestServerReadResource(t *testing.T) { NewState: &testNewStateRemovedDynamicValue, }, }, + "response-private": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + GetResourcesMethod: func(_ context.Context) (map[string]provider.ResourceType, diag.Diagnostics) { + return map[string]provider.ResourceType{ + "test_resource": &testprovider.ResourceType{ + GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { + return tfsdk.Schema{}, nil + }, + NewResourceMethod: func(_ context.Context, _ provider.Provider) (resource.Resource, diag.Diagnostics) { + return &testprovider.Resource{ + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + diags := resp.Private.SetKey(ctx, "providerKey", []byte(`{"key": "value"}`)) + + resp.Diagnostics.Append(diags...) + }, + }, nil + }, + }, + }, nil + }, + }, + }, + }, + request: &tfprotov5.ReadResourceRequest{ + CurrentState: testEmptyDynamicValue, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov5.ReadResourceResponse{ + NewState: testEmptyDynamicValue, + Private: privatestate.MustMarshalToJson(map[string][]byte{ + "providerKey": []byte(`{"key": "value"}`), + }), + }, + }, } for name, testCase := range testCases { diff --git a/internal/proto6server/server_applyresourcechange_test.go b/internal/proto6server/server_applyresourcechange_test.go index e0150e9ad..5c430e784 100644 --- a/internal/proto6server/server_applyresourcechange_test.go +++ b/internal/proto6server/server_applyresourcechange_test.go @@ -2,18 +2,21 @@ package proto6server import ( "context" + "fmt" "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/testing/testprovider" "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/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestServerApplyResourceChange(t *testing.T) { @@ -419,6 +422,55 @@ func TestServerApplyResourceChange(t *testing.T) { NewState: &testEmptyDynamicValue, }, }, + "create-response-private": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + GetResourcesMethod: func(_ context.Context) (map[string]provider.ResourceType, diag.Diagnostics) { + return map[string]provider.ResourceType{ + "test_resource": &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.Resource{ + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data testSchemaData + + // Prevent missing resource state error diagnostic + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + + diags := resp.Private.SetKey(ctx, "providerKey", []byte(`{"key": "value"}`)) + + resp.Diagnostics.Append(diags...) + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Delete") + }, + UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Update") + }, + }, nil + }, + }, + }, nil + }, + }, + }, + }, + request: &tfprotov6.ApplyResourceChangeRequest{ + TypeName: "test_resource", + }, + expectedResponse: &tfprotov6.ApplyResourceChangeResponse{ + NewState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, ""), + "test_required": tftypes.NewValue(tftypes.String, ""), + }), + Private: privatestate.MustMarshalToJson(map[string][]byte{ + "providerKey": []byte(`{"key": "value"}`), + }), + }, + }, "delete-request-priorstate": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -519,6 +571,59 @@ func TestServerApplyResourceChange(t *testing.T) { NewState: &testEmptyDynamicValue, }, }, + "delete-request-private": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + GetResourcesMethod: func(_ context.Context) (map[string]provider.ResourceType, diag.Diagnostics) { + return map[string]provider.ResourceType{ + "test_resource": &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.Resource{ + CreateMethod: func(_ context.Context, _ resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Delete, Got: Create") + }, + DeleteMethod: func(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + expected := `{"key": "value"}` + got, diags := req.Private.GetKey(ctx, "providerKey") + + resp.Diagnostics.Append(diags...) + + if string(got) != expected { + resp.Diagnostics.AddError( + "Unexpected req.Private Value", + fmt.Sprintf("expected %q, got %q", expected, got), + ) + } + }, + UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Delete, Got: Update") + }, + }, nil + }, + }, + }, nil + }, + }, + }, + }, + request: &tfprotov6.ApplyResourceChangeRequest{ + PriorState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-priorstate-value"), + }), + TypeName: "test_resource", + PlannedPrivate: privatestate.MustMarshalToJson(map[string][]byte{ + "providerKey": []byte(`{"key": "value"}`), + }), + }, + expectedResponse: &tfprotov6.ApplyResourceChangeResponse{ + NewState: &testEmptyDynamicValue, + }, + }, "delete-response-diagnostics": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -857,6 +962,78 @@ func TestServerApplyResourceChange(t *testing.T) { }), }, }, + "update-request-private": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithMetaSchema{ + Provider: &testprovider.Provider{ + GetResourcesMethod: func(_ context.Context) (map[string]provider.ResourceType, diag.Diagnostics) { + return map[string]provider.ResourceType{ + "test_resource": &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.Resource{ + CreateMethod: func(_ context.Context, _ resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Create") + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Delete") + }, + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + expected := `{"providerKey": "provider value"}` + got, diags := req.Private.GetKey(ctx, "providerKey") + + resp.Diagnostics.Append(diags...) + + if string(got) != expected { + resp.Diagnostics.AddError( + "Unexpected req.Private Value", + fmt.Sprintf("expected %q, got %q", expected, got), + ) + } + }, + }, nil + }, + }, + }, nil + }, + }, + GetMetaSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { + return testProviderMetaSchema, nil + }, + }, + }, + }, + request: &tfprotov6.ApplyResourceChangeRequest{ + PlannedState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + PriorState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + ProviderMeta: testProviderMetaValue, + TypeName: "test_resource", + PlannedPrivate: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"frameworkKey": "framework value"}`), + "providerKey": []byte(`{"providerKey": "provider value"}`), + }), + }, + expectedResponse: &tfprotov6.ApplyResourceChangeResponse{ + // Intentionally old, Update implementation does not call resp.State.Set() + NewState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"frameworkKey": "framework value"}`), + "providerKey": []byte(`{"providerKey": "provider value"}`), + }), + }, + }, "update-response-diagnostics": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -1031,6 +1208,62 @@ func TestServerApplyResourceChange(t *testing.T) { NewState: &testEmptyDynamicValue, }, }, + "update-response-private": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + GetResourcesMethod: func(_ context.Context) (map[string]provider.ResourceType, diag.Diagnostics) { + return map[string]provider.ResourceType{ + "test_resource": &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.Resource{ + CreateMethod: func(_ context.Context, _ resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Create") + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Delete") + }, + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + diags := resp.Private.SetKey(ctx, "providerKey", []byte(`{"providerKey": "provider value"}`)) + + resp.Diagnostics.Append(diags...) + }, + }, nil + }, + }, + }, nil + }, + }, + }, + }, + request: &tfprotov6.ApplyResourceChangeRequest{ + PlannedState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + PriorState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + TypeName: "test_resource", + PlannedPrivate: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"frameworkKey": "framework value"}`), + }), + }, + expectedResponse: &tfprotov6.ApplyResourceChangeResponse{ + NewState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"frameworkKey": "framework value"}`), + "providerKey": []byte(`{"providerKey": "provider value"}`), + }), + }, + }, } for name, testCase := range testCases { diff --git a/internal/proto6server/server_importresourcestate_test.go b/internal/proto6server/server_importresourcestate_test.go index cf61acd68..67e9e64d9 100644 --- a/internal/proto6server/server_importresourcestate_test.go +++ b/internal/proto6server/server_importresourcestate_test.go @@ -5,16 +5,18 @@ 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/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/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestServerImportResourceState(t *testing.T) { @@ -179,6 +181,50 @@ func TestServerImportResourceState(t *testing.T) { }, }, }, + "response-importedresources-private": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + GetResourcesMethod: func(_ context.Context) (map[string]provider.ResourceType, diag.Diagnostics) { + return map[string]provider.ResourceType{ + "test_resource": &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.ResourceWithImportState{ + Resource: &testprovider.Resource{}, + ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + diags := resp.Private.SetKey(ctx, "providerKey", []byte(`{"key": "value"}`)) + + resp.Diagnostics.Append(diags...) + + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + }, + }, nil + }, + }, + }, nil + }, + }, + }, + }, + request: &tfprotov6.ImportResourceStateRequest{ + ID: "test-id", + TypeName: "test_resource", + }, + expectedResponse: &tfprotov6.ImportResourceStateResponse{ + ImportedResources: []*tfprotov6.ImportedResource{ + { + State: testStateDynamicValue, + TypeName: "test_resource", + Private: privatestate.MustMarshalToJson(map[string][]byte{ + "providerKey": []byte(`{"key": "value"}`), + }), + }, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/proto6server/server_readresource_test.go b/internal/proto6server/server_readresource_test.go index 9ffbd1dfb..746047fc1 100644 --- a/internal/proto6server/server_readresource_test.go +++ b/internal/proto6server/server_readresource_test.go @@ -2,18 +2,21 @@ package proto6server import ( "context" + "fmt" "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/testing/testprovider" "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/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestServerReadResource(t *testing.T) { @@ -172,6 +175,55 @@ func TestServerReadResource(t *testing.T) { NewState: testEmptyDynamicValue, }, }, + "request-private": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + GetResourcesMethod: func(_ context.Context) (map[string]provider.ResourceType, diag.Diagnostics) { + return map[string]provider.ResourceType{ + "test_resource": &testprovider.ResourceType{ + GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { + return tfsdk.Schema{}, nil + }, + NewResourceMethod: func(_ context.Context, _ provider.Provider) (resource.Resource, diag.Diagnostics) { + return &testprovider.Resource{ + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + expected := `{"pKeyOne": {"k0": "zero", "k1": 1}}` + got, diags := req.Private.GetKey(ctx, "providerKey") + + resp.Diagnostics.Append(diags...) + + if string(got) != expected { + resp.Diagnostics.AddError( + "Unexpected req.Private Value", + fmt.Sprintf("expected %q, got %q", expected, got), + ) + } + }, + }, nil + }, + }, + }, nil + }, + }, + }, + }, + request: &tfprotov6.ReadResourceRequest{ + CurrentState: testEmptyDynamicValue, + TypeName: "test_resource", + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKey": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), + }, + expectedResponse: &tfprotov6.ReadResourceResponse{ + NewState: testEmptyDynamicValue, + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKey": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), + }, + }, "response-diagnostics": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -287,6 +339,42 @@ func TestServerReadResource(t *testing.T) { NewState: &testNewStateRemovedDynamicValue, }, }, + "response-private": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + GetResourcesMethod: func(_ context.Context) (map[string]provider.ResourceType, diag.Diagnostics) { + return map[string]provider.ResourceType{ + "test_resource": &testprovider.ResourceType{ + GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { + return tfsdk.Schema{}, nil + }, + NewResourceMethod: func(_ context.Context, _ provider.Provider) (resource.Resource, diag.Diagnostics) { + return &testprovider.Resource{ + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + diags := resp.Private.SetKey(ctx, "providerKey", []byte(`{"key": "value"}`)) + + resp.Diagnostics.Append(diags...) + }, + }, nil + }, + }, + }, nil + }, + }, + }, + }, + request: &tfprotov6.ReadResourceRequest{ + CurrentState: testEmptyDynamicValue, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov6.ReadResourceResponse{ + NewState: testEmptyDynamicValue, + Private: privatestate.MustMarshalToJson(map[string][]byte{ + "providerKey": []byte(`{"key": "value"}`), + }), + }, + }, } for name, testCase := range testCases { 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/applyresourcechange.go b/internal/toproto5/applyresourcechange.go index 0eda49a6e..86013295a 100644 --- a/internal/toproto5/applyresourcechange.go +++ b/internal/toproto5/applyresourcechange.go @@ -3,8 +3,9 @@ package toproto5 import ( "context" - "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" ) // ApplyResourceChangeResponse returns the *tfprotov5.ApplyResourceChangeResponse @@ -16,7 +17,6 @@ func ApplyResourceChangeResponse(ctx context.Context, fw *fwserver.ApplyResource proto5 := &tfprotov5.ApplyResourceChangeResponse{ Diagnostics: Diagnostics(ctx, fw.Diagnostics), - Private: fw.Private, } newState, diags := State(ctx, fw.NewState) @@ -24,5 +24,10 @@ func ApplyResourceChangeResponse(ctx context.Context, fw *fwserver.ApplyResource proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) proto5.NewState = newState + newPrivate, diags := fw.Private.Bytes(ctx) + + proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) + proto5.Private = newPrivate + return proto5 } diff --git a/internal/toproto5/applyresourcechange_test.go b/internal/toproto5/applyresourcechange_test.go index 87d848991..2cfce0055 100644 --- a/internal/toproto5/applyresourcechange_test.go +++ b/internal/toproto5/applyresourcechange_test.go @@ -5,13 +5,15 @@ 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/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestApplyResourceChangeResponse(t *testing.T) { @@ -57,6 +59,12 @@ func TestApplyResourceChangeResponse(t *testing.T) { }, } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + testCases := map[string]struct { input *fwserver.ApplyResourceChangeResponse expected *tfprotov5.ApplyResourceChangeResponse @@ -132,10 +140,18 @@ func TestApplyResourceChangeResponse(t *testing.T) { }, "private": { input: &fwserver.ApplyResourceChangeResponse{ - Private: []byte("{}"), + Private: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + }, + Provider: testProviderData, + }, }, expected: &tfprotov5.ApplyResourceChangeResponse{ - Private: []byte("{}"), + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), }, }, } diff --git a/internal/toproto5/importedresource.go b/internal/toproto5/importedresource.go index fffc46e6a..fb9085cd8 100644 --- a/internal/toproto5/importedresource.go +++ b/internal/toproto5/importedresource.go @@ -3,9 +3,10 @@ package toproto5 import ( "context" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" ) // ImportedResource returns the *tfprotov5.ImportedResource equivalent of a @@ -16,7 +17,6 @@ func ImportedResource(ctx context.Context, fw *fwserver.ImportedResource) (*tfpr } proto5 := &tfprotov5.ImportedResource{ - Private: fw.Private, TypeName: fw.TypeName, } @@ -24,5 +24,10 @@ func ImportedResource(ctx context.Context, fw *fwserver.ImportedResource) (*tfpr proto5.State = state + newPrivate, privateDiags := fw.Private.Bytes(ctx) + + diags = append(diags, privateDiags...) + proto5.Private = newPrivate + return proto5, diags } diff --git a/internal/toproto5/importedresource_test.go b/internal/toproto5/importedresource_test.go new file mode 100644 index 000000000..e8d6cfed6 --- /dev/null +++ b/internal/toproto5/importedresource_test.go @@ -0,0 +1,213 @@ +package toproto5_test + +import ( + "context" + "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/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestImportResourceStateResponse(t *testing.T) { + t.Parallel() + + testProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testEmptyProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + } + + testProto5Value := tftypes.NewValue(testProto5Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testEmptyProto5Value := tftypes.NewValue(testEmptyProto5Type, map[string]tftypes.Value{}) + + testProto5DynamicValue, err := tfprotov5.NewDynamicValue(testProto5Type, testProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testEmptyProto5DynamicValue, err := tfprotov5.NewDynamicValue(testEmptyProto5Type, testEmptyProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testState := tfsdk.State{ + Raw: testProto5Value, + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test_attribute": { + Required: true, + Type: types.StringType, + }, + }, + }, + } + + testStateInvalid := tfsdk.State{ + Raw: testProto5Value, + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test_attribute": { + Required: true, + Type: types.BoolType, + }, + }, + }, + } + + testEmptyState := tfsdk.State{ + Raw: testProto5Value, + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{}, + }, + } + + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testCases := map[string]struct { + input *fwserver.ImportResourceStateResponse + expected *tfprotov5.ImportResourceStateResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &fwserver.ImportResourceStateResponse{}, + expected: &tfprotov5.ImportResourceStateResponse{}, + }, + "diagnostics": { + input: &fwserver.ImportResourceStateResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + }, + expected: &tfprotov5.ImportResourceStateResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }, + "diagnostics-invalid-newstate": { + input: &fwserver.ImportResourceStateResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + ImportedResources: []fwserver.ImportedResource{ + { + State: testStateInvalid, + }, + }, + }, + expected: &tfprotov5.ImportResourceStateResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Unable to Convert State", + Detail: "An unexpected error was encountered when converting the state to the protocol type. " + + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n" + + "Please report this to the provider developer:\n\n" + + "AttributeName(\"test_attribute\"): unexpected value type string, tftypes.Bool values must be of type bool", + }, + }, + }, + }, + "newstate": { + input: &fwserver.ImportResourceStateResponse{ + ImportedResources: []fwserver.ImportedResource{ + { + State: testState, + }, + }, + }, + expected: &tfprotov5.ImportResourceStateResponse{ + ImportedResources: []*tfprotov5.ImportedResource{ + { + State: &testProto5DynamicValue, + }, + }, + }, + }, + "private": { + input: &fwserver.ImportResourceStateResponse{ + ImportedResources: []fwserver.ImportedResource{ + { + State: testEmptyState, + Private: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + }, + Provider: testProviderData, + }, + }, + }, + }, + expected: &tfprotov5.ImportResourceStateResponse{ + ImportedResources: []*tfprotov5.ImportedResource{ + { + State: &testEmptyProto5DynamicValue, + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.ImportResourceStateResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} 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..994f6ad2f 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,14 @@ func TestPlanResourceChangeResponse(t *testing.T) { }, } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + testCases := map[string]struct { input *fwserver.PlanResourceChangeResponse expected *tfprotov5.PlanResourceChangeResponse @@ -123,12 +133,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: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), }, }, "plannedstate": { diff --git a/internal/toproto5/readresource.go b/internal/toproto5/readresource.go index 32bff5f9c..7a8ac372c 100644 --- a/internal/toproto5/readresource.go +++ b/internal/toproto5/readresource.go @@ -3,8 +3,9 @@ package toproto5 import ( "context" - "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" ) // ReadResourceResponse returns the *tfprotov5.ReadResourceResponse @@ -16,7 +17,6 @@ func ReadResourceResponse(ctx context.Context, fw *fwserver.ReadResourceResponse proto5 := &tfprotov5.ReadResourceResponse{ Diagnostics: Diagnostics(ctx, fw.Diagnostics), - Private: fw.Private, } newState, diags := State(ctx, fw.NewState) @@ -24,5 +24,10 @@ func ReadResourceResponse(ctx context.Context, fw *fwserver.ReadResourceResponse proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) proto5.NewState = newState + newPrivate, diags := fw.Private.Bytes(ctx) + + proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) + proto5.Private = newPrivate + return proto5 } diff --git a/internal/toproto5/readresource_test.go b/internal/toproto5/readresource_test.go index 7c9425fe8..d7d44661f 100644 --- a/internal/toproto5/readresource_test.go +++ b/internal/toproto5/readresource_test.go @@ -5,13 +5,15 @@ 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/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestReadResourceResponse(t *testing.T) { @@ -33,6 +35,14 @@ func TestReadResourceResponse(t *testing.T) { t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + testState := &tfsdk.State{ Raw: testProto5Value, Schema: tfsdk.Schema{ @@ -130,12 +140,30 @@ func TestReadResourceResponse(t *testing.T) { NewState: &testProto5DynamicValue, }, }, + "private-empty": { + input: &fwserver.ReadResourceResponse{ + Private: &privatestate.Data{ + Framework: map[string][]byte{}, + Provider: testEmptyProviderData, + }, + }, + expected: &tfprotov5.ReadResourceResponse{ + Private: nil, + }, + }, "private": { input: &fwserver.ReadResourceResponse{ - Private: []byte("{}"), + Private: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`)}, + Provider: testProviderData, + }, }, expected: &tfprotov5.ReadResourceResponse{ - Private: []byte("{}"), + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), }, }, } diff --git a/internal/toproto6/applyresourcechange.go b/internal/toproto6/applyresourcechange.go index 24c01586e..c47c58f5b 100644 --- a/internal/toproto6/applyresourcechange.go +++ b/internal/toproto6/applyresourcechange.go @@ -3,8 +3,9 @@ package toproto6 import ( "context" - "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" ) // ApplyResourceChangeResponse returns the *tfprotov6.ApplyResourceChangeResponse @@ -16,7 +17,6 @@ func ApplyResourceChangeResponse(ctx context.Context, fw *fwserver.ApplyResource proto6 := &tfprotov6.ApplyResourceChangeResponse{ Diagnostics: Diagnostics(ctx, fw.Diagnostics), - Private: fw.Private, } newState, diags := State(ctx, fw.NewState) @@ -24,5 +24,10 @@ func ApplyResourceChangeResponse(ctx context.Context, fw *fwserver.ApplyResource proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) proto6.NewState = newState + newPrivate, diags := fw.Private.Bytes(ctx) + + proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) + proto6.Private = newPrivate + return proto6 } diff --git a/internal/toproto6/applyresourcechange_test.go b/internal/toproto6/applyresourcechange_test.go index cbd500fdc..3f27fd7f9 100644 --- a/internal/toproto6/applyresourcechange_test.go +++ b/internal/toproto6/applyresourcechange_test.go @@ -5,13 +5,15 @@ 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/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestApplyResourceChangeResponse(t *testing.T) { @@ -57,6 +59,12 @@ func TestApplyResourceChangeResponse(t *testing.T) { }, } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + testCases := map[string]struct { input *fwserver.ApplyResourceChangeResponse expected *tfprotov6.ApplyResourceChangeResponse @@ -132,10 +140,17 @@ func TestApplyResourceChangeResponse(t *testing.T) { }, "private": { input: &fwserver.ApplyResourceChangeResponse{ - Private: []byte("{}"), + Private: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`)}, + Provider: testProviderData, + }, }, expected: &tfprotov6.ApplyResourceChangeResponse{ - Private: []byte("{}"), + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), }, }, } diff --git a/internal/toproto6/importedresource.go b/internal/toproto6/importedresource.go index 95a366f67..6ddffbd28 100644 --- a/internal/toproto6/importedresource.go +++ b/internal/toproto6/importedresource.go @@ -3,9 +3,10 @@ package toproto6 import ( "context" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" ) // ImportedResource returns the *tfprotov6.ImportedResource equivalent of a @@ -16,7 +17,6 @@ func ImportedResource(ctx context.Context, fw *fwserver.ImportedResource) (*tfpr } proto6 := &tfprotov6.ImportedResource{ - Private: fw.Private, TypeName: fw.TypeName, } @@ -24,5 +24,10 @@ func ImportedResource(ctx context.Context, fw *fwserver.ImportedResource) (*tfpr proto6.State = state + newPrivate, privateDiags := fw.Private.Bytes(ctx) + + diags = append(diags, privateDiags...) + proto6.Private = newPrivate + return proto6, diags } diff --git a/internal/toproto6/importedresource_test.go b/internal/toproto6/importedresource_test.go new file mode 100644 index 000000000..a35179cb6 --- /dev/null +++ b/internal/toproto6/importedresource_test.go @@ -0,0 +1,213 @@ +package toproto6_test + +import ( + "context" + "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/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestImportResourceStateResponse(t *testing.T) { + t.Parallel() + + testProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testEmptyProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + } + + testProto6Value := tftypes.NewValue(testProto6Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testEmptyProto6Value := tftypes.NewValue(testEmptyProto6Type, map[string]tftypes.Value{}) + + testProto6DynamicValue, err := tfprotov6.NewDynamicValue(testProto6Type, testProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testEmptyProto6DynamicValue, err := tfprotov6.NewDynamicValue(testEmptyProto6Type, testEmptyProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testState := tfsdk.State{ + Raw: testProto6Value, + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test_attribute": { + Required: true, + Type: types.StringType, + }, + }, + }, + } + + testStateInvalid := tfsdk.State{ + Raw: testProto6Value, + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "test_attribute": { + Required: true, + Type: types.BoolType, + }, + }, + }, + } + + testEmptyState := tfsdk.State{ + Raw: testProto6Value, + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{}, + }, + } + + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testCases := map[string]struct { + input *fwserver.ImportResourceStateResponse + expected *tfprotov6.ImportResourceStateResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &fwserver.ImportResourceStateResponse{}, + expected: &tfprotov6.ImportResourceStateResponse{}, + }, + "diagnostics": { + input: &fwserver.ImportResourceStateResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + }, + expected: &tfprotov6.ImportResourceStateResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }, + "diagnostics-invalid-newstate": { + input: &fwserver.ImportResourceStateResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + ImportedResources: []fwserver.ImportedResource{ + { + State: testStateInvalid, + }, + }, + }, + expected: &tfprotov6.ImportResourceStateResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert State", + Detail: "An unexpected error was encountered when converting the state to the protocol type. " + + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n" + + "Please report this to the provider developer:\n\n" + + "AttributeName(\"test_attribute\"): unexpected value type string, tftypes.Bool values must be of type bool", + }, + }, + }, + }, + "newstate": { + input: &fwserver.ImportResourceStateResponse{ + ImportedResources: []fwserver.ImportedResource{ + { + State: testState, + }, + }, + }, + expected: &tfprotov6.ImportResourceStateResponse{ + ImportedResources: []*tfprotov6.ImportedResource{ + { + State: &testProto6DynamicValue, + }, + }, + }, + }, + "private": { + input: &fwserver.ImportResourceStateResponse{ + ImportedResources: []fwserver.ImportedResource{ + { + State: testEmptyState, + Private: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + }, + Provider: testProviderData, + }, + }, + }, + }, + expected: &tfprotov6.ImportResourceStateResponse{ + ImportedResources: []*tfprotov6.ImportedResource{ + { + State: &testEmptyProto6DynamicValue, + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.ImportResourceStateResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} 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..3b373238b 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,14 @@ func TestPlanResourceChangeResponse(t *testing.T) { }, } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + testCases := map[string]struct { input *fwserver.PlanResourceChangeResponse expected *tfprotov6.PlanResourceChangeResponse @@ -123,12 +133,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: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), }, }, "plannedstate": { diff --git a/internal/toproto6/readresource.go b/internal/toproto6/readresource.go index f1709347c..5e983d45c 100644 --- a/internal/toproto6/readresource.go +++ b/internal/toproto6/readresource.go @@ -3,8 +3,9 @@ package toproto6 import ( "context" - "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" ) // ReadResourceResponse returns the *tfprotov6.ReadResourceResponse @@ -16,7 +17,6 @@ func ReadResourceResponse(ctx context.Context, fw *fwserver.ReadResourceResponse proto6 := &tfprotov6.ReadResourceResponse{ Diagnostics: Diagnostics(ctx, fw.Diagnostics), - Private: fw.Private, } newState, diags := State(ctx, fw.NewState) @@ -24,5 +24,10 @@ func ReadResourceResponse(ctx context.Context, fw *fwserver.ReadResourceResponse proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) proto6.NewState = newState + newPrivate, diags := fw.Private.Bytes(ctx) + + proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) + proto6.Private = newPrivate + return proto6 } diff --git a/internal/toproto6/readresource_test.go b/internal/toproto6/readresource_test.go index 4452da552..d65484ddb 100644 --- a/internal/toproto6/readresource_test.go +++ b/internal/toproto6/readresource_test.go @@ -5,13 +5,15 @@ 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/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestReadResourceResponse(t *testing.T) { @@ -57,6 +59,14 @@ func TestReadResourceResponse(t *testing.T) { }, } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + testCases := map[string]struct { input *fwserver.ReadResourceResponse expected *tfprotov6.ReadResourceResponse @@ -130,12 +140,30 @@ func TestReadResourceResponse(t *testing.T) { NewState: &testProto6DynamicValue, }, }, + "private-empty": { + input: &fwserver.ReadResourceResponse{ + Private: &privatestate.Data{ + Framework: map[string][]byte{}, + Provider: testEmptyProviderData, + }, + }, + expected: &tfprotov6.ReadResourceResponse{ + Private: nil, + }, + }, "private": { input: &fwserver.ReadResourceResponse{ - Private: []byte("{}"), + Private: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`)}, + Provider: testProviderData, + }, }, expected: &tfprotov6.ReadResourceResponse{ - Private: []byte("{}"), + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), }, }, } diff --git a/resource/create.go b/resource/create.go index d7f304ca7..0fce34013 100644 --- a/resource/create.go +++ b/resource/create.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/tfsdk" ) @@ -33,6 +34,11 @@ type CreateResponse struct { // should be set during the resource's Create operation. State tfsdk.State + // Private is the private state resource data following the Create operation. + // This field is not pre-populated as there is no pre-existing private state + // data during the resource's Create operation. + Private *privatestate.ProviderData + // Diagnostics report errors or warnings related to creating the // resource. An empty slice indicates a successful operation with no // warnings or errors generated. diff --git a/resource/delete.go b/resource/delete.go index 6ce30b24a..26023b156 100644 --- a/resource/delete.go +++ b/resource/delete.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/tfsdk" ) @@ -15,6 +16,12 @@ type DeleteRequest struct { // ProviderMeta is metadata from the provider_meta block of the module. ProviderMeta tfsdk.Config + + // Private is provider-defined resource private state data which was previously + // stored with the resource state. + // + // Use the GetKey method to read data. + Private *privatestate.ProviderData } // DeleteResponse represents a response to a DeleteRequest. An diff --git a/resource/import_state.go b/resource/import_state.go index e6026faff..7b647cc82 100644 --- a/resource/import_state.go +++ b/resource/import_state.go @@ -4,6 +4,7 @@ import ( "context" "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" ) @@ -35,6 +36,11 @@ type ImportStateResponse struct { // It must contain enough information so Terraform can successfully // refresh the resource, e.g. call the Resource Read method. State tfsdk.State + + // Private is the private state resource data following the Import operation. + // This field is not pre-populated as there is no pre-existing private state + // data during the resource's Import operation. + Private *privatestate.ProviderData } // ImportStatePassthroughID is a helper function to set the import diff --git a/resource/modify_plan.go b/resource/modify_plan.go index 0ead46623..4fcd92d33 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,15 @@ type ModifyPlanRequest struct { // ProviderMeta is metadata from the provider_meta block of the module. ProviderMeta tfsdk.Config + + // Private is provider-defined resource private state data which was previously + // stored with the resource state. This data is opaque to Terraform and does + // not affect plan output. Any existing data is copied to + // ModifyPlanResponse.Private to prevent accidental private state data loss. + // + // Use the GetKey method to read data. Use the SetKey method on + // ModifyPlanResponse.Private to update or remove a value. + Private *privatestate.ProviderData } // ModifyPlanResponse represents a response to a @@ -42,6 +52,11 @@ type ModifyPlanResponse struct { // recreated. RequiresReplace path.Paths + // Private is the private state resource data following the ModifyPlan operation. + // This field is pre-populated from ModifyPlanRequest.Private and + // can be modified during the resource's ModifyPlan operation. + 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/resource/read.go b/resource/read.go index 01b302c51..943a03cd7 100644 --- a/resource/read.go +++ b/resource/read.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/tfsdk" ) @@ -14,6 +15,15 @@ type ReadRequest struct { // operation. State tfsdk.State + // Private is provider-defined resource private state data which was previously + // stored with the resource state. This data is opaque to Terraform and does + // not affect plan output. Any existing data is copied to + // ReadResourceResponse.Private to prevent accidental private state data loss. + // + // Use the GetKey method to read data. Use the SetKey method on + // ReadResourceResponse.Private to update or remove a value. + Private *privatestate.ProviderData + // ProviderMeta is metadata from the provider_meta block of the module. ProviderMeta tfsdk.Config } @@ -28,6 +38,11 @@ type ReadResponse struct { // should be set during the resource's Read operation. State tfsdk.State + // Private is the private state resource data following the Read operation. + // This field is pre-populated from ReadResourceRequest.Private and + // can be modified during the resource's Read operation. + Private *privatestate.ProviderData + // Diagnostics report errors or warnings related to reading the // resource. An empty slice indicates a successful operation with no // warnings or errors generated. diff --git a/resource/update.go b/resource/update.go index 3d0f87cd6..78ca95ef0 100644 --- a/resource/update.go +++ b/resource/update.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/tfsdk" ) @@ -25,6 +26,14 @@ type UpdateRequest struct { // ProviderMeta is metadata from the provider_meta block of the module. ProviderMeta tfsdk.Config + + // Private is provider-defined resource private state data which was previously + // stored with the resource state. Any existing data is copied to + // UpdateResponse.Private to prevent accidental private state data loss. + // + // Use the GetKey method to read data. Use the SetKey method on + // UpdateResponse.Private to update or remove a value. + Private *privatestate.ProviderData } // UpdateResponse represents a response to an UpdateRequest. An @@ -37,6 +46,11 @@ type UpdateResponse struct { // should be set during the resource's Update operation. State tfsdk.State + // Private is the private state resource data following the Update operation. + // This field is pre-populated from UpdateRequest.Private and + // can be modified during the resource's Update operation. + Private *privatestate.ProviderData + // Diagnostics report errors or warnings related to updating the // resource. An empty slice indicates a successful operation with no // warnings or errors generated. diff --git a/tfsdk/attribute_plan_modification.go b/tfsdk/attribute_plan_modification.go index 5dad778e7..5c462520d 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,19 @@ type ModifyAttributePlanRequest struct { // ProviderMeta is metadata from the provider_meta block of the module. ProviderMeta Config + + // Private is provider-defined resource private state data which was previously + // stored with the resource state. This data is opaque to Terraform and does + // not affect plan output. Any existing data is copied to + // ModifyAttributePlanResponse.Private to prevent accidental private state data loss. + // + // The private state data is always the original data when the schema-based plan + // modification began or, is updated as the logic traverses deeper into underlying + // attributes. + // + // Use the GetKey method to read data. Use the SetKey method on + // ModifyAttributePlanResponse.Private to update or remove a value. + Private *privatestate.ProviderData } // ModifyAttributePlanResponse represents a response to a @@ -97,6 +111,15 @@ type ModifyAttributePlanResponse struct { // requires replacement of the whole resource. RequiresReplace bool + // Private is the private state resource data following the ModifyAttributePlan operation. + // This field is pre-populated from ModifyAttributePlanRequest.Private and + // can be modified during the resource's ModifyAttributePlan operation. + // + // The private state data is always the original data when the schema-based plan + // modification began or, is updated as the logic traverses deeper into underlying + // attributes. + 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