stacks: add deferrals to PlanResourceChange

Co-authored-by: Mutahhir Hayat <mutahhir.hayat@hashicorp.com>
TF-13960
Daniel Schmidt 2 years ago
parent 32bf1fed39
commit a26fd2e81f
No known key found for this signature in database
GPG Key ID: 377C3A4D62FBBBE2

@ -483,6 +483,7 @@ func (p *GRPCProvider) PlanResourceChange(r providers.PlanResourceChangeRequest)
Config: &proto.DynamicValue{Msgpack: configMP},
ProposedNewState: &proto.DynamicValue{Msgpack: propMP},
PriorPrivate: r.PriorPrivate,
DeferralAllowed: r.DeferralAllowed,
}
if metaSchema.Block != nil {
@ -521,6 +522,8 @@ func (p *GRPCProvider) PlanResourceChange(r providers.PlanResourceChangeRequest)
resp.LegacyTypeSystem = protoResp.LegacyTypeSystem
resp.Deferred = convert.ProtoToDeferred(protoResp.Deferred)
return resp
}

@ -472,6 +472,7 @@ func (p *GRPCProvider) PlanResourceChange(r providers.PlanResourceChangeRequest)
Config: &proto6.DynamicValue{Msgpack: configMP},
ProposedNewState: &proto6.DynamicValue{Msgpack: propMP},
PriorPrivate: r.PriorPrivate,
DeferralAllowed: r.DeferralAllowed,
}
if metaSchema.Block != nil {
@ -510,6 +511,8 @@ func (p *GRPCProvider) PlanResourceChange(r providers.PlanResourceChangeRequest)
resp.LegacyTypeSystem = protoResp.LegacyTypeSystem
resp.Deferred = convert.ProtoToDeferred(protoResp.Deferred)
return resp
}

@ -324,6 +324,10 @@ type PlanResourceChangeRequest struct {
// each provider, and it should not be used without coordination with
// HashiCorp. It is considered experimental and subject to change.
ProviderMeta cty.Value
// DeferralAllowed signals that the provider is allowed to defer the
// changes. If set the caller needs to handle the deferred response.
DeferralAllowed bool
}
type PlanResourceChangeResponse struct {
@ -349,6 +353,10 @@ type PlanResourceChangeResponse struct {
// otherwise fail due to this imprecise mapping. No other provider or SDK
// implementation is permitted to set this.
LegacyTypeSystem bool
// Deferred if present signals that the provider was not able to fully
// complete this operation and a susequent run is required.
Deferred *Deferred
}
type ApplyResourceChangeRequest struct {

@ -19,6 +19,7 @@ import (
"github.com/hashicorp/terraform/internal/providers"
testing_provider "github.com/hashicorp/terraform/internal/providers/testing"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/tfdiags"
)
type deferredActionsTest struct {
@ -929,7 +930,7 @@ resource "test" "c" {
wantActions: map[string]plans.Action{
"test.b": plans.Create,
},
wantDeferred: map[string]providers.DeferredReason{},
wantDeferred: map[string]ExpectedDeferred{},
allowWarnings: true,
},
},
@ -991,9 +992,9 @@ resource "test" "b" {
wantActions: map[string]plans.Action{
"test.c": plans.Create,
},
wantDeferred: map[string]providers.DeferredReason{
"test.a[\"*\"]": providers.DeferredReasonInstanceCountUnknown,
"test.b": providers.DeferredReasonDeferredPrereq,
wantDeferred: map[string]ExpectedDeferred{
"test.a[\"*\"]": {Reason: providers.DeferredReasonInstanceCountUnknown, Action: plans.Create},
"test.b": {Reason: providers.DeferredReasonDeferredPrereq, Action: plans.Create},
},
wantApplied: map[string]cty.Value{
"c": cty.ObjectVal(map[string]cty.Value{
@ -1047,7 +1048,7 @@ resource "test" "b" {
"test.b": plans.Create,
"test.c": plans.NoOp,
},
wantDeferred: map[string]providers.DeferredReason{},
wantDeferred: map[string]ExpectedDeferred{},
wantApplied: map[string]cty.Value{
"a:0": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("a:0"),
@ -1125,8 +1126,8 @@ resource "test" "b" {
"test.a[0]": plans.Create,
"test.a[1]": plans.Create,
},
wantDeferred: map[string]providers.DeferredReason{
"test.b[\"*\"]": providers.DeferredReasonInstanceCountUnknown,
wantDeferred: map[string]ExpectedDeferred{
"test.b[\"*\"]": {Reason: providers.DeferredReasonInstanceCountUnknown, Action: plans.Create},
},
wantApplied: map[string]cty.Value{
"a:0": cty.ObjectVal(map[string]cty.Value{
@ -1175,7 +1176,7 @@ resource "test" "b" {
"test.b[\"a:0\"]": plans.Create,
"test.b[\"a:1\"]": plans.Create,
},
wantDeferred: make(map[string]providers.DeferredReason),
wantDeferred: make(map[string]ExpectedDeferred),
allowWarnings: true,
complete: false, // because we still did targeting
},
@ -1605,7 +1606,7 @@ output "a" {
output "b" {
value = test.b
}
`,
`,
},
stages: []deferredActionsTestStage{
{
@ -1630,10 +1631,393 @@ output "b" {
})),
"b": cty.NullVal(cty.DynamicPseudoType),
},
wantDeferred: map[string]providers.DeferredReason{
wantDeferred: map[string]ExpectedDeferred{
// data.test.a is not part of the plan so we can only
// observe the indirect consequence on the resource.
"test.b": providers.DeferredReasonDeferredPrereq,
"test.b": {Reason: providers.DeferredReasonDeferredPrereq, Action: plans.Create},
},
complete: false,
},
},
}
// planCreateResourceChange is a test that covers the behavior of planning a resource that is being created.
planCreateResourceChange = deferredActionsTest{
configs: map[string]string{
"main.tf": `
resource "test" "a" {
name = "deferred_resource_change"
}
output "a" {
value = test.a
}
`,
},
stages: []deferredActionsTestStage{
{
inputs: map[string]cty.Value{},
wantPlanned: map[string]cty.Value{
"deferred_resource_change": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("deferred_resource_change"),
"upstream_names": cty.NullVal(cty.Set(cty.String)),
"output": cty.UnknownVal(cty.String),
}),
},
wantActions: map[string]plans.Action{},
wantApplied: map[string]cty.Value{
// The all resources will be deferred, so shouldn't
// have any action at this stage.
},
wantOutputs: map[string]cty.Value{
"a": cty.NullVal(cty.DynamicPseudoType),
},
wantDeferred: map[string]ExpectedDeferred{
"test.a": {Reason: providers.DeferredReasonProviderConfigUnknown, Action: plans.Create},
},
complete: false,
},
},
}
// planUpdateResourceChange is a test that covers the behavior of planning a resource that is being updated
planUpdateResourceChange = deferredActionsTest{
configs: map[string]string{
"main.tf": `
resource "test" "a" {
name = "deferred_resource_change"
}
output "a" {
value = test.a
}
`,
},
state: states.BuildState(func(state *states.SyncState) {
state.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test.a"),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustParseJson(map[string]interface{}{
"name": "old_value",
}),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
})
}),
stages: []deferredActionsTestStage{
{
inputs: map[string]cty.Value{},
wantPlanned: map[string]cty.Value{
"deferred_resource_change": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("deferred_resource_change"),
"upstream_names": cty.NullVal(cty.Set(cty.String)),
"output": cty.UnknownVal(cty.String),
}),
},
wantActions: map[string]plans.Action{},
wantApplied: map[string]cty.Value{
// The all resources will be deferred, so shouldn't
// have any action at this stage.
},
wantOutputs: map[string]cty.Value{
"a": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("old_value"),
"upstream_names": cty.NullVal(cty.Set(cty.String)),
"output": cty.NullVal(cty.String),
}),
},
wantDeferred: map[string]ExpectedDeferred{
"test.a": {Reason: providers.DeferredReasonProviderConfigUnknown, Action: plans.Update},
},
complete: false,
},
},
}
// planNoOpResourceChange is a test that covers the behavior of planning a resource that is the same as the current state.
planNoOpResourceChange = deferredActionsTest{
configs: map[string]string{
"main.tf": `
resource "test" "a" {
name = "deferred_resource_change"
}
output "a" {
value = test.a
}
`,
},
state: states.BuildState(func(state *states.SyncState) {
state.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test.a"),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustParseJson(map[string]interface{}{
"name": "deferred_resource_change",
"output": "computed_output",
}),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
})
}),
stages: []deferredActionsTestStage{
{
inputs: map[string]cty.Value{},
wantPlanned: map[string]cty.Value{
"deferred_resource_change": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("deferred_resource_change"),
"upstream_names": cty.NullVal(cty.Set(cty.String)),
"output": cty.StringVal("computed_output"),
}),
},
wantActions: map[string]plans.Action{},
wantApplied: map[string]cty.Value{
// The all resources will be deferred, so shouldn't
// have any action at this stage.
},
wantOutputs: map[string]cty.Value{
"a": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("deferred_resource_change"),
"upstream_names": cty.NullVal(cty.Set(cty.String)),
"output": cty.StringVal("computed_output"),
}),
},
wantDeferred: map[string]ExpectedDeferred{
"test.a": {Reason: providers.DeferredReasonProviderConfigUnknown, Action: plans.NoOp},
},
complete: false,
},
},
}
// planReplaceResourceChange is a test that covers the behavior of planning a resource that the provider
// marks as needing replacement.
planReplaceResourceChange = deferredActionsTest{
configs: map[string]string{
"main.tf": `
resource "test" "a" {
name = "deferred_resource_change"
}
output "a" {
value = test.a
}
`,
},
state: states.BuildState(func(state *states.SyncState) {
state.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test.a"),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustParseJson(map[string]interface{}{
"name": "old_value",
"output": "mark_for_replacement", // tells the mock provider to replace the resource
}),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
})
}),
stages: []deferredActionsTestStage{
{
inputs: map[string]cty.Value{},
wantPlanned: map[string]cty.Value{
"deferred_resource_change": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("deferred_resource_change"),
"upstream_names": cty.NullVal(cty.Set(cty.String)),
"output": cty.StringVal("mark_for_replacement"),
}),
},
wantActions: map[string]plans.Action{},
wantApplied: map[string]cty.Value{
// The all resources will be deferred, so shouldn't
// have any action at this stage.
},
wantOutputs: map[string]cty.Value{
"a": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("old_value"),
"upstream_names": cty.NullVal(cty.Set(cty.String)),
"output": cty.StringVal("mark_for_replacement"),
}),
},
wantDeferred: map[string]ExpectedDeferred{
"test.a": {Reason: providers.DeferredReasonProviderConfigUnknown, Action: plans.DeleteThenCreate},
},
complete: false,
},
},
}
// planForceReplaceResourceChange is a test that covers the behavior of planning a resource that is marked for replacement
planForceReplaceResourceChange = deferredActionsTest{
configs: map[string]string{
"main.tf": `
resource "test" "a" {
name = "deferred_resource_change"
}
output "a" {
value = test.a
}
`,
},
state: states.BuildState(func(state *states.SyncState) {
state.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test.a"),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustParseJson(map[string]interface{}{
"name": "old_value",
"output": "computed_output",
}),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
})
}),
stages: []deferredActionsTestStage{
{
buildOpts: func(opts *PlanOpts) {
opts.ForceReplace = []addrs.AbsResourceInstance{
{
Module: addrs.RootModuleInstance,
Resource: addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test",
Name: "a",
},
Key: addrs.NoKey,
},
},
}
},
inputs: map[string]cty.Value{},
wantPlanned: map[string]cty.Value{
"deferred_resource_change": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("deferred_resource_change"),
"upstream_names": cty.NullVal(cty.Set(cty.String)),
"output": cty.StringVal("computed_output"),
}),
},
wantActions: map[string]plans.Action{},
wantApplied: map[string]cty.Value{
// The all resources will be deferred, so shouldn't
// have any action at this stage.
},
wantOutputs: map[string]cty.Value{
"a": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("old_value"),
"upstream_names": cty.NullVal(cty.Set(cty.String)),
"output": cty.StringVal("computed_output"),
}),
},
wantDeferred: map[string]ExpectedDeferred{
"test.a": {Reason: providers.DeferredReasonProviderConfigUnknown, Action: plans.DeleteThenCreate},
},
complete: false,
},
},
}
// planDeleteResourceChange is a test that covers the behavior of planning a resource that is removed from the config.
planDeleteResourceChange = deferredActionsTest{
configs: map[string]string{
"main.tf": `
// Empty config, expect to delete everything
`,
},
state: states.BuildState(func(state *states.SyncState) {
state.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test.a"),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustParseJson(map[string]interface{}{
"name": "deferred_resource_change",
"output": "computed_output",
}),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
})
}),
stages: []deferredActionsTestStage{
{
inputs: map[string]cty.Value{},
wantPlanned: map[string]cty.Value{},
wantActions: map[string]plans.Action{},
wantApplied: map[string]cty.Value{
// The all resources will be deferred, so shouldn't
// have any action at this stage.
},
wantOutputs: map[string]cty.Value{},
wantDeferred: map[string]ExpectedDeferred{
"test.a": {Reason: providers.DeferredReasonProviderConfigUnknown, Action: plans.Delete},
},
complete: false,
},
},
}
// planDestroyResourceChange is a test that covers the behavior of planning a resource
planDestroyResourceChange = deferredActionsTest{
configs: map[string]string{
"main.tf": `
resource "test" "a" {
name = "deferred_resource_change"
}
output "a" {
value = test.a
}
`,
},
state: states.BuildState(func(state *states.SyncState) {
state.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test.a"),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: mustParseJson(map[string]interface{}{
"name": "deferred_resource_change",
}),
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
})
}),
stages: []deferredActionsTestStage{
{
buildOpts: func(opts *PlanOpts) {
opts.Mode = plans.DestroyMode
},
inputs: map[string]cty.Value{},
wantPlanned: map[string]cty.Value{
"deferred_resource_change": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("deferred_resource_change"),
"upstream_names": cty.NullVal(cty.Set(cty.String)),
"output": cty.UnknownVal(cty.String),
}),
},
wantActions: map[string]plans.Action{},
wantApplied: map[string]cty.Value{
// The all resources will be deferred, so shouldn't
// have any action at this stage.
},
wantOutputs: map[string]cty.Value{},
wantDeferred: map[string]ExpectedDeferred{
"test.a": {Reason: providers.DeferredReasonProviderConfigUnknown, Action: plans.Delete},
},
complete: false,
},
@ -1657,7 +2041,15 @@ func TestContextApply_deferredActions(t *testing.T) {
"custom_conditions_with_orphans": customConditionsWithOrphansTest,
"resource_read": resourceReadTest,
"data_read": readDataSourceTest,
"plan_create_resource_change": planCreateResourceChange,
"plan_update_resource_change": planUpdateResourceChange,
"plan_noop_resource_change": planNoOpResourceChange,
"plan_replace_resource_change": planReplaceResourceChange,
"plan_force_replace_resource_change": planForceReplaceResourceChange,
"plan_delete_resource_change": planDeleteResourceChange,
"plan_destroy_resource_change": planDestroyResourceChange,
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
if test.skip {
@ -1712,37 +2104,43 @@ func TestContextApply_deferredActions(t *testing.T) {
stage.buildOpts(opts)
}
plan, diags := ctx.Plan(cfg, state, opts)
// We expect the correct planned changes and no diagnostics.
if stage.allowWarnings {
assertNoErrors(t, diags)
} else {
assertNoDiagnostics(t, diags)
}
if plan.Complete != stage.complete {
t.Errorf("wrong completion status in plan: got %v, want %v", plan.Complete, stage.complete)
}
provider.plannedChanges.Test(t, stage.wantPlanned)
// We expect the correct actions.
gotActions := make(map[string]plans.Action)
for _, cs := range plan.Changes.Resources {
gotActions[cs.Addr.String()] = cs.Action
}
if diff := cmp.Diff(stage.wantActions, gotActions); diff != "" {
t.Errorf("wrong actions in plan\n%s", diff)
}
var plan *plans.Plan
t.Run("plan", func(t *testing.T) {
var diags tfdiags.Diagnostics
plan, diags = ctx.Plan(cfg, state, opts)
// We expect the correct planned changes and no diagnostics.
if stage.allowWarnings {
assertNoErrors(t, diags)
} else {
assertNoDiagnostics(t, diags)
}
if plan.Complete != stage.complete {
t.Errorf("wrong completion status in plan: got %v, want %v", plan.Complete, stage.complete)
}
provider.plannedChanges.Test(t, stage.wantPlanned)
// We expect the correct actions.
gotActions := make(map[string]plans.Action)
for _, cs := range plan.Changes.Resources {
gotActions[cs.Addr.String()] = cs.Action
}
if diff := cmp.Diff(stage.wantActions, gotActions); diff != "" {
t.Errorf("wrong actions in plan\n%s", diff)
}
gotDeferred := make(map[string]ExpectedDeferred)
for _, dc := range plan.DeferredResources {
gotDeferred[dc.ChangeSrc.Addr.String()] = ExpectedDeferred{Reason: dc.DeferredReason, Action: dc.ChangeSrc.Action}
}
if diff := cmp.Diff(stage.wantDeferred, gotDeferred); diff != "" {
t.Errorf("wrong deferred reasons or actions in plan\n%s", diff)
}
gotDeferred := make(map[string]ExpectedDeferred)
for _, dc := range plan.DeferredResources {
gotDeferred[dc.ChangeSrc.Addr.String()] = ExpectedDeferred{Reason: dc.DeferredReason, Action: dc.ChangeSrc.Action}
}
if diff := cmp.Diff(stage.wantDeferred, gotDeferred); diff != "" {
t.Errorf("wrong deferred reasons or actions in plan\n%s", diff)
}
})
if stage.wantApplied == nil {
// Don't execute the apply stage if wantApplied is nil.
@ -1750,31 +2148,34 @@ func TestContextApply_deferredActions(t *testing.T) {
}
if opts.Mode == plans.RefreshOnlyMode {
// Don't execute the apply stage if wantApplied is nil.
// Don't execute the apply stage if mode is refresh-only.
return
}
updatedState, diags := ctx.Apply(plan, cfg, nil)
// We expect the correct applied changes and no diagnostics.
if stage.allowWarnings {
assertNoErrors(t, diags)
} else {
assertNoDiagnostics(t, diags)
}
provider.appliedChanges.Test(t, stage.wantApplied)
// We also want the correct output values.
gotOutputs := make(map[string]cty.Value)
for name, output := range updatedState.RootOutputValues {
gotOutputs[name] = output.Value
}
if diff := cmp.Diff(stage.wantOutputs, gotOutputs, ctydebug.CmpOptions); diff != "" {
t.Errorf("wrong output values\n%s", diff)
}
// Update the state for the next stage.
state = updatedState
t.Run("apply", func(t *testing.T) {
updatedState, diags := ctx.Apply(plan, cfg, nil)
// We expect the correct applied changes and no diagnostics.
if stage.allowWarnings {
assertNoErrors(t, diags)
} else {
assertNoDiagnostics(t, diags)
}
provider.appliedChanges.Test(t, stage.wantApplied)
// We also want the correct output values.
gotOutputs := make(map[string]cty.Value)
for name, output := range updatedState.RootOutputValues {
gotOutputs[name] = output.Value
}
if diff := cmp.Diff(stage.wantOutputs, gotOutputs, ctydebug.CmpOptions); diff != "" {
t.Errorf("wrong output values\n%s", diff)
}
// Update the state for the next stage.
state = updatedState
})
})
}
})
@ -1883,10 +2284,19 @@ func (provider *deferredActionsProvider) Provider() providers.Interface {
}
},
PlanResourceChangeFn: func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
var deferred *providers.Deferred
var requiresReplace []cty.Path
if req.ProposedNewState.IsNull() {
// Then we're deleting a concrete instance.
if key := req.PriorState.GetAttr("name"); key.IsKnown() && key.AsString() == "deferred_resource_change" {
deferred = &providers.Deferred{
Reason: providers.DeferredReasonProviderConfigUnknown,
}
}
return providers.PlanResourceChangeResponse{
PlannedState: req.ProposedNewState,
Deferred: deferred,
}
}
@ -1896,22 +2306,38 @@ func (provider *deferredActionsProvider) Provider() providers.Interface {
}
plannedState := req.ProposedNewState
if key == "deferred_resource_change" {
deferred = &providers.Deferred{
Reason: providers.DeferredReasonProviderConfigUnknown,
}
}
if plannedState.GetAttr("output").IsNull() {
plannedStateValues := req.ProposedNewState.AsValueMap()
plannedStateValues["output"] = cty.UnknownVal(cty.String)
plannedState = cty.ObjectVal(plannedStateValues)
} else if plannedState.GetAttr("output").AsString() == "mark_for_replacement" {
requiresReplace = append(requiresReplace, cty.GetAttrPath("name"), cty.GetAttrPath("output"))
}
provider.plannedChanges.Set(key, plannedState)
return providers.PlanResourceChangeResponse{
PlannedState: plannedState,
PlannedState: plannedState,
Deferred: deferred,
RequiresReplace: requiresReplace,
}
},
ApplyResourceChangeFn: func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
key := req.Config.GetAttr("name").AsString()
var key string
if v := req.Config.GetAttr("name"); v.IsKnown() {
key = req.Config.GetAttr("name").AsString()
} else {
key = "<unknown>"
}
newState := req.PlannedState
if !newState.GetAttr("output").IsKnown() {
if req.PlannedState.IsKnown() && !req.PlannedState.IsNull() && !newState.GetAttr("output").IsKnown() {
newStateValues := req.PlannedState.AsValueMap()
newStateValues["output"] = cty.StringVal(key)
newState = cty.ObjectVal(newStateValues)

@ -439,6 +439,7 @@ func (n *NodeAbstractResourceInstance) planDestroy(ctx EvalContext, currentState
ProposedNewState: nullVal,
PriorPrivate: currentState.Private,
ProviderMeta: metaConfigVal,
DeferralAllowed: ctx.Deferrals().DeferralAllowed(),
})
// We may not have a config for all destroys, but we want to reference
@ -451,6 +452,18 @@ func (n *NodeAbstractResourceInstance) planDestroy(ctx EvalContext, currentState
return plan, diags
}
if resp.Deferred != nil {
ctx.Deferrals().ReportResourceInstanceDeferred(n.Addr, resp.Deferred.Reason, &plans.ResourceInstanceChange{
Addr: n.Addr,
Change: plans.Change{
Action: plans.Delete,
Before: unmarkedPriorVal,
After: resp.PlannedState,
},
})
return plan, diags
}
// Check that the provider returned a null value here, since that is the
// only valid value for a destroy plan.
if !resp.PlannedState.IsNull() {
@ -927,6 +940,7 @@ func (n *NodeAbstractResourceInstance) plan(
ProposedNewState: proposedNewVal,
PriorPrivate: priorPrivate,
ProviderMeta: metaConfigVal,
DeferralAllowed: ctx.Deferrals().DeferralAllowed(),
})
}
diags = diags.Append(resp.Diagnostics.InConfigBody(config.Config, n.Addr.String()))
@ -934,6 +948,25 @@ func (n *NodeAbstractResourceInstance) plan(
return nil, nil, keyData, diags
}
if resp.Deferred != nil {
reqRep, reqRepDiags := getRequiredReplaces(priorVal, proposedNewVal, resp.RequiresReplace, n.ResolvedProvider.Provider, n.Addr)
diags = diags.Append(reqRepDiags)
if diags.HasErrors() {
return nil, nil, keyData, diags
}
action, _ := getAction(n.Addr, priorVal, resp.PlannedState, createBeforeDestroy, forceReplace, reqRep)
ctx.Deferrals().ReportResourceInstanceDeferred(n.Addr, resp.Deferred.Reason, &plans.ResourceInstanceChange{
Addr: n.Addr,
Change: plans.Change{
Action: action,
Before: unmarkedPriorVal,
After: unmarkedConfigVal,
},
})
return nil, nil, keyData, diags
}
plannedNewVal := resp.PlannedState
plannedPrivate := resp.PlannedPrivate
@ -1072,6 +1105,7 @@ func (n *NodeAbstractResourceInstance) plan(
ProposedNewState: proposedNewVal,
PriorPrivate: plannedPrivate,
ProviderMeta: metaConfigVal,
DeferralAllowed: ctx.Deferrals().DeferralAllowed(),
})
}
// We need to tread carefully here, since if there are any warnings
@ -1083,6 +1117,19 @@ func (n *NodeAbstractResourceInstance) plan(
diags = diags.Append(resp.Diagnostics.InConfigBody(config.Config, n.Addr.String()))
return nil, nil, keyData, diags
}
if resp.Deferred != nil {
ctx.Deferrals().ReportResourceInstanceDeferred(n.Addr, resp.Deferred.Reason, &plans.ResourceInstanceChange{
Addr: n.Addr,
Change: plans.Change{
Action: action,
Before: nullPriorVal,
After: resp.PlannedState,
},
})
return nil, nil, keyData, diags
}
plannedNewVal = resp.PlannedState
plannedPrivate = resp.PlannedPrivate

@ -181,7 +181,10 @@ func (n *NodePlannableResourceInstanceOrphan) managedResourceExecute(ctx EvalCon
// We might be able to offer an approximate reason for why we are
// planning to delete this object. (This is best-effort; we might
// sometimes not have a reason.)
change.ActionReason = n.deleteActionReason(ctx)
// The change can be nil in case of deferred destroys.
if change != nil {
change.ActionReason = n.deleteActionReason(ctx)
}
// We intentionally write the change before the subsequent checks, because
// all of the checks below this point are for problems caused by the

Loading…
Cancel
Save