command: Restore the "terraform output" functionality

We previously stubbed most of this out because it hadn't yet been updated
to support the new state types, etc.

This restores all of the previous behavior as covered by the tests.

We intentionally remove one behavior that was not covered by the tests:
we used to allow retrieval of outputs from non-root modules using the
-module option, but since we no longer persist non-root outputs in the
state we can no longer support this without a full expression evaluation
walk, and that'd be overkill for this otherwise-simple command. Descendant
module outputs are not part of the public interface of a configuration
anyway, so accessing them from outside in this way is an anti-pattern.

(For debugging scenarios it is still possible to access these from
"terraform console", which _does_ do a full evaluation graph walk to
prepare its evaluation scope.)
This commit is contained in:
Martin Atkins 2018-10-13 09:24:03 -07:00
parent feff10dcb5
commit 025540789c
3 changed files with 84 additions and 107 deletions

View File

@ -11,7 +11,9 @@ import (
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/config/hcl2shim"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/repl"
"github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/tfdiags"
)
@ -375,8 +377,20 @@ func outputsAsString(state *states.State, modPath addrs.ModuleInstance, schema m
continue
}
//v := outputs[k]
outputBuf.WriteString("output printer not yet updated to use the same value formatter as 'terraform console'")
v := outputs[k]
// Our formatter still wants an old-style raw interface{} value, so
// for now we'll just shim it.
// FIXME: Port the formatter to work with cty.Value directly.
legacyVal := hcl2shim.ConfigValueFromHCL2(v.Value)
result, err := repl.FormatResult(legacyVal)
if err != nil {
// We can't really return errors from here, so we'll just have
// to stub this out. This shouldn't happen in practice anyway.
result = "<error during formatting>"
}
outputBuf.WriteString(fmt.Sprintf("%s = %s\n", k, result))
}
}

View File

@ -2,15 +2,17 @@ package command
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"sort"
"strings"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/config/hcl2shim"
"github.com/hashicorp/terraform/repl"
"github.com/hashicorp/terraform/tfdiags"
)
@ -75,9 +77,19 @@ func (c *OutputCommand) Run(args []string) int {
return 1
}
moduleAddr, addrDiags := addrs.ParseModuleInstanceStr(module)
diags = diags.Append(addrDiags)
if addrDiags.HasErrors() {
moduleAddr := addrs.RootModuleInstance
if module != "" {
// This option was supported prior to 0.12.0, but no longer supported
// because we only persist the root module outputs in state.
// (We could perhaps re-introduce this by doing an eval walk here to
// repopulate them, similar to how "terraform console" does it, but
// that requires more thought since it would imply this command
// supporting remote operations, which is a big change.)
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unsupported option",
"The -module option is no longer supported since Terraform 0.12, because now only root outputs are persisted in the state.",
))
c.showDiagnostics(diags)
return 1
}
@ -91,11 +103,6 @@ func (c *OutputCommand) Run(args []string) int {
return 1
}
// TODO: We need to do an eval walk here to make sure all of the output
// values recorded in the state are up-to-date.
c.Ui.Error("output command not yet updated to do eval walk")
return 1
if !jsonOutput && (state.Empty() || len(mod.OutputValues) == 0) {
c.Ui.Error(
"The state file either has no outputs defined, or all the defined\n" +
@ -109,16 +116,45 @@ func (c *OutputCommand) Run(args []string) int {
if name == "" {
if jsonOutput {
vals := make(map[string]cty.Value, len(mod.OutputValues))
for n, os := range mod.OutputValues {
vals[n] = os.Value
// Due to a historical accident, the switch from state version 2 to
// 3 caused our JSON output here to be the full metadata about the
// outputs rather than just the output values themselves as we'd
// show in the single value case. We must now maintain that behavior
// for compatibility, so this is an emulation of the JSON
// serialization of outputs used in state format version 3.
type OutputMeta struct {
Sensitive bool `json:"sensitive"`
Type json.RawMessage `json:"type"`
Value json.RawMessage `json:"value"`
}
valsObj := cty.ObjectVal(vals)
jsonOutputs, err := ctyjson.Marshal(valsObj, valsObj.Type())
if err != nil {
return 1
outputs := map[string]OutputMeta{}
for n, os := range mod.OutputValues {
jsonVal, err := ctyjson.Marshal(os.Value, os.Value.Type())
if err != nil {
diags = diags.Append(err)
c.showDiagnostics(diags)
return 1
}
jsonType, err := ctyjson.MarshalType(os.Value.Type())
if err != nil {
diags = diags.Append(err)
c.showDiagnostics(diags)
return 1
}
outputs[n] = OutputMeta{
Sensitive: os.Sensitive,
Type: json.RawMessage(jsonType),
Value: json.RawMessage(jsonVal),
}
}
jsonOutputs, err := json.MarshalIndent(outputs, "", " ")
if err != nil {
diags = diags.Append(err)
c.showDiagnostics(diags)
return 1
}
c.Ui.Output(string(jsonOutputs))
return 0
} else {
@ -146,8 +182,17 @@ func (c *OutputCommand) Run(args []string) int {
c.Ui.Output(string(jsonOutput))
} else {
c.Ui.Error("TODO: update output command to use the same value renderer as the console")
return 1
// Our formatter still wants an old-style raw interface{} value, so
// for now we'll just shim it.
// FIXME: Port the formatter to work with cty.Value directly.
legacyVal := hcl2shim.ConfigValueFromHCL2(v)
result, err := repl.FormatResult(legacyVal)
if err != nil {
diags = diags.Append(err)
c.showDiagnostics(diags)
return 1
}
c.Ui.Output(result)
}
return 0
@ -282,9 +327,6 @@ Options:
-no-color If specified, output won't contain any color.
-module=name If specified, returns the outputs for a
specific module
-json If specified, machine readable output will be
printed in JSON format

View File

@ -46,85 +46,6 @@ func TestOutput(t *testing.T) {
}
}
func TestModuleOutput(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: "blah"}.Absolute(addrs.Module{"my_module"}.UnkeyedInstanceShim()),
cty.StringVal("tastatur"),
false,
)
})
statePath := testStateFile(t, originalState)
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
args := []string{
"-state", statePath,
"-module", "my_module",
"blah",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
actual := strings.TrimSpace(ui.OutputWriter.String())
if actual != "tastatur" {
t.Fatalf("bad: %#v", actual)
}
}
func TestModuleOutputs(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: "blah"}.Absolute(addrs.Module{"my_module"}.UnkeyedInstanceShim()),
cty.StringVal("tastatur"),
false,
)
})
statePath := testStateFile(t, originalState)
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
args := []string{
"-state", statePath,
"-module", "my_module",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
actual := strings.TrimSpace(ui.OutputWriter.String())
if actual != "blah = tastatur" {
t.Fatalf("bad: %#v", actual)
}
}
func TestOutput_nestedListAndMap(t *testing.T) {
originalState := states.BuildState(func(s *states.SyncState) {
s.SetOutputValue(
@ -159,9 +80,9 @@ func TestOutput_nestedListAndMap(t *testing.T) {
}
actual := strings.TrimSpace(ui.OutputWriter.String())
expected := "foo = [\n {\n key = value,\n key2 = value2\n },\n {\n key = value\n }\n]"
expected := "foo = [\n {\n \"key\" = \"value\"\n \"key2\" = \"value2\"\n },\n {\n \"key\" = \"value\"\n },\n]"
if actual != expected {
t.Fatalf("bad:\n%#v\n%#v", expected, actual)
t.Fatalf("wrong output\ngot: %#v\nwant: %#v", actual, expected)
}
}
@ -193,9 +114,9 @@ func TestOutput_json(t *testing.T) {
}
actual := strings.TrimSpace(ui.OutputWriter.String())
expected := "{\n \"foo\": {\n \"sensitive\": false,\n \"type\": \"string\",\n \"value\": \"bar\"\n }\n}"
expected := "{\n \"foo\": {\n \"sensitive\": false,\n \"type\": \"string\",\n \"value\": \"bar\"\n }\n}"
if actual != expected {
t.Fatalf("bad:\n%#v\n%#v", expected, actual)
t.Fatalf("wrong output\ngot: %#v\nwant: %#v", actual, expected)
}
}
@ -339,7 +260,7 @@ func TestOutput_blank(t *testing.T) {
expectedOutput := "foo = bar\nname = john-doe\n"
output := ui.OutputWriter.String()
if output != expectedOutput {
t.Fatalf("Expected output: %#v\ngiven: %#v", expectedOutput, output)
t.Fatalf("wrong output\ngot: %#v\nwant: %#v", output, expectedOutput)
}
}