github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/command/jsonformat/plan.go (about) 1 package jsonformat 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "sort" 8 "strings" 9 10 "github.com/hashicorp/terraform/internal/command/format" 11 "github.com/hashicorp/terraform/internal/command/jsonformat/computed" 12 "github.com/hashicorp/terraform/internal/command/jsonformat/computed/renderers" 13 "github.com/hashicorp/terraform/internal/command/jsonplan" 14 "github.com/hashicorp/terraform/internal/command/jsonprovider" 15 "github.com/hashicorp/terraform/internal/command/jsonstate" 16 "github.com/hashicorp/terraform/internal/plans" 17 ) 18 19 type PlanRendererOpt int 20 21 const ( 22 detectedDrift string = "drift" 23 proposedChange string = "change" 24 25 Errored PlanRendererOpt = iota 26 CanNotApply 27 ) 28 29 type Plan struct { 30 PlanFormatVersion string `json:"plan_format_version"` 31 OutputChanges map[string]jsonplan.Change `json:"output_changes"` 32 ResourceChanges []jsonplan.ResourceChange `json:"resource_changes"` 33 ResourceDrift []jsonplan.ResourceChange `json:"resource_drift"` 34 RelevantAttributes []jsonplan.ResourceAttr `json:"relevant_attributes"` 35 36 ProviderFormatVersion string `json:"provider_format_version"` 37 ProviderSchemas map[string]*jsonprovider.Provider `json:"provider_schemas"` 38 } 39 40 func (plan Plan) getSchema(change jsonplan.ResourceChange) *jsonprovider.Schema { 41 switch change.Mode { 42 case jsonstate.ManagedResourceMode: 43 return plan.ProviderSchemas[change.ProviderName].ResourceSchemas[change.Type] 44 case jsonstate.DataResourceMode: 45 return plan.ProviderSchemas[change.ProviderName].DataSourceSchemas[change.Type] 46 default: 47 panic("found unrecognized resource mode: " + change.Mode) 48 } 49 } 50 51 func (plan Plan) renderHuman(renderer Renderer, mode plans.Mode, opts ...PlanRendererOpt) { 52 checkOpts := func(target PlanRendererOpt) bool { 53 for _, opt := range opts { 54 if opt == target { 55 return true 56 } 57 } 58 return false 59 } 60 61 diffs := precomputeDiffs(plan, mode) 62 haveRefreshChanges := renderHumanDiffDrift(renderer, diffs, mode) 63 64 willPrintResourceChanges := false 65 counts := make(map[plans.Action]int) 66 var changes []diff 67 for _, diff := range diffs.changes { 68 action := jsonplan.UnmarshalActions(diff.change.Change.Actions) 69 if action == plans.NoOp && !diff.Moved() { 70 // Don't show anything for NoOp changes. 71 continue 72 } 73 if action == plans.Delete && diff.change.Mode != jsonstate.ManagedResourceMode { 74 // Don't render anything for deleted data sources. 75 continue 76 } 77 78 changes = append(changes, diff) 79 80 // Don't count move-only changes 81 if action != plans.NoOp { 82 willPrintResourceChanges = true 83 counts[action]++ 84 } 85 } 86 87 // Precompute the outputs early, so we can make a decision about whether we 88 // display the "there are no changes messages". 89 outputs := renderHumanDiffOutputs(renderer, diffs.outputs) 90 91 if len(changes) == 0 && len(outputs) == 0 { 92 // If we didn't find any changes to report at all then this is a 93 // "No changes" plan. How we'll present this depends on whether 94 // the plan is "applyable" and, if so, whether it had refresh changes 95 // that we already would've presented above. 96 97 if checkOpts(Errored) { 98 if haveRefreshChanges { 99 renderer.Streams.Print(format.HorizontalRule(renderer.Colorize, renderer.Streams.Stdout.Columns())) 100 renderer.Streams.Println() 101 } 102 renderer.Streams.Print( 103 renderer.Colorize.Color("\n[reset][bold][red]Planning failed.[reset][bold] Terraform encountered an error while generating this plan.[reset]\n\n"), 104 ) 105 } else { 106 switch mode { 107 case plans.RefreshOnlyMode: 108 if haveRefreshChanges { 109 // We already generated a sufficient prompt about what will 110 // happen if applying this change above, so we don't need to 111 // say anything more. 112 return 113 } 114 115 renderer.Streams.Print(renderer.Colorize.Color("\n[reset][bold][green]No changes.[reset][bold] Your infrastructure still matches the configuration.[reset]\n\n")) 116 renderer.Streams.Println(format.WordWrap( 117 "Terraform has checked that the real remote objects still match the result of your most recent changes, and found no differences.", 118 renderer.Streams.Stdout.Columns())) 119 case plans.DestroyMode: 120 if haveRefreshChanges { 121 renderer.Streams.Print(format.HorizontalRule(renderer.Colorize, renderer.Streams.Stdout.Columns())) 122 fmt.Fprintln(renderer.Streams.Stdout.File) 123 } 124 renderer.Streams.Print(renderer.Colorize.Color("\n[reset][bold][green]No changes.[reset][bold] No objects need to be destroyed.[reset]\n\n")) 125 renderer.Streams.Println(format.WordWrap( 126 "Either you have not created any objects yet or the existing objects were already deleted outside of Terraform.", 127 renderer.Streams.Stdout.Columns())) 128 default: 129 if haveRefreshChanges { 130 renderer.Streams.Print(format.HorizontalRule(renderer.Colorize, renderer.Streams.Stdout.Columns())) 131 renderer.Streams.Println("") 132 } 133 renderer.Streams.Print( 134 renderer.Colorize.Color("\n[reset][bold][green]No changes.[reset][bold] Your infrastructure matches the configuration.[reset]\n\n"), 135 ) 136 137 if haveRefreshChanges { 138 if !checkOpts(CanNotApply) { 139 // In this case, applying this plan will not change any 140 // remote objects but _will_ update the state to match what 141 // we detected during refresh, so we'll reassure the user 142 // about that. 143 renderer.Streams.Println(format.WordWrap( 144 "Your configuration already matches the changes detected above, so applying this plan will only update the state to include the changes detected above and won't change any real infrastructure.", 145 renderer.Streams.Stdout.Columns(), 146 )) 147 } else { 148 // In this case we detected changes during refresh but this isn't 149 // a planning mode where we consider those to be applyable. The 150 // user must re-run in refresh-only mode in order to update the 151 // state to match the upstream changes. 152 suggestion := "." 153 if !renderer.RunningInAutomation { 154 // The normal message includes a specific command line to run. 155 suggestion = ":\n terraform apply -refresh-only" 156 } 157 renderer.Streams.Println(format.WordWrap( 158 "Your configuration already matches the changes detected above. If you'd like to update the Terraform state to match, create and apply a refresh-only plan"+suggestion, 159 renderer.Streams.Stdout.Columns(), 160 )) 161 } 162 return 163 } 164 165 // If we get down here then we're just in the simple situation where 166 // the plan isn't applyable at all. 167 renderer.Streams.Println(format.WordWrap( 168 "Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.", 169 renderer.Streams.Stdout.Columns(), 170 )) 171 } 172 } 173 } 174 175 if haveRefreshChanges { 176 renderer.Streams.Print(format.HorizontalRule(renderer.Colorize, renderer.Streams.Stdout.Columns())) 177 renderer.Streams.Println() 178 } 179 180 if willPrintResourceChanges { 181 renderer.Streams.Println(format.WordWrap( 182 "\nTerraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:", 183 renderer.Streams.Stdout.Columns())) 184 if counts[plans.Create] > 0 { 185 renderer.Streams.Println(renderer.Colorize.Color(actionDescription(plans.Create))) 186 } 187 if counts[plans.Update] > 0 { 188 renderer.Streams.Println(renderer.Colorize.Color(actionDescription(plans.Update))) 189 } 190 if counts[plans.Delete] > 0 { 191 renderer.Streams.Println(renderer.Colorize.Color(actionDescription(plans.Delete))) 192 } 193 if counts[plans.DeleteThenCreate] > 0 { 194 renderer.Streams.Println(renderer.Colorize.Color(actionDescription(plans.DeleteThenCreate))) 195 } 196 if counts[plans.CreateThenDelete] > 0 { 197 renderer.Streams.Println(renderer.Colorize.Color(actionDescription(plans.CreateThenDelete))) 198 } 199 if counts[plans.Read] > 0 { 200 renderer.Streams.Println(renderer.Colorize.Color(actionDescription(plans.Read))) 201 } 202 } 203 204 if len(changes) > 0 { 205 if checkOpts(Errored) { 206 renderer.Streams.Printf("\nTerraform planned the following actions, but then encountered a problem:\n") 207 } else { 208 renderer.Streams.Printf("\nTerraform will perform the following actions:\n") 209 } 210 211 for _, change := range changes { 212 diff, render := renderHumanDiff(renderer, change, proposedChange) 213 if render { 214 fmt.Fprintln(renderer.Streams.Stdout.File) 215 renderer.Streams.Println(diff) 216 } 217 } 218 219 renderer.Streams.Printf( 220 renderer.Colorize.Color("\n[bold]Plan:[reset] %d to add, %d to change, %d to destroy.\n"), 221 counts[plans.Create]+counts[plans.DeleteThenCreate]+counts[plans.CreateThenDelete], 222 counts[plans.Update], 223 counts[plans.Delete]+counts[plans.DeleteThenCreate]+counts[plans.CreateThenDelete]) 224 } 225 226 if len(outputs) > 0 { 227 renderer.Streams.Print("\nChanges to Outputs:\n") 228 renderer.Streams.Printf("%s\n", outputs) 229 230 if len(counts) == 0 { 231 // If we have output changes but not resource changes then we 232 // won't have output any indication about the changes at all yet, 233 // so we need some extra context about what it would mean to 234 // apply a change that _only_ includes output changes. 235 renderer.Streams.Println(format.WordWrap( 236 "\nYou can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure.", 237 renderer.Streams.Stdout.Columns())) 238 } 239 } 240 } 241 242 func renderHumanDiffOutputs(renderer Renderer, outputs map[string]computed.Diff) string { 243 var rendered []string 244 245 var keys []string 246 escapedKeys := make(map[string]string) 247 var escapedKeyMaxLen int 248 for key := range outputs { 249 escapedKey := renderers.EnsureValidAttributeName(key) 250 keys = append(keys, key) 251 escapedKeys[key] = escapedKey 252 if len(escapedKey) > escapedKeyMaxLen { 253 escapedKeyMaxLen = len(escapedKey) 254 } 255 } 256 sort.Strings(keys) 257 258 for _, key := range keys { 259 output := outputs[key] 260 if output.Action != plans.NoOp { 261 rendered = append(rendered, fmt.Sprintf("%s %-*s = %s", renderer.Colorize.Color(format.DiffActionSymbol(output.Action)), escapedKeyMaxLen, escapedKeys[key], output.RenderHuman(0, computed.NewRenderHumanOpts(renderer.Colorize)))) 262 } 263 } 264 return strings.Join(rendered, "\n") 265 } 266 267 func renderHumanDiffDrift(renderer Renderer, diffs diffs, mode plans.Mode) bool { 268 var drs []diff 269 270 // In refresh-only mode, we show all resources marked as drifted, 271 // including those which have moved without other changes. In other plan 272 // modes, move-only changes will be rendered in the planned changes, so 273 // we skip them here. 274 275 if mode == plans.RefreshOnlyMode { 276 drs = diffs.drift 277 } else { 278 for _, dr := range diffs.drift { 279 if dr.diff.Action != plans.NoOp { 280 drs = append(drs, dr) 281 } 282 } 283 } 284 285 if len(drs) == 0 { 286 return false 287 } 288 289 // If the overall plan is empty, and it's not a refresh only plan then we 290 // won't show any drift changes. 291 if diffs.Empty() && mode != plans.RefreshOnlyMode { 292 return false 293 } 294 295 renderer.Streams.Print(renderer.Colorize.Color("\n[bold][cyan]Note:[reset][bold] Objects have changed outside of Terraform\n")) 296 renderer.Streams.Println() 297 renderer.Streams.Print(format.WordWrap( 298 "Terraform detected the following changes made outside of Terraform since the last \"terraform apply\" which may have affected this plan:\n", 299 renderer.Streams.Stdout.Columns())) 300 301 for _, drift := range drs { 302 diff, render := renderHumanDiff(renderer, drift, detectedDrift) 303 if render { 304 renderer.Streams.Println() 305 renderer.Streams.Println(diff) 306 } 307 } 308 309 switch mode { 310 case plans.RefreshOnlyMode: 311 renderer.Streams.Println(format.WordWrap( 312 "\n\nThis is a refresh-only plan, so Terraform will not take any actions to undo these. If you were expecting these changes then you can apply this plan to record the updated values in the Terraform state without changing any remote objects.", 313 renderer.Streams.Stdout.Columns(), 314 )) 315 default: 316 renderer.Streams.Println(format.WordWrap( 317 "\n\nUnless you have made equivalent changes to your configuration, or ignored the relevant attributes using ignore_changes, the following plan may include actions to undo or respond to these changes.", 318 renderer.Streams.Stdout.Columns(), 319 )) 320 } 321 322 return true 323 } 324 325 func renderHumanDiff(renderer Renderer, diff diff, cause string) (string, bool) { 326 327 // Internally, our computed diffs can't tell the difference between a 328 // replace action (eg. CreateThenDestroy, DestroyThenCreate) and a simple 329 // update action. So, at the top most level we rely on the action provided 330 // by the plan itself instead of what we compute. Nested attributes and 331 // blocks however don't have the replace type of actions, so we can trust 332 // the computed actions of these. 333 334 action := jsonplan.UnmarshalActions(diff.change.Change.Actions) 335 if action == plans.NoOp && (len(diff.change.PreviousAddress) == 0 || diff.change.PreviousAddress == diff.change.Address) { 336 // Skip resource changes that have nothing interesting to say. 337 return "", false 338 } 339 340 var buf bytes.Buffer 341 buf.WriteString(renderer.Colorize.Color(resourceChangeComment(diff.change, action, cause))) 342 buf.WriteString(fmt.Sprintf("%s %s %s", renderer.Colorize.Color(format.DiffActionSymbol(action)), resourceChangeHeader(diff.change), diff.diff.RenderHuman(0, computed.NewRenderHumanOpts(renderer.Colorize)))) 343 return buf.String(), true 344 } 345 346 func resourceChangeComment(resource jsonplan.ResourceChange, action plans.Action, changeCause string) string { 347 var buf bytes.Buffer 348 349 dispAddr := resource.Address 350 if len(resource.Deposed) != 0 { 351 dispAddr = fmt.Sprintf("%s (deposed object %s)", dispAddr, resource.Deposed) 352 } 353 354 switch action { 355 case plans.Create: 356 buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be created", dispAddr)) 357 case plans.Read: 358 buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be read during apply", dispAddr)) 359 switch resource.ActionReason { 360 case jsonplan.ResourceInstanceReadBecauseConfigUnknown: 361 buf.WriteString("\n # (config refers to values not yet known)") 362 case jsonplan.ResourceInstanceReadBecauseDependencyPending: 363 buf.WriteString("\n # (depends on a resource or a module with changes pending)") 364 } 365 case plans.Update: 366 switch changeCause { 367 case proposedChange: 368 buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be updated in-place", dispAddr)) 369 case detectedDrift: 370 buf.WriteString(fmt.Sprintf("[bold] # %s[reset] has changed", dispAddr)) 371 default: 372 buf.WriteString(fmt.Sprintf("[bold] # %s[reset] update (unknown reason %s)", dispAddr, changeCause)) 373 } 374 case plans.CreateThenDelete, plans.DeleteThenCreate: 375 switch resource.ActionReason { 376 case jsonplan.ResourceInstanceReplaceBecauseTainted: 377 buf.WriteString(fmt.Sprintf("[bold] # %s[reset] is tainted, so must be [bold][red]replaced[reset]", dispAddr)) 378 case jsonplan.ResourceInstanceReplaceByRequest: 379 buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be [bold][red]replaced[reset], as requested", dispAddr)) 380 case jsonplan.ResourceInstanceReplaceByTriggers: 381 buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be [bold][red]replaced[reset] due to changes in replace_triggered_by", dispAddr)) 382 default: 383 buf.WriteString(fmt.Sprintf("[bold] # %s[reset] must be [bold][red]replaced[reset]", dispAddr)) 384 } 385 case plans.Delete: 386 switch changeCause { 387 case proposedChange: 388 buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be [bold][red]destroyed[reset]", dispAddr)) 389 case detectedDrift: 390 buf.WriteString(fmt.Sprintf("[bold] # %s[reset] has been deleted", dispAddr)) 391 default: 392 buf.WriteString(fmt.Sprintf("[bold] # %s[reset] delete (unknown reason %s)", dispAddr, changeCause)) 393 } 394 // We can sometimes give some additional detail about why we're 395 // proposing to delete. We show this as additional notes, rather than 396 // as additional wording in the main action statement, in an attempt 397 // to make the "will be destroyed" message prominent and consistent 398 // in all cases, for easier scanning of this often-risky action. 399 switch resource.ActionReason { 400 case jsonplan.ResourceInstanceDeleteBecauseNoResourceConfig: 401 buf.WriteString(fmt.Sprintf("\n # (because %s.%s is not in configuration)", resource.Type, resource.Name)) 402 case jsonplan.ResourceInstanceDeleteBecauseNoMoveTarget: 403 buf.WriteString(fmt.Sprintf("\n # (because %s was moved to %s, which is not in configuration)", resource.PreviousAddress, resource.Address)) 404 case jsonplan.ResourceInstanceDeleteBecauseNoModule: 405 // FIXME: Ideally we'd truncate addr.Module to reflect the earliest 406 // step that doesn't exist, so it's clearer which call this refers 407 // to, but we don't have enough information out here in the UI layer 408 // to decide that; only the "expander" in Terraform Core knows 409 // which module instance keys are actually declared. 410 buf.WriteString(fmt.Sprintf("\n # (because %s is not in configuration)", resource.ModuleAddress)) 411 case jsonplan.ResourceInstanceDeleteBecauseWrongRepetition: 412 var index interface{} 413 if resource.Index != nil { 414 if err := json.Unmarshal(resource.Index, &index); err != nil { 415 panic(err) 416 } 417 } 418 419 // We have some different variations of this one 420 switch index.(type) { 421 case nil: 422 buf.WriteString("\n # (because resource uses count or for_each)") 423 case float64: 424 buf.WriteString("\n # (because resource does not use count)") 425 case string: 426 buf.WriteString("\n # (because resource does not use for_each)") 427 } 428 case jsonplan.ResourceInstanceDeleteBecauseCountIndex: 429 buf.WriteString(fmt.Sprintf("\n # (because index [%s] is out of range for count)", resource.Index)) 430 case jsonplan.ResourceInstanceDeleteBecauseEachKey: 431 buf.WriteString(fmt.Sprintf("\n # (because key [%s] is not in for_each map)", resource.Index)) 432 } 433 if len(resource.Deposed) != 0 { 434 // Some extra context about this unusual situation. 435 buf.WriteString("\n # (left over from a partially-failed replacement of this instance)") 436 } 437 case plans.NoOp: 438 if len(resource.PreviousAddress) > 0 && resource.PreviousAddress != resource.Address { 439 buf.WriteString(fmt.Sprintf("[bold] # %s[reset] has moved to [bold]%s[reset]", resource.PreviousAddress, dispAddr)) 440 break 441 } 442 fallthrough 443 default: 444 // should never happen, since the above is exhaustive 445 buf.WriteString(fmt.Sprintf("%s has an action the plan renderer doesn't support (this is a bug)", dispAddr)) 446 } 447 buf.WriteString("\n") 448 449 if len(resource.PreviousAddress) > 0 && resource.PreviousAddress != resource.Address && action != plans.NoOp { 450 buf.WriteString(fmt.Sprintf(" # [reset](moved from %s)\n", resource.PreviousAddress)) 451 } 452 453 return buf.String() 454 } 455 456 func resourceChangeHeader(change jsonplan.ResourceChange) string { 457 mode := "resource" 458 if change.Mode != jsonstate.ManagedResourceMode { 459 mode = "data" 460 } 461 return fmt.Sprintf("%s \"%s\" \"%s\"", mode, change.Type, change.Name) 462 } 463 464 func actionDescription(action plans.Action) string { 465 switch action { 466 case plans.Create: 467 return " [green]+[reset] create" 468 case plans.Delete: 469 return " [red]-[reset] destroy" 470 case plans.Update: 471 return " [yellow]~[reset] update in-place" 472 case plans.CreateThenDelete: 473 return "[green]+[reset]/[red]-[reset] create replacement and then destroy" 474 case plans.DeleteThenCreate: 475 return "[red]-[reset]/[green]+[reset] destroy and then create replacement" 476 case plans.Read: 477 return " [cyan]<=[reset] read (data resources)" 478 default: 479 panic(fmt.Sprintf("unrecognized change type: %s", action.String())) 480 } 481 }