diff --git a/.changes/v1.16/NEW FEATURES-20251217-113349.yaml b/.changes/v1.16/NEW FEATURES-20251217-113349.yaml new file mode 100644 index 000000000000..5799b36a15e8 --- /dev/null +++ b/.changes/v1.16/NEW FEATURES-20251217-113349.yaml @@ -0,0 +1,5 @@ +kind: NEW FEATURES +body: Store PlannedPrivate data for providers +time: 2025-12-17T11:33:49.911997-05:00 +custom: + Issue: "37986" diff --git a/docs/plugin-protocol/tfplugin6.proto b/docs/plugin-protocol/tfplugin6.proto index 1b2b0ed08ca5..66b471697b4a 100644 --- a/docs/plugin-protocol/tfplugin6.proto +++ b/docs/plugin-protocol/tfplugin6.proto @@ -1,9 +1,9 @@ // Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: MPL-2.0 -// Terraform Plugin RPC protocol version 6.10 +// Terraform Plugin RPC protocol version 6.11 // -// This file defines version 6.10 of the RPC protocol. To implement a plugin +// This file defines version 6.11 of the RPC protocol. To implement a plugin // against this protocol, copy this definition into your own codebase and // use protoc to generate stubs for your target language. // @@ -312,6 +312,11 @@ message ClientCapabilities { // The write_only_attributes_allowed capability signals that the client // is able to handle write_only attributes for managed resources. bool write_only_attributes_allowed = 2; + + // store_planned_private indicates that the client will store the private data + // returned with an initial plan, and send it back to the provider as + // PlannedPrivate data in a subsequent plan request. + bool store_planned_private = 3; } // Deferred is a message that indicates that change is deferred for a reason. @@ -643,6 +648,7 @@ message PlanResourceChange { DynamicValue provider_meta = 6; ClientCapabilities client_capabilities = 7; ResourceIdentityData prior_identity = 8; + bytes planned_private = 9; } message Response { diff --git a/internal/plugin6/grpc_provider.go b/internal/plugin6/grpc_provider.go index 9da603a24723..2b2c2f394cbe 100644 --- a/internal/plugin6/grpc_provider.go +++ b/internal/plugin6/grpc_provider.go @@ -673,6 +673,7 @@ func (p *GRPCProvider) PlanResourceChange(r providers.PlanResourceChangeRequest) ProposedNewState: &proto6.DynamicValue{Msgpack: propMP}, PriorPrivate: r.PriorPrivate, ClientCapabilities: clientCapabilitiesToProto(r.ClientCapabilities), + PlannedPrivate: r.PlannedPrivate, } if metaSchema.Body != nil { @@ -2071,6 +2072,7 @@ func clientCapabilitiesToProto(c providers.ClientCapabilities) *proto6.ClientCap return &proto6.ClientCapabilities{ DeferralAllowed: c.DeferralAllowed, WriteOnlyAttributesAllowed: c.WriteOnlyAttributesAllowed, + StorePlannedPrivate: c.StorePlannedPrivate, } } diff --git a/internal/providers/provider.go b/internal/providers/provider.go index e52de562899e..011294ebfc50 100644 --- a/internal/providers/provider.go +++ b/internal/providers/provider.go @@ -307,6 +307,11 @@ type ClientCapabilities struct { // The write_only_attributes_allowed capability signals that the client // is able to handle write_only attributes for managed resources. WriteOnlyAttributesAllowed bool + + // StorePlannedPrivate indicates that the client is will store private data + // returned from PlanResourceChange, and return it with the final + // PlanResourceChange call. + StorePlannedPrivate bool } type ValidateProviderConfigRequest struct { @@ -556,6 +561,11 @@ type PlanResourceChangeRequest struct { // provider during the last apply. PriorPrivate []byte + // PlannedPrivate is the private data stored from the the last plan. + // PlannedPrivate will only be supplied in the plan immediately preceding an + // ApplyResourceChange call. + PlannedPrivate []byte + // ProviderMeta is the configuration for the provider_meta block for the // module and provider this resource belongs to. Its use is defined by // each provider, and it should not be used without coordination with diff --git a/internal/terraform/context_apply2_test.go b/internal/terraform/context_apply2_test.go index 2d534ea878a0..022ae53d28f5 100644 --- a/internal/terraform/context_apply2_test.go +++ b/internal/terraform/context_apply2_test.go @@ -7,6 +7,7 @@ import ( "bytes" "errors" "fmt" + "math/rand" "path/filepath" "sort" "strings" @@ -4892,3 +4893,63 @@ func TestContext2Apply_outputWithTypeContraint(t *testing.T) { } } } + +func TestContext2Apply_storedPrivatePlanData(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_resource" "foo" { +} + +resource "test_resource" "bar" { + value = test_resource.foo.computed +} +`, + }) + + p := testProvider("test") + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "test_resource": { + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Computed: true}, + "value": {Type: cty.String, Optional: true}, + "computed": {Type: cty.String, Computed: true}, + }, + }, + }, + }) + + // make sure we can correctly re-plan a value which was stored in the + // PlannedPrivate data from our initial plan + p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { + planned := req.ProposedNewState.AsValueMap() + if req.PlannedPrivate != nil { + // fetch the originally planned random string + planned["computed"] = cty.StringVal(string(req.PlannedPrivate)) + } else { + // this is our first plan, so generate a new computed value + s := fmt.Sprintf("%d", rand.Int()) + planned["computed"] = cty.StringVal(s) + resp.PlannedPrivate = []byte(s) + } + + planned["id"] = cty.UnknownVal(cty.String) + resp.PlannedState = cty.ObjectVal(planned) + return resp + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) + tfdiags.AssertNoErrors(t, diags) + + // we don't need to try and determine what the correct random value was, if + // the planing was incorrect apply would fail with "Provider produced + // inconsistent final plan" + _, diags = ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) +} diff --git a/internal/terraform/context_plan_test.go b/internal/terraform/context_plan_test.go index 0e5e73682ea5..22f715e5ce2b 100644 --- a/internal/terraform/context_plan_test.go +++ b/internal/terraform/context_plan_test.go @@ -1748,6 +1748,7 @@ func TestContext2Plan_blockNestingGroup(t *testing.T) { ClientCapabilities: providers.ClientCapabilities{ DeferralAllowed: false, WriteOnlyAttributesAllowed: true, + StorePlannedPrivate: true, }, } if !cmp.Equal(got, want, valueTrans) { diff --git a/internal/terraform/eval_context_builtin.go b/internal/terraform/eval_context_builtin.go index 30f174680fcf..09590c253b1b 100644 --- a/internal/terraform/eval_context_builtin.go +++ b/internal/terraform/eval_context_builtin.go @@ -660,6 +660,7 @@ func (ctx *BuiltinEvalContext) ClientCapabilities() providers.ClientCapabilities return providers.ClientCapabilities{ DeferralAllowed: ctx.Deferrals().DeferralAllowed(), WriteOnlyAttributesAllowed: true, + StorePlannedPrivate: true, } } diff --git a/internal/terraform/node_resource_abstract_instance.go b/internal/terraform/node_resource_abstract_instance.go index 187f8dfb5926..a2bccb0bbec3 100644 --- a/internal/terraform/node_resource_abstract_instance.go +++ b/internal/terraform/node_resource_abstract_instance.go @@ -835,10 +835,12 @@ func (n *NodeAbstractResourceInstance) plan( if n.preDestroyRefresh { checkRuleSeverity = tfdiags.Warning } - + var plannedPrivate []byte if plannedChange != nil { // If we already planned the action, we stick to that plan createBeforeDestroy = plannedChange.Action == plans.CreateThenDelete + + plannedPrivate = plannedChange.Private } // Evaluate the configuration @@ -991,6 +993,7 @@ func (n *NodeAbstractResourceInstance) plan( ProviderMeta: metaConfigVal, ClientCapabilities: ctx.ClientCapabilities(), PriorIdentity: priorIdentity, + PlannedPrivate: plannedPrivate, }) // If we don't support deferrals, but the provider reports a deferral and does not // emit any error level diagnostics, we should emit an error. @@ -1012,7 +1015,7 @@ func (n *NodeAbstractResourceInstance) plan( } plannedNewVal := resp.PlannedState - plannedPrivate := resp.PlannedPrivate + plannedPrivate = resp.PlannedPrivate plannedIdentity := resp.PlannedIdentity // These checks are only relevant if the provider is not deferring the diff --git a/internal/tfplugin6/tfplugin6.pb.go b/internal/tfplugin6/tfplugin6.pb.go index e0df6a375b87..2a2349106156 100644 --- a/internal/tfplugin6/tfplugin6.pb.go +++ b/internal/tfplugin6/tfplugin6.pb.go @@ -1,9 +1,9 @@ // Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: MPL-2.0 -// Terraform Plugin RPC protocol version 6.10 +// Terraform Plugin RPC protocol version 6.11 // -// This file defines version 6.10 of the RPC protocol. To implement a plugin +// This file defines version 6.11 of the RPC protocol. To implement a plugin // against this protocol, copy this definition into your own codebase and // use protoc to generate stubs for your target language. // @@ -1040,8 +1040,12 @@ type ClientCapabilities struct { // The write_only_attributes_allowed capability signals that the client // is able to handle write_only attributes for managed resources. WriteOnlyAttributesAllowed bool `protobuf:"varint,2,opt,name=write_only_attributes_allowed,json=writeOnlyAttributesAllowed,proto3" json:"write_only_attributes_allowed,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // store_planned_private indicates that the client will store the private data + // returned with an initial plan, and send it back to the provider as + // PlannedPrivate data in a subsequent plan request. + StorePlannedPrivate bool `protobuf:"varint,3,opt,name=store_planned_private,json=storePlannedPrivate,proto3" json:"store_planned_private,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ClientCapabilities) Reset() { @@ -1088,6 +1092,13 @@ func (x *ClientCapabilities) GetWriteOnlyAttributesAllowed() bool { return false } +func (x *ClientCapabilities) GetStorePlannedPrivate() bool { + if x != nil { + return x.StorePlannedPrivate + } + return false +} + // Deferred is a message that indicates that change is deferred for a reason. type Deferred struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -4998,6 +5009,7 @@ type PlanResourceChange_Request struct { ProviderMeta *DynamicValue `protobuf:"bytes,6,opt,name=provider_meta,json=providerMeta,proto3" json:"provider_meta,omitempty"` ClientCapabilities *ClientCapabilities `protobuf:"bytes,7,opt,name=client_capabilities,json=clientCapabilities,proto3" json:"client_capabilities,omitempty"` PriorIdentity *ResourceIdentityData `protobuf:"bytes,8,opt,name=prior_identity,json=priorIdentity,proto3" json:"prior_identity,omitempty"` + PlannedPrivate []byte `protobuf:"bytes,9,opt,name=planned_private,json=plannedPrivate,proto3" json:"planned_private,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -5088,6 +5100,13 @@ func (x *PlanResourceChange_Request) GetPriorIdentity() *ResourceIdentityData { return nil } +func (x *PlanResourceChange_Request) GetPlannedPrivate() []byte { + if x != nil { + return x.PlannedPrivate + } + return nil +} + type PlanResourceChange_Response struct { state protoimpl.MessageState `protogen:"open.v1"` PlannedState *DynamicValue `protobuf:"bytes,1,opt,name=planned_state,json=plannedState,proto3" json:"planned_state,omitempty"` @@ -8220,10 +8239,11 @@ const file_tfplugin6_proto_rawDesc = "" + "\fplan_destroy\x18\x01 \x01(\bR\vplanDestroy\x12?\n" + "\x1cget_provider_schema_optional\x18\x02 \x01(\bR\x19getProviderSchemaOptional\x12.\n" + "\x13move_resource_state\x18\x03 \x01(\bR\x11moveResourceState\x128\n" + - "\x18generate_resource_config\x18\x04 \x01(\bR\x16generateResourceConfig\"\x82\x01\n" + + "\x18generate_resource_config\x18\x04 \x01(\bR\x16generateResourceConfig\"\xb6\x01\n" + "\x12ClientCapabilities\x12)\n" + "\x10deferral_allowed\x18\x01 \x01(\bR\x0fdeferralAllowed\x12A\n" + - "\x1dwrite_only_attributes_allowed\x18\x02 \x01(\bR\x1awriteOnlyAttributesAllowed\"\xa2\x01\n" + + "\x1dwrite_only_attributes_allowed\x18\x02 \x01(\bR\x1awriteOnlyAttributesAllowed\x122\n" + + "\x15store_planned_private\x18\x03 \x01(\bR\x13storePlannedPrivate\"\xa2\x01\n" + "\bDeferred\x122\n" + "\x06reason\x18\x01 \x01(\x0e2\x1a.tfplugin6.Deferred.ReasonR\x06reason\"b\n" + "\x06Reason\x12\v\n" + @@ -8361,8 +8381,8 @@ const file_tfplugin6_proto_rawDesc = "" + "\vdiagnostics\x18\x02 \x03(\v2\x15.tfplugin6.DiagnosticR\vdiagnostics\x12\x18\n" + "\aprivate\x18\x03 \x01(\fR\aprivate\x12/\n" + "\bdeferred\x18\x04 \x01(\v2\x13.tfplugin6.DeferredR\bdeferred\x12B\n" + - "\fnew_identity\x18\x05 \x01(\v2\x1f.tfplugin6.ResourceIdentityDataR\vnewIdentity\"\x87\a\n" + - "\x12PlanResourceChange\x1a\xd3\x03\n" + + "\fnew_identity\x18\x05 \x01(\v2\x1f.tfplugin6.ResourceIdentityDataR\vnewIdentity\"\xb0\a\n" + + "\x12PlanResourceChange\x1a\xfc\x03\n" + "\aRequest\x12\x1b\n" + "\ttype_name\x18\x01 \x01(\tR\btypeName\x128\n" + "\vprior_state\x18\x02 \x01(\v2\x17.tfplugin6.DynamicValueR\n" + @@ -8372,7 +8392,8 @@ const file_tfplugin6_proto_rawDesc = "" + "\rprior_private\x18\x05 \x01(\fR\fpriorPrivate\x12<\n" + "\rprovider_meta\x18\x06 \x01(\v2\x17.tfplugin6.DynamicValueR\fproviderMeta\x12N\n" + "\x13client_capabilities\x18\a \x01(\v2\x1d.tfplugin6.ClientCapabilitiesR\x12clientCapabilities\x12F\n" + - "\x0eprior_identity\x18\b \x01(\v2\x1f.tfplugin6.ResourceIdentityDataR\rpriorIdentity\x1a\x9a\x03\n" + + "\x0eprior_identity\x18\b \x01(\v2\x1f.tfplugin6.ResourceIdentityDataR\rpriorIdentity\x12'\n" + + "\x0fplanned_private\x18\t \x01(\fR\x0eplannedPrivate\x1a\x9a\x03\n" + "\bResponse\x12<\n" + "\rplanned_state\x18\x01 \x01(\v2\x17.tfplugin6.DynamicValueR\fplannedState\x12C\n" + "\x10requires_replace\x18\x02 \x03(\v2\x18.tfplugin6.AttributePathR\x0frequiresReplace\x12'\n" + diff --git a/internal/tfplugin6/tfplugin6_grpc.pb.go b/internal/tfplugin6/tfplugin6_grpc.pb.go index 2012d1e6bbcb..9654a95e8bbd 100644 --- a/internal/tfplugin6/tfplugin6_grpc.pb.go +++ b/internal/tfplugin6/tfplugin6_grpc.pb.go @@ -1,9 +1,9 @@ // Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: MPL-2.0 -// Terraform Plugin RPC protocol version 6.10 +// Terraform Plugin RPC protocol version 6.11 // -// This file defines version 6.10 of the RPC protocol. To implement a plugin +// This file defines version 6.11 of the RPC protocol. To implement a plugin // against this protocol, copy this definition into your own codebase and // use protoc to generate stubs for your target language. //