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>
1377 lines
39 KiB
Go
1377 lines
39 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 (
|
|
"bytes"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/opentofu/opentofu/internal/addrs"
|
|
"github.com/opentofu/opentofu/internal/command/arguments"
|
|
"github.com/opentofu/opentofu/internal/encryption"
|
|
"github.com/opentofu/opentofu/internal/lang/globalref"
|
|
"github.com/opentofu/opentofu/internal/plans"
|
|
"github.com/opentofu/opentofu/internal/states"
|
|
"github.com/opentofu/opentofu/internal/states/statefile"
|
|
"github.com/opentofu/opentofu/internal/terminal"
|
|
"github.com/opentofu/opentofu/internal/tofu"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
func TestOperation_stopping(t *testing.T) {
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
v := NewOperation(arguments.ViewHuman, false, NewView(streams))
|
|
|
|
v.Stopping()
|
|
|
|
if got, want := done(t).Stdout(), "Stopping operation...\n"; got != want {
|
|
t.Errorf("wrong result\ngot: %q\nwant: %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestOperation_cancelled(t *testing.T) {
|
|
testCases := map[string]struct {
|
|
planMode plans.Mode
|
|
want string
|
|
}{
|
|
"apply": {
|
|
planMode: plans.NormalMode,
|
|
want: "Apply cancelled.\n",
|
|
},
|
|
"destroy": {
|
|
planMode: plans.DestroyMode,
|
|
want: "Destroy cancelled.\n",
|
|
},
|
|
}
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
v := NewOperation(arguments.ViewHuman, false, NewView(streams))
|
|
|
|
v.Cancelled(tc.planMode)
|
|
|
|
if got, want := done(t).Stdout(), tc.want; got != want {
|
|
t.Errorf("wrong result\ngot: %q\nwant: %q", got, want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOperation_emergencyDumpState(t *testing.T) {
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
v := NewOperation(arguments.ViewHuman, false, NewView(streams))
|
|
|
|
stateFile := statefile.New(nil, "foo", 1)
|
|
|
|
err := v.EmergencyDumpState(stateFile, encryption.StateEncryptionDisabled())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error dumping state: %s", err)
|
|
}
|
|
|
|
// Check that the result (on stderr) looks like JSON state
|
|
raw := done(t).Stderr()
|
|
var state map[string]any
|
|
if err := json.Unmarshal([]byte(raw), &state); err != nil {
|
|
t.Fatalf("unexpected error parsing dumped state: %s\nraw:\n%s", err, raw)
|
|
}
|
|
}
|
|
|
|
func TestOperation_planNoChanges(t *testing.T) {
|
|
|
|
tests := map[string]struct {
|
|
plan func(schemas *tofu.Schemas) *plans.Plan
|
|
wantText string
|
|
}{
|
|
"nothing at all in normal mode": {
|
|
func(schemas *tofu.Schemas) *plans.Plan {
|
|
return &plans.Plan{
|
|
UIMode: plans.NormalMode,
|
|
Changes: plans.NewChanges(),
|
|
}
|
|
},
|
|
"no differences, so no changes are needed.",
|
|
},
|
|
"nothing at all in refresh-only mode": {
|
|
func(schemas *tofu.Schemas) *plans.Plan {
|
|
return &plans.Plan{
|
|
UIMode: plans.RefreshOnlyMode,
|
|
Changes: plans.NewChanges(),
|
|
}
|
|
},
|
|
"OpenTofu has checked that the real remote objects still match",
|
|
},
|
|
"nothing at all in destroy mode": {
|
|
func(schemas *tofu.Schemas) *plans.Plan {
|
|
return &plans.Plan{
|
|
UIMode: plans.DestroyMode,
|
|
Changes: plans.NewChanges(),
|
|
}
|
|
},
|
|
"No objects need to be destroyed.",
|
|
},
|
|
"no drift detected in normal noop": {
|
|
func(schemas *tofu.Schemas) *plans.Plan {
|
|
addr := addrs.Resource{
|
|
Mode: addrs.ManagedResourceMode,
|
|
Type: "test_resource",
|
|
Name: "somewhere",
|
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
|
|
schema, _ := schemas.ResourceTypeConfig(
|
|
addrs.NewDefaultProvider("test"),
|
|
addr.Resource.Resource.Mode,
|
|
addr.Resource.Resource.Type,
|
|
)
|
|
ty := schema.ImpliedType()
|
|
rc := &plans.ResourceInstanceChange{
|
|
Addr: addr,
|
|
PrevRunAddr: addr,
|
|
ProviderAddr: addrs.RootModuleInstance.ProviderConfigDefault(
|
|
addrs.NewDefaultProvider("test"),
|
|
),
|
|
Change: plans.Change{
|
|
Action: plans.Update,
|
|
Before: cty.NullVal(ty),
|
|
After: cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.StringVal("1234"),
|
|
"foo": cty.StringVal("bar"),
|
|
}),
|
|
},
|
|
}
|
|
rcs, err := rc.Encode(ty)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
drs := []*plans.ResourceInstanceChangeSrc{rcs}
|
|
return &plans.Plan{
|
|
UIMode: plans.NormalMode,
|
|
Changes: plans.NewChanges(),
|
|
DriftedResources: drs,
|
|
}
|
|
},
|
|
"No changes",
|
|
},
|
|
"drift detected in normal mode": {
|
|
func(schemas *tofu.Schemas) *plans.Plan {
|
|
addr := addrs.Resource{
|
|
Mode: addrs.ManagedResourceMode,
|
|
Type: "test_resource",
|
|
Name: "somewhere",
|
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
|
|
schema, _ := schemas.ResourceTypeConfig(
|
|
addrs.NewDefaultProvider("test"),
|
|
addr.Resource.Resource.Mode,
|
|
addr.Resource.Resource.Type,
|
|
)
|
|
ty := schema.ImpliedType()
|
|
rc := &plans.ResourceInstanceChange{
|
|
Addr: addr,
|
|
PrevRunAddr: addr,
|
|
ProviderAddr: addrs.RootModuleInstance.ProviderConfigDefault(
|
|
addrs.NewDefaultProvider("test"),
|
|
),
|
|
Change: plans.Change{
|
|
Action: plans.Update,
|
|
Before: cty.NullVal(ty),
|
|
After: cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.StringVal("1234"),
|
|
"foo": cty.StringVal("bar"),
|
|
}),
|
|
},
|
|
}
|
|
rcs, err := rc.Encode(ty)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
drs := []*plans.ResourceInstanceChangeSrc{rcs}
|
|
changes := plans.NewChanges()
|
|
changes.Resources = drs
|
|
return &plans.Plan{
|
|
UIMode: plans.NormalMode,
|
|
Changes: changes,
|
|
DriftedResources: drs,
|
|
RelevantAttributes: []globalref.ResourceAttr{{
|
|
Resource: addr,
|
|
Attr: cty.GetAttrPath("id"),
|
|
}},
|
|
}
|
|
},
|
|
"Objects have changed outside of OpenTofu",
|
|
},
|
|
"drift detected in refresh-only mode": {
|
|
func(schemas *tofu.Schemas) *plans.Plan {
|
|
addr := addrs.Resource{
|
|
Mode: addrs.ManagedResourceMode,
|
|
Type: "test_resource",
|
|
Name: "somewhere",
|
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
|
|
schema, _ := schemas.ResourceTypeConfig(
|
|
addrs.NewDefaultProvider("test"),
|
|
addr.Resource.Resource.Mode,
|
|
addr.Resource.Resource.Type,
|
|
)
|
|
ty := schema.ImpliedType()
|
|
rc := &plans.ResourceInstanceChange{
|
|
Addr: addr,
|
|
PrevRunAddr: addr,
|
|
ProviderAddr: addrs.RootModuleInstance.ProviderConfigDefault(
|
|
addrs.NewDefaultProvider("test"),
|
|
),
|
|
Change: plans.Change{
|
|
Action: plans.Update,
|
|
Before: cty.NullVal(ty),
|
|
After: cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.StringVal("1234"),
|
|
"foo": cty.StringVal("bar"),
|
|
}),
|
|
},
|
|
}
|
|
rcs, err := rc.Encode(ty)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
drs := []*plans.ResourceInstanceChangeSrc{rcs}
|
|
return &plans.Plan{
|
|
UIMode: plans.RefreshOnlyMode,
|
|
Changes: plans.NewChanges(),
|
|
DriftedResources: drs,
|
|
}
|
|
},
|
|
"If you were expecting these changes then you can apply this plan",
|
|
},
|
|
"move-only changes in refresh-only mode": {
|
|
func(schemas *tofu.Schemas) *plans.Plan {
|
|
addr := addrs.Resource{
|
|
Mode: addrs.ManagedResourceMode,
|
|
Type: "test_resource",
|
|
Name: "somewhere",
|
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
|
|
addrPrev := addrs.Resource{
|
|
Mode: addrs.ManagedResourceMode,
|
|
Type: "test_resource",
|
|
Name: "anywhere",
|
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
|
|
schema, _ := schemas.ResourceTypeConfig(
|
|
addrs.NewDefaultProvider("test"),
|
|
addr.Resource.Resource.Mode,
|
|
addr.Resource.Resource.Type,
|
|
)
|
|
ty := schema.ImpliedType()
|
|
rc := &plans.ResourceInstanceChange{
|
|
Addr: addr,
|
|
PrevRunAddr: addrPrev,
|
|
ProviderAddr: addrs.RootModuleInstance.ProviderConfigDefault(
|
|
addrs.NewDefaultProvider("test"),
|
|
),
|
|
Change: plans.Change{
|
|
Action: plans.NoOp,
|
|
Before: cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.StringVal("1234"),
|
|
"foo": cty.StringVal("bar"),
|
|
}),
|
|
After: cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.StringVal("1234"),
|
|
"foo": cty.StringVal("bar"),
|
|
}),
|
|
},
|
|
}
|
|
rcs, err := rc.Encode(ty)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
drs := []*plans.ResourceInstanceChangeSrc{rcs}
|
|
return &plans.Plan{
|
|
UIMode: plans.RefreshOnlyMode,
|
|
Changes: plans.NewChanges(),
|
|
DriftedResources: drs,
|
|
}
|
|
},
|
|
"test_resource.anywhere has moved to test_resource.somewhere",
|
|
},
|
|
"drift detected in destroy mode": {
|
|
func(schemas *tofu.Schemas) *plans.Plan {
|
|
return &plans.Plan{
|
|
UIMode: plans.DestroyMode,
|
|
Changes: plans.NewChanges(),
|
|
PrevRunState: states.BuildState(func(state *states.SyncState) {
|
|
state.SetResourceInstanceCurrent(
|
|
addrs.Resource{
|
|
Mode: addrs.ManagedResourceMode,
|
|
Type: "test_resource",
|
|
Name: "somewhere",
|
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
|
&states.ResourceInstanceObjectSrc{
|
|
Status: states.ObjectReady,
|
|
AttrsJSON: []byte(`{}`),
|
|
},
|
|
addrs.RootModuleInstance.ProviderConfigDefault(addrs.NewDefaultProvider("test")),
|
|
addrs.NoKey,
|
|
)
|
|
}),
|
|
PriorState: states.NewState(),
|
|
}
|
|
},
|
|
"No objects need to be destroyed.",
|
|
},
|
|
}
|
|
|
|
schemas := testSchemas()
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
v := NewOperation(arguments.ViewHuman, false, NewView(streams))
|
|
plan := test.plan(schemas)
|
|
v.Plan(plan, schemas)
|
|
got := done(t).Stdout()
|
|
if want := test.wantText; want != "" && !strings.Contains(got, want) {
|
|
t.Errorf("missing expected message\ngot:\n%s\n\nwant substring: %s", got, want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOperation_plan(t *testing.T) {
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
v := NewOperation(arguments.ViewHuman, true, NewView(streams))
|
|
|
|
plan := testPlan(t)
|
|
schemas := testSchemas()
|
|
v.Plan(plan, schemas)
|
|
|
|
want := `
|
|
OpenTofu used the selected providers to generate the following execution
|
|
plan. Resource actions are indicated with the following symbols:
|
|
+ create
|
|
|
|
OpenTofu will perform the following actions:
|
|
|
|
# test_resource.foo will be created
|
|
+ resource "test_resource" "foo" {
|
|
+ foo = "bar"
|
|
+ id = (known after apply)
|
|
}
|
|
|
|
Plan: 1 to add, 0 to change, 0 to destroy.
|
|
`
|
|
|
|
if got := done(t).Stdout(); got != want {
|
|
t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s", got, want)
|
|
}
|
|
}
|
|
|
|
func TestOperation_planWithDatasource(t *testing.T) {
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
v := NewOperation(arguments.ViewHuman, true, NewView(streams))
|
|
|
|
plan := testPlanWithDatasource(t)
|
|
schemas := testSchemas()
|
|
v.Plan(plan, schemas)
|
|
|
|
want := `
|
|
OpenTofu used the selected providers to generate the following execution
|
|
plan. Resource actions are indicated with the following symbols:
|
|
+ create
|
|
<= read (data resources)
|
|
|
|
OpenTofu will perform the following actions:
|
|
|
|
# data.test_data_source.bar will be read during apply
|
|
<= data "test_data_source" "bar" {
|
|
+ bar = "foo"
|
|
+ id = "C6743020-40BD-4591-81E6-CD08494341D3"
|
|
}
|
|
|
|
# test_resource.foo will be created
|
|
+ resource "test_resource" "foo" {
|
|
+ foo = "bar"
|
|
+ id = (known after apply)
|
|
}
|
|
|
|
Plan: 1 to add, 0 to change, 0 to destroy.
|
|
`
|
|
|
|
if got := done(t).Stdout(); got != want {
|
|
t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s", got, want)
|
|
}
|
|
}
|
|
|
|
func TestOperation_planWithDatasourceAndDrift(t *testing.T) {
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
v := NewOperation(arguments.ViewHuman, true, NewView(streams))
|
|
|
|
plan := testPlanWithDatasource(t)
|
|
schemas := testSchemas()
|
|
v.Plan(plan, schemas)
|
|
|
|
want := `
|
|
OpenTofu used the selected providers to generate the following execution
|
|
plan. Resource actions are indicated with the following symbols:
|
|
+ create
|
|
<= read (data resources)
|
|
|
|
OpenTofu will perform the following actions:
|
|
|
|
# data.test_data_source.bar will be read during apply
|
|
<= data "test_data_source" "bar" {
|
|
+ bar = "foo"
|
|
+ id = "C6743020-40BD-4591-81E6-CD08494341D3"
|
|
}
|
|
|
|
# test_resource.foo will be created
|
|
+ resource "test_resource" "foo" {
|
|
+ foo = "bar"
|
|
+ id = (known after apply)
|
|
}
|
|
|
|
Plan: 1 to add, 0 to change, 0 to destroy.
|
|
`
|
|
|
|
if got := done(t).Stdout(); got != want {
|
|
t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s", got, want)
|
|
}
|
|
}
|
|
|
|
func TestOperation_planWithEphemeral(t *testing.T) {
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
v := NewOperation(arguments.ViewHuman, true, NewView(streams))
|
|
|
|
plan := testPlanWithEphemeral(t)
|
|
schemas := testSchemas()
|
|
v.Plan(plan, schemas)
|
|
|
|
want := `
|
|
OpenTofu used the selected providers to generate the following execution
|
|
plan. Resource actions are indicated with the following symbols:
|
|
+ create
|
|
|
|
OpenTofu will perform the following actions:
|
|
|
|
# test_resource.foo will be created
|
|
+ resource "test_resource" "foo" {
|
|
+ foo = "bar"
|
|
+ id = (known after apply)
|
|
}
|
|
|
|
Plan: 1 to add, 0 to change, 0 to destroy.
|
|
`
|
|
|
|
if got := done(t).Stdout(); got != want {
|
|
t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s", got, want)
|
|
}
|
|
}
|
|
func TestOperation_planNextStep(t *testing.T) {
|
|
testCases := map[string]struct {
|
|
path string
|
|
want string
|
|
}{
|
|
"no state path": {
|
|
path: "",
|
|
want: "You didn't use the -out option",
|
|
},
|
|
"state path": {
|
|
path: "good plan.tfplan",
|
|
want: `tofu apply "good plan.tfplan"`,
|
|
},
|
|
}
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
v := NewOperation(arguments.ViewHuman, false, NewView(streams))
|
|
|
|
v.PlanNextStep(tc.path, "")
|
|
|
|
if got := done(t).Stdout(); !strings.Contains(got, tc.want) {
|
|
t.Errorf("wrong result\ngot: %q\nwant: %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// The in-automation state is on the view itself, so testing it separately is
|
|
// clearer.
|
|
func TestOperation_planNextStepInAutomation(t *testing.T) {
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
v := NewOperation(arguments.ViewHuman, true, NewView(streams))
|
|
|
|
v.PlanNextStep("", "")
|
|
|
|
if got := done(t).Stdout(); got != "" {
|
|
t.Errorf("unexpected output\ngot: %q", got)
|
|
}
|
|
}
|
|
|
|
// Test all the trivial OperationJSON methods together. Y'know, for brevity.
|
|
// This test is not a realistic stream of messages.
|
|
func TestOperationJSON_logs(t *testing.T) {
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
v := &OperationJSON{view: NewJSONView(NewView(streams), nil)}
|
|
|
|
// Added an ephemeral resource change to double-check that it's not
|
|
// shown.
|
|
v.PlannedChange(&plans.ResourceInstanceChangeSrc{
|
|
Addr: addrs.AbsResourceInstance{
|
|
Resource: addrs.ResourceInstance{
|
|
Resource: addrs.Resource{Mode: addrs.EphemeralResourceMode},
|
|
},
|
|
},
|
|
})
|
|
v.Cancelled(plans.NormalMode)
|
|
v.Cancelled(plans.DestroyMode)
|
|
v.Stopping()
|
|
v.Interrupted()
|
|
v.FatalInterrupt()
|
|
|
|
want := []map[string]any{
|
|
{
|
|
"@level": "info",
|
|
"@message": "Apply cancelled",
|
|
"@module": "tofu.ui",
|
|
"type": "log",
|
|
},
|
|
{
|
|
"@level": "info",
|
|
"@message": "Destroy cancelled",
|
|
"@module": "tofu.ui",
|
|
"type": "log",
|
|
},
|
|
{
|
|
"@level": "info",
|
|
"@message": "Stopping operation...",
|
|
"@module": "tofu.ui",
|
|
"type": "log",
|
|
},
|
|
{
|
|
"@level": "info",
|
|
"@message": interrupted,
|
|
"@module": "tofu.ui",
|
|
"type": "log",
|
|
},
|
|
{
|
|
"@level": "info",
|
|
"@message": fatalInterrupt,
|
|
"@module": "tofu.ui",
|
|
"type": "log",
|
|
},
|
|
}
|
|
|
|
testJSONViewOutputEquals(t, done(t).Stdout(), want)
|
|
}
|
|
|
|
// This is a fairly circular test, but it's such a rarely executed code path
|
|
// that I think it's probably still worth having. We're not testing against
|
|
// a fixed state JSON output because this test ought not fail just because
|
|
// we upgrade state format in the future.
|
|
func TestOperationJSON_emergencyDumpState(t *testing.T) {
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
v := &OperationJSON{view: NewJSONView(NewView(streams), nil)}
|
|
|
|
stateFile := statefile.New(nil, "foo", 1)
|
|
stateBuf := new(bytes.Buffer)
|
|
err := statefile.Write(stateFile, stateBuf, encryption.StateEncryptionDisabled())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var stateJSON map[string]any
|
|
err = json.Unmarshal(stateBuf.Bytes(), &stateJSON)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = v.EmergencyDumpState(stateFile, encryption.StateEncryptionDisabled())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error dumping state: %s", err)
|
|
}
|
|
|
|
want := []map[string]any{
|
|
{
|
|
"@level": "info",
|
|
"@message": "Emergency state dump",
|
|
"@module": "tofu.ui",
|
|
"type": "log",
|
|
"state": stateJSON,
|
|
},
|
|
}
|
|
|
|
testJSONViewOutputEquals(t, done(t).Stdout(), want)
|
|
}
|
|
|
|
func TestOperationJSON_planNoChanges(t *testing.T) {
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
v := &OperationJSON{view: NewJSONView(NewView(streams), nil)}
|
|
|
|
plan := &plans.Plan{
|
|
Changes: plans.NewChanges(),
|
|
}
|
|
v.Plan(plan, nil)
|
|
|
|
want := []map[string]any{
|
|
{
|
|
"@level": "info",
|
|
"@message": "Plan: 0 to add, 0 to change, 0 to destroy.",
|
|
"@module": "tofu.ui",
|
|
"type": "change_summary",
|
|
"changes": map[string]any{
|
|
"operation": "plan",
|
|
"add": float64(0),
|
|
"import": float64(0),
|
|
"change": float64(0),
|
|
"forget": float64(0),
|
|
"remove": float64(0),
|
|
},
|
|
},
|
|
}
|
|
|
|
testJSONViewOutputEquals(t, done(t).Stdout(), want)
|
|
}
|
|
|
|
func TestOperationJSON_plan(t *testing.T) {
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
v := &OperationJSON{view: NewJSONView(NewView(streams), nil)}
|
|
|
|
root := addrs.RootModuleInstance
|
|
vpc, diags := addrs.ParseModuleInstanceStr("module.vpc")
|
|
if len(diags) > 0 {
|
|
t.Fatal(diags.Err())
|
|
}
|
|
boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "boop"}
|
|
beep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "beep"}
|
|
derp := addrs.Resource{Mode: addrs.DataResourceMode, Type: "test_source", Name: "derp"}
|
|
|
|
plan := &plans.Plan{
|
|
Changes: &plans.Changes{
|
|
Resources: []*plans.ResourceInstanceChangeSrc{
|
|
{
|
|
Addr: boop.Instance(addrs.IntKey(0)).Absolute(root),
|
|
PrevRunAddr: boop.Instance(addrs.IntKey(0)).Absolute(root),
|
|
ChangeSrc: plans.ChangeSrc{Action: plans.CreateThenDelete},
|
|
},
|
|
{
|
|
Addr: boop.Instance(addrs.IntKey(1)).Absolute(root),
|
|
PrevRunAddr: boop.Instance(addrs.IntKey(1)).Absolute(root),
|
|
ChangeSrc: plans.ChangeSrc{Action: plans.Create},
|
|
},
|
|
{
|
|
Addr: boop.Instance(addrs.IntKey(0)).Absolute(vpc),
|
|
PrevRunAddr: boop.Instance(addrs.IntKey(0)).Absolute(vpc),
|
|
ChangeSrc: plans.ChangeSrc{Action: plans.Delete},
|
|
},
|
|
{
|
|
Addr: beep.Instance(addrs.NoKey).Absolute(root),
|
|
PrevRunAddr: beep.Instance(addrs.NoKey).Absolute(root),
|
|
ChangeSrc: plans.ChangeSrc{Action: plans.DeleteThenCreate},
|
|
},
|
|
{
|
|
Addr: beep.Instance(addrs.NoKey).Absolute(vpc),
|
|
PrevRunAddr: beep.Instance(addrs.NoKey).Absolute(vpc),
|
|
ChangeSrc: plans.ChangeSrc{Action: plans.Update},
|
|
},
|
|
// Data source deletion should not show up in the logs
|
|
{
|
|
Addr: derp.Instance(addrs.NoKey).Absolute(root),
|
|
PrevRunAddr: derp.Instance(addrs.NoKey).Absolute(root),
|
|
ChangeSrc: plans.ChangeSrc{Action: plans.Delete},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
v.Plan(plan, testSchemas())
|
|
|
|
want := []map[string]any{
|
|
// Create-then-delete should result in replace
|
|
{
|
|
"@level": "info",
|
|
"@message": "test_resource.boop[0]: Plan to replace",
|
|
"@module": "tofu.ui",
|
|
"type": "planned_change",
|
|
"change": map[string]any{
|
|
"action": "replace",
|
|
"resource": map[string]any{
|
|
"addr": `test_resource.boop[0]`,
|
|
"implied_provider": "test",
|
|
"module": "",
|
|
"resource": `test_resource.boop[0]`,
|
|
"resource_key": float64(0),
|
|
"resource_name": "boop",
|
|
"resource_type": "test_resource",
|
|
},
|
|
},
|
|
},
|
|
// Simple create
|
|
{
|
|
"@level": "info",
|
|
"@message": "test_resource.boop[1]: Plan to create",
|
|
"@module": "tofu.ui",
|
|
"type": "planned_change",
|
|
"change": map[string]any{
|
|
"action": "create",
|
|
"resource": map[string]any{
|
|
"addr": `test_resource.boop[1]`,
|
|
"implied_provider": "test",
|
|
"module": "",
|
|
"resource": `test_resource.boop[1]`,
|
|
"resource_key": float64(1),
|
|
"resource_name": "boop",
|
|
"resource_type": "test_resource",
|
|
},
|
|
},
|
|
},
|
|
// Simple delete
|
|
{
|
|
"@level": "info",
|
|
"@message": "module.vpc.test_resource.boop[0]: Plan to delete",
|
|
"@module": "tofu.ui",
|
|
"type": "planned_change",
|
|
"change": map[string]any{
|
|
"action": "delete",
|
|
"resource": map[string]any{
|
|
"addr": `module.vpc.test_resource.boop[0]`,
|
|
"implied_provider": "test",
|
|
"module": "module.vpc",
|
|
"resource": `test_resource.boop[0]`,
|
|
"resource_key": float64(0),
|
|
"resource_name": "boop",
|
|
"resource_type": "test_resource",
|
|
},
|
|
},
|
|
},
|
|
// Delete-then-create is also a replace
|
|
{
|
|
"@level": "info",
|
|
"@message": "test_resource.beep: Plan to replace",
|
|
"@module": "tofu.ui",
|
|
"type": "planned_change",
|
|
"change": map[string]any{
|
|
"action": "replace",
|
|
"resource": map[string]any{
|
|
"addr": `test_resource.beep`,
|
|
"implied_provider": "test",
|
|
"module": "",
|
|
"resource": `test_resource.beep`,
|
|
"resource_key": nil,
|
|
"resource_name": "beep",
|
|
"resource_type": "test_resource",
|
|
},
|
|
},
|
|
},
|
|
// Simple update
|
|
{
|
|
"@level": "info",
|
|
"@message": "module.vpc.test_resource.beep: Plan to update",
|
|
"@module": "tofu.ui",
|
|
"type": "planned_change",
|
|
"change": map[string]any{
|
|
"action": "update",
|
|
"resource": map[string]any{
|
|
"addr": `module.vpc.test_resource.beep`,
|
|
"implied_provider": "test",
|
|
"module": "module.vpc",
|
|
"resource": `test_resource.beep`,
|
|
"resource_key": nil,
|
|
"resource_name": "beep",
|
|
"resource_type": "test_resource",
|
|
},
|
|
},
|
|
},
|
|
// These counts are 3 add/1 change/3 destroy because the replace
|
|
// changes result in both add and destroy counts.
|
|
{
|
|
"@level": "info",
|
|
"@message": "Plan: 3 to add, 1 to change, 3 to destroy.",
|
|
"@module": "tofu.ui",
|
|
"type": "change_summary",
|
|
"changes": map[string]any{
|
|
"operation": "plan",
|
|
"add": float64(3),
|
|
"import": float64(0),
|
|
"change": float64(1),
|
|
"forget": float64(0),
|
|
"remove": float64(3),
|
|
},
|
|
},
|
|
}
|
|
|
|
testJSONViewOutputEquals(t, done(t).Stdout(), want)
|
|
}
|
|
|
|
func TestOperationJSON_planWithImport(t *testing.T) {
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
v := &OperationJSON{view: NewJSONView(NewView(streams), nil)}
|
|
|
|
root := addrs.RootModuleInstance
|
|
vpc, diags := addrs.ParseModuleInstanceStr("module.vpc")
|
|
if len(diags) > 0 {
|
|
t.Fatal(diags.Err())
|
|
}
|
|
boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "boop"}
|
|
beep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "beep"}
|
|
|
|
plan := &plans.Plan{
|
|
Changes: &plans.Changes{
|
|
Resources: []*plans.ResourceInstanceChangeSrc{
|
|
{
|
|
Addr: boop.Instance(addrs.IntKey(0)).Absolute(vpc),
|
|
PrevRunAddr: boop.Instance(addrs.IntKey(0)).Absolute(vpc),
|
|
ChangeSrc: plans.ChangeSrc{Action: plans.NoOp, Importing: &plans.ImportingSrc{ID: "DECD6D77"}},
|
|
},
|
|
{
|
|
Addr: boop.Instance(addrs.IntKey(1)).Absolute(vpc),
|
|
PrevRunAddr: boop.Instance(addrs.IntKey(1)).Absolute(vpc),
|
|
ChangeSrc: plans.ChangeSrc{Action: plans.Delete, Importing: &plans.ImportingSrc{ID: "DECD6D77"}},
|
|
},
|
|
{
|
|
Addr: boop.Instance(addrs.IntKey(0)).Absolute(root),
|
|
PrevRunAddr: boop.Instance(addrs.IntKey(0)).Absolute(root),
|
|
ChangeSrc: plans.ChangeSrc{Action: plans.CreateThenDelete, Importing: &plans.ImportingSrc{ID: "DECD6D77"}},
|
|
},
|
|
{
|
|
Addr: beep.Instance(addrs.NoKey).Absolute(root),
|
|
PrevRunAddr: beep.Instance(addrs.NoKey).Absolute(root),
|
|
ChangeSrc: plans.ChangeSrc{Action: plans.Update, Importing: &plans.ImportingSrc{ID: "DECD6D77"}},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
v.Plan(plan, testSchemas())
|
|
|
|
want := []map[string]any{
|
|
// Simple import
|
|
{
|
|
"@level": "info",
|
|
"@message": "module.vpc.test_resource.boop[0]: Plan to import",
|
|
"@module": "tofu.ui",
|
|
"type": "planned_change",
|
|
"change": map[string]any{
|
|
"action": "import",
|
|
"resource": map[string]any{
|
|
"addr": `module.vpc.test_resource.boop[0]`,
|
|
"implied_provider": "test",
|
|
"module": "module.vpc",
|
|
"resource": `test_resource.boop[0]`,
|
|
"resource_key": float64(0),
|
|
"resource_name": "boop",
|
|
"resource_type": "test_resource",
|
|
},
|
|
"importing": map[string]any{
|
|
"id": "DECD6D77",
|
|
},
|
|
},
|
|
},
|
|
// Delete after importing
|
|
{
|
|
"@level": "info",
|
|
"@message": "module.vpc.test_resource.boop[1]: Plan to delete",
|
|
"@module": "tofu.ui",
|
|
"type": "planned_change",
|
|
"change": map[string]any{
|
|
"action": "delete",
|
|
"resource": map[string]any{
|
|
"addr": `module.vpc.test_resource.boop[1]`,
|
|
"implied_provider": "test",
|
|
"module": "module.vpc",
|
|
"resource": `test_resource.boop[1]`,
|
|
"resource_key": float64(1),
|
|
"resource_name": "boop",
|
|
"resource_type": "test_resource",
|
|
},
|
|
"importing": map[string]any{
|
|
"id": "DECD6D77",
|
|
},
|
|
},
|
|
},
|
|
// Create-then-delete after importing.
|
|
{
|
|
"@level": "info",
|
|
"@message": "test_resource.boop[0]: Plan to replace",
|
|
"@module": "tofu.ui",
|
|
"type": "planned_change",
|
|
"change": map[string]any{
|
|
"action": "replace",
|
|
"resource": map[string]any{
|
|
"addr": `test_resource.boop[0]`,
|
|
"implied_provider": "test",
|
|
"module": "",
|
|
"resource": `test_resource.boop[0]`,
|
|
"resource_key": float64(0),
|
|
"resource_name": "boop",
|
|
"resource_type": "test_resource",
|
|
},
|
|
"importing": map[string]any{
|
|
"id": "DECD6D77",
|
|
},
|
|
},
|
|
},
|
|
// Update after importing
|
|
{
|
|
"@level": "info",
|
|
"@message": "test_resource.beep: Plan to update",
|
|
"@module": "tofu.ui",
|
|
"type": "planned_change",
|
|
"change": map[string]any{
|
|
"action": "update",
|
|
"resource": map[string]any{
|
|
"addr": `test_resource.beep`,
|
|
"implied_provider": "test",
|
|
"module": "",
|
|
"resource": `test_resource.beep`,
|
|
"resource_key": nil,
|
|
"resource_name": "beep",
|
|
"resource_type": "test_resource",
|
|
},
|
|
"importing": map[string]any{
|
|
"id": "DECD6D77",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"@level": "info",
|
|
"@message": "Plan: 4 to import, 1 to add, 1 to change, 2 to destroy.",
|
|
"@module": "tofu.ui",
|
|
"type": "change_summary",
|
|
"changes": map[string]any{
|
|
"operation": "plan",
|
|
"add": float64(1),
|
|
"import": float64(4),
|
|
"change": float64(1),
|
|
"forget": float64(0),
|
|
"remove": float64(2),
|
|
},
|
|
},
|
|
}
|
|
|
|
testJSONViewOutputEquals(t, done(t).Stdout(), want)
|
|
}
|
|
|
|
func TestOperationJSON_planDriftWithMove(t *testing.T) {
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
v := &OperationJSON{view: NewJSONView(NewView(streams), nil)}
|
|
|
|
root := addrs.RootModuleInstance
|
|
boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "boop"}
|
|
beep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "beep"}
|
|
blep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "blep"}
|
|
honk := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "honk"}
|
|
|
|
plan := &plans.Plan{
|
|
UIMode: plans.NormalMode,
|
|
Changes: &plans.Changes{
|
|
Resources: []*plans.ResourceInstanceChangeSrc{
|
|
{
|
|
Addr: honk.Instance(addrs.StringKey("bonk")).Absolute(root),
|
|
PrevRunAddr: honk.Instance(addrs.IntKey(0)).Absolute(root),
|
|
ChangeSrc: plans.ChangeSrc{Action: plans.NoOp},
|
|
},
|
|
},
|
|
},
|
|
DriftedResources: []*plans.ResourceInstanceChangeSrc{
|
|
{
|
|
Addr: beep.Instance(addrs.NoKey).Absolute(root),
|
|
PrevRunAddr: beep.Instance(addrs.NoKey).Absolute(root),
|
|
ChangeSrc: plans.ChangeSrc{Action: plans.Delete},
|
|
},
|
|
{
|
|
Addr: boop.Instance(addrs.NoKey).Absolute(root),
|
|
PrevRunAddr: blep.Instance(addrs.NoKey).Absolute(root),
|
|
ChangeSrc: plans.ChangeSrc{Action: plans.Update},
|
|
},
|
|
// Move-only resource drift should not be present in normal mode plans
|
|
{
|
|
Addr: honk.Instance(addrs.StringKey("bonk")).Absolute(root),
|
|
PrevRunAddr: honk.Instance(addrs.IntKey(0)).Absolute(root),
|
|
ChangeSrc: plans.ChangeSrc{Action: plans.NoOp},
|
|
},
|
|
},
|
|
}
|
|
v.Plan(plan, testSchemas())
|
|
|
|
want := []map[string]any{
|
|
// Drift detected: delete
|
|
{
|
|
"@level": "info",
|
|
"@message": "test_resource.beep: Drift detected (delete)",
|
|
"@module": "tofu.ui",
|
|
"type": "resource_drift",
|
|
"change": map[string]any{
|
|
"action": "delete",
|
|
"resource": map[string]any{
|
|
"addr": "test_resource.beep",
|
|
"implied_provider": "test",
|
|
"module": "",
|
|
"resource": "test_resource.beep",
|
|
"resource_key": nil,
|
|
"resource_name": "beep",
|
|
"resource_type": "test_resource",
|
|
},
|
|
},
|
|
},
|
|
// Drift detected: update with move
|
|
{
|
|
"@level": "info",
|
|
"@message": "test_resource.boop: Drift detected (update)",
|
|
"@module": "tofu.ui",
|
|
"type": "resource_drift",
|
|
"change": map[string]any{
|
|
"action": "update",
|
|
"resource": map[string]any{
|
|
"addr": "test_resource.boop",
|
|
"implied_provider": "test",
|
|
"module": "",
|
|
"resource": "test_resource.boop",
|
|
"resource_key": nil,
|
|
"resource_name": "boop",
|
|
"resource_type": "test_resource",
|
|
},
|
|
"previous_resource": map[string]any{
|
|
"addr": "test_resource.blep",
|
|
"implied_provider": "test",
|
|
"module": "",
|
|
"resource": "test_resource.blep",
|
|
"resource_key": nil,
|
|
"resource_name": "blep",
|
|
"resource_type": "test_resource",
|
|
},
|
|
},
|
|
},
|
|
// Move-only change
|
|
{
|
|
"@level": "info",
|
|
"@message": `test_resource.honk["bonk"]: Plan to move`,
|
|
"@module": "tofu.ui",
|
|
"type": "planned_change",
|
|
"change": map[string]any{
|
|
"action": "move",
|
|
"resource": map[string]any{
|
|
"addr": `test_resource.honk["bonk"]`,
|
|
"implied_provider": "test",
|
|
"module": "",
|
|
"resource": `test_resource.honk["bonk"]`,
|
|
"resource_key": "bonk",
|
|
"resource_name": "honk",
|
|
"resource_type": "test_resource",
|
|
},
|
|
"previous_resource": map[string]any{
|
|
"addr": `test_resource.honk[0]`,
|
|
"implied_provider": "test",
|
|
"module": "",
|
|
"resource": `test_resource.honk[0]`,
|
|
"resource_key": float64(0),
|
|
"resource_name": "honk",
|
|
"resource_type": "test_resource",
|
|
},
|
|
},
|
|
},
|
|
// No changes
|
|
{
|
|
"@level": "info",
|
|
"@message": "Plan: 0 to add, 0 to change, 0 to destroy.",
|
|
"@module": "tofu.ui",
|
|
"type": "change_summary",
|
|
"changes": map[string]any{
|
|
"operation": "plan",
|
|
"add": float64(0),
|
|
"import": float64(0),
|
|
"change": float64(0),
|
|
"forget": float64(0),
|
|
"remove": float64(0),
|
|
},
|
|
},
|
|
}
|
|
|
|
testJSONViewOutputEquals(t, done(t).Stdout(), want)
|
|
}
|
|
|
|
func TestOperationJSON_planDriftWithMoveRefreshOnly(t *testing.T) {
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
v := &OperationJSON{view: NewJSONView(NewView(streams), nil)}
|
|
|
|
root := addrs.RootModuleInstance
|
|
boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "boop"}
|
|
beep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "beep"}
|
|
blep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "blep"}
|
|
honk := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "honk"}
|
|
|
|
plan := &plans.Plan{
|
|
UIMode: plans.RefreshOnlyMode,
|
|
Changes: &plans.Changes{
|
|
Resources: []*plans.ResourceInstanceChangeSrc{},
|
|
},
|
|
DriftedResources: []*plans.ResourceInstanceChangeSrc{
|
|
{
|
|
Addr: beep.Instance(addrs.NoKey).Absolute(root),
|
|
PrevRunAddr: beep.Instance(addrs.NoKey).Absolute(root),
|
|
ChangeSrc: plans.ChangeSrc{Action: plans.Delete},
|
|
},
|
|
{
|
|
Addr: boop.Instance(addrs.NoKey).Absolute(root),
|
|
PrevRunAddr: blep.Instance(addrs.NoKey).Absolute(root),
|
|
ChangeSrc: plans.ChangeSrc{Action: plans.Update},
|
|
},
|
|
// Move-only resource drift should be present in refresh-only plans
|
|
{
|
|
Addr: honk.Instance(addrs.StringKey("bonk")).Absolute(root),
|
|
PrevRunAddr: honk.Instance(addrs.IntKey(0)).Absolute(root),
|
|
ChangeSrc: plans.ChangeSrc{Action: plans.NoOp},
|
|
},
|
|
},
|
|
}
|
|
v.Plan(plan, testSchemas())
|
|
|
|
want := []map[string]any{
|
|
// Drift detected: delete
|
|
{
|
|
"@level": "info",
|
|
"@message": "test_resource.beep: Drift detected (delete)",
|
|
"@module": "tofu.ui",
|
|
"type": "resource_drift",
|
|
"change": map[string]any{
|
|
"action": "delete",
|
|
"resource": map[string]any{
|
|
"addr": "test_resource.beep",
|
|
"implied_provider": "test",
|
|
"module": "",
|
|
"resource": "test_resource.beep",
|
|
"resource_key": nil,
|
|
"resource_name": "beep",
|
|
"resource_type": "test_resource",
|
|
},
|
|
},
|
|
},
|
|
// Drift detected: update
|
|
{
|
|
"@level": "info",
|
|
"@message": "test_resource.boop: Drift detected (update)",
|
|
"@module": "tofu.ui",
|
|
"type": "resource_drift",
|
|
"change": map[string]any{
|
|
"action": "update",
|
|
"resource": map[string]any{
|
|
"addr": "test_resource.boop",
|
|
"implied_provider": "test",
|
|
"module": "",
|
|
"resource": "test_resource.boop",
|
|
"resource_key": nil,
|
|
"resource_name": "boop",
|
|
"resource_type": "test_resource",
|
|
},
|
|
"previous_resource": map[string]any{
|
|
"addr": "test_resource.blep",
|
|
"implied_provider": "test",
|
|
"module": "",
|
|
"resource": "test_resource.blep",
|
|
"resource_key": nil,
|
|
"resource_name": "blep",
|
|
"resource_type": "test_resource",
|
|
},
|
|
},
|
|
},
|
|
// Drift detected: Move-only change
|
|
{
|
|
"@level": "info",
|
|
"@message": `test_resource.honk["bonk"]: Drift detected (move)`,
|
|
"@module": "tofu.ui",
|
|
"type": "resource_drift",
|
|
"change": map[string]any{
|
|
"action": "move",
|
|
"resource": map[string]any{
|
|
"addr": `test_resource.honk["bonk"]`,
|
|
"implied_provider": "test",
|
|
"module": "",
|
|
"resource": `test_resource.honk["bonk"]`,
|
|
"resource_key": "bonk",
|
|
"resource_name": "honk",
|
|
"resource_type": "test_resource",
|
|
},
|
|
"previous_resource": map[string]any{
|
|
"addr": `test_resource.honk[0]`,
|
|
"implied_provider": "test",
|
|
"module": "",
|
|
"resource": `test_resource.honk[0]`,
|
|
"resource_key": float64(0),
|
|
"resource_name": "honk",
|
|
"resource_type": "test_resource",
|
|
},
|
|
},
|
|
},
|
|
// No changes
|
|
{
|
|
"@level": "info",
|
|
"@message": "Plan: 0 to add, 0 to change, 0 to destroy.",
|
|
"@module": "tofu.ui",
|
|
"type": "change_summary",
|
|
"changes": map[string]any{
|
|
"operation": "plan",
|
|
"add": float64(0),
|
|
"import": float64(0),
|
|
"change": float64(0),
|
|
"forget": float64(0),
|
|
"remove": float64(0),
|
|
},
|
|
},
|
|
}
|
|
|
|
testJSONViewOutputEquals(t, done(t).Stdout(), want)
|
|
}
|
|
|
|
func TestOperationJSON_planOutputChanges(t *testing.T) {
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
v := &OperationJSON{view: NewJSONView(NewView(streams), nil)}
|
|
|
|
root := addrs.RootModuleInstance
|
|
|
|
plan := &plans.Plan{
|
|
Changes: &plans.Changes{
|
|
Resources: []*plans.ResourceInstanceChangeSrc{},
|
|
Outputs: []*plans.OutputChangeSrc{
|
|
{
|
|
Addr: root.OutputValue("boop"),
|
|
ChangeSrc: plans.ChangeSrc{
|
|
Action: plans.NoOp,
|
|
},
|
|
},
|
|
{
|
|
Addr: root.OutputValue("beep"),
|
|
ChangeSrc: plans.ChangeSrc{
|
|
Action: plans.Create,
|
|
},
|
|
},
|
|
{
|
|
Addr: root.OutputValue("bonk"),
|
|
ChangeSrc: plans.ChangeSrc{
|
|
Action: plans.Delete,
|
|
},
|
|
},
|
|
{
|
|
Addr: root.OutputValue("honk"),
|
|
ChangeSrc: plans.ChangeSrc{
|
|
Action: plans.Update,
|
|
},
|
|
Sensitive: true,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
v.Plan(plan, testSchemas())
|
|
|
|
want := []map[string]any{
|
|
// No resource changes
|
|
{
|
|
"@level": "info",
|
|
"@message": "Plan: 0 to add, 0 to change, 0 to destroy.",
|
|
"@module": "tofu.ui",
|
|
"type": "change_summary",
|
|
"changes": map[string]any{
|
|
"operation": "plan",
|
|
"add": float64(0),
|
|
"import": float64(0),
|
|
"change": float64(0),
|
|
"remove": float64(0),
|
|
"forget": float64(0),
|
|
},
|
|
},
|
|
// Output changes
|
|
{
|
|
"@level": "info",
|
|
"@message": "Outputs: 4",
|
|
"@module": "tofu.ui",
|
|
"type": "outputs",
|
|
"outputs": map[string]any{
|
|
"boop": map[string]any{
|
|
"action": "noop",
|
|
"sensitive": false,
|
|
},
|
|
"beep": map[string]any{
|
|
"action": "create",
|
|
"sensitive": false,
|
|
},
|
|
"bonk": map[string]any{
|
|
"action": "delete",
|
|
"sensitive": false,
|
|
},
|
|
"honk": map[string]any{
|
|
"action": "update",
|
|
"sensitive": true,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
testJSONViewOutputEquals(t, done(t).Stdout(), want)
|
|
}
|
|
|
|
func TestOperationJSON_plannedChange(t *testing.T) {
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
v := &OperationJSON{view: NewJSONView(NewView(streams), nil)}
|
|
|
|
root := addrs.RootModuleInstance
|
|
boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "boop"}
|
|
derp := addrs.Resource{Mode: addrs.DataResourceMode, Type: "test_source", Name: "derp"}
|
|
|
|
// Replace requested by user
|
|
v.PlannedChange(&plans.ResourceInstanceChangeSrc{
|
|
Addr: boop.Instance(addrs.IntKey(0)).Absolute(root),
|
|
PrevRunAddr: boop.Instance(addrs.IntKey(0)).Absolute(root),
|
|
ChangeSrc: plans.ChangeSrc{Action: plans.DeleteThenCreate},
|
|
ActionReason: plans.ResourceInstanceReplaceByRequest,
|
|
})
|
|
|
|
// Simple create
|
|
v.PlannedChange(&plans.ResourceInstanceChangeSrc{
|
|
Addr: boop.Instance(addrs.IntKey(1)).Absolute(root),
|
|
PrevRunAddr: boop.Instance(addrs.IntKey(1)).Absolute(root),
|
|
ChangeSrc: plans.ChangeSrc{Action: plans.Create},
|
|
})
|
|
|
|
// Data source deletion
|
|
v.PlannedChange(&plans.ResourceInstanceChangeSrc{
|
|
Addr: derp.Instance(addrs.NoKey).Absolute(root),
|
|
PrevRunAddr: derp.Instance(addrs.NoKey).Absolute(root),
|
|
ChangeSrc: plans.ChangeSrc{Action: plans.Delete},
|
|
})
|
|
|
|
// Expect only two messages, as the data source deletion should be a no-op
|
|
want := []map[string]any{
|
|
{
|
|
"@level": "info",
|
|
"@message": "test_instance.boop[0]: Plan to replace",
|
|
"@module": "tofu.ui",
|
|
"type": "planned_change",
|
|
"change": map[string]any{
|
|
"action": "replace",
|
|
"reason": "requested",
|
|
"resource": map[string]any{
|
|
"addr": `test_instance.boop[0]`,
|
|
"implied_provider": "test",
|
|
"module": "",
|
|
"resource": `test_instance.boop[0]`,
|
|
"resource_key": float64(0),
|
|
"resource_name": "boop",
|
|
"resource_type": "test_instance",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"@level": "info",
|
|
"@message": "test_instance.boop[1]: Plan to create",
|
|
"@module": "tofu.ui",
|
|
"type": "planned_change",
|
|
"change": map[string]any{
|
|
"action": "create",
|
|
"resource": map[string]any{
|
|
"addr": `test_instance.boop[1]`,
|
|
"implied_provider": "test",
|
|
"module": "",
|
|
"resource": `test_instance.boop[1]`,
|
|
"resource_key": float64(1),
|
|
"resource_name": "boop",
|
|
"resource_type": "test_instance",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
testJSONViewOutputEquals(t, done(t).Stdout(), want)
|
|
}
|