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  }