github.com/opentofu/opentofu@v1.7.1/internal/backend/local/backend_plan.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package local
     7  
     8  import (
     9  	"context"
    10  	"fmt"
    11  	"io"
    12  	"log"
    13  
    14  	"github.com/opentofu/opentofu/internal/backend"
    15  	"github.com/opentofu/opentofu/internal/genconfig"
    16  	"github.com/opentofu/opentofu/internal/logging"
    17  	"github.com/opentofu/opentofu/internal/plans"
    18  	"github.com/opentofu/opentofu/internal/plans/planfile"
    19  	"github.com/opentofu/opentofu/internal/states/statefile"
    20  	"github.com/opentofu/opentofu/internal/states/statemgr"
    21  	"github.com/opentofu/opentofu/internal/tfdiags"
    22  	"github.com/opentofu/opentofu/internal/tofu"
    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  		op.ReportResult(runningOp, diags)
    43  		return
    44  	}
    45  
    46  	// Local planning requires a config, unless we're planning to destroy.
    47  	if op.PlanMode != plans.DestroyMode && !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 OpenTofu configuration file (.tf file) and try again.",
    55  		))
    56  		op.ReportResult(runningOp, diags)
    57  		return
    58  	}
    59  
    60  	if len(op.GenerateConfigOut) > 0 {
    61  		if op.PlanMode != plans.NormalMode {
    62  			diags = diags.Append(tfdiags.Sourceless(
    63  				tfdiags.Error,
    64  				"Invalid generate-config-out flag",
    65  				"Config can only be generated during a normal plan operation, and not during a refresh-only or destroy plan."))
    66  			op.ReportResult(runningOp, diags)
    67  			return
    68  		}
    69  
    70  		diags = diags.Append(genconfig.ValidateTargetFile(op.GenerateConfigOut))
    71  		if diags.HasErrors() {
    72  			op.ReportResult(runningOp, diags)
    73  			return
    74  		}
    75  	}
    76  
    77  	if b.ContextOpts == nil {
    78  		b.ContextOpts = new(tofu.ContextOpts)
    79  	}
    80  
    81  	// Get our context
    82  	lr, configSnap, opState, ctxDiags := b.localRun(op)
    83  	diags = diags.Append(ctxDiags)
    84  	if ctxDiags.HasErrors() {
    85  		op.ReportResult(runningOp, diags)
    86  		return
    87  	}
    88  	// the state was locked during succesfull context creation; unlock the state
    89  	// when the operation completes
    90  	defer func() {
    91  		diags := op.StateLocker.Unlock()
    92  		if diags.HasErrors() {
    93  			op.View.Diagnostics(diags)
    94  			runningOp.Result = backend.OperationFailure
    95  		}
    96  	}()
    97  
    98  	// Since planning doesn't immediately change the persisted state, the
    99  	// resulting state is always just the input state.
   100  	runningOp.State = lr.InputState
   101  
   102  	// Perform the plan in a goroutine so we can be interrupted
   103  	var plan *plans.Plan
   104  	var planDiags tfdiags.Diagnostics
   105  	doneCh := make(chan struct{})
   106  	panicHandler := logging.PanicHandlerWithTraceFn()
   107  	go func() {
   108  		defer panicHandler()
   109  		defer close(doneCh)
   110  		log.Printf("[INFO] backend/local: plan calling Plan")
   111  		plan, planDiags = lr.Core.Plan(lr.Config, lr.InputState, lr.PlanOpts)
   112  	}()
   113  
   114  	if b.opWait(doneCh, stopCtx, cancelCtx, lr.Core, opState, op.View) {
   115  		// If we get in here then the operation was cancelled, which is always
   116  		// considered to be a failure.
   117  		log.Printf("[INFO] backend/local: plan operation was force-cancelled by interrupt")
   118  		runningOp.Result = backend.OperationFailure
   119  		return
   120  	}
   121  	log.Printf("[INFO] backend/local: plan operation completed")
   122  
   123  	// NOTE: We intentionally don't stop here on errors because we always want
   124  	// to try to present a partial plan report and, if the user chose to,
   125  	// generate a partial saved plan file for external analysis.
   126  	diags = diags.Append(planDiags)
   127  
   128  	// Even if there are errors we need to handle anything that may be
   129  	// contained within the plan, so only exit if there is no data at all.
   130  	if plan == nil {
   131  		runningOp.PlanEmpty = true
   132  		op.ReportResult(runningOp, diags)
   133  		return
   134  	}
   135  
   136  	// Record whether this plan includes any side-effects that could be applied.
   137  	runningOp.PlanEmpty = !plan.CanApply()
   138  
   139  	// Save the plan to disk
   140  	if path := op.PlanOutPath; path != "" {
   141  		if op.PlanOutBackend == nil {
   142  			// This is always a bug in the operation caller; it's not valid
   143  			// to set PlanOutPath without also setting PlanOutBackend.
   144  			diags = diags.Append(fmt.Errorf(
   145  				"PlanOutPath set without also setting PlanOutBackend (this is a bug in OpenTofu)"),
   146  			)
   147  			op.ReportResult(runningOp, diags)
   148  			return
   149  		}
   150  		plan.Backend = *op.PlanOutBackend
   151  
   152  		// We may have updated the state in the refresh step above, but we
   153  		// will freeze that updated state in the plan file for now and
   154  		// only write it if this plan is subsequently applied.
   155  		plannedStateFile := statemgr.PlannedStateUpdate(opState, plan.PriorState)
   156  
   157  		// We also include a file containing the state as it existed before
   158  		// we took any action at all, but this one isn't intended to ever
   159  		// be saved to the backend (an equivalent snapshot should already be
   160  		// there) and so we just use a stub state file header in this case.
   161  		// NOTE: This won't be exactly identical to the latest state snapshot
   162  		// in the backend because it's still been subject to state upgrading
   163  		// to make it consumable by the current OpenTofu version, and
   164  		// intentionally doesn't preserve the header info.
   165  		prevStateFile := &statefile.File{
   166  			State: plan.PrevRunState,
   167  		}
   168  
   169  		log.Printf("[INFO] backend/local: writing plan output to: %s", path)
   170  		err := planfile.Create(path, planfile.CreateArgs{
   171  			ConfigSnapshot:       configSnap,
   172  			PreviousRunStateFile: prevStateFile,
   173  			StateFile:            plannedStateFile,
   174  			Plan:                 plan,
   175  			DependencyLocks:      op.DependencyLocks,
   176  		}, op.Encryption.Plan())
   177  		if err != nil {
   178  			diags = diags.Append(tfdiags.Sourceless(
   179  				tfdiags.Error,
   180  				"Failed to write plan file",
   181  				fmt.Sprintf("The plan file could not be written: %s.", err),
   182  			))
   183  			op.ReportResult(runningOp, diags)
   184  			return
   185  		}
   186  	}
   187  
   188  	// Render the plan, if we produced one.
   189  	// (This might potentially be a partial plan with Errored set to true)
   190  	schemas, moreDiags := lr.Core.Schemas(lr.Config, lr.InputState)
   191  	diags = diags.Append(moreDiags)
   192  	if moreDiags.HasErrors() {
   193  		op.ReportResult(runningOp, diags)
   194  		return
   195  	}
   196  
   197  	// Write out any generated config, before we render the plan.
   198  	wroteConfig, moreDiags := maybeWriteGeneratedConfig(plan, op.GenerateConfigOut)
   199  	diags = diags.Append(moreDiags)
   200  	if moreDiags.HasErrors() {
   201  		op.ReportResult(runningOp, diags)
   202  		return
   203  	}
   204  
   205  	op.View.Plan(plan, schemas)
   206  
   207  	// If we've accumulated any diagnostics along the way then we'll show them
   208  	// here just before we show the summary and next steps. This can potentially
   209  	// include errors, because we intentionally try to show a partial plan
   210  	// above even if OpenTofu Core encountered an error partway through
   211  	// creating it.
   212  	op.ReportResult(runningOp, diags)
   213  
   214  	if !runningOp.PlanEmpty {
   215  		if wroteConfig {
   216  			op.View.PlanNextStep(op.PlanOutPath, op.GenerateConfigOut)
   217  		} else {
   218  			op.View.PlanNextStep(op.PlanOutPath, "")
   219  		}
   220  	}
   221  }
   222  
   223  func maybeWriteGeneratedConfig(plan *plans.Plan, out string) (wroteConfig bool, diags tfdiags.Diagnostics) {
   224  	if genconfig.ShouldWriteConfig(out) {
   225  		diags := genconfig.ValidateTargetFile(out)
   226  		if diags.HasErrors() {
   227  			return false, diags
   228  		}
   229  
   230  		var writer io.Writer
   231  		for _, c := range plan.Changes.Resources {
   232  			change := genconfig.Change{
   233  				Addr:            c.Addr.String(),
   234  				GeneratedConfig: c.GeneratedConfig,
   235  			}
   236  			if c.Importing != nil {
   237  				change.ImportID = c.Importing.ID
   238  			}
   239  
   240  			var moreDiags tfdiags.Diagnostics
   241  			writer, wroteConfig, moreDiags = change.MaybeWriteConfig(writer, out)
   242  			if moreDiags.HasErrors() {
   243  				return false, diags.Append(moreDiags)
   244  			}
   245  		}
   246  	}
   247  
   248  	if wroteConfig {
   249  		diags = diags.Append(tfdiags.Sourceless(
   250  			tfdiags.Warning,
   251  			"Config generation is experimental",
   252  			"Generating configuration during import is currently experimental, and the generated configuration format may change in future versions."))
   253  	}
   254  
   255  	return wroteConfig, diags
   256  }