Skip to content

Commit 1784fb8

Browse files
actions: connect resource instance nodes to after actions
1 parent 1a54777 commit 1784fb8

File tree

6 files changed

+155
-8
lines changed

6 files changed

+155
-8
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: BUG FIXES
2+
body: 'actions: make after_create & after_update actions run after the resource has applied'
3+
time: 2025-11-24T15:00:00.316597+01:00
4+
custom:
5+
Issue: "37936"

internal/terraform/context_apply_action_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
package terraform
55

66
import (
7+
"fmt"
78
"path/filepath"
89
"sync"
910
"testing"
11+
"time"
1012

1113
"github.com/google/go-cmp/cmp"
1214
"github.com/hashicorp/hcl/v2"
@@ -2819,3 +2821,117 @@ func (a *actionHookCapture) CompleteAction(identity HookActionIdentity, _ error)
28192821
a.completeActionHooks = append(a.completeActionHooks, identity)
28202822
return HookActionContinue, nil
28212823
}
2824+
2825+
func TestContextApply_actions_after_trigger_runs_after_expanded_resource(t *testing.T) {
2826+
m := testModuleInline(t, map[string]string{
2827+
"main.tf": `
2828+
locals {
2829+
each = toset(["one"])
2830+
}
2831+
action "action_example" "hello" {
2832+
config {
2833+
attr = "hello"
2834+
}
2835+
}
2836+
resource "test_object" "a" {
2837+
for_each = local.each
2838+
name = each.value
2839+
lifecycle {
2840+
action_trigger {
2841+
events = [after_create]
2842+
actions = [action.action_example.hello]
2843+
}
2844+
}
2845+
}
2846+
`,
2847+
})
2848+
2849+
orderedCalls := []string{}
2850+
2851+
testProvider := &testing_provider.MockProvider{
2852+
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
2853+
ResourceTypes: map[string]providers.Schema{
2854+
"test_object": {
2855+
Body: &configschema.Block{
2856+
Attributes: map[string]*configschema.Attribute{
2857+
"name": {
2858+
Type: cty.String,
2859+
Optional: true,
2860+
},
2861+
},
2862+
},
2863+
},
2864+
},
2865+
},
2866+
ApplyResourceChangeFn: func(arcr providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
2867+
time.Sleep(100 * time.Millisecond)
2868+
orderedCalls = append(orderedCalls, fmt.Sprintf("ApplyResourceChangeFn %s", arcr.TypeName))
2869+
return providers.ApplyResourceChangeResponse{
2870+
NewState: arcr.PlannedState,
2871+
NewIdentity: arcr.PlannedIdentity,
2872+
}
2873+
},
2874+
}
2875+
2876+
actionProvider := &testing_provider.MockProvider{
2877+
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
2878+
Actions: map[string]providers.ActionSchema{
2879+
"action_example": {
2880+
ConfigSchema: &configschema.Block{
2881+
Attributes: map[string]*configschema.Attribute{
2882+
"attr": {
2883+
Type: cty.String,
2884+
Optional: true,
2885+
},
2886+
},
2887+
},
2888+
},
2889+
},
2890+
ResourceTypes: map[string]providers.Schema{},
2891+
},
2892+
InvokeActionFn: func(iar providers.InvokeActionRequest) providers.InvokeActionResponse {
2893+
orderedCalls = append(orderedCalls, fmt.Sprintf("InvokeAction %s", iar.ActionType))
2894+
return providers.InvokeActionResponse{
2895+
Events: func(yield func(providers.InvokeActionEvent) bool) {
2896+
yield(providers.InvokeActionEvent_Completed{})
2897+
},
2898+
}
2899+
},
2900+
}
2901+
2902+
hookCapture := newActionHookCapture()
2903+
ctx := testContext2(t, &ContextOpts{
2904+
Providers: map[addrs.Provider]providers.Factory{
2905+
addrs.NewDefaultProvider("test"): testProviderFuncFixed(testProvider),
2906+
addrs.NewDefaultProvider("action"): testProviderFuncFixed(actionProvider),
2907+
},
2908+
Hooks: []Hook{
2909+
&hookCapture,
2910+
},
2911+
})
2912+
2913+
// Just a sanity check that the module is valid
2914+
diags := ctx.Validate(m, &ValidateOpts{})
2915+
tfdiags.AssertNoDiagnostics(t, diags)
2916+
2917+
planOpts := SimplePlanOpts(plans.NormalMode, InputValues{})
2918+
2919+
plan, diags := ctx.Plan(m, nil, planOpts)
2920+
tfdiags.AssertNoDiagnostics(t, diags)
2921+
2922+
if !plan.Applyable {
2923+
t.Fatalf("plan is not applyable but should be")
2924+
}
2925+
2926+
_, diags = ctx.Apply(plan, m, nil)
2927+
tfdiags.AssertNoDiagnostics(t, diags)
2928+
2929+
expectedOrder := []string{
2930+
"ApplyResourceChangeFn test_object",
2931+
"InvokeAction action_example",
2932+
}
2933+
2934+
if diff := cmp.Diff(expectedOrder, orderedCalls); diff != "" {
2935+
t.Fatalf("expected calls in order did not match actual calls (-expected +actual):\n%s", diff)
2936+
}
2937+
}

internal/terraform/graph_builder_apply.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,8 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer {
175175
}
176176
},
177177
// we want before_* actions to run before and after_* actions to run after the resource
178-
CreateNodesAsAfter: false,
178+
CreateNodesAsAfter: false,
179+
ConnectToResourceInstanceNodes: true,
179180
},
180181

181182
&ActionInvokeApplyTransformer{

internal/terraform/graph_builder_plan.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,8 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer {
185185
},
186186

187187
// We plan all actions after the resource is handled
188-
CreateNodesAsAfter: true,
188+
CreateNodesAsAfter: true,
189+
ConnectToResourceInstanceNodes: false,
189190
},
190191

191192
&ActionInvokePlanTransformer{

internal/terraform/transform_action_trigger_config.go

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ type ActionTriggerConfigTransformer struct {
1919

2020
queryPlanMode bool
2121

22-
ConcreteActionTriggerNodeFunc ConcreteActionTriggerNodeFunc
23-
CreateNodesAsAfter bool
22+
ConcreteActionTriggerNodeFunc ConcreteActionTriggerNodeFunc
23+
CreateNodesAsAfter bool
24+
ConnectToResourceInstanceNodes bool // if false it connects to resource nodes instead of resource instance nodes
2425
}
2526

2627
func (t *ActionTriggerConfigTransformer) Transform(g *Graph) error {
@@ -55,7 +56,11 @@ func (t *ActionTriggerConfigTransformer) transformSingle(g *Graph, config *confi
5556
}
5657

5758
resourceNodes := addrs.MakeMap[addrs.ConfigResource, []GraphNodeConfigResource]()
59+
resourceInstanceNodes := addrs.MakeMap[addrs.ConfigResource, []GraphNodeResourceInstance]()
5860
for _, node := range g.Vertices() {
61+
if rin, ok := node.(GraphNodeResourceInstance); ok {
62+
resourceInstanceNodes.Put(rin.ResourceInstanceAddr().ConfigResource(), append(resourceInstanceNodes.Get(rin.ResourceInstanceAddr().ConfigResource()), rin))
63+
}
5964
rn, ok := node.(GraphNodeConfigResource)
6065
if !ok {
6166
continue
@@ -141,8 +146,14 @@ func (t *ActionTriggerConfigTransformer) transformSingle(g *Graph, config *confi
141146
g.Add(nat)
142147

143148
// We want to run before the resource nodes
144-
for _, node := range resourceNode {
145-
g.Connect(dag.BasicEdge(node, nat))
149+
if t.ConnectToResourceInstanceNodes {
150+
for _, node := range resourceInstanceNodes.Get(resourceAddr) {
151+
g.Connect(dag.BasicEdge(node, nat))
152+
}
153+
} else {
154+
for _, node := range resourceNode {
155+
g.Connect(dag.BasicEdge(node, nat))
156+
}
146157
}
147158

148159
// We want to run after all prior nodes
@@ -157,8 +168,14 @@ func (t *ActionTriggerConfigTransformer) transformSingle(g *Graph, config *confi
157168
g.Add(nat)
158169

159170
// We want to run after the resource nodes
160-
for _, node := range resourceNode {
161-
g.Connect(dag.BasicEdge(nat, node))
171+
if t.ConnectToResourceInstanceNodes {
172+
for _, node := range resourceInstanceNodes.Get(resourceAddr) {
173+
g.Connect(dag.BasicEdge(nat, node))
174+
}
175+
} else {
176+
for _, node := range resourceNode {
177+
g.Connect(dag.BasicEdge(nat, node))
178+
}
162179
}
163180

164181
// We want to run after all prior nodes

internal/terraform/transform_targets.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,4 +231,11 @@ func (t *TargetsTransformer) addVertexDependenciesToTargetedNodes(g *Graph, v da
231231
}
232232
}
233233
}
234+
if _, ok := v.(*NodeApplyableResourceInstance); ok {
235+
for _, f := range g.UpEdges(v) {
236+
if _, ok := f.(*nodeActionTriggerApplyExpand); ok {
237+
t.addVertexDependenciesToTargetedNodes(g, f, targetedNodes, addrs)
238+
}
239+
}
240+
}
234241
}

0 commit comments

Comments
 (0)