| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package views | |
| import ( | |
| "errors" | |
| "fmt" | |
| "sync" | |
| "testing" | |
| "testing/synctest" | |
| "time" | |
| "github.com/zclconf/go-cty/cty" | |
| "github.com/hashicorp/terraform/internal/addrs" | |
| "github.com/hashicorp/terraform/internal/configs" | |
| "github.com/hashicorp/terraform/internal/plans" | |
| "github.com/hashicorp/terraform/internal/terminal" | |
| "github.com/hashicorp/terraform/internal/terraform" | |
| ) | |
| func testJSONHookResourceID(addr addrs.AbsResourceInstance) terraform.HookResourceIdentity { | |
| return terraform.HookResourceIdentity{ | |
| Addr: addr, | |
| ProviderAddr: addrs.Provider{ | |
| Type: "test", | |
| Namespace: "hashicorp", | |
| Hostname: "example.com", | |
| }, | |
| } | |
| } | |
| func testJSONLifecycleHook(actionAddr addrs.AbsActionInstance, triggeringResourceAddr addrs.AbsResourceInstance, actionTriggerIndex int, actionsListIndex int) terraform.HookActionIdentity { | |
| return terraform.HookActionIdentity{ | |
| Addr: actionAddr, | |
| ActionTrigger: &plans.ResourceActionTrigger{ | |
| TriggeringResourceAddr: triggeringResourceAddr, | |
| ActionTriggerBlockIndex: actionTriggerIndex, | |
| ActionsListIndex: actionsListIndex, | |
| ActionTriggerEvent: configs.AfterCreate, | |
| ActionOnFailure: configs.ActionOnFailureContinue, | |
| }, | |
| } | |
| } | |
| func testJSONInvokeHook(actionAddr addrs.AbsActionInstance) terraform.HookActionIdentity { | |
| return terraform.HookActionIdentity{ | |
| Addr: actionAddr, | |
| ActionTrigger: new(plans.InvokeActionTrigger), | |
| } | |
| } | |
| // Test a sequence of hooks associated with creating a resource | |
| func TestJSONHook_create(t *testing.T) { | |
| streams, done := terminal.StreamsForTesting(t) | |
| hook := newJSONHook(NewJSONView(NewView(streams))) | |
| var nowMu sync.Mutex | |
| now := time.Now() | |
| hook.timeNow = func() time.Time { | |
| nowMu.Lock() | |
| defer nowMu.Unlock() | |
| return now | |
| } | |
| after := make(chan time.Time, 1) | |
| hook.timeAfter = func(time.Duration) <-chan time.Time { return after } | |
| addr := addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "boop", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) | |
| priorState := cty.NullVal(cty.Object(map[string]cty.Type{ | |
| "id": cty.String, | |
| "bar": cty.List(cty.String), | |
| })) | |
| plannedNewState := cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("test"), | |
| "bar": cty.ListVal([]cty.Value{ | |
| cty.StringVal("baz"), | |
| }), | |
| }) | |
| action, err := hook.PreApply(testJSONHookResourceID(addr), addrs.NotDeposed, plans.Create, priorState, plannedNewState) | |
| testHookReturnValues(t, action, err) | |
| action, err = hook.PreProvisionInstanceStep(testJSONHookResourceID(addr), "local-exec") | |
| testHookReturnValues(t, action, err) | |
| hook.ProvisionOutput(testJSONHookResourceID(addr), "local-exec", `Executing: ["/bin/sh" "-c" "touch /etc/motd"]`) | |
| action, err = hook.PostProvisionInstanceStep(testJSONHookResourceID(addr), "local-exec", nil) | |
| testHookReturnValues(t, action, err) | |
| // Travel 10s into the future, notify the progress goroutine, and sleep | |
| // briefly to allow it to execute | |
| nowMu.Lock() | |
| now = now.Add(10 * time.Second) | |
| after <- now | |
| nowMu.Unlock() | |
| time.Sleep(10 * time.Millisecond) | |
| // Travel 10s into the future, notify the progress goroutine, and sleep | |
| // briefly to allow it to execute | |
| nowMu.Lock() | |
| now = now.Add(10 * time.Second) | |
| after <- now | |
| nowMu.Unlock() | |
| time.Sleep(10 * time.Millisecond) | |
| // Travel 2s into the future. We have arrived! | |
| nowMu.Lock() | |
| now = now.Add(2 * time.Second) | |
| nowMu.Unlock() | |
| action, err = hook.PostApply(testJSONHookResourceID(addr), addrs.NotDeposed, plannedNewState, nil) | |
| testHookReturnValues(t, action, err) | |
| // Shut down the progress goroutine if still active | |
| hook.resourceProgressMu.Lock() | |
| for key, progress := range hook.resourceProgress { | |
| close(progress.done) | |
| <-progress.heartbeatDone | |
| delete(hook.resourceProgress, key) | |
| } | |
| hook.resourceProgressMu.Unlock() | |
| wantResource := map[string]interface{}{ | |
| "addr": string("test_instance.boop"), | |
| "implied_provider": string("test"), | |
| "module": string(""), | |
| "resource": string("test_instance.boop"), | |
| "resource_key": nil, | |
| "resource_name": string("boop"), | |
| "resource_type": string("test_instance"), | |
| } | |
| want := []map[string]interface{}{ | |
| { | |
| "@level": "info", | |
| "@message": "test_instance.boop: Creating...", | |
| "@module": "terraform.ui", | |
| "type": "apply_start", | |
| "hook": map[string]interface{}{ | |
| "action": string("create"), | |
| "resource": wantResource, | |
| }, | |
| }, | |
| { | |
| "@level": "info", | |
| "@message": "test_instance.boop: Provisioning with 'local-exec'...", | |
| "@module": "terraform.ui", | |
| "type": "provision_start", | |
| "hook": map[string]interface{}{ | |
| "provisioner": "local-exec", | |
| "resource": wantResource, | |
| }, | |
| }, | |
| { | |
| "@level": "info", | |
| "@message": `test_instance.boop: (local-exec): Executing: ["/bin/sh" "-c" "touch /etc/motd"]`, | |
| "@module": "terraform.ui", | |
| "type": "provision_progress", | |
| "hook": map[string]interface{}{ | |
| "output": `Executing: ["/bin/sh" "-c" "touch /etc/motd"]`, | |
| "provisioner": "local-exec", | |
| "resource": wantResource, | |
| }, | |
| }, | |
| { | |
| "@level": "info", | |
| "@message": "test_instance.boop: (local-exec) Provisioning complete", | |
| "@module": "terraform.ui", | |
| "type": "provision_complete", | |
| "hook": map[string]interface{}{ | |
| "provisioner": "local-exec", | |
| "resource": wantResource, | |
| }, | |
| }, | |
| { | |
| "@level": "info", | |
| "@message": "test_instance.boop: Still creating... [10s elapsed]", | |
| "@module": "terraform.ui", | |
| "type": "apply_progress", | |
| "hook": map[string]interface{}{ | |
| "action": string("create"), | |
| "elapsed_seconds": float64(10), | |
| "resource": wantResource, | |
| }, | |
| }, | |
| { | |
| "@level": "info", | |
| "@message": "test_instance.boop: Still creating... [20s elapsed]", | |
| "@module": "terraform.ui", | |
| "type": "apply_progress", | |
| "hook": map[string]interface{}{ | |
| "action": string("create"), | |
| "elapsed_seconds": float64(20), | |
| "resource": wantResource, | |
| }, | |
| }, | |
| { | |
| "@level": "info", | |
| "@message": "test_instance.boop: Creation complete after 22s [id=test]", | |
| "@module": "terraform.ui", | |
| "type": "apply_complete", | |
| "hook": map[string]interface{}{ | |
| "action": string("create"), | |
| "elapsed_seconds": float64(22), | |
| "id_key": "id", | |
| "id_value": "test", | |
| "resource": wantResource, | |
| }, | |
| }, | |
| } | |
| testJSONViewOutputEquals(t, done(t).Stdout(), want) | |
| } | |
| func TestJSONHook_errors(t *testing.T) { | |
| streams, done := terminal.StreamsForTesting(t) | |
| hook := newJSONHook(NewJSONView(NewView(streams))) | |
| addr := addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "boop", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) | |
| priorState := cty.NullVal(cty.Object(map[string]cty.Type{ | |
| "id": cty.String, | |
| "bar": cty.List(cty.String), | |
| })) | |
| plannedNewState := cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("test"), | |
| "bar": cty.ListVal([]cty.Value{ | |
| cty.StringVal("baz"), | |
| }), | |
| }) | |
| action, err := hook.PreApply(testJSONHookResourceID(addr), addrs.NotDeposed, plans.Delete, priorState, plannedNewState) | |
| testHookReturnValues(t, action, err) | |
| provisionError := fmt.Errorf("provisioner didn't want to") | |
| action, err = hook.PostProvisionInstanceStep(testJSONHookResourceID(addr), "local-exec", provisionError) | |
| testHookReturnValues(t, action, err) | |
| applyError := fmt.Errorf("provider was sad") | |
| action, err = hook.PostApply(testJSONHookResourceID(addr), addrs.NotDeposed, plannedNewState, applyError) | |
| testHookReturnValues(t, action, err) | |
| // Shut down the progress goroutine | |
| hook.resourceProgressMu.Lock() | |
| for key, progress := range hook.resourceProgress { | |
| close(progress.done) | |
| <-progress.heartbeatDone | |
| delete(hook.resourceProgress, key) | |
| } | |
| hook.resourceProgressMu.Unlock() | |
| wantResource := map[string]interface{}{ | |
| "addr": string("test_instance.boop"), | |
| "implied_provider": string("test"), | |
| "module": string(""), | |
| "resource": string("test_instance.boop"), | |
| "resource_key": nil, | |
| "resource_name": string("boop"), | |
| "resource_type": string("test_instance"), | |
| } | |
| want := []map[string]interface{}{ | |
| { | |
| "@level": "info", | |
| "@message": "test_instance.boop: Destroying...", | |
| "@module": "terraform.ui", | |
| "type": "apply_start", | |
| "hook": map[string]interface{}{ | |
| "action": string("delete"), | |
| "resource": wantResource, | |
| }, | |
| }, | |
| { | |
| "@level": "info", | |
| "@message": "test_instance.boop: (local-exec) Provisioning errored", | |
| "@module": "terraform.ui", | |
| "type": "provision_errored", | |
| "hook": map[string]interface{}{ | |
| "provisioner": "local-exec", | |
| "resource": wantResource, | |
| }, | |
| }, | |
| { | |
| "@level": "info", | |
| "@message": "test_instance.boop: Destruction errored after 0s", | |
| "@module": "terraform.ui", | |
| "type": "apply_errored", | |
| "hook": map[string]interface{}{ | |
| "action": string("delete"), | |
| "elapsed_seconds": float64(0), | |
| "resource": wantResource, | |
| }, | |
| }, | |
| } | |
| testJSONViewOutputEquals(t, done(t).Stdout(), want) | |
| } | |
| func TestJSONHook_refresh(t *testing.T) { | |
| streams, done := terminal.StreamsForTesting(t) | |
| hook := newJSONHook(NewJSONView(NewView(streams))) | |
| addr := addrs.Resource{ | |
| Mode: addrs.DataResourceMode, | |
| Type: "test_data_source", | |
| Name: "beep", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) | |
| state := cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("honk"), | |
| "bar": cty.ListVal([]cty.Value{ | |
| cty.StringVal("baz"), | |
| }), | |
| }) | |
| action, err := hook.PreRefresh(testJSONHookResourceID(addr), addrs.NotDeposed, state) | |
| testHookReturnValues(t, action, err) | |
| action, err = hook.PostRefresh(testJSONHookResourceID(addr), addrs.NotDeposed, state, state) | |
| testHookReturnValues(t, action, err) | |
| wantResource := map[string]interface{}{ | |
| "addr": string("data.test_data_source.beep"), | |
| "implied_provider": string("test"), | |
| "module": string(""), | |
| "resource": string("data.test_data_source.beep"), | |
| "resource_key": nil, | |
| "resource_name": string("beep"), | |
| "resource_type": string("test_data_source"), | |
| } | |
| want := []map[string]interface{}{ | |
| { | |
| "@level": "info", | |
| "@message": "data.test_data_source.beep: Refreshing state... [id=honk]", | |
| "@module": "terraform.ui", | |
| "type": "refresh_start", | |
| "hook": map[string]interface{}{ | |
| "resource": wantResource, | |
| "id_key": "id", | |
| "id_value": "honk", | |
| }, | |
| }, | |
| { | |
| "@level": "info", | |
| "@message": "data.test_data_source.beep: Refresh complete [id=honk]", | |
| "@module": "terraform.ui", | |
| "type": "refresh_complete", | |
| "hook": map[string]interface{}{ | |
| "resource": wantResource, | |
| "id_key": "id", | |
| "id_value": "honk", | |
| }, | |
| }, | |
| } | |
| testJSONViewOutputEquals(t, done(t).Stdout(), want) | |
| } | |
| func TestJSONHook_EphemeralOp(t *testing.T) { | |
| streams, done := terminal.StreamsForTesting(t) | |
| hook := newJSONHook(NewJSONView(NewView(streams))) | |
| addr := addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "boop", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) | |
| action, err := hook.PreEphemeralOp(testJSONHookResourceID(addr), plans.Open) | |
| testHookReturnValues(t, action, err) | |
| action, err = hook.PostEphemeralOp(testJSONHookResourceID(addr), plans.Open, nil) | |
| testHookReturnValues(t, action, err) | |
| want := []map[string]interface{}{ | |
| { | |
| "@level": "info", | |
| "@message": "test_instance.boop: Opening...", | |
| "@module": "terraform.ui", | |
| "type": "ephemeral_op_start", | |
| "hook": map[string]interface{}{ | |
| "action": string("open"), | |
| "resource": map[string]interface{}{ | |
| "addr": string("test_instance.boop"), | |
| "implied_provider": string("test"), | |
| "module": string(""), | |
| "resource": string("test_instance.boop"), | |
| "resource_key": nil, | |
| "resource_name": string("boop"), | |
| "resource_type": string("test_instance"), | |
| }, | |
| }, | |
| }, | |
| { | |
| "@level": "info", | |
| "@message": "test_instance.boop: Opening complete after 0s", | |
| "@module": "terraform.ui", | |
| "type": "ephemeral_op_complete", | |
| "hook": map[string]interface{}{ | |
| "action": string("open"), | |
| "elapsed_seconds": float64(0), | |
| "resource": map[string]interface{}{ | |
| "addr": string("test_instance.boop"), | |
| "implied_provider": string("test"), | |
| "module": string(""), | |
| "resource": string("test_instance.boop"), | |
| "resource_key": nil, | |
| "resource_name": string("boop"), | |
| "resource_type": string("test_instance"), | |
| }, | |
| }, | |
| }, | |
| } | |
| testJSONViewOutputEquals(t, done(t).Stdout(), want) | |
| } | |
| func TestJSONHook_EphemeralOp_progress(t *testing.T) { | |
| syncTest, streams, done := streamableSyncTest(t) | |
| syncTest(t, func(t *testing.T) { | |
| start := time.Now() | |
| hook := newJSONHook(NewJSONView(NewView(streams))) | |
| hook.periodicUiTimer = 1 * time.Second | |
| t.Log(time.Since(start)) | |
| addr := addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "boop", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) | |
| action, err := hook.PreEphemeralOp(testJSONHookResourceID(addr), plans.Open) | |
| testHookReturnValues(t, action, err) | |
| time.Sleep(2005 * time.Millisecond) | |
| action, err = hook.PostEphemeralOp(testJSONHookResourceID(addr), plans.Open, nil) | |
| testHookReturnValues(t, action, err) | |
| want := []map[string]interface{}{ | |
| { | |
| "@level": "info", | |
| "@message": "test_instance.boop: Opening...", | |
| "@module": "terraform.ui", | |
| "type": "ephemeral_op_start", | |
| "hook": map[string]interface{}{ | |
| "action": string("open"), | |
| "resource": map[string]interface{}{ | |
| "addr": string("test_instance.boop"), | |
| "implied_provider": string("test"), | |
| "module": string(""), | |
| "resource": string("test_instance.boop"), | |
| "resource_key": nil, | |
| "resource_name": string("boop"), | |
| "resource_type": string("test_instance"), | |
| }, | |
| }, | |
| }, | |
| { | |
| "@level": "info", | |
| "@message": "test_instance.boop: Still opening... [1s elapsed]", | |
| "@module": "terraform.ui", | |
| "type": "ephemeral_op_progress", | |
| "hook": map[string]interface{}{ | |
| "action": string("open"), | |
| "elapsed_seconds": float64(1), | |
| "resource": map[string]interface{}{ | |
| "addr": string("test_instance.boop"), | |
| "implied_provider": string("test"), | |
| "module": string(""), | |
| "resource": string("test_instance.boop"), | |
| "resource_key": nil, | |
| "resource_name": string("boop"), | |
| "resource_type": string("test_instance"), | |
| }, | |
| }, | |
| }, | |
| { | |
| "@level": "info", | |
| "@message": "test_instance.boop: Still opening... [2s elapsed]", | |
| "@module": "terraform.ui", | |
| "type": "ephemeral_op_progress", | |
| "hook": map[string]interface{}{ | |
| "action": string("open"), | |
| "elapsed_seconds": float64(2), | |
| "resource": map[string]interface{}{ | |
| "addr": string("test_instance.boop"), | |
| "implied_provider": string("test"), | |
| "module": string(""), | |
| "resource": string("test_instance.boop"), | |
| "resource_key": nil, | |
| "resource_name": string("boop"), | |
| "resource_type": string("test_instance"), | |
| }, | |
| }, | |
| }, | |
| { | |
| "@level": "info", | |
| "@message": "test_instance.boop: Opening complete after 2s", | |
| "@module": "terraform.ui", | |
| "type": "ephemeral_op_complete", | |
| "hook": map[string]interface{}{ | |
| "action": string("open"), | |
| "elapsed_seconds": float64(2), | |
| "resource": map[string]interface{}{ | |
| "addr": string("test_instance.boop"), | |
| "implied_provider": string("test"), | |
| "module": string(""), | |
| "resource": string("test_instance.boop"), | |
| "resource_key": nil, | |
| "resource_name": string("boop"), | |
| "resource_type": string("test_instance"), | |
| }, | |
| }, | |
| }, | |
| } | |
| stdout := done(t).Stdout() | |
| testJSONViewOutputEquals(t, stdout, want) | |
| }) | |
| } | |
| func TestJSONHook_EphemeralOp_error(t *testing.T) { | |
| streams, done := terminal.StreamsForTesting(t) | |
| hook := newJSONHook(NewJSONView(NewView(streams))) | |
| addr := addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "boop", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) | |
| action, err := hook.PreEphemeralOp(testJSONHookResourceID(addr), plans.Open) | |
| testHookReturnValues(t, action, err) | |
| action, err = hook.PostEphemeralOp(testJSONHookResourceID(addr), plans.Open, errors.New("test error")) | |
| testHookReturnValues(t, action, err) | |
| want := []map[string]interface{}{ | |
| { | |
| "@level": "info", | |
| "@message": "test_instance.boop: Opening...", | |
| "@module": "terraform.ui", | |
| "type": "ephemeral_op_start", | |
| "hook": map[string]interface{}{ | |
| "action": string("open"), | |
| "resource": map[string]interface{}{ | |
| "addr": string("test_instance.boop"), | |
| "implied_provider": string("test"), | |
| "module": string(""), | |
| "resource": string("test_instance.boop"), | |
| "resource_key": nil, | |
| "resource_name": string("boop"), | |
| "resource_type": string("test_instance"), | |
| }, | |
| }, | |
| }, | |
| { | |
| "@level": "info", | |
| "@message": "test_instance.boop: Opening errored after 0s", | |
| "@module": "terraform.ui", | |
| "type": "ephemeral_op_errored", | |
| "hook": map[string]interface{}{ | |
| "action": string("open"), | |
| "elapsed_seconds": float64(0), | |
| "resource": map[string]interface{}{ | |
| "addr": string("test_instance.boop"), | |
| "implied_provider": string("test"), | |
| "module": string(""), | |
| "resource": string("test_instance.boop"), | |
| "resource_key": nil, | |
| "resource_name": string("boop"), | |
| "resource_type": string("test_instance"), | |
| }, | |
| }, | |
| }, | |
| } | |
| testJSONViewOutputEquals(t, done(t).Stdout(), want) | |
| } | |
| func TestJSONHook_actions(t *testing.T) { | |
| streams, done := terminal.StreamsForTesting(t) | |
| hook := newJSONHook(NewJSONView(NewView(streams))) | |
| actionA := addrs.AbsActionInstance{ | |
| Module: addrs.RootModuleInstance, | |
| Action: addrs.Action{ | |
| Type: "aws_lambda_invocation", | |
| Name: "notify_slack", | |
| }.Instance(addrs.IntKey(42)), | |
| } | |
| resourceA := addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "boop", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) | |
| subModule := addrs.RootModuleInstance.Child("childMod", addrs.StringKey("infra")) | |
| actionB := addrs.AbsActionInstance{ | |
| Module: subModule, | |
| Action: addrs.Action{ | |
| Type: "ansible_playbook", | |
| Name: "webserver", | |
| }.Instance(addrs.NoKey), | |
| } | |
| resourceB := addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "boop", | |
| }.Instance(addrs.NoKey).Absolute(subModule) | |
| actionC := addrs.AbsActionInstance{ | |
| Module: addrs.RootModuleInstance, | |
| Action: addrs.Action{ | |
| Type: "aws_lambda_invocation", | |
| Name: "invoke_me", | |
| }.Instance(addrs.IntKey(42)), | |
| } | |
| actionAHookId := testJSONLifecycleHook(actionA, resourceA, 0, 1) | |
| actionBHookId := testJSONLifecycleHook(actionB, resourceB, 2, 3) | |
| actionCHookId := testJSONInvokeHook(actionC) | |
| action, err := hook.StartAction(actionAHookId) | |
| testHookReturnValues(t, action, err) | |
| action, err = hook.ProgressAction(actionAHookId, "Hello world from the lambda function") | |
| testHookReturnValues(t, action, err) | |
| action, err = hook.StartAction(actionBHookId) | |
| testHookReturnValues(t, action, err) | |
| action, err = hook.ProgressAction(actionBHookId, "TASK: [hello]\n ok: [localhost] => (item=Hello world from the ansible playbook]") | |
| testHookReturnValues(t, action, err) | |
| action, err = hook.CompleteAction(actionBHookId, nil) | |
| testHookReturnValues(t, action, err) | |
| action, err = hook.CompleteAction(actionAHookId, errors.New("lambda terminated with exit code 1")) | |
| testHookReturnValues(t, action, err) | |
| action, err = hook.StartAction(actionCHookId) | |
| testHookReturnValues(t, action, err) | |
| action, err = hook.ProgressAction(actionCHookId, "Hello world from the invoked action") | |
| testHookReturnValues(t, action, err) | |
| action, err = hook.CompleteAction(actionCHookId, nil) | |
| testHookReturnValues(t, action, err) | |
| want := []map[string]interface{}{ | |
| { | |
| "@level": "info", | |
| "@message": "test_instance.boop.trigger[0]: Action started: action.aws_lambda_invocation.notify_slack[42]", | |
| "@module": "terraform.ui", | |
| "type": "action_start", | |
| "hook": map[string]interface{}{ | |
| "action": map[string]interface{}{ | |
| "addr": "action.aws_lambda_invocation.notify_slack[42]", | |
| "module": "", | |
| "implied_provider": "aws", | |
| "action": "action.aws_lambda_invocation.notify_slack[42]", | |
| "action_key": float64(42), | |
| "action_name": "notify_slack", | |
| "action_type": "aws_lambda_invocation", | |
| }, | |
| "lifecycle": map[string]interface{}{ | |
| "actions_index": float64(1), | |
| "resource": map[string]interface{}{ | |
| "addr": "test_instance.boop", | |
| "implied_provider": "test", | |
| "module": "", | |
| "resource": "test_instance.boop", | |
| "resource_key": nil, | |
| "resource_name": "boop", | |
| "resource_type": "test_instance", | |
| }, | |
| "on_failure": "ActionOnFailureContinue", | |
| "trigger_event": "AfterCreate", | |
| "trigger_index": float64(0), | |
| }, | |
| }, | |
| }, | |
| { | |
| "@level": "info", | |
| "@message": "test_instance.boop (0): action.aws_lambda_invocation.notify_slack[42] - Hello world from the lambda function", | |
| "@module": "terraform.ui", | |
| "type": "action_progress", | |
| "hook": map[string]interface{}{ | |
| "action": map[string]interface{}{ | |
| "addr": "action.aws_lambda_invocation.notify_slack[42]", | |
| "module": "", | |
| "implied_provider": "aws", | |
| "action": "action.aws_lambda_invocation.notify_slack[42]", | |
| "action_key": float64(42), | |
| "action_name": "notify_slack", | |
| "action_type": "aws_lambda_invocation", | |
| }, | |
| "message": "Hello world from the lambda function", | |
| "lifecycle": map[string]interface{}{ | |
| "actions_index": float64(1), | |
| "resource": map[string]interface{}{ | |
| "addr": "test_instance.boop", | |
| "implied_provider": "test", | |
| "module": "", | |
| "resource": "test_instance.boop", | |
| "resource_key": nil, | |
| "resource_name": "boop", | |
| "resource_type": "test_instance", | |
| }, | |
| "on_failure": "ActionOnFailureContinue", | |
| "trigger_event": "AfterCreate", | |
| "trigger_index": float64(0), | |
| }, | |
| }, | |
| }, | |
| { | |
| "@level": "info", | |
| "@message": "module.childMod[\"infra\"].test_instance.boop.trigger[2]: Action started: module.childMod[\"infra\"].action.ansible_playbook.webserver", | |
| "@module": "terraform.ui", | |
| "type": "action_start", | |
| "hook": map[string]interface{}{ | |
| "action": map[string]interface{}{ | |
| "addr": "module.childMod[\"infra\"].action.ansible_playbook.webserver", | |
| "module": "module.childMod[\"infra\"]", | |
| "implied_provider": "ansible", | |
| "action": "action.ansible_playbook.webserver", | |
| "action_key": nil, | |
| "action_name": "webserver", | |
| "action_type": "ansible_playbook", | |
| }, | |
| "lifecycle": map[string]interface{}{ | |
| "actions_index": float64(3), | |
| "resource": map[string]interface{}{ | |
| "addr": "module.childMod[\"infra\"].test_instance.boop", | |
| "implied_provider": "test", | |
| "module": "module.childMod[\"infra\"]", | |
| "resource": "test_instance.boop", | |
| "resource_key": nil, | |
| "resource_name": "boop", | |
| "resource_type": "test_instance", | |
| }, | |
| "on_failure": "ActionOnFailureContinue", | |
| "trigger_event": "AfterCreate", | |
| "trigger_index": float64(2), | |
| }, | |
| }, | |
| }, | |
| { | |
| "@level": "info", | |
| "@message": "module.childMod[\"infra\"].test_instance.boop (2): module.childMod[\"infra\"].action.ansible_playbook.webserver - TASK: [hello]\n ok: [localhost] => (item=Hello world from the ansible playbook]", | |
| "@module": "terraform.ui", | |
| "type": "action_progress", | |
| "hook": map[string]interface{}{ | |
| "action": map[string]interface{}{ | |
| "addr": "module.childMod[\"infra\"].action.ansible_playbook.webserver", | |
| "module": "module.childMod[\"infra\"]", | |
| "implied_provider": "ansible", | |
| "action": "action.ansible_playbook.webserver", | |
| "action_key": nil, | |
| "action_name": "webserver", | |
| "action_type": "ansible_playbook", | |
| }, | |
| "message": "TASK: [hello]\n ok: [localhost] => (item=Hello world from the ansible playbook]", | |
| "lifecycle": map[string]interface{}{ | |
| "actions_index": float64(3), | |
| "resource": map[string]interface{}{ | |
| "addr": "module.childMod[\"infra\"].test_instance.boop", | |
| "implied_provider": "test", | |
| "module": "module.childMod[\"infra\"]", | |
| "resource": "test_instance.boop", | |
| "resource_key": nil, | |
| "resource_name": "boop", | |
| "resource_type": "test_instance", | |
| }, | |
| "on_failure": "ActionOnFailureContinue", | |
| "trigger_event": "AfterCreate", | |
| "trigger_index": float64(2), | |
| }, | |
| }, | |
| }, | |
| { | |
| "@level": "info", | |
| "@message": "module.childMod[\"infra\"].test_instance.boop (2): Action complete: module.childMod[\"infra\"].action.ansible_playbook.webserver", | |
| "@module": "terraform.ui", | |
| "type": "action_complete", | |
| "hook": map[string]interface{}{ | |
| "action": map[string]interface{}{ | |
| "addr": "module.childMod[\"infra\"].action.ansible_playbook.webserver", | |
| "module": "module.childMod[\"infra\"]", | |
| "implied_provider": "ansible", | |
| "action": "action.ansible_playbook.webserver", | |
| "action_key": nil, | |
| "action_name": "webserver", | |
| "action_type": "ansible_playbook", | |
| }, | |
| "lifecycle": map[string]interface{}{ | |
| "actions_index": float64(3), | |
| "resource": map[string]interface{}{ | |
| "addr": "module.childMod[\"infra\"].test_instance.boop", | |
| "implied_provider": "test", | |
| "module": "module.childMod[\"infra\"]", | |
| "resource": "test_instance.boop", | |
| "resource_key": nil, | |
| "resource_name": "boop", | |
| "resource_type": "test_instance", | |
| }, | |
| "on_failure": "ActionOnFailureContinue", | |
| "trigger_event": "AfterCreate", | |
| "trigger_index": float64(2), | |
| }, | |
| }, | |
| }, | |
| { | |
| "@level": "info", | |
| "@message": "test_instance.boop (0): Action errored: action.aws_lambda_invocation.notify_slack[42] - lambda terminated with exit code 1", | |
| "@module": "terraform.ui", | |
| "type": "action_errored", | |
| "hook": map[string]interface{}{ | |
| "action": map[string]interface{}{ | |
| "addr": "action.aws_lambda_invocation.notify_slack[42]", | |
| "module": "", | |
| "implied_provider": "aws", | |
| "action": "action.aws_lambda_invocation.notify_slack[42]", | |
| "action_key": float64(42), | |
| "action_name": "notify_slack", | |
| "action_type": "aws_lambda_invocation", | |
| }, | |
| "error": "lambda terminated with exit code 1", | |
| "lifecycle": map[string]interface{}{ | |
| "actions_index": float64(1), | |
| "resource": map[string]interface{}{ | |
| "addr": "test_instance.boop", | |
| "implied_provider": "test", | |
| "module": "", | |
| "resource": "test_instance.boop", | |
| "resource_key": nil, | |
| "resource_name": "boop", | |
| "resource_type": "test_instance", | |
| }, | |
| "on_failure": "ActionOnFailureContinue", | |
| "trigger_event": "AfterCreate", | |
| "trigger_index": float64(0), | |
| }, | |
| }, | |
| }, | |
| { | |
| "@level": "info", | |
| "@message": "Action started: action.aws_lambda_invocation.invoke_me[42]", | |
| "@module": "terraform.ui", | |
| "type": "action_start", | |
| "hook": map[string]interface{}{ | |
| "action": map[string]interface{}{ | |
| "addr": "action.aws_lambda_invocation.invoke_me[42]", | |
| "module": "", | |
| "implied_provider": "aws", | |
| "action": "action.aws_lambda_invocation.invoke_me[42]", | |
| "action_key": float64(42), | |
| "action_name": "invoke_me", | |
| "action_type": "aws_lambda_invocation", | |
| }, | |
| "invoke": map[string]interface{}{}, | |
| }, | |
| }, | |
| { | |
| "@level": "info", | |
| "@message": "action.aws_lambda_invocation.invoke_me[42] - Hello world from the invoked action", | |
| "@module": "terraform.ui", | |
| "type": "action_progress", | |
| "hook": map[string]interface{}{ | |
| "action": map[string]interface{}{ | |
| "addr": "action.aws_lambda_invocation.invoke_me[42]", | |
| "module": "", | |
| "implied_provider": "aws", | |
| "action": "action.aws_lambda_invocation.invoke_me[42]", | |
| "action_key": float64(42), | |
| "action_name": "invoke_me", | |
| "action_type": "aws_lambda_invocation", | |
| }, | |
| "message": "Hello world from the invoked action", | |
| "invoke": map[string]interface{}{}, | |
| }, | |
| }, | |
| { | |
| "@level": "info", | |
| "@message": "Action complete: action.aws_lambda_invocation.invoke_me[42]", | |
| "@module": "terraform.ui", | |
| "type": "action_complete", | |
| "hook": map[string]interface{}{ | |
| "action": map[string]interface{}{ | |
| "addr": "action.aws_lambda_invocation.invoke_me[42]", | |
| "module": "", | |
| "implied_provider": "aws", | |
| "action": "action.aws_lambda_invocation.invoke_me[42]", | |
| "action_key": float64(42), | |
| "action_name": "invoke_me", | |
| "action_type": "aws_lambda_invocation", | |
| }, | |
| "invoke": map[string]interface{}{}, | |
| }, | |
| }, | |
| } | |
| testJSONViewOutputEquals(t, done(t).Stdout(), want) | |
| } | |
| func testHookReturnValues(t *testing.T, action terraform.HookAction, err error) { | |
| t.Helper() | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if action != terraform.HookActionContinue { | |
| t.Fatalf("Expected hook to continue, given: %#v", action) | |
| } | |
| } | |
| // streamableSyncTest is a helper to ensure that the long-running streaming goroutines are started outside of the synctest bubble. | |
| // Otherwise, the sync bubble will be unable to advance time, and the main goroutine will become infinitely paused on any time.Sleep operation. | |
| func streamableSyncTest(t *testing.T) (func(t *testing.T, f func(*testing.T)), *terminal.Streams, func(*testing.T) *terminal.TestOutput) { | |
| streams, done := terminal.StreamsForTesting(t) | |
| return synctest.Test, streams, done | |
| } |