after/before_destroy actions should run on terraform destroy

This commit is contained in:
Daniel Schmidt 2025-12-01 14:54:58 +01:00
parent 82e073fc19
commit 44f9cc5dc4
No known key found for this signature in database
GPG key ID: 377C3A4D62FBBBE2
9 changed files with 408 additions and 357 deletions

View file

@ -104,11 +104,15 @@ func decodeActionTriggerBlock(block *hcl.Block) (*ActionTrigger, hcl.Diagnostics
event = BeforeUpdate
case "after_update":
event = AfterUpdate
case "before_destroy":
event = BeforeDestroy
case "after_destroy":
event = AfterDestroy
default:
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Invalid \"event\" value %s", hcl.ExprAsKeyword(expr)),
Detail: "The \"event\" argument supports the following values: before_create, after_create, before_update, after_update.",
Detail: "The \"event\" argument supports the following values: before_create, after_create, before_update, after_update, before_destroy, after_destroy.",
Subject: expr.Range().Ptr(),
})
continue

View file

@ -183,7 +183,7 @@ func TestDecodeActionTriggerBlock(t *testing.T) {
},
},
[]string{
"MockExprTraversal:0,0-12: Invalid \"event\" value not_an_event; The \"event\" argument supports the following values: before_create, after_create, before_update, after_update.",
"MockExprTraversal:0,0-12: Invalid \"event\" value not_an_event; The \"event\" argument supports the following values: before_create, after_create, before_update, after_update, before_destroy, after_destroy.",
":0,0-0: No events specified; At least one event must be specified for an action_trigger.",
},
},

View file

@ -4,6 +4,7 @@
package terraform
import (
"fmt"
"maps"
"path/filepath"
"slices"
@ -4132,29 +4133,29 @@ resource "test_object" "b" {
"don't trigger during create or update": {
module: map[string]string{
"main.tf": `
action "test_action" "hello" {}
resource "test_object" "a" {
name = "a"
lifecycle {
action_trigger {
events = [before_destroy]
actions = [action.test_action.hello]
}
action "test_action" "hello" {}
resource "test_object" "a" {
name = "a"
lifecycle {
action_trigger {
events = [before_destroy]
actions = [action.test_action.hello]
}
}
}
}
action "test_action" "world" {}
resource "test_object" "b" {
name = "b"
lifecycle {
action_trigger {
events = [after_destroy]
actions = [action.test_action.hello]
}
action "test_action" "world" {}
resource "test_object" "b" {
name = "b"
lifecycle {
action_trigger {
events = [after_destroy]
actions = [action.test_action.hello]
}
}
}
}
`,
`,
},
expectPlanActionCalled: false,
buildState: func(s *states.SyncState) {
@ -4168,23 +4169,22 @@ resource "test_object" "b" {
"replace - after_destroy triggers before before_create": {
module: map[string]string{
"main.tf": `
action "test_action" "hello" {}
action "test_action" "world" {}
resource "test_object" "a" {
lifecycle {
action_trigger {
events = [before_create]
actions = [action.test_action.world]
}
action_trigger {
events = [after_destroy]
actions = [action.test_action.hello]
}
}
}
`,
action "test_action" "hello" {}
action "test_action" "world" {}
resource "test_object" "a" {
lifecycle {
action_trigger {
events = [before_create]
actions = [action.test_action.world]
}
action_trigger {
events = [after_destroy]
actions = [action.test_action.hello]
}
}
}
`,
},
buildState: func(s *states.SyncState) {
addr := mustResourceInstanceAddr("test_object.a")
s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{
@ -4197,6 +4197,8 @@ resource "test_object" "a" {
if len(p.Changes.ActionInvocations) != 2 {
t.Fatalf("expected 2 action invocations, got %d", len(p.Changes.ActionInvocations))
}
fmt.Printf("\n\t p.Changes.ActionInvocations[0].Addr.String() --> %#v\n", p.Changes.ActionInvocations[0].Addr.String())
fmt.Printf("\n\t p.Changes.ActionInvocations[1].Addr.String() --> %#v\n", p.Changes.ActionInvocations[1].Addr.String())
// The first triggered action should be the after_destroy
if p.Changes.ActionInvocations[0].Addr.String() != "action.test_action.hello" {
t.Fatalf("expected first action to be 'action.test_action.hello', got %s", p.Changes.ActionInvocations[0].Addr.String())
@ -4207,29 +4209,30 @@ resource "test_object" "a" {
}
},
},
"replace - before_destroy triggers before before_create and before after_destroy": {
module: map[string]string{
"main.tf": `
action "test_action" "hi" {}
action "test_action" "hello" {}
action "test_action" "world" {}
resource "test_object" "a" {
lifecycle {
action_trigger {
events = [before_create]
actions = [action.test_action.world]
}
action_trigger {
events = [after_destroy]
actions = [action.test_action.hello]
}
action_trigger {
events = [before_destroy]
actions = [action.test_action.hi]
}
}
}
`,
action "test_action" "hi" {}
action "test_action" "hello" {}
action "test_action" "world" {}
resource "test_object" "a" {
lifecycle {
action_trigger {
events = [before_create]
actions = [action.test_action.world]
}
action_trigger {
events = [after_destroy]
actions = [action.test_action.hello]
}
action_trigger {
events = [before_destroy]
actions = [action.test_action.hi]
}
}
}
`,
},
buildState: func(s *states.SyncState) {
@ -4262,27 +4265,27 @@ resource "test_object" "a" {
"replace with create_before_destroy - before_create and after_create trigger before before_destroy triggers": {
module: map[string]string{
"main.tf": `
action "test_action" "hi" {}
action "test_action" "hello" {}
action "test_action" "world" {}
resource "test_object" "a" {
lifecycle {
create_before_destroy = true
action_trigger {
events = [before_destroy]
actions = [action.test_action.world]
}
action_trigger {
events = [after_create]
actions = [action.test_action.hello]
}
action_trigger {
events = [before_create]
actions = [action.test_action.hi]
}
}
}
`,
action "test_action" "hi" {}
action "test_action" "hello" {}
action "test_action" "world" {}
resource "test_object" "a" {
lifecycle {
create_before_destroy = true
action_trigger {
events = [before_destroy]
actions = [action.test_action.world]
}
action_trigger {
events = [after_create]
actions = [action.test_action.hello]
}
action_trigger {
events = [before_create]
actions = [action.test_action.hi]
}
}
}
`,
},
buildState: func(s *states.SyncState) {
@ -4315,16 +4318,17 @@ resource "test_object" "a" {
"destroy - triggers destroy actions": {
module: map[string]string{
"main.tf": `
action "test_action" "hello" {}
resource "test_object" "a" {
lifecycle {
action_trigger {
events = [after_destroy]
actions = [action.test_action.hello]
}
}
}
`,
action "test_action" "hello" {}
resource "test_object" "a" {
name = "name"
lifecycle {
action_trigger {
events = [after_destroy]
actions = [action.test_action.hello]
}
}
}
`,
},
planOpts: &PlanOpts{
@ -4333,7 +4337,7 @@ resource "test_object" "a" {
buildState: func(s *states.SyncState) {
addr := mustResourceInstanceAddr("test_object.a")
s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{
AttrsJSON: []byte(`{"name":"previous_run"}`),
AttrsJSON: []byte(`{"name":"name"}`),
Status: states.ObjectReady,
}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`))
},
@ -4351,16 +4355,16 @@ resource "test_object" "a" {
"forget - don't trigger destroy actions": {
module: map[string]string{
"main.tf": `
action "test_action" "hello" {}
resource "test_object" "a" {
lifecycle {
action_trigger {
events = [after_destroy]
actions = [action.test_action.hello]
}
}
}
`,
action "test_action" "hello" {}
resource "test_object" "a" {
lifecycle {
action_trigger {
events = [after_destroy]
actions = [action.test_action.hello]
}
}
}
`,
},
planOpts: &PlanOpts{
@ -4385,18 +4389,18 @@ resource "test_object" "a" {
"removing an instance triggers destroy actions": {
module: map[string]string{
"main.tf": `
action "test_action" "hello" {}
resource "test_object" "a" {
name = "instance"
count = 1
lifecycle {
action_trigger {
events = [after_destroy]
actions = [action.test_action.hello]
}
}
}
`,
action "test_action" "hello" {}
resource "test_object" "a" {
name = "instance"
count = 1
lifecycle {
action_trigger {
events = [after_destroy]
actions = [action.test_action.hello]
}
}
}
`,
},
buildState: func(s *states.SyncState) {
@ -4433,18 +4437,18 @@ resource "test_object" "a" {
"removed block - does trigger destroy actions": {
module: map[string]string{
"main.tf": `
action "test_action" "hello" {}
action "test_action" "hello" {}
removed {
from = "test_object.a"
lifecycle {
action_trigger {
events = [after_destroy]
actions = [action.test_action.hello]
}
}
}
`,
removed {
from = "test_object.a"
lifecycle {
action_trigger {
events = [after_destroy]
actions = [action.test_action.hello]
}
}
}
`,
},
buildState: func(s *states.SyncState) {
@ -4468,19 +4472,19 @@ removed {
"removed block with destroy set to false - does not trigger destroy actions": {
module: map[string]string{
"main.tf": `
action "test_action" "hello" {}
action "test_action" "hello" {}
removed {
from = "test_object.a"
lifecycle {
destroy = true
action_trigger {
events = [after_destroy]
actions = [action.test_action.hello]
}
}
}
`,
removed {
from = "test_object.a"
lifecycle {
destroy = true
action_trigger {
events = [after_destroy]
actions = [action.test_action.hello]
}
}
}
`,
},
buildState: func(s *states.SyncState) {
@ -4501,21 +4505,21 @@ removed {
"before_destroy actions can access the triggering resource": {
module: map[string]string{
"main.tf": `
action "test_action" "hello" {
config {
attr = test_object.a.name
}
}
resource "test_object" "a" {
name = "instance"
lifecycle {
action_trigger {
events = [before_destroy]
actions = [action.test_action.hello]
}
}
}
`,
action "test_action" "hello" {
config {
attr = test_object.a.name
}
}
resource "test_object" "a" {
name = "instance"
lifecycle {
action_trigger {
events = [before_destroy]
actions = [action.test_action.hello]
}
}
}
`,
},
planOpts: &PlanOpts{
@ -4549,23 +4553,23 @@ resource "test_object" "a" {
"before_destroy actions can access the triggering resource instance": {
module: map[string]string{
"main.tf": `
action "test_action" "hello" {
count = 1
config {
attr = test_object.a[count.index].name
}
}
resource "test_object" "a" {
count = 1
name = "instance#{count.index}"
lifecycle {
action_trigger {
events = [before_destroy]
actions = [action.test_action.hello[count.index]]
}
}
}
`,
action "test_action" "hello" {
count = 1
config {
attr = test_object.a[count.index].name
}
}
resource "test_object" "a" {
count = 1
name = "instance#{count.index}"
lifecycle {
action_trigger {
events = [before_destroy]
actions = [action.test_action.hello[count.index]]
}
}
}
`,
},
planOpts: &PlanOpts{
@ -4599,21 +4603,21 @@ resource "test_object" "a" {
"after_destroy actions can access the triggering resource": {
module: map[string]string{
"main.tf": `
action "test_action" "hello" {
config {
attr = test_object.a.name
}
}
resource "test_object" "a" {
name = "instance"
lifecycle {
action_trigger {
events = [after_destroy]
actions = [action.test_action.hello]
}
}
}
`,
action "test_action" "hello" {
config {
attr = test_object.a.name
}
}
resource "test_object" "a" {
name = "instance"
lifecycle {
action_trigger {
events = [after_destroy]
actions = [action.test_action.hello]
}
}
}
`,
},
planOpts: &PlanOpts{
@ -4647,23 +4651,23 @@ resource "test_object" "a" {
"after_destroy actions can access the triggering resource instance": {
module: map[string]string{
"main.tf": `
action "test_action" "hello" {
count = 1
config {
attr = test_object.a[count.index].name
}
}
resource "test_object" "a" {
count = 1
name = "instance#{count.index}"
lifecycle {
action_trigger {
events = [after_destroy]
actions = [action.test_action.hello[count.index]]
}
}
}
`,
action "test_action" "hello" {
count = 1
config {
attr = test_object.a[count.index].name
}
}
resource "test_object" "a" {
count = 1
name = "instance#{count.index}"
lifecycle {
action_trigger {
events = [after_destroy]
actions = [action.test_action.hello[count.index]]
}
}
}
`,
},
planOpts: &PlanOpts{
@ -4697,24 +4701,24 @@ resource "test_object" "a" {
"after_destroy actions can not access other resources than the triggering resource": {
module: map[string]string{
"main.tf": `
action "test_action" "hello" {
config {
attr = test_object.a.name + test_object.forbidden.name
}
}
resource "test_object" "a" {
name = "instance"
lifecycle {
action_trigger {
events = [after_destroy]
actions = [action.test_action.hello]
}
}
}
resource "test_object" "forbidden" {
name = "you-should-not-access-me"
}
`,
action "test_action" "hello" {
config {
attr = test_object.a.name + test_object.forbidden.name
}
}
resource "test_object" "a" {
name = "instance"
lifecycle {
action_trigger {
events = [after_destroy]
actions = [action.test_action.hello]
}
}
}
resource "test_object" "forbidden" {
name = "you-should-not-access-me"
}
`,
},
planOpts: &PlanOpts{
@ -4739,24 +4743,24 @@ resource "test_object" "forbidden" {
"before_destroy actions can not access other resources than the triggering resource": {
module: map[string]string{
"main.tf": `
action "test_action" "hello" {
config {
attr = test_object.a.name + test_object.forbidden.name
}
}
resource "test_object" "a" {
name = "instance"
lifecycle {
action_trigger {
events = [before_destroy]
actions = [action.test_action.hello]
}
}
}
resource "test_object" "forbidden" {
name = "you-should-not-access-me"
}
`,
action "test_action" "hello" {
config {
attr = test_object.a.name + test_object.forbidden.name
}
}
resource "test_object" "a" {
name = "instance"
lifecycle {
action_trigger {
events = [before_destroy]
actions = [action.test_action.hello]
}
}
}
resource "test_object" "forbidden" {
name = "you-should-not-access-me"
}
`,
},
planOpts: &PlanOpts{

View file

@ -137,11 +137,13 @@ type PlanGraphBuilder struct {
// See GraphBuilder
func (b *PlanGraphBuilder) Build(path addrs.ModuleInstance) (*Graph, tfdiags.Diagnostics) {
log.Printf("[TRACE] building graph for %s", b.Operation)
return (&BasicGraphBuilder{
g, d := (&BasicGraphBuilder{
Steps: b.Steps(),
Name: "PlanGraphBuilder",
SkipGraphValidation: b.SkipGraphValidation,
}).Build(path)
return g, d
}
// See GraphBuilder
@ -172,26 +174,6 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer {
generateConfigPathForImportTargets: b.GenerateConfigPath,
},
&ActionTriggerConfigTransformer{
Config: b.Config,
Operation: b.Operation,
ActionTargets: b.ActionTargets,
queryPlanMode: b.queryPlan,
ConcreteActionTriggerNodeFunc: func(node *nodeAbstractActionTriggerExpand, _ RelativeActionTiming) dag.Vertex {
return &nodeActionTriggerPlanExpand{
nodeAbstractActionTriggerExpand: node,
}
},
},
&ActionInvokePlanTransformer{
Config: b.Config,
Operation: b.Operation,
ActionTargets: b.ActionTargets,
queryPlanMode: b.queryPlan,
},
// Add dynamic values
&RootVariableTransformer{
Config: b.Config,
@ -249,6 +231,26 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer {
State: b.State,
},
&ActionTriggerConfigTransformer{
Config: b.Config,
Operation: b.Operation,
ActionTargets: b.ActionTargets,
queryPlanMode: b.queryPlan,
ConcreteActionTriggerNodeFunc: func(node *nodeAbstractActionTriggerExpand, _ RelativeActionTiming) dag.Vertex {
return &nodeActionTriggerPlanExpand{
nodeAbstractActionTriggerExpand: node,
}
},
},
&ActionInvokePlanTransformer{
Config: b.Config,
Operation: b.Operation,
ActionTargets: b.ActionTargets,
queryPlanMode: b.queryPlan,
},
// Attach the state
&AttachStateTransformer{State: b.State},

View file

@ -296,7 +296,7 @@ func evaluateActionCondition(ctx EvalContext, at actionConditionContext) (bool,
func containsBeforeEvent(events []configs.ActionTriggerEvent) bool {
for _, event := range events {
switch event {
case configs.BeforeCreate, configs.BeforeUpdate:
case configs.BeforeCreate, configs.BeforeUpdate, configs.BeforeDestroy:
return true
default:
continue
@ -304,3 +304,32 @@ func containsBeforeEvent(events []configs.ActionTriggerEvent) bool {
}
return false
}
func actionIsTriggeredByEvent(events []configs.ActionTriggerEvent, action plans.Action) []configs.ActionTriggerEvent {
triggeredEvents := []configs.ActionTriggerEvent{}
for _, event := range events {
switch event {
case configs.BeforeCreate, configs.AfterCreate:
if action.IsReplace() || action == plans.Create {
triggeredEvents = append(triggeredEvents, event)
} else {
continue
}
case configs.BeforeUpdate, configs.AfterUpdate:
if action == plans.Update {
triggeredEvents = append(triggeredEvents, event)
} else {
continue
}
case configs.BeforeDestroy, configs.AfterDestroy:
if action == plans.DeleteThenCreate || action == plans.CreateThenDelete || action == plans.Delete {
triggeredEvents = append(triggeredEvents, event)
} else {
continue
}
default:
panic(fmt.Sprintf("unknown action trigger event %s", event))
}
}
return triggeredEvents
}

View file

@ -17,6 +17,11 @@ type nodeActionTriggerPlanExpand struct {
*nodeAbstractActionTriggerExpand
resourceTargets []addrs.Targetable
// During planDestroy we don't expand the resource, but instead
// use the state to set the resource instances to be processed.
// This means we need to track them here and not rely on the expander.
manualResourceExpansion []addrs.AbsResourceInstance
}
var (
@ -69,74 +74,86 @@ func (n *nodeActionTriggerPlanExpand) DynamicExpand(ctx EvalContext) (*Graph, tf
}
}
// First we expand the module
moduleInstances := expander.ExpandModule(n.lifecycleActionTrigger.resourceAddress.Module, false)
for _, module := range moduleInstances {
_, keys, _ := expander.ResourceInstanceKeys(n.lifecycleActionTrigger.resourceAddress.Absolute(module))
for _, key := range keys {
absResourceInstanceAddr := n.lifecycleActionTrigger.resourceAddress.Absolute(module).Instance(key)
absResourceInstanceAddrs := []addrs.AbsResourceInstance{}
// If the triggering resource was targeted, make sure the instance
// that triggered this was targeted specifically.
// This is necessary since the expansion of a resource instance (and of an action trigger)
// happens during the graph walk / execution, therefore the target transformer can not
// filter out individual instances, this needs to happen during the graph walk / execution.
if n.resourceTargets != nil {
matched := false
for _, resourceTarget := range n.resourceTargets {
if resourceTarget.TargetContains(absResourceInstanceAddr) {
matched = true
break
}
}
if !matched {
continue
// If we are in a walk that does not expand on its own, we need to use the
// manualResourceExpansion list.
if len(n.manualResourceExpansion) > 0 {
absResourceInstanceAddrs = n.manualResourceExpansion
} else {
// First we expand the module
moduleInstances := expander.ExpandModule(n.lifecycleActionTrigger.resourceAddress.Module, false)
for _, module := range moduleInstances {
_, keys, _ := expander.ResourceInstanceKeys(n.lifecycleActionTrigger.resourceAddress.Absolute(module))
for _, key := range keys {
absResourceInstanceAddr := n.lifecycleActionTrigger.resourceAddress.Absolute(module).Instance(key)
absResourceInstanceAddrs = append(absResourceInstanceAddrs, absResourceInstanceAddr)
}
}
}
for _, absResourceInstanceAddr := range absResourceInstanceAddrs {
module := absResourceInstanceAddr.Module
key := absResourceInstanceAddr.Resource.Key
// If the triggering resource was targeted, make sure the instance
// that triggered this was targeted specifically.
// This is necessary since the expansion of a resource instance (and of an action trigger)
// happens during the graph walk / execution, therefore the target transformer can not
// filter out individual instances, this needs to happen during the graph walk / execution.
if n.resourceTargets != nil {
matched := false
for _, resourceTarget := range n.resourceTargets {
if resourceTarget.TargetContains(absResourceInstanceAddr) {
matched = true
break
}
}
// The n.Addr was derived from the ActionRef hcl.Expression referenced inside the resource's lifecycle block, and has not yet been
// expanded or fully evaluated, so we will do that now.
// Grab the instance key, necessary if the action uses [count.index] or [each.key]
repData := instances.RepetitionData{}
switch k := key.(type) {
case addrs.IntKey:
repData.CountIndex = k.Value()
case addrs.StringKey:
repData.EachKey = k.Value()
repData.EachValue = cty.DynamicVal
}
ref, evalActionDiags := evaluateActionExpression(n.lifecycleActionTrigger.actionExpr, repData)
diags = append(diags, evalActionDiags...)
if diags.HasErrors() {
if !matched {
continue
}
// The reference is either an action or action instance
var actionAddr addrs.AbsActionInstance
switch sub := ref.Subject.(type) {
case addrs.Action:
actionAddr = sub.Absolute(module).Instance(addrs.NoKey)
case addrs.ActionInstance:
actionAddr = sub.Absolute(module)
}
node := nodeActionTriggerPlanInstance{
actionAddress: actionAddr,
resolvedProvider: n.resolvedProvider,
actionConfig: n.Config,
lifecycleActionTrigger: &lifecycleActionTriggerInstance{
resourceAddress: absResourceInstanceAddr,
events: n.lifecycleActionTrigger.events,
actionTriggerBlockIndex: n.lifecycleActionTrigger.actionTriggerBlockIndex,
actionListIndex: n.lifecycleActionTrigger.actionListIndex,
invokingSubject: n.lifecycleActionTrigger.invokingSubject,
conditionExpr: n.lifecycleActionTrigger.conditionExpr,
},
}
g.Add(&node)
}
// The n.Addr was derived from the ActionRef hcl.Expression referenced inside the resource's lifecycle block, and has not yet been
// expanded or fully evaluated, so we will do that now.
// Grab the instance key, necessary if the action uses [count.index] or [each.key]
repData := instances.RepetitionData{}
switch k := key.(type) {
case addrs.IntKey:
repData.CountIndex = k.Value()
case addrs.StringKey:
repData.EachKey = k.Value()
repData.EachValue = cty.DynamicVal
}
ref, evalActionDiags := evaluateActionExpression(n.lifecycleActionTrigger.actionExpr, repData)
diags = append(diags, evalActionDiags...)
if diags.HasErrors() {
continue
}
// The reference is either an action or action instance
var actionAddr addrs.AbsActionInstance
switch sub := ref.Subject.(type) {
case addrs.Action:
actionAddr = sub.Absolute(module).Instance(addrs.NoKey)
case addrs.ActionInstance:
actionAddr = sub.Absolute(module)
}
node := nodeActionTriggerPlanInstance{
actionAddress: actionAddr,
resolvedProvider: n.resolvedProvider,
actionConfig: n.Config,
lifecycleActionTrigger: &lifecycleActionTriggerInstance{
resourceAddress: absResourceInstanceAddr,
events: n.lifecycleActionTrigger.events,
actionTriggerBlockIndex: n.lifecycleActionTrigger.actionTriggerBlockIndex,
actionListIndex: n.lifecycleActionTrigger.actionListIndex,
invokingSubject: n.lifecycleActionTrigger.invokingSubject,
conditionExpr: n.lifecycleActionTrigger.conditionExpr,
},
}
g.Add(&node)
}
addRootNodeToGraph(&g)

View file

@ -1065,32 +1065,3 @@ func depsEqual(a, b []addrs.ConfigResource) bool {
}
return true
}
func actionIsTriggeredByEvent(events []configs.ActionTriggerEvent, action plans.Action) []configs.ActionTriggerEvent {
triggeredEvents := []configs.ActionTriggerEvent{}
for _, event := range events {
switch event {
case configs.BeforeCreate, configs.AfterCreate:
if action.IsReplace() || action == plans.Create {
triggeredEvents = append(triggeredEvents, event)
} else {
continue
}
case configs.BeforeUpdate, configs.AfterUpdate:
if action == plans.Update {
triggeredEvents = append(triggeredEvents, event)
} else {
continue
}
case configs.BeforeDestroy, configs.AfterDestroy:
if action == plans.DeleteThenCreate || action == plans.CreateThenDelete || action == plans.Delete {
triggeredEvents = append(triggeredEvents, event)
} else {
continue
}
default:
panic(fmt.Sprintf("unknown action trigger event %s", event))
}
}
return triggeredEvents
}

View file

@ -5,6 +5,7 @@ package terraform
import (
"fmt"
"slices"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
@ -22,9 +23,11 @@ type ActionTriggerConfigTransformer struct {
ConcreteActionTriggerNodeFunc ConcreteActionTriggerNodeFunc
}
var allowedOperations = []walkOperation{walkPlan, walkApply, walkDestroy, walkPlanDestroy}
func (t *ActionTriggerConfigTransformer) Transform(g *Graph) error {
// We don't want to run if we are using the query plan mode or have targets in place
if (t.Operation != walkPlan && t.Operation != walkApply) || t.queryPlanMode || len(t.ActionTargets) > 0 {
if (!slices.Contains(allowedOperations, t.Operation)) || t.queryPlanMode || len(t.ActionTargets) > 0 {
return nil
}
@ -50,8 +53,8 @@ func (t *ActionTriggerConfigTransformer) transform(g *Graph, config *configs.Con
func (t *ActionTriggerConfigTransformer) transformSingle(g *Graph, config *configs.Config) error {
// During plan we only want to create all triggers to run after the resource
createNodesAsAfter := t.Operation == walkPlan
// During apply we want all after trigger to also connect to the resource instance nodes
connectToResourceInstanceNodes := t.Operation == walkApply
// During apply/destroy we want all after trigger to also connect to the resource instance nodes
connectToResourceInstanceNodes := t.Operation == walkApply || t.Operation == walkDestroy || t.Operation == walkPlanDestroy
actionConfigs := addrs.MakeMap[addrs.ConfigAction, *configs.Action]()
for _, a := range config.Module.Actions {
actionConfigs.Put(a.Addr().InModule(config.Path), a)
@ -86,9 +89,9 @@ func (t *ActionTriggerConfigTransformer) transformSingle(g *Graph, config *confi
containsAfterEvent := false
for _, event := range at.Events {
switch event {
case configs.BeforeCreate, configs.BeforeUpdate:
case configs.BeforeCreate, configs.BeforeUpdate, configs.BeforeDestroy:
containsBeforeEvent = true
case configs.AfterCreate, configs.AfterUpdate:
case configs.AfterCreate, configs.AfterUpdate, configs.AfterDestroy:
containsAfterEvent = true
}
}
@ -123,7 +126,7 @@ func (t *ActionTriggerConfigTransformer) transformSingle(g *Graph, config *confi
resourceAddr := r.Addr().InModule(config.Path)
resourceNode, ok := resourceNodes.GetOk(resourceAddr)
if !ok {
if !ok && !connectToResourceInstanceNodes {
panic(fmt.Sprintf("Could not find node for %s", resourceAddr))
}
@ -141,10 +144,12 @@ func (t *ActionTriggerConfigTransformer) transformSingle(g *Graph, config *confi
},
}
var nat dag.Vertex
// If CreateNodesAsAfter is set we want all nodes to run after the resource
// If not we want expansion nodes only to exist if they are being used
if !createNodesAsAfter && containsBeforeEvent {
nat := t.ConcreteActionTriggerNodeFunc(abstract, RelativeActionTimingBefore)
nat = t.ConcreteActionTriggerNodeFunc(abstract, RelativeActionTimingBefore)
g.Add(nat)
// We want to run before the resource nodes
@ -165,7 +170,7 @@ func (t *ActionTriggerConfigTransformer) transformSingle(g *Graph, config *confi
}
if createNodesAsAfter || containsAfterEvent {
nat := t.ConcreteActionTriggerNodeFunc(abstract, RelativeActionTimingAfter)
nat = t.ConcreteActionTriggerNodeFunc(abstract, RelativeActionTimingAfter)
g.Add(nat)
// We want to run after the resource nodes
@ -184,6 +189,15 @@ func (t *ActionTriggerConfigTransformer) transformSingle(g *Graph, config *confi
}
priorAfterNodes = append(priorAfterNodes, nat)
}
// TODO: Clarify when this should be done versus the normal expansion process.
if natpe, ok := nat.(*nodeActionTriggerPlanExpand); ok {
aris := []addrs.AbsResourceInstance{}
for _, ri := range resourceInstanceNodes.Get(resourceAddr) {
aris = append(aris, ri.ResourceInstanceAddr())
}
natpe.manualResourceExpansion = aris
}
}
}
}

View file

@ -329,6 +329,16 @@ func (t *pruneUnusedNodesTransformer) Transform(g *Graph) error {
continue
}
// another special case are destroy actions, therefore we keep all action triggers
_, ok := n.(*nodeActionTriggerPlanExpand)
if ok {
keep.Add(n)
}
_, ok = n.(*nodeExpandActionDeclaration)
if ok {
keep.Add(n)
}
// from here we only search for managed resource destroy nodes
n, ok := n.(GraphNodeDestroyer)
if !ok {