Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/v1.15/ENHANCEMENTS-20251128-112428.yaml
Original file line number Diff line number Diff line change
@@ -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"
33 changes: 33 additions & 0 deletions internal/configs/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type ActionTrigger struct {
Condition hcl.Expression
Events []ActionTriggerEvent
Actions []ActionRef // References to actions
OnFailure ActionTriggerOnFailure

DeclRange hcl.Range
}
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -266,6 +295,10 @@ var actionTriggerSchema = &hcl.BodySchema{
Name: "actions",
Required: true,
},
{
Name: "on_failure",
Required: false,
},
},
}

Expand Down
43 changes: 43 additions & 0 deletions internal/configs/action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
}
25 changes: 25 additions & 0 deletions internal/configs/actiontriggeronfailure_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion internal/configs/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
13 changes: 13 additions & 0 deletions internal/plans/action_invocation.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ type ActionTrigger interface {

TriggerEvent() configs.ActionTriggerEvent

TriggerOnFailure() configs.ActionTriggerOnFailure

String() string

Equals(to ActionTrigger) bool
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
Loading