github.com/hugorut/terraform@v1.1.3/src/command/views/plan.go (about)

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