actions: connect resource instance nodes to after actions

pull/37900/head
Daniel Schmidt 6 months ago
parent 4269633657
commit 698b7bb4b4

@ -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"

@ -4,9 +4,11 @@
package terraform
import (
"fmt"
"path/filepath"
"sync"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl/v2"
@ -2811,3 +2813,117 @@ func (a *actionHookCapture) Stopping() {}
func (a *actionHookCapture) PostStateUpdate(*states.State) (HookAction, error) {
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)
}
}

@ -167,7 +167,8 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer {
}
},
// we want before_* actions to run before and after_* actions to run after the resource
CreateNodesAsAfter: false,
CreateNodesAsAfter: false,
ConnectToResourceInstanceNodes: true,
},
&ActionInvokeApplyTransformer{

@ -178,7 +178,8 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer {
},
// We plan all actions after the resource is handled
CreateNodesAsAfter: true,
CreateNodesAsAfter: true,
ConnectToResourceInstanceNodes: false,
},
&ActionInvokePlanTransformer{

@ -19,8 +19,9 @@ type ActionTriggerConfigTransformer struct {
queryPlanMode bool
ConcreteActionTriggerNodeFunc ConcreteActionTriggerNodeFunc
CreateNodesAsAfter bool
ConcreteActionTriggerNodeFunc ConcreteActionTriggerNodeFunc
CreateNodesAsAfter bool
ConnectToResourceInstanceNodes bool // if false it connects to resource nodes instead of resource instance nodes
}
func (t *ActionTriggerConfigTransformer) Transform(g *Graph) error {
@ -55,7 +56,11 @@ func (t *ActionTriggerConfigTransformer) transformSingle(g *Graph, config *confi
}
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
@ -141,8 +146,14 @@ func (t *ActionTriggerConfigTransformer) transformSingle(g *Graph, config *confi
g.Add(nat)
// We want to run before the resource nodes
for _, node := range resourceNode {
g.Connect(dag.BasicEdge(node, nat))
if t.ConnectToResourceInstanceNodes {
for _, node := range resourceInstanceNodes.Get(resourceAddr) {
g.Connect(dag.BasicEdge(node, nat))
}
} else {
for _, node := range resourceNode {
g.Connect(dag.BasicEdge(node, nat))
}
}
// We want to run after all prior nodes
@ -157,8 +168,14 @@ func (t *ActionTriggerConfigTransformer) transformSingle(g *Graph, config *confi
g.Add(nat)
// We want to run after the resource nodes
for _, node := range resourceNode {
g.Connect(dag.BasicEdge(nat, node))
if t.ConnectToResourceInstanceNodes {
for _, node := range resourceInstanceNodes.Get(resourceAddr) {
g.Connect(dag.BasicEdge(nat, node))
}
} else {
for _, node := range resourceNode {
g.Connect(dag.BasicEdge(nat, node))
}
}
// We want to run after all prior nodes

@ -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)
}
}
}
}

Loading…
Cancel
Save