From 0942808b40c3143b3ddd34626095a901e8fd3ccb Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Mon, 18 Aug 2025 16:17:29 +0200 Subject: [PATCH 1/2] failing actions should stop the following actions --- internal/terraform/context_plan_actions_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/terraform/context_plan_actions_test.go b/internal/terraform/context_plan_actions_test.go index 1f5697ed1ef2..117ed1b5406f 100644 --- a/internal/terraform/context_plan_actions_test.go +++ b/internal/terraform/context_plan_actions_test.go @@ -665,7 +665,6 @@ resource "test_object" "e" { }, "failing actions cancel next ones": { - toBeImplemented: true, // TODO: Look into this module: map[string]string{ "main.tf": ` action "test_unlinked" "failure" {} From c9f105b5a1b54e5e33bb0c5843e724956275bd0f Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Mon, 18 Aug 2025 16:26:44 +0200 Subject: [PATCH 2/2] improve error message --- .../terraform/context_apply_action_test.go | 224 +++++++++--------- .../terraform/context_plan_actions_test.go | 78 +++++- .../terraform/node_action_trigger_apply.go | 98 +++++--- .../node_action_trigger_instance_plan.go | 22 +- internal/terraform/transform_action_diff.go | 24 ++ 5 files changed, 289 insertions(+), 157 deletions(-) diff --git a/internal/terraform/context_apply_action_test.go b/internal/terraform/context_apply_action_test.go index 9ab6699f1ea1..6abec8b86950 100644 --- a/internal/terraform/context_apply_action_test.go +++ b/internal/terraform/context_apply_action_test.go @@ -4,9 +4,12 @@ package terraform import ( + "path/filepath" "testing" + "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans" @@ -31,7 +34,7 @@ func TestContext2Apply_actions(t *testing.T) { expectInvokeActionCalled bool expectInvokeActionCalls []providers.InvokeActionRequest - expectDiagnostics tfdiags.Diagnostics + expectDiagnostics func(m *configs.Config) tfdiags.Diagnostics }{ "unreferenced": { module: map[string]string{ @@ -147,7 +150,6 @@ resource "test_object" "a" { }, "before_create failing": { - toBeImplemented: true, // We need to revisit the diagnostic enhancement module: map[string]string{ "main.tf": ` action "act_unlinked" "hello" {} @@ -176,17 +178,21 @@ resource "test_object" "a" { } }, - expectDiagnostics: tfdiags.Diagnostics{ - tfdiags.Sourceless( - tfdiags.Error, - "Failed to apply actions before test_object.a", - "An error occured while invoking action action.act_unlinked.hello: test case for failing: this simulates a provider failing\n", - ), + expectDiagnostics: func(m *configs.Config) tfdiags.Diagnostics { + return tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Error when invoking action", + Detail: "test case for failing: this simulates a provider failing", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 7, Column: 18, Byte: 146}, + End: hcl.Pos{Line: 7, Column: 43, Byte: 171}, + }, + }) }, }, "before_create failing with successfully completed actions": { - toBeImplemented: true, // We need to revisit the diagnostic enhancement module: map[string]string{ "main.tf": ` action "act_unlinked" "hello" {} @@ -227,21 +233,24 @@ resource "test_object" "a" { } }, - expectDiagnostics: tfdiags.Diagnostics{ - tfdiags.Sourceless( - tfdiags.Error, - "Failed to apply actions before test_object.a", - `An error occured while invoking action action.act_unlinked.failure: test case for failing: this simulates a provider failing -The following actions were successfully invoked: -- action.act_unlinked.hello -- action.act_unlinked.world -As the resource did not change, these actions will be re-invoked in the next apply.`, - ), + expectDiagnostics: func(m *configs.Config) tfdiags.Diagnostics { + return tfdiags.Diagnostics{}.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Error when invoking action", + Detail: `test case for failing: this simulates a provider failing`, + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 13, Column: 72, Byte: 305}, + End: hcl.Pos{Line: 13, Column: 99, Byte: 332}, + }, + }, + ) + }, }, "before_create failing when calling invoke": { - toBeImplemented: true, // We need to revisit the diagnostic enhancement module: map[string]string{ "main.tf": ` action "act_unlinked" "hello" {} @@ -265,115 +274,87 @@ resource "test_object" "a" { ), } }, - expectDiagnostics: tfdiags.Diagnostics{ - tfdiags.Sourceless( - tfdiags.Error, - "Failed to apply actions before test_object.a", - "An error occured while invoking action action.act_unlinked.hello: test case for failing: this simulates a provider failing before the action is invoked\n", - ), - }, - }, - - "after_create failing": { - toBeImplemented: true, // We need to revisit the diagnostic enhancement - module: map[string]string{ - "main.tf": ` -action "act_unlinked" "hello" {} -resource "test_object" "a" { - lifecycle { - action_trigger { - events = [after_create] - actions = [action.act_unlinked.hello] - } - } -} -`, - }, - expectInvokeActionCalled: true, - events: func(req providers.InvokeActionRequest) []providers.InvokeActionEvent { - return []providers.InvokeActionEvent{ - providers.InvokeActionEvent_Completed{ - Diagnostics: tfdiags.Diagnostics{ - tfdiags.Sourceless( - tfdiags.Error, - "test case for failing", - "this simulates a provider failing", - ), + expectDiagnostics: func(m *configs.Config) tfdiags.Diagnostics { + return tfdiags.Diagnostics{}.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Error when invoking action", + Detail: "test case for failing: this simulates a provider failing before the action is invoked", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 7, Column: 18, Byte: 146}, + End: hcl.Pos{Line: 7, Column: 43, Byte: 171}, }, }, - } - }, - - expectDiagnostics: tfdiags.Diagnostics{ - tfdiags.Sourceless( - tfdiags.Error, - "Failed to apply actions after test_object.a", - `An error occured while invoking action action.act_unlinked.hello: test case for failing: this simulates a provider failing - -The following actions were not yet invoked: -- action.act_unlinked.hello -These actions will not be triggered in the next apply, please run "terraform invoke" to invoke them.`, - ), + ) }, }, - "after_create failing with successfully completed actions": { - toBeImplemented: true, // We need to revisit the diagnostic enhancement + "failing an action by action event stops next actions in list": { module: map[string]string{ "main.tf": ` action "act_unlinked" "hello" {} -action "act_unlinked" "world" {} action "act_unlinked" "failure" { config { attr = "failure" } } +action "act_unlinked" "goodbye" {} resource "test_object" "a" { lifecycle { action_trigger { - events = [after_create] - actions = [action.act_unlinked.hello, action.act_unlinked.world, action.act_unlinked.failure] + events = [before_create] + actions = [action.act_unlinked.hello, action.act_unlinked.failure, action.act_unlinked.goodbye] } } } `, }, expectInvokeActionCalled: true, - events: func(req providers.InvokeActionRequest) []providers.InvokeActionEvent { - if !req.PlannedActionData.IsNull() && req.PlannedActionData.GetAttr("attr").AsString() == "failure" { + events: func(r providers.InvokeActionRequest) []providers.InvokeActionEvent { + if !r.PlannedActionData.IsNull() && r.PlannedActionData.GetAttr("attr").AsString() == "failure" { return []providers.InvokeActionEvent{ providers.InvokeActionEvent_Completed{ - Diagnostics: tfdiags.Diagnostics{ - tfdiags.Sourceless( - tfdiags.Error, - "test case for failing", - "this simulates a provider failing", - ), - }, + Diagnostics: tfdiags.Diagnostics{}.Append(tfdiags.Sourceless(tfdiags.Error, "test case for failing", "this simulates a provider failing")), }, } - } else { - return []providers.InvokeActionEvent{ - providers.InvokeActionEvent_Completed{}, - } } - }, - expectDiagnostics: tfdiags.Diagnostics{ - tfdiags.Sourceless( - tfdiags.Error, - "Failed to apply actions after test_object.a", - `An error occured while invoking action action.act_unlinked.failure: test case for failing: this simulates a provider failing + return []providers.InvokeActionEvent{ + providers.InvokeActionEvent_Completed{}, + } -The following actions were not yet invoked: -- action.act_unlinked.failure -These actions will not be triggered in the next apply, please run "terraform invoke" to invoke them.`, - ), }, + expectDiagnostics: func(m *configs.Config) tfdiags.Diagnostics { + return tfdiags.Diagnostics{}.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Error when invoking action", + Detail: "test case for failing: this simulates a provider failing", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 13, Column: 45, Byte: 280}, + End: hcl.Pos{Line: 13, Column: 72, Byte: 307}, + }, + }, + ) + }, + + // We expect two calls but not the third one, because the second action fails + expectInvokeActionCalls: []providers.InvokeActionRequest{{ + ActionType: "act_unlinked", + PlannedActionData: cty.NullVal(cty.Object(map[string]cty.Type{ + "attr": cty.String, + })), + }, { + ActionType: "act_unlinked", + PlannedActionData: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("failure"), + }), + }}, }, - "failing an action stops next actions in list": { - toBeImplemented: true, // We need to revisit the diagnostic enhancement + "failing an action during invocation stops next actions in list": { module: map[string]string{ "main.tf": ` action "act_unlinked" "hello" {} @@ -407,15 +388,19 @@ resource "test_object" "a" { } return tfdiags.Diagnostics{} }, - expectDiagnostics: tfdiags.Diagnostics{ - tfdiags.Sourceless( - tfdiags.Error, - "Failed to apply actions before test_object.a", - `An error occured while invoking action action.act_unlinked.failure: test case for failing: this simulates a provider failing -The following actions were successfully invoked: -- action.act_unlinked.hello -As the resource did not change, these actions will be re-invoked in the next apply.`, - ), + expectDiagnostics: func(m *configs.Config) tfdiags.Diagnostics { + return tfdiags.Diagnostics{}.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Error when invoking action", + Detail: "test case for failing: this simulates a provider failing", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 13, Column: 45, Byte: 280}, + End: hcl.Pos{Line: 13, Column: 72, Byte: 307}, + }, + }, + ) }, // We expect two calls but not the third one, because the second action fails @@ -433,7 +418,6 @@ As the resource did not change, these actions will be re-invoked in the next app }, "failing an action stops next action triggers": { - toBeImplemented: true, // We need to revisit the diagnostic enhancement module: map[string]string{ "main.tf": ` action "act_unlinked" "hello" {} @@ -475,15 +459,19 @@ resource "test_object" "a" { } return tfdiags.Diagnostics{} }, - expectDiagnostics: tfdiags.Diagnostics{ - tfdiags.Sourceless( - tfdiags.Error, - "Failed to apply actions before test_object.a", - `An error occured while invoking action action.act_unlinked.failure: test case for failing: this simulates a provider failing -The following actions were successfully invoked: -- action.act_unlinked.hello -As the resource did not change, these actions will be re-invoked in the next apply.`, - ), + expectDiagnostics: func(m *configs.Config) tfdiags.Diagnostics { + return tfdiags.Diagnostics{}.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Error when invoking action", + Detail: "test case for failing: this simulates a provider failing", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 17, Column: 18, Byte: 355}, + End: hcl.Pos{Line: 17, Column: 45, Byte: 382}, + }, + }, + ) }, // We expect two calls but not the third one, because the second action fails expectInvokeActionCalls: []providers.InvokeActionRequest{{ @@ -962,8 +950,8 @@ resource "test_object" "a" { tfdiags.AssertNoDiagnostics(t, diags) _, diags = ctx.Apply(plan, m, nil) - if tc.expectDiagnostics.HasErrors() { - tfdiags.AssertDiagnosticsMatch(t, diags, tc.expectDiagnostics) + if tc.expectDiagnostics != nil { + tfdiags.AssertDiagnosticsMatch(t, diags, tc.expectDiagnostics(m)) } else { tfdiags.AssertNoDiagnostics(t, diags) } diff --git a/internal/terraform/context_plan_actions_test.go b/internal/terraform/context_plan_actions_test.go index 117ed1b5406f..d2ac2def9927 100644 --- a/internal/terraform/context_plan_actions_test.go +++ b/internal/terraform/context_plan_actions_test.go @@ -692,9 +692,81 @@ resource "test_object" "a" { expectPlanActionCalled: true, // We only expect a single diagnostic here, the other should not have been called because the first one failed. expectPlanDiagnostics: func(m *configs.Config) tfdiags.Diagnostics { - return tfdiags.Diagnostics{ - tfdiags.Sourceless(tfdiags.Error, "Planning failed", "Test case simulates an error while planning"), - } + return tfdiags.Diagnostics{}.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Failed to plan action", + Detail: "Planning failed: Test case simulates an error while planning", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 7, Column: 8, Byte: 149}, + End: hcl.Pos{Line: 7, Column: 46, Byte: 177}, + }, + }, + ) + }, + }, + + "actions with warnings don't cancel": { + module: map[string]string{ + "main.tf": ` +action "test_unlinked" "failure" {} +resource "test_object" "a" { + lifecycle { + action_trigger { + events = [before_create] + actions = [action.test_unlinked.failure, action.test_unlinked.failure] + } + action_trigger { + events = [before_create] + actions = [action.test_unlinked.failure] + } + } +} +`, + }, + + planActionResponse: &providers.PlanActionResponse{ + Diagnostics: tfdiags.Diagnostics{ + tfdiags.Sourceless(tfdiags.Warning, "Warning during planning", "Test case simulates a warning while planning"), + }, + }, + + expectPlanActionCalled: true, + // We only expect a single diagnostic here, the other should not have been called because the first one failed. + expectPlanDiagnostics: func(m *configs.Config) tfdiags.Diagnostics { + return tfdiags.Diagnostics{}.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Warnings when planning action", + Detail: "Warning during planning: Test case simulates a warning while planning", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 7, Column: 8, Byte: 149}, + End: hcl.Pos{Line: 7, Column: 46, Byte: 177}, + }, + }, + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Warnings when planning action", + Detail: "Warning during planning: Test case simulates a warning while planning", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 7, Column: 48, Byte: 179}, + End: hcl.Pos{Line: 7, Column: 76, Byte: 207}, + }, + }, + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Warnings when planning action", + Detail: "Warning during planning: Test case simulates a warning while planning", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 11, Column: 8, Byte: 284}, + End: hcl.Pos{Line: 11, Column: 46, Byte: 312}, + }, + }, + ) }, }, diff --git a/internal/terraform/node_action_trigger_apply.go b/internal/terraform/node_action_trigger_apply.go index 196b6200a85a..f3fb8257a455 100644 --- a/internal/terraform/node_action_trigger_apply.go +++ b/internal/terraform/node_action_trigger_apply.go @@ -6,6 +6,7 @@ package terraform import ( "fmt" + "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/plans/objchange" @@ -14,8 +15,9 @@ import ( ) type nodeActionTriggerApply struct { - ActionInvocation *plans.ActionInvocationInstanceSrc - resolvedProvider addrs.AbsProviderConfig + ActionInvocation *plans.ActionInvocationInstanceSrc + resolvedProvider addrs.AbsProviderConfig + ActionTriggerRange *hcl.Range } var ( @@ -34,40 +36,44 @@ func (n *nodeActionTriggerApply) Execute(ctx EvalContext, wo walkOperation) tfdi // TODO: Handle verifying the condition here, if we have any. ai := ctx.Changes().GetActionInvocation(actionInvocation.Addr, actionInvocation.ActionTrigger) if ai == nil { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Action invocation not found in plan", - "Could not find action invocation for address "+actionInvocation.Addr.String(), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Action invocation not found in plan", + Detail: "Could not find action invocation for address " + actionInvocation.Addr.String(), + Subject: n.ActionTriggerRange, + }) return diags } actionData, ok := ctx.Actions().GetActionInstance(ai.Addr) if !ok { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Action instance not found", - "Could not find action instance for address "+ai.Addr.String(), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Action instance not found", + Detail: "Could not find action instance for address " + ai.Addr.String(), + Subject: n.ActionTriggerRange, + }) return diags } provider, schema, err := getProvider(ctx, actionData.ProviderAddr) if err != nil { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - fmt.Sprintf("Failed to get provider for %s", ai.Addr), - fmt.Sprintf("Failed to get provider: %s", err), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Failed to get provider for %s", ai.Addr), + Detail: fmt.Sprintf("Failed to get provider: %s", err), + Subject: n.ActionTriggerRange, + }) return diags } actionSchema, ok := schema.Actions[ai.Addr.Action.Action.Type] if !ok { // This should have been caught earlier - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - fmt.Sprintf("Action %s not found in provider schema", ai.Addr), - fmt.Sprintf("The action %s was not found in the provider schema for %s", ai.Addr.Action.Action.Type, actionData.ProviderAddr), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Action %s not found in provider schema", ai.Addr), + Detail: fmt.Sprintf("The action %s was not found in the provider schema for %s", ai.Addr.Action.Action.Type, actionData.ProviderAddr), + Subject: n.ActionTriggerRange, + }) return diags } @@ -77,14 +83,13 @@ func (n *nodeActionTriggerApply) Execute(ctx EvalContext, wo walkOperation) tfdi // Validate that what we planned matches the action data we have. errs := objchange.AssertObjectCompatible(actionSchema.ConfigSchema, ai.ConfigValue, unmarkedConfigValue) for _, err := range errs { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Provider produced inconsistent final plan", - fmt.Sprintf( - "When expanding the plan for %s to include new values learned so far during apply, provider %q produced an invalid new value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", - ai.Addr, actionData.ProviderAddr.Provider.String(), tfdiags.FormatError(err), - ), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Provider produced inconsistent final plan", + Detail: fmt.Sprintf("When expanding the plan for %s to include new values learned so far during apply, provider %q produced an invalid new value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", + ai.Addr, actionData.ProviderAddr.Provider.String(), tfdiags.FormatError(err)), + Subject: n.ActionTriggerRange, + }) } hookIdentity := HookActionIdentity{ @@ -101,8 +106,12 @@ func (n *nodeActionTriggerApply) Execute(ctx EvalContext, wo walkOperation) tfdi ClientCapabilities: ctx.ClientCapabilities(), }) - diags = diags.Append(resp.Diagnostics) - if resp.Diagnostics.HasErrors() { + respDiags := n.AddSubjectToDiagnostics(resp.Diagnostics) + diags = diags.Append(respDiags) + if respDiags.HasErrors() { + ctx.Hook(func(h Hook) (HookAction, error) { + return h.CompleteAction(hookIdentity, respDiags.Err()) + }) return diags } @@ -113,7 +122,8 @@ func (n *nodeActionTriggerApply) Execute(ctx EvalContext, wo walkOperation) tfdi return h.ProgressAction(hookIdentity, ev.Message) }) case providers.InvokeActionEvent_Completed: - diags = diags.Append(ev.Diagnostics) + // Enhance the diagnostics + diags = diags.Append(n.AddSubjectToDiagnostics(ev.Diagnostics)) ctx.Hook(func(h Hook) (HookAction, error) { return h.CompleteAction(hookIdentity, ev.Diagnostics.Err()) }) @@ -155,3 +165,25 @@ func (n *nodeActionTriggerApply) References() []*addrs.Reference { func (n *nodeActionTriggerApply) ModulePath() addrs.Module { return n.ActionInvocation.Addr.Module.Module() } + +func (n *nodeActionTriggerApply) AddSubjectToDiagnostics(input tfdiags.Diagnostics) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + if len(input) > 0 { + severity := hcl.DiagWarning + message := "Warning when invoking action" + err := input.Warnings().ErrWithWarnings() + if input.HasErrors() { + severity = hcl.DiagError + message = "Error when invoking action" + err = input.ErrWithWarnings() + } + + diags = diags.Append(&hcl.Diagnostic{ + Severity: severity, + Summary: message, + Detail: err.Error(), + Subject: n.ActionTriggerRange, + }) + } + return diags +} diff --git a/internal/terraform/node_action_trigger_instance_plan.go b/internal/terraform/node_action_trigger_instance_plan.go index 4876558e0d39..a2e3f4c10870 100644 --- a/internal/terraform/node_action_trigger_instance_plan.go +++ b/internal/terraform/node_action_trigger_instance_plan.go @@ -100,12 +100,28 @@ func (n *nodeActionTriggerPlanInstance) Execute(ctx EvalContext, operation walkO ClientCapabilities: ctx.ClientCapabilities(), }) - // TODO: Deal with deferred responses - diags = diags.Append(resp.Diagnostics) - if diags.HasErrors() { + if len(resp.Diagnostics) > 0 { + severity := hcl.DiagWarning + message := "Warnings when planning action" + err := resp.Diagnostics.Warnings().ErrWithWarnings() + if resp.Diagnostics.HasErrors() { + severity = hcl.DiagError + message = "Failed to plan action" + err = resp.Diagnostics.ErrWithWarnings() + } + + diags = diags.Append(&hcl.Diagnostic{ + Severity: severity, + Summary: message, + Detail: err.Error(), + Subject: n.lifecycleActionTrigger.invokingSubject, + }) + } + if resp.Diagnostics.HasErrors() { return diags } + // TODO: Deal with deferred responses ctx.Changes().AppendActionInvocation(&plans.ActionInvocationInstance{ Addr: n.actionAddress, ProviderAddr: actionInstance.ProviderAddr, diff --git a/internal/terraform/transform_action_diff.go b/internal/terraform/transform_action_diff.go index c70d995ca113..b991092b7f3d 100644 --- a/internal/terraform/transform_action_diff.go +++ b/internal/terraform/transform_action_diff.go @@ -4,6 +4,8 @@ package terraform import ( + "fmt" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/dag" @@ -35,6 +37,28 @@ func (t *ActionDiffTransformer) Transform(g *Graph) error { ActionInvocation: action, } + // If the action invocations is triggered within the lifecycle of a resource + // we want to add information about the source location to the apply node + if at, ok := action.ActionTrigger.(plans.LifecycleActionTrigger); ok { + moduleInstance := t.Config.DescendantForInstance(at.TriggeringResourceAddr.Module) + if moduleInstance == nil { + panic(fmt.Sprintf("Could not find module instance for resource %s in config", at.TriggeringResourceAddr.String())) + } + + resourceInstance := moduleInstance.Module.ResourceByAddr(at.TriggeringResourceAddr.Resource.Resource) + if resourceInstance == nil { + panic(fmt.Sprintf("Could not find resource instance for resource %s in config", at.TriggeringResourceAddr.String())) + } + + triggerBlock := resourceInstance.Managed.ActionTriggers[at.ActionTriggerBlockIndex] + if triggerBlock == nil { + panic(fmt.Sprintf("Could not find action trigger block %d for resource %s in config", at.ActionTriggerBlockIndex, at.TriggeringResourceAddr.String())) + } + + act := triggerBlock.Actions[at.ActionsListIndex] + node.ActionTriggerRange = &act.Range + } + g.Add(node) invocationMap[action] = node