diff --git a/.changes/v1.15/ENHANCEMENTS-20251128-112428.yaml b/.changes/v1.15/ENHANCEMENTS-20251128-112428.yaml new file mode 100644 index 000000000000..18dd6428f1fe --- /dev/null +++ b/.changes/v1.15/ENHANCEMENTS-20251128-112428.yaml @@ -0,0 +1,5 @@ +kind: ENHANCEMENTS +body: 'actions: `action_trigger` block now supports `on_failure` attribute, which allows specifying terraform behavior on action failure.' +time: 2025-11-28T11:24:28.415647+01:00 +custom: + Issue: "37945" diff --git a/internal/configs/action.go b/internal/configs/action.go index 8cd220fc284a..d718ee76b3a7 100644 --- a/internal/configs/action.go +++ b/internal/configs/action.go @@ -45,6 +45,7 @@ type ActionTrigger struct { Condition hcl.Expression Events []ActionTriggerEvent Actions []ActionRef // References to actions + OnFailure ActionTriggerOnFailure DeclRange hcl.Range } @@ -55,6 +56,17 @@ type ActionTriggerEvent int //go:generate go tool golang.org/x/tools/cmd/stringer -type ActionTriggerEvent +// ActionTriggerOnFailure is an enum capturing the types of behaviors available +// to action trigger on_failure attribute. +type ActionTriggerOnFailure int + +const ( + ActionTriggerOnFailureFail ActionTriggerOnFailure = iota + ActionTriggerOnFailureContinue +) + +//go:generate go tool golang.org/x/tools/cmd/stringer -type ActionTriggerOnFailure + const ( Unknown ActionTriggerEvent = iota BeforeCreate @@ -78,6 +90,7 @@ func decodeActionTriggerBlock(block *hcl.Block) (*ActionTrigger, hcl.Diagnostics Events: []ActionTriggerEvent{}, Actions: []ActionRef{}, Condition: nil, + OnFailure: ActionTriggerOnFailureFail, } content, bodyDiags := block.Body.Content(actionTriggerSchema) @@ -137,6 +150,22 @@ func decodeActionTriggerBlock(block *hcl.Block) (*ActionTrigger, hcl.Diagnostics a.Actions = actionRefs } + if attr, exists := content.Attributes["on_failure"]; exists { + switch hcl.ExprAsKeyword(attr.Expr) { + case "continue": + a.OnFailure = ActionTriggerOnFailureContinue + case "fail": + a.OnFailure = ActionTriggerOnFailureFail + default: + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid \"on_failure\" keyword", + Detail: "The \"on_failure\" argument requires one of the following keywords: continue or fail.", + Subject: attr.Expr.Range().Ptr(), + }) + } + } + if len(a.Actions) == 0 { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, @@ -266,6 +295,10 @@ var actionTriggerSchema = &hcl.BodySchema{ Name: "actions", Required: true, }, + { + Name: "on_failure", + Required: false, + }, }, } diff --git a/internal/configs/action_test.go b/internal/configs/action_test.go index 6500b1bf0201..334f38700420 100644 --- a/internal/configs/action_test.go +++ b/internal/configs/action_test.go @@ -223,3 +223,46 @@ func TestDecodeActionTriggerBlock(t *testing.T) { }) } } + +func TestDecodeActionTriggerBlock_onFailure(t *testing.T) { + fooActionExpr := hcltest.MockExprTraversalSrc("action.action_type.foo") + + testData := map[string]struct { + valid bool + diagMsg string + }{ + "continue": {true, ""}, + "fail": {true, ""}, + "foo": {false, "MockExprLiteral:0,0-0: Invalid " + + "\"on_failure\" keyword; The \"on_failure\" argument requires " + + "one of the following keywords: continue or fail."}, + } + for keyword, td := range testData { + t.Run("", func(t *testing.T) { + givenActionTriggerBlock := &hcl.Block{ + Type: "action_trigger", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ + "events": hcltest.MockExprList([]hcl.Expression{ + hcltest.MockExprTraversalSrc("before_create"), + }), + "actions": hcltest.MockExprList([]hcl.Expression{ + fooActionExpr, + }), + "on_failure": hcltest.MockExprTraversalSrc(keyword), + }), + }), + } + + _, diags := decodeActionTriggerBlock(givenActionTriggerBlock) + + if diags.HasErrors() && td.valid { + t.Fatalf("keyword %s should be valid but has returned"+ + " diags: %v", keyword, diags) + } else if !diags.HasErrors() && !td.valid { + t.Fatalf("keyword %s should have been invalid but was"+ + "valid.", keyword) + } + }) + } +} diff --git a/internal/configs/actiontriggeronfailure_string.go b/internal/configs/actiontriggeronfailure_string.go new file mode 100644 index 000000000000..9bc3950e1040 --- /dev/null +++ b/internal/configs/actiontriggeronfailure_string.go @@ -0,0 +1,25 @@ +// Code generated by "stringer -type ActionTriggerOnFailure"; DO NOT EDIT. + +package configs + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[ActionTriggerOnFailureFail-0] + _ = x[ActionTriggerOnFailureContinue-1] +} + +const _ActionTriggerOnFailure_name = "ActionTriggerOnFailureFailActionTriggerOnFailureContinue" + +var _ActionTriggerOnFailure_index = [...]uint8{0, 26, 56} + +func (i ActionTriggerOnFailure) String() string { + idx := int(i) - 0 + if i < 0 || idx >= len(_ActionTriggerOnFailure_index)-1 { + return "ActionTriggerOnFailure(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _ActionTriggerOnFailure_name[_ActionTriggerOnFailure_index[idx]:_ActionTriggerOnFailure_index[idx+1]] +} diff --git a/internal/configs/parser_test.go b/internal/configs/parser_test.go index af80a343b273..69f9c77ccf92 100644 --- a/internal/configs/parser_test.go +++ b/internal/configs/parser_test.go @@ -13,7 +13,7 @@ import ( "github.com/davecgh/go-spew/spew" - version "github.com/hashicorp/go-version" + "github.com/hashicorp/go-version" "github.com/hashicorp/hcl/v2" "github.com/spf13/afero" ) diff --git a/internal/plans/action_invocation.go b/internal/plans/action_invocation.go index 3788580f5d72..9317d1e4f7dd 100644 --- a/internal/plans/action_invocation.go +++ b/internal/plans/action_invocation.go @@ -38,6 +38,8 @@ type ActionTrigger interface { TriggerEvent() configs.ActionTriggerEvent + TriggerOnFailure() configs.ActionTriggerOnFailure + String() string Equals(to ActionTrigger) bool @@ -55,6 +57,8 @@ type LifecycleActionTrigger struct { // Information about the trigger // The event that triggered this action invocation. ActionTriggerEvent configs.ActionTriggerEvent + // Hint to Terraform how to handle action failure + ActionTriggerOnFailure configs.ActionTriggerOnFailure // The index of the action_trigger block that triggered this invocation. ActionTriggerBlockIndex int // The index of the action in the events list of the action_trigger block @@ -65,6 +69,10 @@ func (t *LifecycleActionTrigger) TriggerEvent() configs.ActionTriggerEvent { return t.ActionTriggerEvent } +func (t *LifecycleActionTrigger) TriggerOnFailure() configs.ActionTriggerOnFailure { + return t.ActionTriggerOnFailure +} + func (t *LifecycleActionTrigger) actionTriggerSigil() {} func (t *LifecycleActionTrigger) String() string { @@ -110,6 +118,11 @@ func (t *InvokeActionTrigger) TriggerEvent() configs.ActionTriggerEvent { return configs.Invoke } +func (t *InvokeActionTrigger) TriggerOnFailure() configs.ActionTriggerOnFailure { + // We always fail on direct invocation + return configs.ActionTriggerOnFailureFail +} + func (t *InvokeActionTrigger) Equals(other ActionTrigger) bool { _, ok := other.(*InvokeActionTrigger) if !ok { diff --git a/internal/terraform/context_apply_action_test.go b/internal/terraform/context_apply_action_test.go index c6f363d68d9c..da5e72029aa5 100644 --- a/internal/terraform/context_apply_action_test.go +++ b/internal/terraform/context_apply_action_test.go @@ -202,7 +202,11 @@ resource "test_object" "a" { "before_create failing": { module: map[string]string{ "main.tf": ` -action "action_example" "hello" {} +action "action_example" "hello" { + config { + attr = "failure" + } +} resource "test_object" "a" { lifecycle { action_trigger { @@ -214,19 +218,7 @@ resource "test_object" "a" { `, }, 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", - ), - }, - }, - } - }, + events: generateTestActionEventsFunc(), expectDiagnostics: func(m *configs.Config) tfdiags.Diagnostics { return tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ @@ -235,8 +227,8 @@ resource "test_object" "a" { 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: 148}, - End: hcl.Pos{Line: 7, Column: 45, Byte: 175}, + Start: hcl.Pos{Line: 7, Column: 18, Byte: 185}, + End: hcl.Pos{Line: 7, Column: 45, Byte: 212}, }, }) }, @@ -361,20 +353,7 @@ resource "test_object" "a" { `, }, expectInvokeActionCalled: true, - 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{}.Append(tfdiags.Sourceless(tfdiags.Error, "test case for failing", "this simulates a provider failing")), - }, - } - } - - return []providers.InvokeActionEvent{ - providers.InvokeActionEvent_Completed{}, - } - - }, + events: generateTestActionEventsFunc(), expectDiagnostics: func(m *configs.Config) tfdiags.Diagnostics { return tfdiags.Diagnostics{}.Append( &hcl.Diagnostic{ @@ -2554,6 +2533,195 @@ lifecycle { }, }, }, + + "trigger on_failure set to 'fail' fails the resource and doesn't run the remainder of actions": { + module: map[string]string{ + "main.tf": ` +action "action_example" "failing_action" { + config { + attr = "failure" + } +} +action "action_example" "last_action" {} +resource "test_object" "dummy_resource" { + lifecycle { + action_trigger { + events = [before_create] + actions = [action.action_example.failing_action, action.action_example.last_action] + on_failure = fail + } + } +} +`, + }, + events: generateTestActionEventsFunc(), + expectInvokeActionCalled: true, + expectInvokeActionCalls: []providers.InvokeActionRequest{{ + ActionType: "action_example", + PlannedActionData: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("failure"), + }), + }}, + 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: 248}, + End: hcl.Pos{Line: 7, Column: 54, Byte: 284}, + }, + }, + ) + }, + }, + + "trigger on_failure set to 'continue' doesn't cause failure and proceeds invoking remaining actions": { + module: map[string]string{ + "main.tf": ` +action "action_example" "first_action" {} +action "action_example" "failing_action" { + config { + attr = "failure" + } +} +action "action_example" "last_action" {} +resource "test_object" "dummy_resource" { + lifecycle { + action_trigger { + events = [before_create] + actions = [action.action_example.first_action, action.action_example.failing_action, action.action_example.last_action] + on_failure = continue + } + + action_trigger { + events = [after_create] + actions = [action.action_example.first_action, action.action_example.failing_action, action.action_example.last_action] + on_failure = fail + } + } +} +`, + }, + events: generateTestActionEventsFunc(), + expectInvokeActionCalled: true, + expectInvokeActionCalls: []providers.InvokeActionRequest{ + // Before create skips over the failing action and continues + // current run due to 'on_failure' set to 'continue'. + { + ActionType: "action_example", + PlannedActionData: cty.NullVal(cty.Object(map[string]cty.Type{ + "attr": cty.String, + })), + }, { + ActionType: "action_example", + PlannedActionData: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("failure"), + }), + }, { + ActionType: "action_example", + PlannedActionData: cty.NullVal(cty.Object(map[string]cty.Type{ + "attr": cty.String, + })), + }, + // After create will fail on the second action due to it being + // a failing action and having 'on_failure' set to 'fail' + { + ActionType: "action_example", + PlannedActionData: cty.NullVal(cty.Object(map[string]cty.Type{ + "attr": cty.String, + })), + }, { + ActionType: "action_example", + PlannedActionData: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("failure"), + }), + }, + }, + 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: 54, Byte: 535}, + End: hcl.Pos{Line: 7, Column: 90, Byte: 571}, + }, + }, + ) + }, + }, + + "when omitted 'on_failure' behaves as 'on_failure = fail'": { + module: map[string]string{ + "main.tf": ` +action "action_example" "failing_action" { + config { + attr = "failure" + } +} +action "action_example" "last_action" {} + +resource "test_object" "dummy_resource" { + lifecycle { + action_trigger { + events = [before_create] + actions = [action.action_example.failing_action, action.action_example.last_action] + on_failure = continue + } + + action_trigger { + events = [after_create] + actions = [action.action_example.failing_action, action.action_example.last_action] + } + } +} +`}, + events: generateTestActionEventsFunc(), + expectInvokeActionCalled: true, + expectInvokeActionCalls: []providers.InvokeActionRequest{ + // Before create skips over the failing action and continues + // current run due to 'on_failure' set to 'continue'. + { + ActionType: "action_example", + PlannedActionData: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("failure"), + }), + }, { + ActionType: "action_example", + PlannedActionData: cty.NullVal(cty.Object(map[string]cty.Type{ + "attr": cty.String, + })), + }, + // After create will fail on the second action due to it being + // a failing action and having 'on_failure' not being explicitly + // set. + { + ActionType: "action_example", + PlannedActionData: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("failure"), + }), + }, + }, + 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: 422}, + End: hcl.Pos{Line: 7, Column: 54, Byte: 458}, + }, + }, + ) + }, + }, } { t.Run(name, func(t *testing.T) { if tc.toBeImplemented { @@ -2804,6 +2972,7 @@ func newActionHookCapture() actionHookCapture { mu: &sync.Mutex{}, } } + func (a *actionHookCapture) StartAction(identity HookActionIdentity) (HookAction, error) { a.mu.Lock() defer a.mu.Unlock() @@ -2935,3 +3104,29 @@ resource "test_object" "a" { t.Fatalf("expected calls in order did not match actual calls (-expected +actual):\n%s", diff) } } + +// generateTestActionEventsFunc returns function which would in turn generate +// a slice of InvokeActionEvents. +// +// By the default it returns a slice comprised of a single Completed event +// without additional context. +// +// The default behavior can be modified by configuring actions accordingly: +// - setting the config `attr` to `failure` will create a slice of a single +// Completed event populated with a dummy Diagnostics which simulates failed +// action, i.e. this setting is used to simulate Action failure. +func generateTestActionEventsFunc() func(providers.InvokeActionRequest) []providers.InvokeActionEvent { + return 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{}.Append(tfdiags.Sourceless(tfdiags.Error, "test case for failing", "this simulates a provider failing")), + }, + } + } + + return []providers.InvokeActionEvent{ + providers.InvokeActionEvent_Completed{}, + } + } +} diff --git a/internal/terraform/node_action_trigger_abstract.go b/internal/terraform/node_action_trigger_abstract.go index 60aceb955e03..9f2ff59e46e6 100644 --- a/internal/terraform/node_action_trigger_abstract.go +++ b/internal/terraform/node_action_trigger_abstract.go @@ -36,6 +36,7 @@ type nodeAbstractActionTriggerExpand struct { type lifecycleActionTrigger struct { resourceAddress addrs.ConfigResource events []configs.ActionTriggerEvent + onFailure configs.ActionTriggerOnFailure actionTriggerBlockIndex int actionListIndex int invokingSubject *hcl.Range diff --git a/internal/terraform/node_action_trigger_instance_apply.go b/internal/terraform/node_action_trigger_instance_apply.go index 103b52efc25e..a9ea0433f424 100644 --- a/internal/terraform/node_action_trigger_instance_apply.go +++ b/internal/terraform/node_action_trigger_instance_apply.go @@ -5,6 +5,7 @@ package terraform import ( "fmt" + "log" "github.com/hashicorp/hcl/v2" @@ -159,7 +160,7 @@ func (n *nodeActionTriggerApplyInstance) Execute(ctx EvalContext, wo walkOperati diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) { return h.CompleteAction(hookIdentity, respDiags.Err()) })) - return diags + return diagsIfNeeded(ai, diags) } if resp.Events != nil { // should only occur in misconfigured tests @@ -169,21 +170,12 @@ func (n *nodeActionTriggerApplyInstance) Execute(ctx EvalContext, wo walkOperati diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) { return h.ProgressAction(hookIdentity, ev.Message) })) - if diags.HasErrors() { - return diags - } case providers.InvokeActionEvent_Completed: // Enhance the diagnostics diags = diags.Append(n.AddSubjectToDiagnostics(ev.Diagnostics)) diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) { return h.CompleteAction(hookIdentity, ev.Diagnostics.Err()) })) - if ev.Diagnostics.HasErrors() { - return diags - } - if diags.HasErrors() { - return diags - } default: panic(fmt.Sprintf("unexpected action event type %T", ev)) } @@ -197,7 +189,26 @@ func (n *nodeActionTriggerApplyInstance) Execute(ctx EvalContext, wo walkOperati }) } - return diags + return diagsIfNeeded(ai, diags) +} + +func diagsIfNeeded( + aii *plans.ActionInvocationInstance, + currentDiags tfdiags.Diagnostics, +) tfdiags.Diagnostics { + switch aii.ActionTrigger.TriggerOnFailure() { + case configs.ActionTriggerOnFailureContinue: + if currentDiags.HasErrors() { + log.Printf("[WARN] Errors while running action %s, but"+ + " continuing as request in configuration.", aii.Addr) + } + return nil + default: + // Nothing to do for now - here to make it exhaustive and to denote the + // place to put potential new `on failure` cases. + } + + return currentDiags } func (n *nodeActionTriggerApplyInstance) ProvidedBy() (addr addrs.ProviderConfig, exact bool) { diff --git a/internal/terraform/node_action_trigger_instance_plan.go b/internal/terraform/node_action_trigger_instance_plan.go index a32b71f04f67..d9b0d18b240f 100644 --- a/internal/terraform/node_action_trigger_instance_plan.go +++ b/internal/terraform/node_action_trigger_instance_plan.go @@ -36,6 +36,7 @@ type nodeActionTriggerPlanInstance struct { type lifecycleActionTriggerInstance struct { resourceAddress addrs.AbsResourceInstance events []configs.ActionTriggerEvent + onFailure configs.ActionTriggerOnFailure actionTriggerBlockIndex int actionListIndex int invokingSubject *hcl.Range @@ -52,6 +53,7 @@ func (at *lifecycleActionTriggerInstance) ActionTrigger(triggeringEvent configs. ActionTriggerBlockIndex: at.actionTriggerBlockIndex, ActionsListIndex: at.actionListIndex, ActionTriggerEvent: triggeringEvent, + ActionTriggerOnFailure: at.onFailure, } } diff --git a/internal/terraform/node_action_trigger_partialexp.go b/internal/terraform/node_action_trigger_partialexp.go index 742d34db847f..0da55cbbd9ec 100644 --- a/internal/terraform/node_action_trigger_partialexp.go +++ b/internal/terraform/node_action_trigger_partialexp.go @@ -33,6 +33,7 @@ type NodeActionTriggerPartialExpanded struct { type lifecycleActionTriggerPartialExpanded struct { resourceAddress addrs.PartialExpandedResource events []configs.ActionTriggerEvent + onFailure configs.ActionTriggerOnFailure actionTriggerBlockIndex int actionListIndex int invokingSubject *hcl.Range @@ -121,6 +122,7 @@ func (n *NodeActionTriggerPartialExpanded) Execute(ctx EvalContext, op walkOpera ActionTrigger: &plans.LifecycleActionTrigger{ TriggeringResourceAddr: n.lifecycleActionTrigger.resourceAddress.UnknownResourceInstance(), ActionTriggerEvent: triggeringEvent, + ActionTriggerOnFailure: n.lifecycleActionTrigger.onFailure, ActionTriggerBlockIndex: n.lifecycleActionTrigger.actionTriggerBlockIndex, ActionsListIndex: n.lifecycleActionTrigger.actionListIndex, }, diff --git a/internal/terraform/node_action_trigger_plan.go b/internal/terraform/node_action_trigger_plan.go index 04ff5d2d4222..7331a6011195 100644 --- a/internal/terraform/node_action_trigger_plan.go +++ b/internal/terraform/node_action_trigger_plan.go @@ -128,6 +128,7 @@ func (n *nodeActionTriggerPlanExpand) DynamicExpand(ctx EvalContext) (*Graph, tf lifecycleActionTrigger: &lifecycleActionTriggerInstance{ resourceAddress: absResourceInstanceAddr, events: n.lifecycleActionTrigger.events, + onFailure: n.lifecycleActionTrigger.onFailure, actionTriggerBlockIndex: n.lifecycleActionTrigger.actionTriggerBlockIndex, actionListIndex: n.lifecycleActionTrigger.actionListIndex, invokingSubject: n.lifecycleActionTrigger.invokingSubject, diff --git a/internal/terraform/transform_action_trigger_config.go b/internal/terraform/transform_action_trigger_config.go index 42ed56bc7886..8d86733093c9 100644 --- a/internal/terraform/transform_action_trigger_config.go +++ b/internal/terraform/transform_action_trigger_config.go @@ -132,6 +132,7 @@ func (t *ActionTriggerConfigTransformer) transformSingle(g *Graph, config *confi Config: actionConfig, lifecycleActionTrigger: &lifecycleActionTrigger{ events: at.Events, + onFailure: at.OnFailure, resourceAddress: resourceAddr, actionExpr: action.Expr, actionTriggerBlockIndex: i,