From 025540789cde604aa56eef11a4de3e8ed08ec857 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Sat, 13 Oct 2018 09:24:03 -0700 Subject: [PATCH] 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.) --- command/apply.go | 18 ++++++++- command/output.go | 84 +++++++++++++++++++++++++++++---------- command/output_test.go | 89 +++--------------------------------------- 3 files changed, 84 insertions(+), 107 deletions(-) diff --git a/command/apply.go b/command/apply.go index 1437dc1ab..5c68f36e6 100644 --- a/command/apply.go +++ b/command/apply.go @@ -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 = "" + } + + outputBuf.WriteString(fmt.Sprintf("%s = %s\n", k, result)) } } diff --git a/command/output.go b/command/output.go index e8e172923..8d83ce662 100644 --- a/command/output.go +++ b/command/output.go @@ -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 diff --git a/command/output_test.go b/command/output_test.go index c210dff8f..dd447c60f 100644 --- a/command/output_test.go +++ b/command/output_test.go @@ -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) } }