github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/views/output.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package views
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/json"
     9  	"fmt"
    10  	"sort"
    11  	"strings"
    12  
    13  	"github.com/zclconf/go-cty/cty"
    14  	"github.com/zclconf/go-cty/cty/convert"
    15  	ctyjson "github.com/zclconf/go-cty/cty/json"
    16  
    17  	"github.com/terramate-io/tf/command/arguments"
    18  	"github.com/terramate-io/tf/repl"
    19  	"github.com/terramate-io/tf/states"
    20  	"github.com/terramate-io/tf/tfdiags"
    21  )
    22  
    23  // The Output view renders either one or all outputs, depending on whether or
    24  // not the name argument is empty.
    25  type Output interface {
    26  	Output(name string, outputs map[string]*states.OutputValue) tfdiags.Diagnostics
    27  	Diagnostics(diags tfdiags.Diagnostics)
    28  }
    29  
    30  // NewOutput returns an initialized Output implementation for the given ViewType.
    31  func NewOutput(vt arguments.ViewType, view *View) Output {
    32  	switch vt {
    33  	case arguments.ViewJSON:
    34  		return &OutputJSON{view: view}
    35  	case arguments.ViewRaw:
    36  		return &OutputRaw{view: view}
    37  	case arguments.ViewHuman:
    38  		return &OutputHuman{view: view}
    39  	default:
    40  		panic(fmt.Sprintf("unknown view type %v", vt))
    41  	}
    42  }
    43  
    44  // The OutputHuman implementation renders outputs in a format equivalent to HCL
    45  // source. This uses the same formatting logic as in the console REPL.
    46  type OutputHuman struct {
    47  	view *View
    48  }
    49  
    50  var _ Output = (*OutputHuman)(nil)
    51  
    52  func (v *OutputHuman) Output(name string, outputs map[string]*states.OutputValue) tfdiags.Diagnostics {
    53  	var diags tfdiags.Diagnostics
    54  
    55  	if len(outputs) == 0 {
    56  		diags = diags.Append(noOutputsWarning())
    57  		return diags
    58  	}
    59  
    60  	if name != "" {
    61  		output, ok := outputs[name]
    62  		if !ok {
    63  			diags = diags.Append(missingOutputError(name))
    64  			return diags
    65  		}
    66  		result := repl.FormatValue(output.Value, 0)
    67  		v.view.streams.Println(result)
    68  		return nil
    69  	}
    70  
    71  	outputBuf := new(bytes.Buffer)
    72  	if len(outputs) > 0 {
    73  		// Output the outputs in alphabetical order
    74  		keyLen := 0
    75  		ks := make([]string, 0, len(outputs))
    76  		for key := range outputs {
    77  			ks = append(ks, key)
    78  			if len(key) > keyLen {
    79  				keyLen = len(key)
    80  			}
    81  		}
    82  		sort.Strings(ks)
    83  
    84  		for _, k := range ks {
    85  			v := outputs[k]
    86  			if v.Sensitive {
    87  				outputBuf.WriteString(fmt.Sprintf("%s = <sensitive>\n", k))
    88  				continue
    89  			}
    90  
    91  			result := repl.FormatValue(v.Value, 0)
    92  			outputBuf.WriteString(fmt.Sprintf("%s = %s\n", k, result))
    93  		}
    94  	}
    95  
    96  	v.view.streams.Println(strings.TrimSpace(outputBuf.String()))
    97  
    98  	return nil
    99  }
   100  
   101  func (v *OutputHuman) Diagnostics(diags tfdiags.Diagnostics) {
   102  	v.view.Diagnostics(diags)
   103  }
   104  
   105  // The OutputRaw implementation renders single string, number, or boolean
   106  // output values directly and without quotes or other formatting. This is
   107  // intended for use in shell scripting or other environments where the exact
   108  // type of an output value is not important.
   109  type OutputRaw struct {
   110  	view *View
   111  }
   112  
   113  var _ Output = (*OutputRaw)(nil)
   114  
   115  func (v *OutputRaw) Output(name string, outputs map[string]*states.OutputValue) tfdiags.Diagnostics {
   116  	var diags tfdiags.Diagnostics
   117  
   118  	if len(outputs) == 0 {
   119  		diags = diags.Append(noOutputsWarning())
   120  		return diags
   121  	}
   122  
   123  	if name == "" {
   124  		diags = diags.Append(fmt.Errorf("Raw output format is only supported for single outputs"))
   125  		return diags
   126  	}
   127  
   128  	output, ok := outputs[name]
   129  	if !ok {
   130  		diags = diags.Append(missingOutputError(name))
   131  		return diags
   132  	}
   133  
   134  	strV, err := convert.Convert(output.Value, cty.String)
   135  	if err != nil {
   136  		diags = diags.Append(tfdiags.Sourceless(
   137  			tfdiags.Error,
   138  			"Unsupported value for raw output",
   139  			fmt.Sprintf(
   140  				"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.",
   141  				name, output.Value.Type().FriendlyName(),
   142  			),
   143  		))
   144  		return diags
   145  	}
   146  	if strV.IsNull() {
   147  		diags = diags.Append(tfdiags.Sourceless(
   148  			tfdiags.Error,
   149  			"Unsupported value for raw output",
   150  			fmt.Sprintf(
   151  				"The value for output value %q is null, so -raw mode cannot print it.",
   152  				name,
   153  			),
   154  		))
   155  		return diags
   156  	}
   157  	if !strV.IsKnown() {
   158  		// Since we're working with values from the state it would be very
   159  		// odd to end up in here, but we'll handle it anyway to avoid a
   160  		// panic in case our rules somehow change in future.
   161  		diags = diags.Append(tfdiags.Sourceless(
   162  			tfdiags.Error,
   163  			"Unsupported value for raw output",
   164  			fmt.Sprintf(
   165  				"The value for output value %q won't be known until after a successful terraform apply, so -raw mode cannot print it.",
   166  				name,
   167  			),
   168  		))
   169  		return diags
   170  	}
   171  	// If we get out here then we should have a valid string to print.
   172  	// We're writing it using Print here so that a shell caller will get
   173  	// exactly the value and no extra whitespace (including trailing newline).
   174  	v.view.streams.Print(strV.AsString())
   175  	return nil
   176  }
   177  
   178  func (v *OutputRaw) Diagnostics(diags tfdiags.Diagnostics) {
   179  	v.view.Diagnostics(diags)
   180  }
   181  
   182  // The OutputJSON implementation renders outputs as JSON values. When rendering
   183  // a single output, only the value is displayed. When rendering all outputs,
   184  // the result is a JSON object with keys matching the output names and object
   185  // values including type and sensitivity metadata.
   186  type OutputJSON struct {
   187  	view *View
   188  }
   189  
   190  var _ Output = (*OutputJSON)(nil)
   191  
   192  func (v *OutputJSON) Output(name string, outputs map[string]*states.OutputValue) tfdiags.Diagnostics {
   193  	var diags tfdiags.Diagnostics
   194  
   195  	if name != "" {
   196  		output, ok := outputs[name]
   197  		if !ok {
   198  			diags = diags.Append(missingOutputError(name))
   199  			return diags
   200  		}
   201  		value := output.Value
   202  
   203  		jsonOutput, err := ctyjson.Marshal(value, value.Type())
   204  		if err != nil {
   205  			diags = diags.Append(err)
   206  			return diags
   207  		}
   208  
   209  		v.view.streams.Println(string(jsonOutput))
   210  
   211  		return nil
   212  	}
   213  
   214  	// Due to a historical accident, the switch from state version 2 to
   215  	// 3 caused our JSON output here to be the full metadata about the
   216  	// outputs rather than just the output values themselves as we'd
   217  	// show in the single value case. We must now maintain that behavior
   218  	// for compatibility, so this is an emulation of the JSON
   219  	// serialization of outputs used in state format version 3.
   220  	type OutputMeta struct {
   221  		Sensitive bool            `json:"sensitive"`
   222  		Type      json.RawMessage `json:"type"`
   223  		Value     json.RawMessage `json:"value"`
   224  	}
   225  	outputMetas := map[string]OutputMeta{}
   226  
   227  	for n, os := range outputs {
   228  		jsonVal, err := ctyjson.Marshal(os.Value, os.Value.Type())
   229  		if err != nil {
   230  			diags = diags.Append(err)
   231  			return diags
   232  		}
   233  		jsonType, err := ctyjson.MarshalType(os.Value.Type())
   234  		if err != nil {
   235  			diags = diags.Append(err)
   236  			return diags
   237  		}
   238  		outputMetas[n] = OutputMeta{
   239  			Sensitive: os.Sensitive,
   240  			Type:      json.RawMessage(jsonType),
   241  			Value:     json.RawMessage(jsonVal),
   242  		}
   243  	}
   244  
   245  	jsonOutputs, err := json.MarshalIndent(outputMetas, "", "  ")
   246  	if err != nil {
   247  		diags = diags.Append(err)
   248  		return diags
   249  	}
   250  
   251  	v.view.streams.Println(string(jsonOutputs))
   252  
   253  	return nil
   254  }
   255  
   256  func (v *OutputJSON) Diagnostics(diags tfdiags.Diagnostics) {
   257  	v.view.Diagnostics(diags)
   258  }
   259  
   260  // For text and raw output modes, an empty map of outputs is considered a
   261  // separate and higher priority failure mode than an output not being present
   262  // in a non-empty map. This warning diagnostic explains how this might have
   263  // happened.
   264  func noOutputsWarning() tfdiags.Diagnostic {
   265  	return tfdiags.Sourceless(
   266  		tfdiags.Warning,
   267  		"No outputs found",
   268  		"The state file either has no outputs defined, or all the defined "+
   269  			"outputs are empty. Please define an output in your configuration "+
   270  			"with the `output` keyword and run `terraform refresh` for it to "+
   271  			"become available. If you are using interpolation, please verify "+
   272  			"the interpolated value is not empty. You can use the "+
   273  			"`terraform console` command to assist.",
   274  	)
   275  }
   276  
   277  // Attempting to display a missing output results in this failure, which
   278  // includes suggestions on how to rectify the problem.
   279  func missingOutputError(name string) tfdiags.Diagnostic {
   280  	return tfdiags.Sourceless(
   281  		tfdiags.Error,
   282  		fmt.Sprintf("Output %q not found", name),
   283  		"The output variable requested could not be found in the state "+
   284  			"file. If you recently added this to your configuration, be "+
   285  			"sure to run `terraform apply`, since the state won't be updated "+
   286  			"with new output variables until that command is run.",
   287  	)
   288  }