diff --git a/internal/terraform/context_apply_action_test.go b/internal/terraform/context_apply_action_test.go index b695c3b0a8..201fdce158 100644 --- a/internal/terraform/context_apply_action_test.go +++ b/internal/terraform/context_apply_action_test.go @@ -24,7 +24,7 @@ func TestContext2Apply_actions(t *testing.T) { mode plans.Mode prevRunState *states.State events []providers.InvokeActionEvent - callingInvokeReturnsDiagnostics tfdiags.Diagnostics + callingInvokeReturnsDiagnostics func(providers.InvokeActionRequest) tfdiags.Diagnostics planOpts *PlanOpts expectInvokeActionCalled bool @@ -181,7 +181,7 @@ resource "test_object" "a" { }, }, - "before_create failing to call invoke": { + "before_create failing when calling invoke": { module: map[string]string{ "main.tf": ` action "act_unlinked" "hello" {} @@ -196,13 +196,121 @@ resource "test_object" "a" { `, }, expectInvokeActionCalled: true, - callingInvokeReturnsDiagnostics: tfdiags.Diagnostics{ + callingInvokeReturnsDiagnostics: func(providers.InvokeActionRequest) tfdiags.Diagnostics { + return tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "test case for failing", + "this simulates a provider failing before the action is invoked", + ), + } + }, + expectDiagnostics: tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "test case for failing", + "this simulates a provider failing before the action is invoked", + ), + }, + }, + + "failing an action stops next actions in list": { + module: map[string]string{ + "main.tf": ` +action "act_unlinked" "hello" {} +action "act_unlinked" "failure" { + config { + attr = "failure" + } +} +action "act_unlinked" "goodbye" {} +resource "test_object" "a" { + lifecycle { + action_trigger { + events = [before_create] + actions = [action.act_unlinked.hello, action.act_unlinked.failure, action.act_unlinked.goodbye] + } + } +} +`, + }, + expectInvokeActionCalled: true, + callingInvokeReturnsDiagnostics: func(r providers.InvokeActionRequest) tfdiags.Diagnostics { + if !r.PlannedActionData.IsNull() && r.PlannedActionData.GetAttr("attr").AsString() == "failure" { + // Simulate a failure for the second action + return tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "test case for failing", + "this simulates a provider failing before the action is invoked", + ), + } + } + return tfdiags.Diagnostics{} + }, + expectDiagnostics: tfdiags.Diagnostics{ tfdiags.Sourceless( tfdiags.Error, "test case for failing", "this simulates a provider failing before the action is invoked", ), }, + // We expect two calls but not the third one, because the second action fails + expectInvokeActionCalls: []providers.InvokeActionRequest{{ + ActionType: "act_unlinked", + PlannedActionData: cty.NullVal(cty.Object(map[string]cty.Type{ + "attr": cty.String, + })), + }, { + ActionType: "act_unlinked", + PlannedActionData: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("failure"), + }), + }}, + }, + + "failing an action stops next action triggers": { + module: map[string]string{ + "main.tf": ` +action "act_unlinked" "hello" {} +action "act_unlinked" "failure" { + config { + attr = "failure" + } +} +action "act_unlinked" "goodbye" {} +resource "test_object" "a" { + lifecycle { + action_trigger { + events = [before_create] + actions = [action.act_unlinked.hello] + } + action_trigger { + events = [before_create] + actions = [action.act_unlinked.failure] + } + action_trigger { + events = [before_create] + actions = [action.act_unlinked.goodbye] + } + } +} +`, + }, + expectInvokeActionCalled: true, + callingInvokeReturnsDiagnostics: func(r providers.InvokeActionRequest) tfdiags.Diagnostics { + if !r.PlannedActionData.IsNull() && r.PlannedActionData.GetAttr("attr").AsString() == "failure" { + // Simulate a failure for the second action + return tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "test case for failing", + "this simulates a provider failing before the action is invoked", + ), + } + } + return tfdiags.Diagnostics{} + }, expectDiagnostics: tfdiags.Diagnostics{ tfdiags.Sourceless( tfdiags.Error, @@ -210,6 +318,18 @@ resource "test_object" "a" { "this simulates a provider failing before the action is invoked", ), }, + // We expect two calls but not the third one, because the second action fails + expectInvokeActionCalls: []providers.InvokeActionRequest{{ + ActionType: "act_unlinked", + PlannedActionData: cty.NullVal(cty.Object(map[string]cty.Type{ + "attr": cty.String, + })), + }, { + ActionType: "act_unlinked", + PlannedActionData: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("failure"), + }), + }}, }, "action with configuration": { @@ -364,9 +484,9 @@ resource "test_object" "b" { InvokeActionFn: func(req providers.InvokeActionRequest) providers.InvokeActionResponse { invokeActionCalls = append(invokeActionCalls, req) - if len(tc.callingInvokeReturnsDiagnostics) > 0 { + if tc.callingInvokeReturnsDiagnostics != nil && len(tc.callingInvokeReturnsDiagnostics(req)) > 0 { return providers.InvokeActionResponse{ - Diagnostics: tc.callingInvokeReturnsDiagnostics, + Diagnostics: tc.callingInvokeReturnsDiagnostics(req), } } @@ -428,11 +548,12 @@ resource "test_object" "b" { } for i, expectedCall := range tc.expectInvokeActionCalls { actualCall := invokeActionCalls[i] + if actualCall.ActionType != expectedCall.ActionType { t.Fatalf("expected invoke action call %d ActionType to be %s, got %s", i, expectedCall.ActionType, actualCall.ActionType) } if !actualCall.PlannedActionData.RawEquals(expectedCall.PlannedActionData) { - t.Fatalf("expected invoke action call %d PlannedActionData to be %s, got %s", i, expectedCall.PlannedActionData, actualCall.PlannedActionData) + t.Fatalf("expected invoke action call %d PlannedActionData to be %s, got %s", i, expectedCall.PlannedActionData.GoString(), actualCall.PlannedActionData.GoString()) } } })