github.com/opentofu/opentofu@v1.7.1/internal/backend/local/backend_apply.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  	"errors"
    11  	"fmt"
    12  	"log"
    13  	"time"
    14  
    15  	"github.com/opentofu/opentofu/internal/addrs"
    16  	"github.com/opentofu/opentofu/internal/backend"
    17  	"github.com/opentofu/opentofu/internal/command/views"
    18  	"github.com/opentofu/opentofu/internal/logging"
    19  	"github.com/opentofu/opentofu/internal/plans"
    20  	"github.com/opentofu/opentofu/internal/states"
    21  	"github.com/opentofu/opentofu/internal/states/statefile"
    22  	"github.com/opentofu/opentofu/internal/states/statemgr"
    23  	"github.com/opentofu/opentofu/internal/tfdiags"
    24  	"github.com/opentofu/opentofu/internal/tofu"
    25  )
    26  
    27  // test hook called between plan+apply during opApply
    28  var testHookStopPlanApply func()
    29  
    30  func (b *Local) opApply(
    31  	stopCtx context.Context,
    32  	cancelCtx context.Context,
    33  	op *backend.Operation,
    34  	runningOp *backend.RunningOperation) {
    35  	log.Printf("[INFO] backend/local: starting Apply operation")
    36  
    37  	var diags, moreDiags tfdiags.Diagnostics
    38  
    39  	// If we have a nil module at this point, then set it to an empty tree
    40  	// to avoid any potential crashes.
    41  	if op.PlanFile == nil && op.PlanMode != plans.DestroyMode && !op.HasConfig() {
    42  		diags = diags.Append(tfdiags.Sourceless(
    43  			tfdiags.Error,
    44  			"No configuration files",
    45  			"Apply requires configuration to be present. Applying without a configuration "+
    46  				"would mark everything for destruction, which is normally not what is desired. "+
    47  				"If you would like to destroy everything, run 'tofu destroy' instead.",
    48  		))
    49  		op.ReportResult(runningOp, diags)
    50  		return
    51  	}
    52  
    53  	stateHook := new(StateHook)
    54  	op.Hooks = append(op.Hooks, stateHook)
    55  
    56  	// Get our context
    57  	lr, _, opState, contextDiags := b.localRun(op)
    58  	diags = diags.Append(contextDiags)
    59  	if contextDiags.HasErrors() {
    60  		op.ReportResult(runningOp, diags)
    61  		return
    62  	}
    63  	// the state was locked during successful context creation; unlock the state
    64  	// when the operation completes
    65  	defer func() {
    66  		diags := op.StateLocker.Unlock()
    67  		if diags.HasErrors() {
    68  			op.View.Diagnostics(diags)
    69  			runningOp.Result = backend.OperationFailure
    70  		}
    71  	}()
    72  
    73  	// We'll start off with our result being the input state, and replace it
    74  	// with the result state only if we eventually complete the apply
    75  	// operation.
    76  	runningOp.State = lr.InputState
    77  
    78  	schemas, moreDiags := lr.Core.Schemas(lr.Config, lr.InputState)
    79  	diags = diags.Append(moreDiags)
    80  	if moreDiags.HasErrors() {
    81  		op.ReportResult(runningOp, diags)
    82  		return
    83  	}
    84  	// stateHook uses schemas for when it periodically persists state to the
    85  	// persistent storage backend.
    86  	stateHook.Schemas = schemas
    87  	stateHook.PersistInterval = 20 * time.Second // arbitrary interval that's hopefully a sweet spot
    88  
    89  	var plan *plans.Plan
    90  	// If we weren't given a plan, then we refresh/plan
    91  	if op.PlanFile == nil {
    92  		// Perform the plan
    93  		log.Printf("[INFO] backend/local: apply calling Plan")
    94  		plan, moreDiags = lr.Core.Plan(lr.Config, lr.InputState, lr.PlanOpts)
    95  		diags = diags.Append(moreDiags)
    96  		if moreDiags.HasErrors() {
    97  			// If OpenTofu Core generated a partial plan despite the errors
    98  			// then we'll make the best effort to render it. OpenTofu Core
    99  			// promises that if it returns a non-nil plan along with errors
   100  			// then the plan won't necessarily contain all the needed
   101  			// actions but that any it does include will be properly-formed.
   102  			// plan.Errored will be true in this case, which our plan
   103  			// renderer can rely on to tailor its messaging.
   104  			if plan != nil && (len(plan.Changes.Resources) != 0 || len(plan.Changes.Outputs) != 0) {
   105  				op.View.Plan(plan, schemas)
   106  			}
   107  			op.ReportResult(runningOp, diags)
   108  			return
   109  		}
   110  
   111  		trivialPlan := !plan.CanApply()
   112  		hasUI := op.UIOut != nil && op.UIIn != nil
   113  		mustConfirm := hasUI && !op.AutoApprove && !trivialPlan
   114  		op.View.Plan(plan, schemas)
   115  
   116  		if testHookStopPlanApply != nil {
   117  			testHookStopPlanApply()
   118  		}
   119  
   120  		// Check if we've been stopped before going through confirmation, or
   121  		// skipping confirmation in the case of -auto-approve.
   122  		// This can currently happen if a single stop request was received
   123  		// during the final batch of resource plan calls, so no operations were
   124  		// forced to abort, and no errors were returned from Plan.
   125  		if stopCtx.Err() != nil {
   126  			diags = diags.Append(errors.New("execution halted"))
   127  			runningOp.Result = backend.OperationFailure
   128  			op.ReportResult(runningOp, diags)
   129  			return
   130  		}
   131  
   132  		if mustConfirm {
   133  			var desc, query string
   134  			switch op.PlanMode {
   135  			case plans.DestroyMode:
   136  				if op.Workspace != "default" {
   137  					query = "Do you really want to destroy all resources in workspace \"" + op.Workspace + "\"?"
   138  				} else {
   139  					query = "Do you really want to destroy all resources?"
   140  				}
   141  				desc = "OpenTofu will destroy all your managed infrastructure, as shown above.\n" +
   142  					"There is no undo. Only 'yes' will be accepted to confirm."
   143  			case plans.RefreshOnlyMode:
   144  				if op.Workspace != "default" {
   145  					query = "Would you like to update the OpenTofu state for \"" + op.Workspace + "\" to reflect these detected changes?"
   146  				} else {
   147  					query = "Would you like to update the OpenTofu state to reflect these detected changes?"
   148  				}
   149  				desc = "OpenTofu will write these changes to the state without modifying any real infrastructure.\n" +
   150  					"There is no undo. Only 'yes' will be accepted to confirm."
   151  			default:
   152  				if op.Workspace != "default" {
   153  					query = "Do you want to perform these actions in workspace \"" + op.Workspace + "\"?"
   154  				} else {
   155  					query = "Do you want to perform these actions?"
   156  				}
   157  				desc = "OpenTofu will perform the actions described above.\n" +
   158  					"Only 'yes' will be accepted to approve."
   159  			}
   160  
   161  			// We'll show any accumulated warnings before we display the prompt,
   162  			// so the user can consider them when deciding how to answer.
   163  			if len(diags) > 0 {
   164  				op.View.Diagnostics(diags)
   165  				diags = nil // reset so we won't show the same diagnostics again later
   166  			}
   167  
   168  			v, err := op.UIIn.Input(stopCtx, &tofu.InputOpts{
   169  				Id:          "approve",
   170  				Query:       "\n" + query,
   171  				Description: desc,
   172  			})
   173  			if err != nil {
   174  				diags = diags.Append(fmt.Errorf("error asking for approval: %w", err))
   175  				op.ReportResult(runningOp, diags)
   176  				return
   177  			}
   178  			if v != "yes" {
   179  				op.View.Cancelled(op.PlanMode)
   180  				runningOp.Result = backend.OperationFailure
   181  				return
   182  			}
   183  		} else {
   184  			// If we didn't ask for confirmation from the user, and they have
   185  			// included any failing checks in their configuration, then they
   186  			// will see a very confusing output after the apply operation
   187  			// completes. This is because all the diagnostics from the plan
   188  			// operation will now be shown alongside the diagnostics from the
   189  			// apply operation. For check diagnostics, the plan output is
   190  			// irrelevant and simple noise after the same set of checks have
   191  			// been executed again during the apply stage. As such, we are going
   192  			// to remove all diagnostics marked as check diagnostics at this
   193  			// stage, so we will only show the user the check results from the
   194  			// apply operation.
   195  			//
   196  			// Note, if we did ask for approval then we would have displayed the
   197  			// plan check results at that point which is useful as the user can
   198  			// use them to make a decision about whether to apply the changes.
   199  			// It's just that if we didn't ask for approval then showing the
   200  			// user the checks from the plan alongside the checks from the apply
   201  			// is needlessly confusing.
   202  			var filteredDiags tfdiags.Diagnostics
   203  			for _, diag := range diags {
   204  				if rule, ok := addrs.DiagnosticOriginatesFromCheckRule(diag); ok && rule.Container.CheckableKind() == addrs.CheckableCheck {
   205  					continue
   206  				}
   207  				filteredDiags = filteredDiags.Append(diag)
   208  			}
   209  			diags = filteredDiags
   210  		}
   211  	} else {
   212  		plan = lr.Plan
   213  		if plan.Errored {
   214  			diags = diags.Append(tfdiags.Sourceless(
   215  				tfdiags.Error,
   216  				"Cannot apply incomplete plan",
   217  				"OpenTofu encountered an error when generating this plan, so it cannot be applied.",
   218  			))
   219  			op.ReportResult(runningOp, diags)
   220  			return
   221  		}
   222  		for _, change := range plan.Changes.Resources {
   223  			if change.Action != plans.NoOp {
   224  				op.View.PlannedChange(change)
   225  			}
   226  		}
   227  	}
   228  
   229  	// Set up our hook for continuous state updates
   230  	stateHook.StateMgr = opState
   231  
   232  	// Start to apply in a goroutine so that we can be interrupted.
   233  	var applyState *states.State
   234  	var applyDiags tfdiags.Diagnostics
   235  	doneCh := make(chan struct{})
   236  	panicHandler := logging.PanicHandlerWithTraceFn()
   237  	go func() {
   238  		defer panicHandler()
   239  		defer close(doneCh)
   240  		log.Printf("[INFO] backend/local: apply calling Apply")
   241  		applyState, applyDiags = lr.Core.Apply(plan, lr.Config)
   242  	}()
   243  
   244  	if b.opWait(doneCh, stopCtx, cancelCtx, lr.Core, opState, op.View) {
   245  		return
   246  	}
   247  	diags = diags.Append(applyDiags)
   248  
   249  	// Even on error with an empty state, the state value should not be nil.
   250  	// Return early here to prevent corrupting any existing state.
   251  	if diags.HasErrors() && applyState == nil {
   252  		log.Printf("[ERROR] backend/local: apply returned nil state")
   253  		op.ReportResult(runningOp, diags)
   254  		return
   255  	}
   256  
   257  	// Store the final state
   258  	runningOp.State = applyState
   259  	err := statemgr.WriteAndPersist(opState, applyState, schemas)
   260  	if err != nil {
   261  		// Export the state file from the state manager and assign the new
   262  		// state. This is needed to preserve the existing serial and lineage.
   263  		stateFile := statemgr.Export(opState)
   264  		if stateFile == nil {
   265  			stateFile = &statefile.File{}
   266  		}
   267  		stateFile.State = applyState
   268  
   269  		diags = diags.Append(b.backupStateForError(stateFile, err, op.View))
   270  		op.ReportResult(runningOp, diags)
   271  		return
   272  	}
   273  
   274  	if applyDiags.HasErrors() {
   275  		op.ReportResult(runningOp, diags)
   276  		return
   277  	}
   278  
   279  	// If we've accumulated any warnings along the way then we'll show them
   280  	// here just before we show the summary and next steps. If we encountered
   281  	// errors then we would've returned early at some other point above.
   282  	op.View.Diagnostics(diags)
   283  }
   284  
   285  // backupStateForError is called in a scenario where we're unable to persist the
   286  // state for some reason, and will attempt to save a backup copy of the state
   287  // to local disk to help the user recover. This is a "last ditch effort" sort
   288  // of thing, so we really don't want to end up in this codepath; we should do
   289  // everything we possibly can to get the state saved _somewhere_.
   290  func (b *Local) backupStateForError(stateFile *statefile.File, err error, view views.Operation) tfdiags.Diagnostics {
   291  	var diags tfdiags.Diagnostics
   292  
   293  	diags = diags.Append(tfdiags.Sourceless(
   294  		tfdiags.Error,
   295  		"Failed to save state",
   296  		fmt.Sprintf("Error saving state: %s", err),
   297  	))
   298  
   299  	local := statemgr.NewFilesystem("errored.tfstate", b.encryption)
   300  	writeErr := local.WriteStateForMigration(stateFile, true)
   301  	if writeErr != nil {
   302  		diags = diags.Append(tfdiags.Sourceless(
   303  			tfdiags.Error,
   304  			"Failed to create local state file",
   305  			fmt.Sprintf("Error creating local state file for recovery: %s", writeErr),
   306  		))
   307  
   308  		// To avoid leaving the user with no state at all, our last resort
   309  		// is to print the JSON state out onto the terminal. This is an awful
   310  		// UX, so we should definitely avoid doing this if at all possible,
   311  		// but at least the user has _some_ path to recover if we end up
   312  		// here for some reason.
   313  		if dumpErr := view.EmergencyDumpState(stateFile, b.encryption); dumpErr != nil {
   314  			diags = diags.Append(tfdiags.Sourceless(
   315  				tfdiags.Error,
   316  				"Failed to serialize state",
   317  				fmt.Sprintf(stateWriteFatalErrorFmt, dumpErr),
   318  			))
   319  		}
   320  
   321  		diags = diags.Append(tfdiags.Sourceless(
   322  			tfdiags.Error,
   323  			"Failed to persist state to backend",
   324  			stateWriteConsoleFallbackError,
   325  		))
   326  		return diags
   327  	}
   328  
   329  	diags = diags.Append(tfdiags.Sourceless(
   330  		tfdiags.Error,
   331  		"Failed to persist state to backend",
   332  		stateWriteBackedUpError,
   333  	))
   334  
   335  	return diags
   336  }
   337  
   338  const stateWriteBackedUpError = `The error shown above has prevented OpenTofu from writing the updated state to the configured backend. To allow for recovery, the state has been written to the file "errored.tfstate" in the current working directory.
   339  
   340  Running "tofu apply" again at this point will create a forked state, making it harder to recover.
   341  
   342  To retry writing this state, use the following command:
   343      tofu state push errored.tfstate
   344  `
   345  
   346  const stateWriteConsoleFallbackError = `The errors shown above prevented OpenTofu from writing the updated state to
   347  the configured backend and from creating a local backup file. As a fallback,
   348  the raw state data is printed above as a JSON object.
   349  
   350  To retry writing this state, copy the state data (from the first { to the last } inclusive) and save it into a local file called errored.tfstate, then run the following command:
   351      tofu state push errored.tfstate
   352  `
   353  
   354  const stateWriteFatalErrorFmt = `Failed to save state after apply.
   355  
   356  Error serializing state: %s
   357  
   358  A catastrophic error has prevented OpenTofu from persisting the state file or creating a backup. Unfortunately this means that the record of any resources created during this apply has been lost, and such resources may exist outside of OpenTofu's management.
   359  
   360  For resources that support import, it is possible to recover by manually importing each resource using its id from the target system.
   361  
   362  This is a serious bug in OpenTofu and should be reported.
   363  `