Skip to content

Commit 6ee5b4a

Browse files
actions: ensure we detect a cycle when before actions refer to the triggering resource
1 parent 1c09e58 commit 6ee5b4a

File tree

2 files changed

+22
-57
lines changed

2 files changed

+22
-57
lines changed

internal/terraform/context_plan_actions_test.go

Lines changed: 21 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package terraform
66
import (
77
"path/filepath"
88
"slices"
9+
"strings"
910
"testing"
1011

1112
"github.com/hashicorp/hcl/v2"
@@ -67,7 +68,9 @@ func TestContextPlan_actions(t *testing.T) {
6768
assertValidateDiagnostics func(*testing.T, tfdiags.Diagnostics)
6869

6970
expectPlanDiagnostics func(m *configs.Config) tfdiags.Diagnostics
70-
assertPlan func(*testing.T, *plans.Plan)
71+
assertPlanDiagnostics func(*testing.T, tfdiags.Diagnostics)
72+
73+
assertPlan func(*testing.T, *plans.Plan)
7174
}{
7275
"unreferenced": {
7376
module: map[string]string{
@@ -366,30 +369,6 @@ resource "test_object" "a" {
366369
},
367370
},
368371

369-
"action for_each with auto-expansion": {
370-
toBeImplemented: true, // TODO: Look into this
371-
module: map[string]string{
372-
"main.tf": `
373-
action "test_unlinked" "hello" {
374-
for_each = toset(["a", "b"])
375-
376-
config {
377-
attr = "value-${each.key}"
378-
}
379-
}
380-
resource "test_object" "a" {
381-
lifecycle {
382-
action_trigger {
383-
events = [before_create]
384-
actions = [action.test_unlinked.hello] # This will auto-expand to action.test_unlinked.hello["a"] and action.test_unlinked.hello["b"]
385-
}
386-
}
387-
}
388-
`,
389-
},
390-
expectPlanActionCalled: true,
391-
},
392-
393372
"action count": {
394373
module: map[string]string{
395374
"main.tf": `
@@ -435,31 +414,6 @@ resource "test_object" "a" {
435414
},
436415
},
437416

438-
"action count with auto-expansion": {
439-
toBeImplemented: true, // TODO: Look into this
440-
module: map[string]string{
441-
"main.tf": `
442-
action "test_unlinked" "hello" {
443-
count = 2
444-
445-
config {
446-
attr = "value-${count.index}"
447-
}
448-
}
449-
450-
resource "test_object" "a" {
451-
lifecycle {
452-
action_trigger {
453-
events = [before_create]
454-
actions = [action.test_unlinked.hello] # This will auto-expand to action.test_unlinked.hello[0] and action.test_unlinked.hello[1]
455-
}
456-
}
457-
}
458-
`,
459-
},
460-
expectPlanActionCalled: true,
461-
},
462-
463417
"action for_each invalid access": {
464418
module: map[string]string{
465419
"main.tf": `
@@ -1311,7 +1265,6 @@ resource "test_object" "a" {
13111265
},
13121266

13131267
"action config refers to before triggering resource leads to validation error": {
1314-
toBeImplemented: true,
13151268
module: map[string]string{
13161269
"main.tf": `
13171270
action "test_unlinked" "hello" {
@@ -1320,25 +1273,35 @@ action "test_unlinked" "hello" {
13201273
}
13211274
}
13221275
resource "test_object" "a" {
1276+
name = "test_name"
13231277
lifecycle {
13241278
action_trigger {
1325-
events = [after_create]
1279+
events = [before_create]
13261280
actions = [action.test_unlinked.hello]
13271281
}
13281282
}
13291283
}
13301284
`,
13311285
},
1332-
expectPlanActionCalled: false,
1333-
assertValidateDiagnostics: func(t *testing.T, diags tfdiags.Diagnostics) {
1286+
expectPlanActionCalled: true, // The cycle only appears in the apply graph
1287+
assertPlanDiagnostics: func(t *testing.T, diags tfdiags.Diagnostics) {
13341288
if !diags.HasErrors() {
13351289
t.Fatalf("expected diagnostics to have errors, but it does not")
13361290
}
13371291
if len(diags) != 1 {
13381292
t.Fatalf("expected diagnostics to have 1 error, but it has %d", len(diags))
13391293
}
1340-
if diags[0].Description().Summary != "Cycle: test_object.a, action.test_unlinked.hello (expand)" && diags[0].Description().Summary != "Cycle: action.test_unlinked.hello (expand), test_object.a" {
1341-
t.Fatalf("expected diagnostic to have summary 'Cycle: test_object.a, action.test_unlinked.hello (expand)' or 'Cycle: action.test_unlinked.hello (expand), test_object.a', but got '%s'", diags[0].Description().Summary)
1294+
// We expect the diagnostic to be about a cycle
1295+
if !strings.Contains(diags[0].Description().Summary, "Cycle") {
1296+
t.Fatalf("expected diagnostic summary to contain 'Cycle', got '%s'", diags[0].Description().Summary)
1297+
}
1298+
// We expect the action node to be part of the cycle
1299+
if !strings.Contains(diags[0].Description().Summary, "action.test_unlinked.hello") {
1300+
t.Fatalf("expected diagnostic summary to contain 'action.test_unlinked.hello', got '%s'", diags[0].Description().Summary)
1301+
}
1302+
// We expect the resource node to be part of the cycle
1303+
if !strings.Contains(diags[0].Description().Summary, "test_object.a") {
1304+
t.Fatalf("expected diagnostic summary to contain 'test_object.a', got '%s'", diags[0].Description().Summary)
13421305
}
13431306
},
13441307
},
@@ -1925,6 +1888,8 @@ action "test_unlinked_wo" "hello" {
19251888

19261889
if tc.expectPlanDiagnostics != nil {
19271890
tfdiags.AssertDiagnosticsMatch(t, diags, tc.expectPlanDiagnostics(m))
1891+
} else if tc.assertPlanDiagnostics != nil {
1892+
tc.assertPlanDiagnostics(t, diags)
19281893
} else {
19291894
tfdiags.AssertNoDiagnostics(t, diags)
19301895
}

internal/terraform/node_action_trigger_apply.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ var (
2828
)
2929

3030
func (n *nodeActionTriggerApply) Name() string {
31-
return "action_apply_" + n.ActionInvocation.Addr.String()
31+
return n.ActionInvocation.Addr.String() + " (instance)"
3232
}
3333

3434
func (n *nodeActionTriggerApply) Execute(ctx EvalContext, wo walkOperation) tfdiags.Diagnostics {

0 commit comments

Comments
 (0)