opentofu/command/output_test.go
Martin Atkins 3268a7eaba command/output: Raw output mode
So far the output command has had a default output format intended for
human consumption and a JSON output format intended for machine
consumption.

However, until Terraform v0.14 the default output format for primitive
types happened to be _almost_ a raw string representation of the value,
and so users started using that as a more convenient way to access
primitive-typed output values from shell scripts, avoiding the need to
also use a tool like "jq" to decode the JSON.

Recognizing that primitive-typed output values are common and that
processing them with shell scripts is common, this commit introduces a new
-raw mode which is explicitly intended for that use-case, guaranteeing
that the result will always be the direct result of a string conversion
of the output value, or an error if no such conversion is possible.

Our policy elsewhere in Terraform is that we always use JSON for
machine-readable output. We adopted that policy because our other
machine-readable output has typically been complex data structures rather
than single primitive values. A special mode seems justified for output
values because it is common for root module output values to be just
strings, and so it's pragmatic to offer access to the raw value directly
rather than requiring a round-trip through JSON.
2020-12-09 10:10:02 -08:00

496 lines
11 KiB
Go

package command
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/mitchellh/cli"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/states"
)
func TestOutput(t *testing.T) {
originalState := states.BuildState(func(s *states.SyncState) {
s.SetOutputValue(
addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance),
cty.StringVal("bar"),
false,
)
})
statePath := testStateFile(t, originalState)
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
args := []string{
"-state", statePath,
"foo",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
actual := strings.TrimSpace(ui.OutputWriter.String())
if actual != `"bar"` {
t.Fatalf("bad: %#v", actual)
}
}
func TestOutput_nestedListAndMap(t *testing.T) {
originalState := states.BuildState(func(s *states.SyncState) {
s.SetOutputValue(
addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance),
cty.ListVal([]cty.Value{
cty.MapVal(map[string]cty.Value{
"key": cty.StringVal("value"),
"key2": cty.StringVal("value2"),
}),
cty.MapVal(map[string]cty.Value{
"key": cty.StringVal("value"),
}),
}),
false,
)
})
statePath := testStateFile(t, originalState)
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
args := []string{
"-state", statePath,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
actual := strings.TrimSpace(ui.OutputWriter.String())
expected := strings.TrimSpace(`
foo = tolist([
tomap({
"key" = "value"
"key2" = "value2"
}),
tomap({
"key" = "value"
}),
])
`)
if actual != expected {
t.Fatalf("wrong output\ngot: %s\nwant: %s", actual, expected)
}
}
func TestOutput_json(t *testing.T) {
originalState := states.BuildState(func(s *states.SyncState) {
s.SetOutputValue(
addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance),
cty.StringVal("bar"),
false,
)
})
statePath := testStateFile(t, originalState)
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
args := []string{
"-state", statePath,
"-json",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
actual := strings.TrimSpace(ui.OutputWriter.String())
expected := "{\n \"foo\": {\n \"sensitive\": false,\n \"type\": \"string\",\n \"value\": \"bar\"\n }\n}"
if actual != expected {
t.Fatalf("wrong output\ngot: %#v\nwant: %#v", actual, expected)
}
}
func TestOutput_raw(t *testing.T) {
originalState := states.BuildState(func(s *states.SyncState) {
s.SetOutputValue(
addrs.OutputValue{Name: "str"}.Absolute(addrs.RootModuleInstance),
cty.StringVal("bar"),
false,
)
s.SetOutputValue(
addrs.OutputValue{Name: "multistr"}.Absolute(addrs.RootModuleInstance),
cty.StringVal("bar\nbaz"),
false,
)
s.SetOutputValue(
addrs.OutputValue{Name: "num"}.Absolute(addrs.RootModuleInstance),
cty.NumberIntVal(2),
false,
)
s.SetOutputValue(
addrs.OutputValue{Name: "bool"}.Absolute(addrs.RootModuleInstance),
cty.True,
false,
)
s.SetOutputValue(
addrs.OutputValue{Name: "obj"}.Absolute(addrs.RootModuleInstance),
cty.EmptyObjectVal,
false,
)
s.SetOutputValue(
addrs.OutputValue{Name: "null"}.Absolute(addrs.RootModuleInstance),
cty.NullVal(cty.String),
false,
)
})
statePath := testStateFile(t, originalState)
tests := map[string]struct {
WantOutput string
WantErr bool
}{
"str": {WantOutput: "bar"},
"multistr": {WantOutput: "bar\nbaz"},
"num": {WantOutput: "2"},
"bool": {WantOutput: "true"},
"obj": {WantErr: true},
"null": {WantErr: true},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
var printed string
ui := cli.NewMockUi()
c := &OutputCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
rawPrint: func(s string) {
printed = s
},
}
args := []string{
"-state", statePath,
"-raw",
name,
}
code := c.Run(args)
if code != 0 {
if !test.WantErr {
t.Errorf("unexpected failure\n%s", ui.ErrorWriter.String())
}
return
}
if test.WantErr {
t.Fatalf("succeeded, but want error")
}
if got, want := printed, test.WantOutput; got != want {
t.Errorf("wrong result\ngot: %q\nwant: %q", got, want)
}
})
}
}
func TestOutput_emptyOutputs(t *testing.T) {
originalState := states.NewState()
statePath := testStateFile(t, originalState)
p := testProvider()
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
args := []string{
"-state", statePath,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
if got, want := ui.ErrorWriter.String(), "Warning: No outputs found"; !strings.Contains(got, want) {
t.Fatalf("bad output: expected to contain %q, got:\n%s", want, got)
}
}
func TestOutput_jsonEmptyOutputs(t *testing.T) {
originalState := states.NewState()
statePath := testStateFile(t, originalState)
p := testProvider()
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
args := []string{
"-state", statePath,
"-json",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
actual := strings.TrimSpace(ui.OutputWriter.String())
expected := "{}"
if actual != expected {
t.Fatalf("bad:\n%#v\n%#v", expected, actual)
}
}
func TestMissingModuleOutput(t *testing.T) {
originalState := states.BuildState(func(s *states.SyncState) {
s.SetOutputValue(
addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance),
cty.StringVal("bar"),
false,
)
})
statePath := testStateFile(t, originalState)
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
args := []string{
"-state", statePath,
"-module", "not_existing_module",
"blah",
}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
}
func TestOutput_badVar(t *testing.T) {
originalState := states.BuildState(func(s *states.SyncState) {
s.SetOutputValue(
addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance),
cty.StringVal("bar"),
false,
)
})
statePath := testStateFile(t, originalState)
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
args := []string{
"-state", statePath,
"bar",
}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
}
func TestOutput_blank(t *testing.T) {
originalState := states.BuildState(func(s *states.SyncState) {
s.SetOutputValue(
addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance),
cty.StringVal("bar"),
false,
)
s.SetOutputValue(
addrs.OutputValue{Name: "name"}.Absolute(addrs.RootModuleInstance),
cty.StringVal("john-doe"),
false,
)
})
statePath := testStateFile(t, originalState)
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
args := []string{
"-state", statePath,
"",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
expectedOutput := "foo = \"bar\"\nname = \"john-doe\"\n"
output := ui.OutputWriter.String()
if output != expectedOutput {
t.Fatalf("wrong output\ngot: %#v\nwant: %#v", output, expectedOutput)
}
}
func TestOutput_manyArgs(t *testing.T) {
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
args := []string{
"bad",
"bad",
}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: \n%s", ui.OutputWriter.String())
}
}
func TestOutput_noArgs(t *testing.T) {
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
args := []string{}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.OutputWriter.String())
}
}
func TestOutput_noState(t *testing.T) {
originalState := states.NewState()
statePath := testStateFile(t, originalState)
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
args := []string{
"-state", statePath,
"foo",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
}
func TestOutput_noVars(t *testing.T) {
originalState := states.NewState()
statePath := testStateFile(t, originalState)
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
args := []string{
"-state", statePath,
"bar",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
}
func TestOutput_stateDefault(t *testing.T) {
originalState := states.BuildState(func(s *states.SyncState) {
s.SetOutputValue(
addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance),
cty.StringVal("bar"),
false,
)
})
// Write the state file in a temporary directory with the
// default filename.
td := testTempDir(t)
statePath := filepath.Join(td, DefaultStateFilename)
f, err := os.Create(statePath)
if err != nil {
t.Fatalf("err: %s", err)
}
err = writeStateForTesting(originalState, f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
// Change to that directory
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("err: %s", err)
}
if err := os.Chdir(filepath.Dir(statePath)); err != nil {
t.Fatalf("err: %s", err)
}
defer os.Chdir(cwd)
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
args := []string{
"foo",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
actual := strings.TrimSpace(ui.OutputWriter.String())
if actual != `"bar"` {
t.Fatalf("bad: %#v", actual)
}
}