diff --git a/internal/plans/deferring/deferred.go b/internal/plans/deferring/deferred.go index cb46792370..9560fee015 100644 --- a/internal/plans/deferring/deferred.go +++ b/internal/plans/deferring/deferred.go @@ -768,6 +768,12 @@ func (d *Deferred) ShouldDeferActionInvocation(ai plans.ActionInvocationInstance return false } +// ShouldDeferAction returns true if the action should be deferred. This is the case if a +// dependency of the action is deferred. +func (d *Deferred) ShouldDeferAction(deps []addrs.ConfigResource) bool { + return d.DependenciesDeferred(deps) +} + // UnexpectedProviderDeferralDiagnostic is a diagnostic that indicates that a // provider was deferred although deferrals were not allowed. func UnexpectedProviderDeferralDiagnostic(addrs fmt.Stringer) tfdiags.Diagnostic { diff --git a/internal/terraform/context_plan_actions_test.go b/internal/terraform/context_plan_actions_test.go index 583dc1050c..c02957233f 100644 --- a/internal/terraform/context_plan_actions_test.go +++ b/internal/terraform/context_plan_actions_test.go @@ -2781,7 +2781,6 @@ resource "test_object" "a" { } }, }, - "splat is not supported": { module: map[string]string{ "main.tf": ` @@ -2792,7 +2791,7 @@ resource "test_object" "a" { lifecycle { action_trigger { events = [before_create] - actions = [action.test_unlinked.hello[*]] + actions = [action.test_unlinked.hello[*]] } } } @@ -2812,6 +2811,61 @@ resource "test_object" "a" { }) }, }, + "deferring resource dependencies should defer action": { + module: map[string]string{ + "main.tf": ` +resource "test_object" "origin" { + name = "origin" +} +action "test_unlinked" "hello" { + config { + attr = test_object.origin.name + } +} +resource "test_object" "a" { + name = "a" + lifecycle { + action_trigger { + events = [after_create] + actions = [action.test_unlinked.hello] + } + } +} +`, + }, + expectPlanActionCalled: false, + + planOpts: &PlanOpts{ + Mode: plans.NormalMode, + DeferralAllowed: true, + }, + planResourceFn: func(t *testing.T, req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + if req.Config.GetAttr("name").AsString() == "origin" { + return providers.PlanResourceChangeResponse{ + Deferred: &providers.Deferred{ + Reason: providers.DeferredReasonAbsentPrereq, + }, + } + } + return providers.PlanResourceChangeResponse{ + PlannedState: req.ProposedNewState, + PlannedPrivate: req.PriorPrivate, + PlannedIdentity: req.PriorIdentity, + } + }, + + assertPlan: func(t *testing.T, p *plans.Plan) { + if len(p.DeferredActionInvocations) != 1 { + t.Errorf("Expected 1 deferred action invocation, got %d", len(p.DeferredActionInvocations)) + } + if p.DeferredActionInvocations[0].ActionInvocationInstanceSrc.Addr.String() != "action.test_unlinked.hello" { + t.Errorf("Expected action.test_unlinked.hello, got %s", p.DeferredActionInvocations[0].ActionInvocationInstanceSrc.Addr.String()) + } + if p.DeferredActionInvocations[0].DeferredReason != providers.DeferredReasonDeferredPrereq { + t.Errorf("Expected DeferredReasonDeferredPrereq, got %s", p.DeferredActionInvocations[0].DeferredReason) + } + }, + }, } { t.Run(name, func(t *testing.T) { if tc.toBeImplemented { diff --git a/internal/terraform/node_action.go b/internal/terraform/node_action.go index 64d5996746..bb7c02d1a9 100644 --- a/internal/terraform/node_action.go +++ b/internal/terraform/node_action.go @@ -80,6 +80,7 @@ func (n *nodeExpandActionDeclaration) DynamicExpand(ctx EvalContext) (*Graph, tf Config: &n.Config, Schema: n.Schema, ResolvedProvider: n.ResolvedProvider, + Dependencies: n.Dependencies, } g.Add(&node) } else { @@ -90,6 +91,7 @@ func (n *nodeExpandActionDeclaration) DynamicExpand(ctx EvalContext) (*Graph, tf Config: &n.Config, Schema: n.Schema, ResolvedProvider: n.ResolvedProvider, + Dependencies: n.Dependencies, } g.Add(&node) diff --git a/internal/terraform/node_action_abstract.go b/internal/terraform/node_action_abstract.go index b5c2c7a1b5..2af00048d5 100644 --- a/internal/terraform/node_action_abstract.go +++ b/internal/terraform/node_action_abstract.go @@ -24,6 +24,7 @@ type NodeAbstractAction struct { // The address of the provider this action will use ResolvedProvider addrs.AbsProviderConfig Schema *providers.ActionSchema + Dependencies []addrs.ConfigResource } var ( @@ -32,6 +33,7 @@ var ( _ GraphNodeConfigAction = (*NodeAbstractAction)(nil) _ GraphNodeAttachActionSchema = (*NodeAbstractAction)(nil) _ GraphNodeProviderConsumer = (*NodeAbstractAction)(nil) + _ GraphNodeAttachDependencies = (*NodeAbstractAction)(nil) ) func (n NodeAbstractAction) Name() string { @@ -110,3 +112,7 @@ func (n *NodeAbstractAction) Provider() addrs.Provider { func (n *NodeAbstractAction) SetProvider(p addrs.AbsProviderConfig) { n.ResolvedProvider = p } + +func (n *NodeAbstractAction) AttachDependencies(deps []addrs.ConfigResource) { + n.Dependencies = deps +} diff --git a/internal/terraform/node_action_instance.go b/internal/terraform/node_action_instance.go index e822513c6b..f27a3d5352 100644 --- a/internal/terraform/node_action_instance.go +++ b/internal/terraform/node_action_instance.go @@ -9,7 +9,6 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" - "github.com/hashicorp/terraform/internal/dag" "github.com/hashicorp/terraform/internal/lang/langrefs" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/tfdiags" @@ -25,6 +24,7 @@ type NodeActionDeclarationInstance struct { Config *configs.Action Schema *providers.ActionSchema ResolvedProvider addrs.AbsProviderConfig + Dependencies []addrs.ConfigResource } var ( @@ -32,29 +32,23 @@ var ( _ GraphNodeExecutable = (*NodeActionDeclarationInstance)(nil) _ GraphNodeReferencer = (*NodeActionDeclarationInstance)(nil) _ GraphNodeReferenceable = (*NodeActionDeclarationInstance)(nil) - _ dag.GraphNodeDotter = (*NodeActionDeclarationInstance)(nil) ) func (n *NodeActionDeclarationInstance) Name() string { return n.Addr.String() } -func (n *NodeActionDeclarationInstance) DotNode(string, *dag.DotOpts) *dag.DotNode { - return &dag.DotNode{ - Name: n.Name(), - } -} - func (n *NodeActionDeclarationInstance) Path() addrs.ModuleInstance { return n.Addr.Module } func (n *NodeActionDeclarationInstance) Execute(ctx EvalContext, _ walkOperation) tfdiags.Diagnostics { var diags tfdiags.Diagnostics + deferrals := ctx.Deferrals() if n.Addr.Action.Key == addrs.WildcardKey { - if ctx.Deferrals().DeferralAllowed() { - ctx.Deferrals().ReportActionDeferred(n.Addr, providers.DeferredReasonInstanceCountUnknown) + if deferrals.DeferralAllowed() { + deferrals.ReportActionDeferred(n.Addr, providers.DeferredReasonInstanceCountUnknown) } else { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, @@ -66,6 +60,11 @@ func (n *NodeActionDeclarationInstance) Execute(ctx EvalContext, _ walkOperation return diags } + if deferrals.DeferralAllowed() && deferrals.ShouldDeferAction(n.Dependencies) { + deferrals.ReportActionDeferred(n.Addr, providers.DeferredReasonDeferredPrereq) + return diags + } + // This should have been caught already if n.Schema == nil { panic("NodeActionDeclarationInstance.Execute called without a schema")