opentofu/internal/command/views/output.go
2021-09-01 11:36:21 -04:00

286 lines
8.1 KiB
Go

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/internal/command/arguments"
"github.com/hashicorp/terraform/internal/repl"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/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 = <sensitive>\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
}
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.",
)
}