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) } }