github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/backend/local/backend_plan.go (about)

     1  package local
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"log"
     8  	"sort"
     9  	"strings"
    10  
    11  	"github.com/mitchellh/cli"
    12  	"github.com/mitchellh/colorstring"
    13  
    14  	"github.com/hashicorp/terraform/addrs"
    15  	"github.com/hashicorp/terraform/backend"
    16  	"github.com/hashicorp/terraform/command/format"
    17  	"github.com/hashicorp/terraform/plans"
    18  	"github.com/hashicorp/terraform/plans/planfile"
    19  	"github.com/hashicorp/terraform/states"
    20  	"github.com/hashicorp/terraform/states/statemgr"
    21  	"github.com/hashicorp/terraform/terraform"
    22  	"github.com/hashicorp/terraform/tfdiags"
    23  )
    24  
    25  func (b *Local) opPlan(
    26  	stopCtx context.Context,
    27  	cancelCtx context.Context,
    28  	op *backend.Operation,
    29  	runningOp *backend.RunningOperation) {
    30  
    31  	log.Printf("[INFO] backend/local: starting Plan operation")
    32  
    33  	var diags tfdiags.Diagnostics
    34  
    35  	if op.PlanFile != nil {
    36  		diags = diags.Append(tfdiags.Sourceless(
    37  			tfdiags.Error,
    38  			"Can't re-plan a saved plan",
    39  			"The plan command was given a saved plan file as its input. This command generates "+
    40  				"a new plan, and so it requires a configuration directory as its argument.",
    41  		))
    42  		b.ReportResult(runningOp, diags)
    43  		return
    44  	}
    45  
    46  	// Local planning requires a config, unless we're planning to destroy.
    47  	if !op.Destroy && !op.HasConfig() {
    48  		diags = diags.Append(tfdiags.Sourceless(
    49  			tfdiags.Error,
    50  			"No configuration files",
    51  			"Plan requires configuration to be present. Planning without a configuration would "+
    52  				"mark everything for destruction, which is normally not what is desired. If you "+
    53  				"would like to destroy everything, run plan with the -destroy option. Otherwise, "+
    54  				"create a Terraform configuration file (.tf file) and try again.",
    55  		))
    56  		b.ReportResult(runningOp, diags)
    57  		return
    58  	}
    59  
    60  	// Setup our count hook that keeps track of resource changes
    61  	countHook := new(CountHook)
    62  	if b.ContextOpts == nil {
    63  		b.ContextOpts = new(terraform.ContextOpts)
    64  	}
    65  	old := b.ContextOpts.Hooks
    66  	defer func() { b.ContextOpts.Hooks = old }()
    67  	b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, countHook)
    68  
    69  	// Get our context
    70  	tfCtx, configSnap, opState, ctxDiags := b.context(op)
    71  	diags = diags.Append(ctxDiags)
    72  	if ctxDiags.HasErrors() {
    73  		b.ReportResult(runningOp, diags)
    74  		return
    75  	}
    76  
    77  	// Setup the state
    78  	runningOp.State = tfCtx.State()
    79  
    80  	// If we're refreshing before plan, perform that
    81  	baseState := runningOp.State
    82  	if op.PlanRefresh {
    83  		log.Printf("[INFO] backend/local: plan calling Refresh")
    84  
    85  		if b.CLI != nil {
    86  			b.CLI.Output(b.Colorize().Color(strings.TrimSpace(planRefreshing) + "\n"))
    87  		}
    88  
    89  		refreshedState, refreshDiags := tfCtx.Refresh()
    90  		diags = diags.Append(refreshDiags)
    91  		if diags.HasErrors() {
    92  			b.ReportResult(runningOp, diags)
    93  			return
    94  		}
    95  		baseState = refreshedState // plan will be relative to our refreshed state
    96  		if b.CLI != nil {
    97  			b.CLI.Output("\n------------------------------------------------------------------------")
    98  		}
    99  	}
   100  
   101  	// Perform the plan in a goroutine so we can be interrupted
   102  	var plan *plans.Plan
   103  	var planDiags tfdiags.Diagnostics
   104  	doneCh := make(chan struct{})
   105  	go func() {
   106  		defer close(doneCh)
   107  		log.Printf("[INFO] backend/local: plan calling Plan")
   108  		plan, planDiags = tfCtx.Plan()
   109  	}()
   110  
   111  	if b.opWait(doneCh, stopCtx, cancelCtx, tfCtx, opState) {
   112  		// If we get in here then the operation was cancelled, which is always
   113  		// considered to be a failure.
   114  		log.Printf("[INFO] backend/local: plan operation was force-cancelled by interrupt")
   115  		runningOp.Result = backend.OperationFailure
   116  		return
   117  	}
   118  	log.Printf("[INFO] backend/local: plan operation completed")
   119  
   120  	diags = diags.Append(planDiags)
   121  	if planDiags.HasErrors() {
   122  		b.ReportResult(runningOp, diags)
   123  		return
   124  	}
   125  	// Record state
   126  	runningOp.PlanEmpty = plan.Changes.Empty()
   127  
   128  	// Save the plan to disk
   129  	if path := op.PlanOutPath; path != "" {
   130  		if op.PlanOutBackend == nil {
   131  			// This is always a bug in the operation caller; it's not valid
   132  			// to set PlanOutPath without also setting PlanOutBackend.
   133  			diags = diags.Append(fmt.Errorf(
   134  				"PlanOutPath set without also setting PlanOutBackend (this is a bug in Terraform)"),
   135  			)
   136  			b.ReportResult(runningOp, diags)
   137  			return
   138  		}
   139  		plan.Backend = *op.PlanOutBackend
   140  
   141  		// We may have updated the state in the refresh step above, but we
   142  		// will freeze that updated state in the plan file for now and
   143  		// only write it if this plan is subsequently applied.
   144  		plannedStateFile := statemgr.PlannedStateUpdate(opState, baseState)
   145  
   146  		log.Printf("[INFO] backend/local: writing plan output to: %s", path)
   147  		err := planfile.Create(path, configSnap, plannedStateFile, plan)
   148  		if err != nil {
   149  			diags = diags.Append(tfdiags.Sourceless(
   150  				tfdiags.Error,
   151  				"Failed to write plan file",
   152  				fmt.Sprintf("The plan file could not be written: %s.", err),
   153  			))
   154  			b.ReportResult(runningOp, diags)
   155  			return
   156  		}
   157  	}
   158  
   159  	// Perform some output tasks if we have a CLI to output to.
   160  	if b.CLI != nil {
   161  		schemas := tfCtx.Schemas()
   162  
   163  		if plan.Changes.Empty() {
   164  			b.CLI.Output("\n" + b.Colorize().Color(strings.TrimSpace(planNoChanges)))
   165  			// Even if there are no changes, there still could be some warnings
   166  			b.ShowDiagnostics(diags)
   167  			return
   168  		}
   169  
   170  		b.renderPlan(plan, baseState, schemas)
   171  
   172  		// If we've accumulated any warnings along the way then we'll show them
   173  		// here just before we show the summary and next steps. If we encountered
   174  		// errors then we would've returned early at some other point above.
   175  		b.ShowDiagnostics(diags)
   176  
   177  		// Give the user some next-steps, unless we're running in an automation
   178  		// tool which is presumed to provide its own UI for further actions.
   179  		if !b.RunningInAutomation {
   180  
   181  			b.CLI.Output("\n------------------------------------------------------------------------")
   182  
   183  			if path := op.PlanOutPath; path == "" {
   184  				b.CLI.Output(fmt.Sprintf(
   185  					"\n" + strings.TrimSpace(planHeaderNoOutput) + "\n",
   186  				))
   187  			} else {
   188  				b.CLI.Output(fmt.Sprintf(
   189  					"\n"+strings.TrimSpace(planHeaderYesOutput)+"\n",
   190  					path, path,
   191  				))
   192  			}
   193  		}
   194  	}
   195  }
   196  
   197  func (b *Local) renderPlan(plan *plans.Plan, state *states.State, schemas *terraform.Schemas) {
   198  	RenderPlan(plan, state, schemas, b.CLI, b.Colorize())
   199  }
   200  
   201  // RenderPlan renders the given plan to the given UI.
   202  //
   203  // This is exported only so that the "terraform show" command can re-use it.
   204  // Ideally it would be somewhere outside of this backend code so that both
   205  // can call into it, but we're leaving it here for now in order to avoid
   206  // disruptive refactoring.
   207  //
   208  // If you find yourself wanting to call this function from a third callsite,
   209  // please consider whether it's time to do the more disruptive refactoring
   210  // so that something other than the local backend package is offering this
   211  // functionality.
   212  func RenderPlan(plan *plans.Plan, state *states.State, schemas *terraform.Schemas, ui cli.Ui, colorize *colorstring.Colorize) {
   213  	counts := map[plans.Action]int{}
   214  	var rChanges []*plans.ResourceInstanceChangeSrc
   215  	for _, change := range plan.Changes.Resources {
   216  		if change.Action == plans.Delete && change.Addr.Resource.Resource.Mode == addrs.DataResourceMode {
   217  			// Avoid rendering data sources on deletion
   218  			continue
   219  		}
   220  
   221  		rChanges = append(rChanges, change)
   222  		counts[change.Action]++
   223  	}
   224  
   225  	headerBuf := &bytes.Buffer{}
   226  	fmt.Fprintf(headerBuf, "\n%s\n", strings.TrimSpace(planHeaderIntro))
   227  	if counts[plans.Create] > 0 {
   228  		fmt.Fprintf(headerBuf, "%s create\n", format.DiffActionSymbol(plans.Create))
   229  	}
   230  	if counts[plans.Update] > 0 {
   231  		fmt.Fprintf(headerBuf, "%s update in-place\n", format.DiffActionSymbol(plans.Update))
   232  	}
   233  	if counts[plans.Delete] > 0 {
   234  		fmt.Fprintf(headerBuf, "%s destroy\n", format.DiffActionSymbol(plans.Delete))
   235  	}
   236  	if counts[plans.DeleteThenCreate] > 0 {
   237  		fmt.Fprintf(headerBuf, "%s destroy and then create replacement\n", format.DiffActionSymbol(plans.DeleteThenCreate))
   238  	}
   239  	if counts[plans.CreateThenDelete] > 0 {
   240  		fmt.Fprintf(headerBuf, "%s create replacement and then destroy\n", format.DiffActionSymbol(plans.CreateThenDelete))
   241  	}
   242  	if counts[plans.Read] > 0 {
   243  		fmt.Fprintf(headerBuf, "%s read (data resources)\n", format.DiffActionSymbol(plans.Read))
   244  	}
   245  
   246  	ui.Output(colorize.Color(headerBuf.String()))
   247  
   248  	ui.Output("Terraform will perform the following actions:\n")
   249  
   250  	// Note: we're modifying the backing slice of this plan object in-place
   251  	// here. The ordering of resource changes in a plan is not significant,
   252  	// but we can only do this safely here because we can assume that nobody
   253  	// is concurrently modifying our changes while we're trying to print it.
   254  	sort.Slice(rChanges, func(i, j int) bool {
   255  		iA := rChanges[i].Addr
   256  		jA := rChanges[j].Addr
   257  		if iA.String() == jA.String() {
   258  			return rChanges[i].DeposedKey < rChanges[j].DeposedKey
   259  		}
   260  		return iA.Less(jA)
   261  	})
   262  
   263  	for _, rcs := range rChanges {
   264  		if rcs.Action == plans.NoOp {
   265  			continue
   266  		}
   267  		providerSchema := schemas.ProviderSchema(rcs.ProviderAddr.ProviderConfig.Type.LegacyString())
   268  		if providerSchema == nil {
   269  			// Should never happen
   270  			ui.Output(fmt.Sprintf("(schema missing for %s)\n", rcs.ProviderAddr))
   271  			continue
   272  		}
   273  		rSchema, _ := providerSchema.SchemaForResourceAddr(rcs.Addr.Resource.Resource)
   274  		if rSchema == nil {
   275  			// Should never happen
   276  			ui.Output(fmt.Sprintf("(schema missing for %s)\n", rcs.Addr))
   277  			continue
   278  		}
   279  
   280  		// check if the change is due to a tainted resource
   281  		tainted := false
   282  		if !state.Empty() {
   283  			if is := state.ResourceInstance(rcs.Addr); is != nil {
   284  				if obj := is.GetGeneration(rcs.DeposedKey.Generation()); obj != nil {
   285  					tainted = obj.Status == states.ObjectTainted
   286  				}
   287  			}
   288  		}
   289  
   290  		ui.Output(format.ResourceChange(
   291  			rcs,
   292  			tainted,
   293  			rSchema,
   294  			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  	ui.Output(colorize.Color(fmt.Sprintf(
   312  		"[reset][bold]Plan:[reset] "+
   313  			"%d to add, %d to change, %d to destroy.",
   314  		stats[plans.Create], stats[plans.Update], stats[plans.Delete],
   315  	)))
   316  }
   317  
   318  const planHeaderIntro = `
   319  An execution plan has been generated and is shown below.
   320  Resource actions are indicated with the following symbols:
   321  `
   322  
   323  const planHeaderNoOutput = `
   324  Note: You didn't specify an "-out" parameter to save this plan, so Terraform
   325  can't guarantee that exactly these actions will be performed if
   326  "terraform apply" is subsequently run.
   327  `
   328  
   329  const planHeaderYesOutput = `
   330  This plan was saved to: %s
   331  
   332  To perform exactly these actions, run the following command to apply:
   333      terraform apply %q
   334  `
   335  
   336  const planNoChanges = `
   337  [reset][bold][green]No changes. Infrastructure is up-to-date.[reset][green]
   338  
   339  This means that Terraform did not detect any differences between your
   340  configuration and real physical resources that exist. As a result, no
   341  actions need to be performed.
   342  `
   343  
   344  const planRefreshing = `
   345  [reset][bold]Refreshing Terraform state in-memory prior to plan...[reset]
   346  The refreshed state will be used to calculate this plan, but will not be
   347  persisted to local or remote state storage.
   348  `