github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/command/views/plan.go (about)

     1  package views
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"sort"
     7  	"strings"
     8  
     9  	"github.com/hashicorp/terraform/internal/addrs"
    10  	"github.com/hashicorp/terraform/internal/command/arguments"
    11  	"github.com/hashicorp/terraform/internal/command/format"
    12  	"github.com/hashicorp/terraform/internal/plans"
    13  	"github.com/hashicorp/terraform/internal/states"
    14  	"github.com/hashicorp/terraform/internal/terraform"
    15  	"github.com/hashicorp/terraform/internal/tfdiags"
    16  )
    17  
    18  // The Plan view is used for the plan command.
    19  type Plan interface {
    20  	Operation() Operation
    21  	Hooks() []terraform.Hook
    22  
    23  	Diagnostics(diags tfdiags.Diagnostics)
    24  	HelpPrompt()
    25  }
    26  
    27  // NewPlan returns an initialized Plan implementation for the given ViewType.
    28  func NewPlan(vt arguments.ViewType, view *View) Plan {
    29  	switch vt {
    30  	case arguments.ViewJSON:
    31  		return &PlanJSON{
    32  			view: NewJSONView(view),
    33  		}
    34  	case arguments.ViewHuman:
    35  		return &PlanHuman{
    36  			view:         view,
    37  			inAutomation: view.RunningInAutomation(),
    38  		}
    39  	default:
    40  		panic(fmt.Sprintf("unknown view type %v", vt))
    41  	}
    42  }
    43  
    44  // The PlanHuman implementation renders human-readable text logs, suitable for
    45  // a scrolling terminal.
    46  type PlanHuman struct {
    47  	view *View
    48  
    49  	inAutomation bool
    50  }
    51  
    52  var _ Plan = (*PlanHuman)(nil)
    53  
    54  func (v *PlanHuman) Operation() Operation {
    55  	return NewOperation(arguments.ViewHuman, v.inAutomation, v.view)
    56  }
    57  
    58  func (v *PlanHuman) Hooks() []terraform.Hook {
    59  	return []terraform.Hook{
    60  		NewUiHook(v.view),
    61  	}
    62  }
    63  
    64  func (v *PlanHuman) Diagnostics(diags tfdiags.Diagnostics) {
    65  	v.view.Diagnostics(diags)
    66  }
    67  
    68  func (v *PlanHuman) HelpPrompt() {
    69  	v.view.HelpPrompt("plan")
    70  }
    71  
    72  // The PlanJSON implementation renders streaming JSON logs, suitable for
    73  // integrating with other software.
    74  type PlanJSON struct {
    75  	view *JSONView
    76  }
    77  
    78  var _ Plan = (*PlanJSON)(nil)
    79  
    80  func (v *PlanJSON) Operation() Operation {
    81  	return &OperationJSON{view: v.view}
    82  }
    83  
    84  func (v *PlanJSON) Hooks() []terraform.Hook {
    85  	return []terraform.Hook{
    86  		newJSONHook(v.view),
    87  	}
    88  }
    89  
    90  func (v *PlanJSON) Diagnostics(diags tfdiags.Diagnostics) {
    91  	v.view.Diagnostics(diags)
    92  }
    93  
    94  func (v *PlanJSON) HelpPrompt() {
    95  }
    96  
    97  // The plan renderer is used by the Operation view (for plan and apply
    98  // commands) and the Show view (for the show command).
    99  func renderPlan(plan *plans.Plan, schemas *terraform.Schemas, view *View) {
   100  	haveRefreshChanges := renderChangesDetectedByRefresh(plan.PrevRunState, plan.PriorState, schemas, view)
   101  	if haveRefreshChanges {
   102  		switch plan.UIMode {
   103  		case plans.RefreshOnlyMode:
   104  			view.streams.Println(format.WordWrap(
   105  				"\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.",
   106  				view.outputColumns(),
   107  			))
   108  		default:
   109  			view.streams.Println(format.WordWrap(
   110  				"\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.",
   111  				view.outputColumns(),
   112  			))
   113  		}
   114  	}
   115  
   116  	counts := map[plans.Action]int{}
   117  	var rChanges []*plans.ResourceInstanceChangeSrc
   118  	for _, change := range plan.Changes.Resources {
   119  		if change.Action == plans.NoOp && !change.Moved() {
   120  			continue // We don't show anything for no-op changes
   121  		}
   122  		if change.Action == plans.Delete && change.Addr.Resource.Resource.Mode == addrs.DataResourceMode {
   123  			// Avoid rendering data sources on deletion
   124  			continue
   125  		}
   126  
   127  		rChanges = append(rChanges, change)
   128  
   129  		// Don't count move-only changes
   130  		if change.Action != plans.NoOp {
   131  			counts[change.Action]++
   132  		}
   133  	}
   134  	var changedRootModuleOutputs []*plans.OutputChangeSrc
   135  	for _, output := range plan.Changes.Outputs {
   136  		if !output.Addr.Module.IsRoot() {
   137  			continue
   138  		}
   139  		if output.ChangeSrc.Action == plans.NoOp {
   140  			continue
   141  		}
   142  		changedRootModuleOutputs = append(changedRootModuleOutputs, output)
   143  	}
   144  
   145  	if len(rChanges) == 0 && len(changedRootModuleOutputs) == 0 {
   146  		// If we didn't find any changes to report at all then this is a
   147  		// "No changes" plan. How we'll present this depends on whether
   148  		// the plan is "applyable" and, if so, whether it had refresh changes
   149  		// that we already would've presented above.
   150  
   151  		switch plan.UIMode {
   152  		case plans.RefreshOnlyMode:
   153  			if haveRefreshChanges {
   154  				// We already generated a sufficient prompt about what will
   155  				// happen if applying this change above, so we don't need to
   156  				// say anything more.
   157  				return
   158  			}
   159  
   160  			view.streams.Print(
   161  				view.colorize.Color("\n[reset][bold][green]No changes.[reset][bold] Your infrastructure still matches the configuration.[reset]\n\n"),
   162  			)
   163  			view.streams.Println(format.WordWrap(
   164  				"Terraform has checked that the real remote objects still match the result of your most recent changes, and found no differences.",
   165  				view.outputColumns(),
   166  			))
   167  
   168  		case plans.DestroyMode:
   169  			if haveRefreshChanges {
   170  				view.streams.Print(format.HorizontalRule(view.colorize, view.outputColumns()))
   171  				view.streams.Println("")
   172  			}
   173  			view.streams.Print(
   174  				view.colorize.Color("\n[reset][bold][green]No changes.[reset][bold] No objects need to be destroyed.[reset]\n\n"),
   175  			)
   176  			view.streams.Println(format.WordWrap(
   177  				"Either you have not created any objects yet or the existing objects were already deleted outside of Terraform.",
   178  				view.outputColumns(),
   179  			))
   180  
   181  		default:
   182  			if haveRefreshChanges {
   183  				view.streams.Print(format.HorizontalRule(view.colorize, view.outputColumns()))
   184  				view.streams.Println("")
   185  			}
   186  			view.streams.Print(
   187  				view.colorize.Color("\n[reset][bold][green]No changes.[reset][bold] Your infrastructure matches the configuration.[reset]\n\n"),
   188  			)
   189  
   190  			if haveRefreshChanges && !plan.CanApply() {
   191  				if plan.CanApply() {
   192  					// In this case, applying this plan will not change any
   193  					// remote objects but _will_ update the state to match what
   194  					// we detected during refresh, so we'll reassure the user
   195  					// about that.
   196  					view.streams.Println(format.WordWrap(
   197  						"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.",
   198  						view.outputColumns(),
   199  					))
   200  				} else {
   201  					// In this case we detected changes during refresh but this isn't
   202  					// a planning mode where we consider those to be applyable. The
   203  					// user must re-run in refresh-only mode in order to update the
   204  					// state to match the upstream changes.
   205  					suggestion := "."
   206  					if !view.runningInAutomation {
   207  						// The normal message includes a specific command line to run.
   208  						suggestion = ":\n  terraform apply -refresh-only"
   209  					}
   210  					view.streams.Println(format.WordWrap(
   211  						"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,
   212  						view.outputColumns(),
   213  					))
   214  				}
   215  				return
   216  			}
   217  
   218  			// If we get down here then we're just in the simple situation where
   219  			// the plan isn't applyable at all.
   220  			view.streams.Println(format.WordWrap(
   221  				"Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.",
   222  				view.outputColumns(),
   223  			))
   224  		}
   225  		return
   226  	}
   227  	if haveRefreshChanges {
   228  		view.streams.Print(format.HorizontalRule(view.colorize, view.outputColumns()))
   229  		view.streams.Println("")
   230  	}
   231  
   232  	if len(counts) > 0 {
   233  		headerBuf := &bytes.Buffer{}
   234  		fmt.Fprintf(headerBuf, "\n%s\n", strings.TrimSpace(format.WordWrap(planHeaderIntro, view.outputColumns())))
   235  		if counts[plans.Create] > 0 {
   236  			fmt.Fprintf(headerBuf, "%s create\n", format.DiffActionSymbol(plans.Create))
   237  		}
   238  		if counts[plans.Update] > 0 {
   239  			fmt.Fprintf(headerBuf, "%s update in-place\n", format.DiffActionSymbol(plans.Update))
   240  		}
   241  		if counts[plans.Delete] > 0 {
   242  			fmt.Fprintf(headerBuf, "%s destroy\n", format.DiffActionSymbol(plans.Delete))
   243  		}
   244  		if counts[plans.DeleteThenCreate] > 0 {
   245  			fmt.Fprintf(headerBuf, "%s destroy and then create replacement\n", format.DiffActionSymbol(plans.DeleteThenCreate))
   246  		}
   247  		if counts[plans.CreateThenDelete] > 0 {
   248  			fmt.Fprintf(headerBuf, "%s create replacement and then destroy\n", format.DiffActionSymbol(plans.CreateThenDelete))
   249  		}
   250  		if counts[plans.Read] > 0 {
   251  			fmt.Fprintf(headerBuf, "%s read (data resources)\n", format.DiffActionSymbol(plans.Read))
   252  		}
   253  
   254  		view.streams.Print(view.colorize.Color(headerBuf.String()))
   255  	}
   256  
   257  	if len(rChanges) > 0 {
   258  		view.streams.Printf("\nTerraform will perform the following actions:\n\n")
   259  
   260  		// Note: we're modifying the backing slice of this plan object in-place
   261  		// here. The ordering of resource changes in a plan is not significant,
   262  		// but we can only do this safely here because we can assume that nobody
   263  		// is concurrently modifying our changes while we're trying to print it.
   264  		sort.Slice(rChanges, func(i, j int) bool {
   265  			iA := rChanges[i].Addr
   266  			jA := rChanges[j].Addr
   267  			if iA.String() == jA.String() {
   268  				return rChanges[i].DeposedKey < rChanges[j].DeposedKey
   269  			}
   270  			return iA.Less(jA)
   271  		})
   272  
   273  		for _, rcs := range rChanges {
   274  			if rcs.Action == plans.NoOp && !rcs.Moved() {
   275  				continue
   276  			}
   277  
   278  			providerSchema := schemas.ProviderSchema(rcs.ProviderAddr.Provider)
   279  			if providerSchema == nil {
   280  				// Should never happen
   281  				view.streams.Printf("(schema missing for %s)\n\n", rcs.ProviderAddr)
   282  				continue
   283  			}
   284  			rSchema, _ := providerSchema.SchemaForResourceAddr(rcs.Addr.Resource.Resource)
   285  			if rSchema == nil {
   286  				// Should never happen
   287  				view.streams.Printf("(schema missing for %s)\n\n", rcs.Addr)
   288  				continue
   289  			}
   290  
   291  			view.streams.Println(format.ResourceChange(
   292  				rcs,
   293  				rSchema,
   294  				view.colorize,
   295  			))
   296  		}
   297  
   298  		// stats is similar to counts above, but:
   299  		// - it considers only resource changes
   300  		// - it simplifies "replace" into both a create and a delete
   301  		stats := map[plans.Action]int{}
   302  		for _, change := range rChanges {
   303  			switch change.Action {
   304  			case plans.CreateThenDelete, plans.DeleteThenCreate:
   305  				stats[plans.Create]++
   306  				stats[plans.Delete]++
   307  			default:
   308  				stats[change.Action]++
   309  			}
   310  		}
   311  		view.streams.Printf(
   312  			view.colorize.Color("[reset][bold]Plan:[reset] %d to add, %d to change, %d to destroy.\n"),
   313  			stats[plans.Create], stats[plans.Update], stats[plans.Delete],
   314  		)
   315  	}
   316  
   317  	// If there is at least one planned change to the root module outputs
   318  	// then we'll render a summary of those too.
   319  	if len(changedRootModuleOutputs) > 0 {
   320  		view.streams.Println(
   321  			view.colorize.Color("[reset]\n[bold]Changes to Outputs:[reset]") +
   322  				format.OutputChanges(changedRootModuleOutputs, view.colorize),
   323  		)
   324  
   325  		if len(counts) == 0 {
   326  			// If we have output changes but not resource changes then we
   327  			// won't have output any indication about the changes at all yet,
   328  			// so we need some extra context about what it would mean to
   329  			// apply a change that _only_ includes output changes.
   330  			view.streams.Println(format.WordWrap(
   331  				"\nYou can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure.",
   332  				view.outputColumns(),
   333  			))
   334  		}
   335  	}
   336  }
   337  
   338  // renderChangesDetectedByRefresh is a part of renderPlan that generates
   339  // the note about changes detected by refresh (sometimes considered as "drift").
   340  //
   341  // It will only generate output if there's at least one difference detected.
   342  // Otherwise, it will produce nothing at all. To help the caller recognize
   343  // those two situations incase subsequent output must accommodate it,
   344  // renderChangesDetectedByRefresh returns true if it produced at least one
   345  // line of output, and guarantees to always produce whole lines terminated
   346  // by newline characters.
   347  func renderChangesDetectedByRefresh(before, after *states.State, schemas *terraform.Schemas, view *View) bool {
   348  	// ManagedResourceEqual checks that the state is exactly equal for all
   349  	// managed resources; but semantically equivalent states, or changes to
   350  	// deposed instances may not actually represent changes we need to present
   351  	// to the user, so for now this only serves as a short-circuit to skip
   352  	// attempting to render the diffs below.
   353  	if after.ManagedResourcesEqual(before) {
   354  		return false
   355  	}
   356  
   357  	var diffs []string
   358  
   359  	for _, bms := range before.Modules {
   360  		for _, brs := range bms.Resources {
   361  			if brs.Addr.Resource.Mode != addrs.ManagedResourceMode {
   362  				continue // only managed resources can "drift"
   363  			}
   364  			addr := brs.Addr
   365  			prs := after.Resource(brs.Addr)
   366  
   367  			provider := brs.ProviderConfig.Provider
   368  			providerSchema := schemas.ProviderSchema(provider)
   369  			if providerSchema == nil {
   370  				// Should never happen
   371  				view.streams.Printf("(schema missing for %s)\n", provider)
   372  				continue
   373  			}
   374  			rSchema, _ := providerSchema.SchemaForResourceAddr(addr.Resource)
   375  			if rSchema == nil {
   376  				// Should never happen
   377  				view.streams.Printf("(schema missing for %s)\n", addr)
   378  				continue
   379  			}
   380  
   381  			for key, bis := range brs.Instances {
   382  				if bis.Current == nil {
   383  					// No current instance to render here
   384  					continue
   385  				}
   386  				var pis *states.ResourceInstance
   387  				if prs != nil {
   388  					pis = prs.Instance(key)
   389  				}
   390  
   391  				diff := format.ResourceInstanceDrift(
   392  					addr.Instance(key),
   393  					bis, pis,
   394  					rSchema,
   395  					view.colorize,
   396  				)
   397  				if diff != "" {
   398  					diffs = append(diffs, diff)
   399  				}
   400  			}
   401  		}
   402  	}
   403  
   404  	// If we only have changes regarding deposed instances, or the diff
   405  	// renderer is suppressing irrelevant changes from the legacy SDK, there
   406  	// may not have been anything to display to the user.
   407  	if len(diffs) > 0 {
   408  		view.streams.Print(
   409  			view.colorize.Color("[reset]\n[bold][cyan]Note:[reset][bold] Objects have changed outside of Terraform[reset]\n\n"),
   410  		)
   411  		view.streams.Print(format.WordWrap(
   412  			"Terraform detected the following changes made outside of Terraform since the last \"terraform apply\":\n\n",
   413  			view.outputColumns(),
   414  		))
   415  
   416  		for _, diff := range diffs {
   417  			view.streams.Print(diff)
   418  		}
   419  		return true
   420  	}
   421  
   422  	return false
   423  }
   424  
   425  const planHeaderIntro = `
   426  Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
   427  `