github.com/kevinklinger/open_terraform@v1.3.6/noninternal/command/views/output.go (about)

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