diff --git a/internal/plugin/grpc_provider.go b/internal/plugin/grpc_provider.go index b70dd7dcd6..40eac9daf8 100644 --- a/internal/plugin/grpc_provider.go +++ b/internal/plugin/grpc_provider.go @@ -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 } diff --git a/internal/plugin6/grpc_provider.go b/internal/plugin6/grpc_provider.go index 43ea86b883..530fcd254e 100644 --- a/internal/plugin6/grpc_provider.go +++ b/internal/plugin6/grpc_provider.go @@ -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 } diff --git a/internal/providers/provider.go b/internal/providers/provider.go index b57da8de89..c684c5d9c5 100644 --- a/internal/providers/provider.go +++ b/internal/providers/provider.go @@ -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 { diff --git a/internal/terraform/context_apply_deferred_test.go b/internal/terraform/context_apply_deferred_test.go index e0bfbc0c84..7382664e94 100644 --- a/internal/terraform/context_apply_deferred_test.go +++ b/internal/terraform/context_apply_deferred_test.go @@ -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 = "" + } 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) diff --git a/internal/terraform/node_resource_abstract_instance.go b/internal/terraform/node_resource_abstract_instance.go index 18531e89f7..47cad41787 100644 --- a/internal/terraform/node_resource_abstract_instance.go +++ b/internal/terraform/node_resource_abstract_instance.go @@ -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 diff --git a/internal/terraform/node_resource_plan_orphan.go b/internal/terraform/node_resource_plan_orphan.go index 0cf597e89b..6b009f37d7 100644 --- a/internal/terraform/node_resource_plan_orphan.go +++ b/internal/terraform/node_resource_plan_orphan.go @@ -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