package views import ( "bytes" "encoding/json" "fmt" "sort" "strings" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/convert" ctyjson "github.com/zclconf/go-cty/cty/json" "github.com/hashicorp/terraform/command/arguments" "github.com/hashicorp/terraform/repl" "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/tfdiags" ) // The Output view renders either one or all outputs, depending on whether or // not the name argument is empty. type Output interface { Output(name string, outputs map[string]*states.OutputValue) tfdiags.Diagnostics Diagnostics(diags tfdiags.Diagnostics) } // NewOutput returns an initialized Output implementation for the given ViewType. func NewOutput(vt arguments.ViewType, view *View) Output { switch vt { case arguments.ViewJSON: return &OutputJSON{view: view} case arguments.ViewRaw: return &OutputRaw{view: view} case arguments.ViewHuman: return &OutputHuman{view: view} default: panic(fmt.Sprintf("unknown view type %v", vt)) } } // The OutputHuman implementation renders outputs in a format equivalent to HCL // source. This uses the same formatting logic as in the console REPL. type OutputHuman struct { view *View } var _ Output = (*OutputHuman)(nil) func (v *OutputHuman) Output(name string, outputs map[string]*states.OutputValue) tfdiags.Diagnostics { var diags tfdiags.Diagnostics if len(outputs) == 0 { diags = diags.Append(noOutputsWarning()) return diags } if name != "" { output, ok := outputs[name] if !ok { diags = diags.Append(missingOutputError(name)) return diags } result := repl.FormatValue(output.Value, 0) v.view.streams.Println(result) return nil } outputBuf := new(bytes.Buffer) if len(outputs) > 0 { // Output the outputs in alphabetical order keyLen := 0 ks := make([]string, 0, len(outputs)) for key := range outputs { ks = append(ks, key) if len(key) > keyLen { keyLen = len(key) } } sort.Strings(ks) for _, k := range ks { v := outputs[k] if v.Sensitive { outputBuf.WriteString(fmt.Sprintf("%s = \n", k)) continue } result := repl.FormatValue(v.Value, 0) outputBuf.WriteString(fmt.Sprintf("%s = %s\n", k, result)) } } v.view.streams.Println(strings.TrimSpace(outputBuf.String())) return nil } func (v *OutputHuman) Diagnostics(diags tfdiags.Diagnostics) { v.view.Diagnostics(diags) } // The OutputRaw implementation renders single string, number, or boolean // output values directly and without quotes or other formatting. This is // intended for use in shell scripting or other environments where the exact // type of an output value is not important. type OutputRaw struct { view *View // Unit tests may set rawPrint to capture the output from the Output // method, which would normally go to stdout directly. rawPrint func(string) } var _ Output = (*OutputRaw)(nil) func (v *OutputRaw) Output(name string, outputs map[string]*states.OutputValue) tfdiags.Diagnostics { var diags tfdiags.Diagnostics if len(outputs) == 0 { diags = diags.Append(noOutputsWarning()) return diags } if name == "" { diags = diags.Append(fmt.Errorf("Raw output format is only supported for single outputs")) return diags } output, ok := outputs[name] if !ok { diags = diags.Append(missingOutputError(name)) return diags } strV, err := convert.Convert(output.Value, cty.String) if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Unsupported value for raw output", fmt.Sprintf( "The -raw option only supports strings, numbers, and boolean values, but output value %q is %s.\n\nUse the -json option for machine-readable representations of output values that have complex types.", name, output.Value.Type().FriendlyName(), ), )) return diags } if strV.IsNull() { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Unsupported value for raw output", fmt.Sprintf( "The value for output value %q is null, so -raw mode cannot print it.", name, ), )) return diags } if !strV.IsKnown() { // Since we're working with values from the state it would be very // odd to end up in here, but we'll handle it anyway to avoid a // panic in case our rules somehow change in future. diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Unsupported value for raw output", fmt.Sprintf( "The value for output value %q won't be known until after a successful terraform apply, so -raw mode cannot print it.", name, ), )) return diags } // If we get out here then we should have a valid string to print. // We're writing it using Print here so that a shell caller will get // exactly the value and no extra whitespace (including trailing newline). v.view.streams.Print(strV.AsString()) return nil } func (v *OutputRaw) Diagnostics(diags tfdiags.Diagnostics) { v.view.Diagnostics(diags) } // The OutputJSON implementation renders outputs as JSON values. When rendering // a single output, only the value is displayed. When rendering all outputs, // the result is a JSON object with keys matching the output names and object // values including type and sensitivity metadata. type OutputJSON struct { view *View } var _ Output = (*OutputJSON)(nil) func (v *OutputJSON) Output(name string, outputs map[string]*states.OutputValue) tfdiags.Diagnostics { var diags tfdiags.Diagnostics if name != "" { output, ok := outputs[name] if !ok { diags = diags.Append(missingOutputError(name)) return diags } value := output.Value jsonOutput, err := ctyjson.Marshal(value, value.Type()) if err != nil { diags = diags.Append(err) return diags } v.view.streams.Println(string(jsonOutput)) return nil } // 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"` } outputMetas := map[string]OutputMeta{} for n, os := range outputs { jsonVal, err := ctyjson.Marshal(os.Value, os.Value.Type()) if err != nil { diags = diags.Append(err) return diags } jsonType, err := ctyjson.MarshalType(os.Value.Type()) if err != nil { diags = diags.Append(err) return diags } outputMetas[n] = OutputMeta{ Sensitive: os.Sensitive, Type: json.RawMessage(jsonType), Value: json.RawMessage(jsonVal), } } jsonOutputs, err := json.MarshalIndent(outputMetas, "", " ") if err != nil { diags = diags.Append(err) return diags } v.view.streams.Println(string(jsonOutputs)) return nil } func (v *OutputJSON) Diagnostics(diags tfdiags.Diagnostics) { v.view.Diagnostics(diags) } // For text and raw output modes, an empty map of outputs is considered a // separate and higher priority failure mode than an output not being present // in a non-empty map. This warning diagnostic explains how this might have // happened. func noOutputsWarning() tfdiags.Diagnostic { return tfdiags.Sourceless( tfdiags.Warning, "No outputs found", "The state file either has no outputs defined, or all the defined "+ "outputs are empty. Please define an output in your configuration "+ "with the `output` keyword and run `terraform refresh` for it to "+ "become available. If you are using interpolation, please verify "+ "the interpolated value is not empty. You can use the "+ "`terraform console` command to assist.", ) } // Attempting to display a missing output results in this failure, which // includes suggestions on how to rectify the problem. func missingOutputError(name string) tfdiags.Diagnostic { return tfdiags.Sourceless( tfdiags.Error, fmt.Sprintf("Output %q not found", name), "The output variable requested could not be found in the state "+ "file. If you recently added this to your configuration, be "+ "sure to run `terraform apply`, since the state won't be updated "+ "with new output variables until that command is run.", ) }