mirror of
https://github.com/opentffoundation/opentf.git
synced 2026-03-20 22:01:25 -04:00
We don't typically just broadly run automatic rewriting tools like "go fix" across our codebase because that tends to cause annoying and unnecessary merge conflicts when we're backporting to earlier release branches. But all of the files in this commit were changed in some non-trivial way already during the OpenTofu v1.11 development period anyway, and so the likelyhood we'd be able to successfully backport from them is reduced and therefore this seems like a good opportunity to do some focused modernization using "go fix". My rules for what to include or not are admittedly quite "vibes-based", but the general idea was: - Focusing on files under the "command" directory only, because that's already been an area of intentional refactoring during this development period. - If the existing diff in a file is already significantly larger than the changes the fixer proposed to make, or if the fixer is proposing to change a line that was already changed in this development period. - More willing to include "_test.go" files than non-test files, even if they hadn't changed as much already, just because backports from test files for bug fixes tend to be entirely new test cases more than they are modifications to existing test cases, and so the risk of conflicts is lower there. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
544 lines
15 KiB
Go
544 lines
15 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package views
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
|
|
"github.com/opentofu/opentofu/internal/addrs"
|
|
"github.com/opentofu/opentofu/internal/plans"
|
|
"github.com/opentofu/opentofu/internal/states"
|
|
"github.com/opentofu/opentofu/internal/terminal"
|
|
"github.com/opentofu/opentofu/internal/tofu"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
// 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), nil))
|
|
|
|
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(addr, states.CurrentGen, plans.Create, priorState, plannedNewState)
|
|
testHookReturnValues(t, action, err)
|
|
|
|
action, err = hook.PreProvisionInstanceStep(addr, "local-exec")
|
|
testHookReturnValues(t, action, err)
|
|
|
|
hook.ProvisionOutput(addr, "local-exec", `Executing: ["/bin/sh" "-c" "touch /etc/motd"]`)
|
|
|
|
action, err = hook.PostProvisionInstanceStep(addr, "local-exec", nil)
|
|
testHookReturnValues(t, action, err)
|
|
|
|
elapsedChan := hook.applying[addr.String()].elapsed
|
|
|
|
// Travel 10s into the future, notify the progress goroutine, and wait
|
|
// for execution via 'elapsed' progress
|
|
nowMu.Lock()
|
|
now = now.Add(10 * time.Second)
|
|
after <- now
|
|
nowMu.Unlock()
|
|
elapsed := <-elapsedChan
|
|
testDurationEqual(t, 10*time.Second, elapsed)
|
|
|
|
// Travel 10s into the future, notify the progress goroutine, and wait
|
|
// for execution via 'elapsed' progress
|
|
nowMu.Lock()
|
|
now = now.Add(10 * time.Second)
|
|
after <- now
|
|
nowMu.Unlock()
|
|
elapsed = <-elapsedChan
|
|
testDurationEqual(t, 20*time.Second, elapsed)
|
|
|
|
// Travel 2s into the future. We have arrived!
|
|
nowMu.Lock()
|
|
now = now.Add(2 * time.Second)
|
|
nowMu.Unlock()
|
|
|
|
action, err = hook.PostApply(addr, states.CurrentGen, plannedNewState, nil)
|
|
testHookReturnValues(t, action, err)
|
|
|
|
// Shut down the progress goroutine if still active
|
|
hook.applyingLock.Lock()
|
|
for key, progress := range hook.applying {
|
|
close(progress.done)
|
|
close(progress.elapsed)
|
|
<-progress.heartbeatDone
|
|
delete(hook.applying, key)
|
|
}
|
|
hook.applyingLock.Unlock()
|
|
|
|
wantResource := map[string]any{
|
|
"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]any{
|
|
{
|
|
"@level": "info",
|
|
"@message": "test_instance.boop: Creating...",
|
|
"@module": "tofu.ui",
|
|
"type": "apply_start",
|
|
"hook": map[string]any{
|
|
"action": string("create"),
|
|
"resource": wantResource,
|
|
},
|
|
},
|
|
{
|
|
"@level": "info",
|
|
"@message": "test_instance.boop: Provisioning with 'local-exec'...",
|
|
"@module": "tofu.ui",
|
|
"type": "provision_start",
|
|
"hook": map[string]any{
|
|
"provisioner": "local-exec",
|
|
"resource": wantResource,
|
|
},
|
|
},
|
|
{
|
|
"@level": "info",
|
|
"@message": `test_instance.boop: (local-exec): Executing: ["/bin/sh" "-c" "touch /etc/motd"]`,
|
|
"@module": "tofu.ui",
|
|
"type": "provision_progress",
|
|
"hook": map[string]any{
|
|
"output": `Executing: ["/bin/sh" "-c" "touch /etc/motd"]`,
|
|
"provisioner": "local-exec",
|
|
"resource": wantResource,
|
|
},
|
|
},
|
|
{
|
|
"@level": "info",
|
|
"@message": "test_instance.boop: (local-exec) Provisioning complete",
|
|
"@module": "tofu.ui",
|
|
"type": "provision_complete",
|
|
"hook": map[string]any{
|
|
"provisioner": "local-exec",
|
|
"resource": wantResource,
|
|
},
|
|
},
|
|
{
|
|
"@level": "info",
|
|
"@message": "test_instance.boop: Still creating... [10s elapsed]",
|
|
"@module": "tofu.ui",
|
|
"type": "apply_progress",
|
|
"hook": map[string]any{
|
|
"action": string("create"),
|
|
"elapsed_seconds": float64(10),
|
|
"resource": wantResource,
|
|
},
|
|
},
|
|
{
|
|
"@level": "info",
|
|
"@message": "test_instance.boop: Still creating... [20s elapsed]",
|
|
"@module": "tofu.ui",
|
|
"type": "apply_progress",
|
|
"hook": map[string]any{
|
|
"action": string("create"),
|
|
"elapsed_seconds": float64(20),
|
|
"resource": wantResource,
|
|
},
|
|
},
|
|
{
|
|
"@level": "info",
|
|
"@message": "test_instance.boop: Creation complete after 22s [id=test]",
|
|
"@module": "tofu.ui",
|
|
"type": "apply_complete",
|
|
"hook": map[string]any{
|
|
"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), nil))
|
|
|
|
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(addr, states.CurrentGen, plans.Delete, priorState, plannedNewState)
|
|
testHookReturnValues(t, action, err)
|
|
|
|
provisionError := fmt.Errorf("provisioner didn't want to")
|
|
action, err = hook.PostProvisionInstanceStep(addr, "local-exec", provisionError)
|
|
testHookReturnValues(t, action, err)
|
|
|
|
applyError := fmt.Errorf("provider was sad")
|
|
action, err = hook.PostApply(addr, states.CurrentGen, plannedNewState, applyError)
|
|
testHookReturnValues(t, action, err)
|
|
|
|
// Shut down the progress goroutine
|
|
hook.applyingLock.Lock()
|
|
for key, progress := range hook.applying {
|
|
close(progress.done)
|
|
close(progress.elapsed)
|
|
<-progress.heartbeatDone
|
|
delete(hook.applying, key)
|
|
}
|
|
hook.applyingLock.Unlock()
|
|
|
|
wantResource := map[string]any{
|
|
"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]any{
|
|
{
|
|
"@level": "info",
|
|
"@message": "test_instance.boop: Destroying...",
|
|
"@module": "tofu.ui",
|
|
"type": "apply_start",
|
|
"hook": map[string]any{
|
|
"action": string("delete"),
|
|
"resource": wantResource,
|
|
},
|
|
},
|
|
{
|
|
"@level": "info",
|
|
"@message": "test_instance.boop: (local-exec) Provisioning errored",
|
|
"@module": "tofu.ui",
|
|
"type": "provision_errored",
|
|
"hook": map[string]any{
|
|
"provisioner": "local-exec",
|
|
"resource": wantResource,
|
|
},
|
|
},
|
|
{
|
|
"@level": "info",
|
|
"@message": "test_instance.boop: Destruction errored after 0s",
|
|
"@module": "tofu.ui",
|
|
"type": "apply_errored",
|
|
"hook": map[string]any{
|
|
"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), nil))
|
|
|
|
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(addr, states.CurrentGen, state)
|
|
testHookReturnValues(t, action, err)
|
|
|
|
action, err = hook.PostRefresh(addr, states.CurrentGen, state, state)
|
|
testHookReturnValues(t, action, err)
|
|
|
|
wantResource := map[string]any{
|
|
"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]any{
|
|
{
|
|
"@level": "info",
|
|
"@message": "data.test_data_source.beep: Refreshing state... [id=honk]",
|
|
"@module": "tofu.ui",
|
|
"type": "refresh_start",
|
|
"hook": map[string]any{
|
|
"resource": wantResource,
|
|
"id_key": "id",
|
|
"id_value": "honk",
|
|
},
|
|
},
|
|
{
|
|
"@level": "info",
|
|
"@message": "data.test_data_source.beep: Refresh complete [id=honk]",
|
|
"@module": "tofu.ui",
|
|
"type": "refresh_complete",
|
|
"hook": map[string]any{
|
|
"resource": wantResource,
|
|
"id_key": "id",
|
|
"id_value": "honk",
|
|
},
|
|
},
|
|
}
|
|
|
|
testJSONViewOutputEquals(t, done(t).Stdout(), want)
|
|
}
|
|
|
|
func TestJSONHook_ephemeral(t *testing.T) {
|
|
addr := addrs.Resource{
|
|
Mode: addrs.EphemeralResourceMode,
|
|
Type: "test_instance",
|
|
Name: "foo",
|
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
|
|
|
|
cases := []struct {
|
|
name string
|
|
preF func(hook tofu.Hook) (tofu.HookAction, error)
|
|
postF func(hook tofu.Hook) (tofu.HookAction, error)
|
|
want []map[string]any
|
|
}{
|
|
{
|
|
name: "opening",
|
|
preF: func(hook tofu.Hook) (tofu.HookAction, error) {
|
|
return hook.PreOpen(addr)
|
|
},
|
|
postF: func(hook tofu.Hook) (tofu.HookAction, error) {
|
|
return hook.PostOpen(addr, nil)
|
|
},
|
|
want: []map[string]any{
|
|
{
|
|
"@level": "info",
|
|
"@message": "ephemeral.test_instance.foo: Opening...",
|
|
"@module": "tofu.ui",
|
|
"hook": map[string]any{
|
|
"Msg": "Opening...",
|
|
"resource": map[string]any{
|
|
"addr": "ephemeral.test_instance.foo",
|
|
"implied_provider": "test",
|
|
"module": "",
|
|
"resource": "ephemeral.test_instance.foo",
|
|
"resource_key": nil,
|
|
"resource_name": "foo",
|
|
"resource_type": "test_instance",
|
|
},
|
|
},
|
|
"type": "ephemeral_action_started",
|
|
},
|
|
{
|
|
"@level": "info",
|
|
"@message": "ephemeral.test_instance.foo: Open complete",
|
|
"@module": "tofu.ui",
|
|
"hook": map[string]any{
|
|
"Msg": "Open complete",
|
|
"resource": map[string]any{
|
|
"addr": "ephemeral.test_instance.foo",
|
|
"implied_provider": "test",
|
|
"module": "",
|
|
"resource": "ephemeral.test_instance.foo",
|
|
"resource_key": nil,
|
|
"resource_name": "foo",
|
|
"resource_type": "test_instance",
|
|
},
|
|
},
|
|
"type": "ephemeral_action_complete",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "renewing",
|
|
preF: func(hook tofu.Hook) (tofu.HookAction, error) {
|
|
return hook.PreRenew(addr)
|
|
},
|
|
postF: func(hook tofu.Hook) (tofu.HookAction, error) {
|
|
return hook.PostRenew(addr, nil)
|
|
},
|
|
want: []map[string]any{
|
|
{
|
|
"@level": "info",
|
|
"@message": "ephemeral.test_instance.foo: Renewing...",
|
|
"@module": "tofu.ui",
|
|
"hook": map[string]any{
|
|
"Msg": "Renewing...",
|
|
"resource": map[string]any{
|
|
"addr": "ephemeral.test_instance.foo",
|
|
"implied_provider": "test",
|
|
"module": "",
|
|
"resource": "ephemeral.test_instance.foo",
|
|
"resource_key": nil,
|
|
"resource_name": "foo",
|
|
"resource_type": "test_instance",
|
|
},
|
|
},
|
|
"type": "ephemeral_action_started",
|
|
},
|
|
{
|
|
"@level": "info",
|
|
"@message": "ephemeral.test_instance.foo: Renew complete",
|
|
"@module": "tofu.ui",
|
|
"hook": map[string]any{
|
|
"Msg": "Renew complete",
|
|
"resource": map[string]any{
|
|
"addr": "ephemeral.test_instance.foo",
|
|
"implied_provider": "test",
|
|
"module": "",
|
|
"resource": "ephemeral.test_instance.foo",
|
|
"resource_key": nil,
|
|
"resource_name": "foo",
|
|
"resource_type": "test_instance",
|
|
},
|
|
},
|
|
"type": "ephemeral_action_complete",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "closing",
|
|
preF: func(hook tofu.Hook) (tofu.HookAction, error) {
|
|
return hook.PreClose(addr)
|
|
},
|
|
postF: func(hook tofu.Hook) (tofu.HookAction, error) {
|
|
return hook.PostClose(addr, nil)
|
|
},
|
|
want: []map[string]any{
|
|
{
|
|
"@level": "info",
|
|
"@message": "ephemeral.test_instance.foo: Closing...",
|
|
"@module": "tofu.ui",
|
|
"hook": map[string]any{
|
|
"Msg": "Closing...",
|
|
"resource": map[string]any{
|
|
"addr": "ephemeral.test_instance.foo",
|
|
"implied_provider": "test",
|
|
"module": "",
|
|
"resource": "ephemeral.test_instance.foo",
|
|
"resource_key": nil,
|
|
"resource_name": "foo",
|
|
"resource_type": "test_instance",
|
|
},
|
|
},
|
|
"type": "ephemeral_action_started",
|
|
},
|
|
{
|
|
"@level": "info",
|
|
"@message": "ephemeral.test_instance.foo: Close complete",
|
|
"@module": "tofu.ui",
|
|
"hook": map[string]any{
|
|
"Msg": "Close complete",
|
|
"resource": map[string]any{
|
|
"addr": "ephemeral.test_instance.foo",
|
|
"implied_provider": "test",
|
|
"module": "",
|
|
"resource": "ephemeral.test_instance.foo",
|
|
"resource_key": nil,
|
|
"resource_name": "foo",
|
|
"resource_type": "test_instance",
|
|
},
|
|
},
|
|
"type": "ephemeral_action_complete",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range cases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
h := newJSONHook(NewJSONView(NewView(streams), nil))
|
|
|
|
action, err := tt.preF(h)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if action != tofu.HookActionContinue {
|
|
t.Fatalf("Expected hook to continue, given: %#v", action)
|
|
}
|
|
|
|
<-time.After(1100 * time.Millisecond)
|
|
|
|
// call postF that will stop the waiting for the action
|
|
action, err = tt.postF(h)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if action != tofu.HookActionContinue {
|
|
t.Errorf("Expected hook to continue, given: %#v", action)
|
|
}
|
|
|
|
testJSONViewOutputEquals(t, done(t).Stdout(), tt.want)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testHookReturnValues(t *testing.T, action tofu.HookAction, err error) {
|
|
t.Helper()
|
|
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if action != tofu.HookActionContinue {
|
|
t.Fatalf("Expected hook to continue, given: %#v", action)
|
|
}
|
|
}
|
|
|
|
func testDurationEqual(t *testing.T, wantedDuration time.Duration, gotDuration time.Duration) {
|
|
t.Helper()
|
|
|
|
if !cmp.Equal(wantedDuration, gotDuration) {
|
|
t.Errorf("unexpected time elapsed:%s\n", cmp.Diff(wantedDuration, gotDuration))
|
|
}
|
|
}
|