Skip to content
Merged
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.14/BUG FIXES-20251124-150000.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: BUG FIXES
body: 'actions: make after_create & after_update actions run after the resource has applied'
time: 2025-11-24T15:00:00.316597+01:00
custom:
Issue: "37936"
116 changes: 116 additions & 0 deletions internal/terraform/context_apply_action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
package terraform

import (
"fmt"
"path/filepath"
"sync"
"testing"
"time"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl/v2"
Expand Down Expand Up @@ -2819,3 +2821,117 @@ func (a *actionHookCapture) CompleteAction(identity HookActionIdentity, _ error)
a.completeActionHooks = append(a.completeActionHooks, identity)
return HookActionContinue, nil
}

func TestContextApply_actions_after_trigger_runs_after_expanded_resource(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
locals {
each = toset(["one"])
}
action "action_example" "hello" {
config {
attr = "hello"
}
}
resource "test_object" "a" {
for_each = local.each
name = each.value
lifecycle {
action_trigger {
events = [after_create]
actions = [action.action_example.hello]
}
}
}
`,
})

orderedCalls := []string{}

testProvider := &testing_provider.MockProvider{
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"test_object": {
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"name": {
Type: cty.String,
Optional: true,
},
},
},
},
},
},
ApplyResourceChangeFn: func(arcr providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
time.Sleep(100 * time.Millisecond)
orderedCalls = append(orderedCalls, fmt.Sprintf("ApplyResourceChangeFn %s", arcr.TypeName))
return providers.ApplyResourceChangeResponse{
NewState: arcr.PlannedState,
NewIdentity: arcr.PlannedIdentity,
}
},
}

actionProvider := &testing_provider.MockProvider{
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
Actions: map[string]providers.ActionSchema{
"action_example": {
ConfigSchema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"attr": {
Type: cty.String,
Optional: true,
},
},
},
},
},
ResourceTypes: map[string]providers.Schema{},
},
InvokeActionFn: func(iar providers.InvokeActionRequest) providers.InvokeActionResponse {
orderedCalls = append(orderedCalls, fmt.Sprintf("InvokeAction %s", iar.ActionType))
return providers.InvokeActionResponse{
Events: func(yield func(providers.InvokeActionEvent) bool) {
yield(providers.InvokeActionEvent_Completed{})
},
}
},
}

hookCapture := newActionHookCapture()
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(testProvider),
addrs.NewDefaultProvider("action"): testProviderFuncFixed(actionProvider),
},
Hooks: []Hook{
&hookCapture,
},
})

// Just a sanity check that the module is valid
diags := ctx.Validate(m, &ValidateOpts{})
tfdiags.AssertNoDiagnostics(t, diags)

planOpts := SimplePlanOpts(plans.NormalMode, InputValues{})

plan, diags := ctx.Plan(m, nil, planOpts)
tfdiags.AssertNoDiagnostics(t, diags)

if !plan.Applyable {
t.Fatalf("plan is not applyable but should be")
}

_, diags = ctx.Apply(plan, m, nil)
tfdiags.AssertNoDiagnostics(t, diags)

expectedOrder := []string{
"ApplyResourceChangeFn test_object",
"InvokeAction action_example",
}

if diff := cmp.Diff(expectedOrder, orderedCalls); diff != "" {
t.Fatalf("expected calls in order did not match actual calls (-expected +actual):\n%s", diff)
}
}
2 changes: 0 additions & 2 deletions internal/terraform/graph_builder_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,6 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer {
relativeTiming: timing,
}
},
// we want before_* actions to run before and after_* actions to run after the resource
CreateNodesAsAfter: false,
},

&ActionInvokeApplyTransformer{
Expand Down
3 changes: 0 additions & 3 deletions internal/terraform/graph_builder_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,6 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer {
nodeAbstractActionTriggerExpand: node,
}
},

// We plan all actions after the resource is handled
CreateNodesAsAfter: true,
},

&ActionInvokePlanTransformer{
Expand Down
23 changes: 20 additions & 3 deletions internal/terraform/transform_action_trigger_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ type ActionTriggerConfigTransformer struct {
queryPlanMode bool

ConcreteActionTriggerNodeFunc ConcreteActionTriggerNodeFunc
CreateNodesAsAfter bool
}

func (t *ActionTriggerConfigTransformer) Transform(g *Graph) error {
Expand Down Expand Up @@ -49,13 +48,21 @@ 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
actionConfigs := addrs.MakeMap[addrs.ConfigAction, *configs.Action]()
for _, a := range config.Module.Actions {
actionConfigs.Put(a.Addr().InModule(config.Path), a)
}

resourceNodes := addrs.MakeMap[addrs.ConfigResource, []GraphNodeConfigResource]()
resourceInstanceNodes := addrs.MakeMap[addrs.ConfigResource, []GraphNodeResourceInstance]()
for _, node := range g.Vertices() {
if rin, ok := node.(GraphNodeResourceInstance); ok {
resourceInstanceNodes.Put(rin.ResourceInstanceAddr().ConfigResource(), append(resourceInstanceNodes.Get(rin.ResourceInstanceAddr().ConfigResource()), rin))
}
rn, ok := node.(GraphNodeConfigResource)
if !ok {
continue
Expand Down Expand Up @@ -136,14 +143,19 @@ func (t *ActionTriggerConfigTransformer) transformSingle(g *Graph, config *confi

// 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 !t.CreateNodesAsAfter && containsBeforeEvent {
if !createNodesAsAfter && containsBeforeEvent {
nat := t.ConcreteActionTriggerNodeFunc(abstract, RelativeActionTimingBefore)
g.Add(nat)

// We want to run before the resource nodes
for _, node := range resourceNode {
g.Connect(dag.BasicEdge(node, nat))
}
if connectToResourceInstanceNodes {
for _, node := range resourceInstanceNodes.Get(resourceAddr) {
g.Connect(dag.BasicEdge(node, nat))
}
}

// We want to run after all prior nodes
for _, priorNode := range priorBeforeNodes {
Expand All @@ -152,14 +164,19 @@ func (t *ActionTriggerConfigTransformer) transformSingle(g *Graph, config *confi
priorBeforeNodes = append(priorBeforeNodes, nat)
}

if t.CreateNodesAsAfter || containsAfterEvent {
if createNodesAsAfter || containsAfterEvent {
nat := t.ConcreteActionTriggerNodeFunc(abstract, RelativeActionTimingAfter)
g.Add(nat)

// We want to run after the resource nodes
for _, node := range resourceNode {
g.Connect(dag.BasicEdge(nat, node))
}
if connectToResourceInstanceNodes {
for _, node := range resourceInstanceNodes.Get(resourceAddr) {
g.Connect(dag.BasicEdge(nat, node))
}
}

// We want to run after all prior nodes
for _, priorNode := range priorAfterNodes {
Expand Down
7 changes: 7 additions & 0 deletions internal/terraform/transform_targets.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,4 +231,11 @@ func (t *TargetsTransformer) addVertexDependenciesToTargetedNodes(g *Graph, v da
}
}
}
if _, ok := v.(*NodeApplyableResourceInstance); ok {
for _, f := range g.UpEdges(v) {
if _, ok := f.(*nodeActionTriggerApplyExpand); ok {
t.addVertexDependenciesToTargetedNodes(g, f, targetedNodes, addrs)
}
}
}
}