diff --git a/internal/terraform/context_apply_action_test.go b/internal/terraform/context_apply_action_test.go index 01922d8967..b8bee939e7 100644 --- a/internal/terraform/context_apply_action_test.go +++ b/internal/terraform/context_apply_action_test.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" testing_provider "github.com/hashicorp/terraform/internal/providers/testing" @@ -23,8 +24,10 @@ func TestContext2Apply_actions(t *testing.T) { prevRunState *states.State events []providers.InvokeActionEvent callingInvokeReturnsDiagnostics tfdiags.Diagnostics + planOpts *PlanOpts expectInvokeActionCalled bool + expectInvokeActionCalls []providers.InvokeActionRequest expectDiagnostics tfdiags.Diagnostics }{ @@ -207,6 +210,110 @@ resource "test_object" "a" { ), }, }, + + "action with configuration": { + module: map[string]string{ + "main.tf": ` +resource "test_object" "a" { + name = "foo" +} +action "test_unlinked" "hello" { + config { + attr = resource.test_object.a.name + } +} +resource "test_object" "b" { + lifecycle { + action_trigger { + events = [before_create] + actions = [action.test_unlinked.hello] + } + } +} +`, + }, + expectInvokeActionCalled: true, + expectInvokeActionCalls: []providers.InvokeActionRequest{{ + ActionType: "test_unlinked", + PlannedActionData: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("foo"), + }), + }}, + }, + + // Providers can handle unknown values in the configuration + "action with unknown configuration": { + module: map[string]string{ + "main.tf": ` +variable "unknown_value" { + type = string +} +action "test_unlinked" "hello" { + config { + attr = var.unknown_value + } +} +resource "test_object" "b" { + lifecycle { + action_trigger { + events = [before_create] + actions = [action.test_unlinked.hello] + } + } +} +`, + }, + planOpts: SimplePlanOpts(plans.NormalMode, InputValues{ + "unknown_value": &InputValue{ + Value: cty.UnknownVal(cty.String), + }, + }), + + expectInvokeActionCalled: true, + expectInvokeActionCalls: []providers.InvokeActionRequest{{ + ActionType: "test_unlinked", + PlannedActionData: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.UnknownVal(cty.String), + }), + }}, + }, + + "action with secrets in configuration": { + module: map[string]string{ + "main.tf": ` +variable "secret_value" { + type = string + sensitive = true +} +action "test_unlinked" "hello" { + config { + attr = var.secret_value + } +} +resource "test_object" "b" { + lifecycle { + action_trigger { + events = [before_create] + actions = [action.test_unlinked.hello] + } + } +} +`, + }, + planOpts: SimplePlanOpts(plans.NormalMode, InputValues{ + "secret_value": &InputValue{ + Value: cty.StringVal("psst, I'm secret").Mark(marks.Sensitive), // Not sure if we need the mark here, but it doesn't hurt + }, + }), + + expectInvokeActionCalled: true, + expectInvokeActionCalls: []providers.InvokeActionRequest{{ + ActionType: "test_unlinked", + PlannedActionData: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("psst, I'm secret"), + }), + }}, + }, } { t.Run(name, func(t *testing.T) { m := testModuleInline(t, tc.module) @@ -286,7 +393,12 @@ resource "test_object" "a" { diags := ctx.Validate(m, &ValidateOpts{}) tfdiags.AssertNoDiagnostics(t, diags) - plan, diags := ctx.Plan(m, tc.prevRunState, SimplePlanOpts(plans.NormalMode, InputValues{})) + planOpts := SimplePlanOpts(plans.NormalMode, InputValues{}) + if tc.planOpts != nil { + planOpts = tc.planOpts + } + + plan, diags := ctx.Plan(m, tc.prevRunState, planOpts) tfdiags.AssertNoDiagnostics(t, diags) _, diags = ctx.Apply(plan, m, nil) @@ -299,6 +411,19 @@ resource "test_object" "a" { if tc.expectInvokeActionCalled && len(invokeActionCalls) == 0 { t.Fatalf("expected invoke action to be called, but it was not") } + + if len(tc.expectInvokeActionCalls) > 0 && len(invokeActionCalls) != len(tc.expectInvokeActionCalls) { + t.Fatalf("expected %d invoke action calls, got %d", len(tc.expectInvokeActionCalls), len(invokeActionCalls)) + } + 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) + } + } }) } }