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 }