github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/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/hashicorp/terraform/internal/command/arguments" 15 "github.com/hashicorp/terraform/internal/repl" 16 "github.com/hashicorp/terraform/internal/states" 17 "github.com/hashicorp/terraform/internal/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 }