terraform/internal/command/views/hook_json_test.go
Kristin Laemmert 70e074b8aa chore (actions): rename LifecycleActionTrigger -> ResourceActionTrigger in plan and proto
We'd previously removed all other references to "lifecycle" actions, which made this reference stand out. ResourceLifecycleActionTrigger is probably the most accurate name, but as this type just needs to be differentiated from InvokeActionTrigger I thought "resource" was enough (and I specifically wanted= to avoid lifecycle at all). I'm not super attached to the name, but I did think it would be clearer if we avoided Lifecycle as much as possible, since that's got some overlap with action subtypes.

In this instance, we call it a LifecycleActionTrigger because it's come from the resource's `lifecycle` block. This doesn't directly relate to the concept of LifecycleActions - even if we expand the design to have multiple action types (for example generic and lifecycle actions), both those actions types would use this same Trigger struct.
2026-02-23 15:17:45 -05:00

925 lines
30 KiB
Go

// 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,
},
}
}
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",
},
"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",
},
"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",
},
"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",
},
"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",
},
"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",
},
"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
}