From 1f4bf797ff8d5fa8fbc222bedf401ac1f705e1a5 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Wed, 10 Dec 2025 10:40:49 -0500 Subject: [PATCH] add ability to forward PlannedPrivate data Terraform providers sometimes want to be able to plan a computed field which may not be deterministically generated. For example, a random GUID could be generated during plan, making the overall Terraform plan more precise, but there is currently no way to carry that planned value forward to apply. The problem is that there are always two entirely independent plans for every resource change. Terraform compares these two plans for consistency, so a provider may not return a different computed value, but there is no way for the provider to see what the prior planned value was. This means that a planned value which is not reproducible from the given inputs is not plan-able at all. This PR allows Terraform to take the private data from the first plan, which is usually discarded, and send it along in the second plan request. The provider can then use that data to recreate any previously planned values, either because they are stored directly, or contain enough seed data to recreate the same result. --- docs/plugin-protocol/tfplugin6.proto | 10 ++++++++-- internal/plugin6/grpc_provider.go | 2 ++ internal/providers/provider.go | 10 ++++++++++ internal/terraform/context_plan_test.go | 1 + internal/terraform/eval_context_builtin.go | 1 + internal/terraform/node_resource_abstract_instance.go | 7 +++++-- 6 files changed, 27 insertions(+), 4 deletions(-) diff --git a/docs/plugin-protocol/tfplugin6.proto b/docs/plugin-protocol/tfplugin6.proto index 1b2b0ed08c..66b471697b 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 9da603a247..2b2c2f394c 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 e52de56289..011294ebfc 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_plan_test.go b/internal/terraform/context_plan_test.go index 0e5e73682e..22f715e5ce 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 30f174680f..09590c253b 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 187f8dfb59..a2bccb0bbe 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