github.com/hugorut/terraform@v1.1.3/src/backend/local/backend_apply.go (about)

     1  package local
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"log"
     7  
     8  	"github.com/hugorut/terraform/src/backend"
     9  	"github.com/hugorut/terraform/src/command/views"
    10  	"github.com/hugorut/terraform/src/logging"
    11  	"github.com/hugorut/terraform/src/plans"
    12  	"github.com/hugorut/terraform/src/states"
    13  	"github.com/hugorut/terraform/src/states/statefile"
    14  	"github.com/hugorut/terraform/src/states/statemgr"
    15  	"github.com/hugorut/terraform/src/terraform"
    16  	"github.com/hugorut/terraform/src/tfdiags"
    17  )
    18  
    19  func (b *Local) opApply(
    20  	stopCtx context.Context,
    21  	cancelCtx context.Context,
    22  	op *backend.Operation,
    23  	runningOp *backend.RunningOperation) {
    24  	log.Printf("[INFO] backend/local: starting Apply operation")
    25  
    26  	var diags, moreDiags tfdiags.Diagnostics
    27  
    28  	// If we have a nil module at this point, then set it to an empty tree
    29  	// to avoid any potential crashes.
    30  	if op.PlanFile == nil && op.PlanMode != plans.DestroyMode && !op.HasConfig() {
    31  		diags = diags.Append(tfdiags.Sourceless(
    32  			tfdiags.Error,
    33  			"No configuration files",
    34  			"Apply requires configuration to be present. Applying without a configuration "+
    35  				"would mark everything for destruction, which is normally not what is desired. "+
    36  				"If you would like to destroy everything, run 'terraform destroy' instead.",
    37  		))
    38  		op.ReportResult(runningOp, diags)
    39  		return
    40  	}
    41  
    42  	stateHook := new(StateHook)
    43  	op.Hooks = append(op.Hooks, stateHook)
    44  
    45  	// Get our context
    46  	lr, _, opState, contextDiags := b.localRun(op)
    47  	diags = diags.Append(contextDiags)
    48  	if contextDiags.HasErrors() {
    49  		op.ReportResult(runningOp, diags)
    50  		return
    51  	}
    52  	// the state was locked during succesfull context creation; unlock the state
    53  	// when the operation completes
    54  	defer func() {
    55  		diags := op.StateLocker.Unlock()
    56  		if diags.HasErrors() {
    57  			op.View.Diagnostics(diags)
    58  			runningOp.Result = backend.OperationFailure
    59  		}
    60  	}()
    61  
    62  	// We'll start off with our result being the input state, and replace it
    63  	// with the result state only if we eventually complete the apply
    64  	// operation.
    65  	runningOp.State = lr.InputState
    66  
    67  	var plan *plans.Plan
    68  	// If we weren't given a plan, then we refresh/plan
    69  	if op.PlanFile == nil {
    70  		// Perform the plan
    71  		log.Printf("[INFO] backend/local: apply calling Plan")
    72  		plan, moreDiags = lr.Core.Plan(lr.Config, lr.InputState, lr.PlanOpts)
    73  		diags = diags.Append(moreDiags)
    74  		if moreDiags.HasErrors() {
    75  			op.ReportResult(runningOp, diags)
    76  			return
    77  		}
    78  
    79  		schemas, moreDiags := lr.Core.Schemas(lr.Config, lr.InputState)
    80  		diags = diags.Append(moreDiags)
    81  		if moreDiags.HasErrors() {
    82  			op.ReportResult(runningOp, diags)
    83  			return
    84  		}
    85  
    86  		trivialPlan := !plan.CanApply()
    87  		hasUI := op.UIOut != nil && op.UIIn != nil
    88  		mustConfirm := hasUI && !op.AutoApprove && !trivialPlan
    89  		op.View.Plan(plan, schemas)
    90  
    91  		if mustConfirm {
    92  			var desc, query string
    93  			switch op.PlanMode {
    94  			case plans.DestroyMode:
    95  				if op.Workspace != "default" {
    96  					query = "Do you really want to destroy all resources in workspace \"" + op.Workspace + "\"?"
    97  				} else {
    98  					query = "Do you really want to destroy all resources?"
    99  				}
   100  				desc = "Terraform will destroy all your managed infrastructure, as shown above.\n" +
   101  					"There is no undo. Only 'yes' will be accepted to confirm."
   102  			case plans.RefreshOnlyMode:
   103  				if op.Workspace != "default" {
   104  					query = "Would you like to update the Terraform state for \"" + op.Workspace + "\" to reflect these detected changes?"
   105  				} else {
   106  					query = "Would you like to update the Terraform state to reflect these detected changes?"
   107  				}
   108  				desc = "Terraform will write these changes to the state without modifying any real infrastructure.\n" +
   109  					"There is no undo. Only 'yes' will be accepted to confirm."
   110  			default:
   111  				if op.Workspace != "default" {
   112  					query = "Do you want to perform these actions in workspace \"" + op.Workspace + "\"?"
   113  				} else {
   114  					query = "Do you want to perform these actions?"
   115  				}
   116  				desc = "Terraform will perform the actions described above.\n" +
   117  					"Only 'yes' will be accepted to approve."
   118  			}
   119  
   120  			// We'll show any accumulated warnings before we display the prompt,
   121  			// so the user can consider them when deciding how to answer.
   122  			if len(diags) > 0 {
   123  				op.View.Diagnostics(diags)
   124  				diags = nil // reset so we won't show the same diagnostics again later
   125  			}
   126  
   127  			v, err := op.UIIn.Input(stopCtx, &terraform.InputOpts{
   128  				Id:          "approve",
   129  				Query:       "\n" + query,
   130  				Description: desc,
   131  			})
   132  			if err != nil {
   133  				diags = diags.Append(fmt.Errorf("error asking for approval: %w", err))
   134  				op.ReportResult(runningOp, diags)
   135  				return
   136  			}
   137  			if v != "yes" {
   138  				op.View.Cancelled(op.PlanMode)
   139  				runningOp.Result = backend.OperationFailure
   140  				return
   141  			}
   142  		}
   143  	} else {
   144  		plan = lr.Plan
   145  		for _, change := range plan.Changes.Resources {
   146  			if change.Action != plans.NoOp {
   147  				op.View.PlannedChange(change)
   148  			}
   149  		}
   150  	}
   151  
   152  	// Set up our hook for continuous state updates
   153  	stateHook.StateMgr = opState
   154  
   155  	// Start the apply in a goroutine so that we can be interrupted.
   156  	var applyState *states.State
   157  	var applyDiags tfdiags.Diagnostics
   158  	doneCh := make(chan struct{})
   159  	go func() {
   160  		defer logging.PanicHandler()
   161  		defer close(doneCh)
   162  		log.Printf("[INFO] backend/local: apply calling Apply")
   163  		applyState, applyDiags = lr.Core.Apply(plan, lr.Config)
   164  	}()
   165  
   166  	if b.opWait(doneCh, stopCtx, cancelCtx, lr.Core, opState, op.View) {
   167  		return
   168  	}
   169  	diags = diags.Append(applyDiags)
   170  
   171  	// Even on error with an empty state, the state value should not be nil.
   172  	// Return early here to prevent corrupting any existing state.
   173  	if diags.HasErrors() && applyState == nil {
   174  		log.Printf("[ERROR] backend/local: apply returned nil state")
   175  		op.ReportResult(runningOp, diags)
   176  		return
   177  	}
   178  
   179  	// Store the final state
   180  	runningOp.State = applyState
   181  	err := statemgr.WriteAndPersist(opState, applyState)
   182  	if err != nil {
   183  		// Export the state file from the state manager and assign the new
   184  		// state. This is needed to preserve the existing serial and lineage.
   185  		stateFile := statemgr.Export(opState)
   186  		if stateFile == nil {
   187  			stateFile = &statefile.File{}
   188  		}
   189  		stateFile.State = applyState
   190  
   191  		diags = diags.Append(b.backupStateForError(stateFile, err, op.View))
   192  		op.ReportResult(runningOp, diags)
   193  		return
   194  	}
   195  
   196  	if applyDiags.HasErrors() {
   197  		op.ReportResult(runningOp, diags)
   198  		return
   199  	}
   200  
   201  	// If we've accumulated any warnings along the way then we'll show them
   202  	// here just before we show the summary and next steps. If we encountered
   203  	// errors then we would've returned early at some other point above.
   204  	op.View.Diagnostics(diags)
   205  }
   206  
   207  // backupStateForError is called in a scenario where we're unable to persist the
   208  // state for some reason, and will attempt to save a backup copy of the state
   209  // to local disk to help the user recover. This is a "last ditch effort" sort
   210  // of thing, so we really don't want to end up in this codepath; we should do
   211  // everything we possibly can to get the state saved _somewhere_.
   212  func (b *Local) backupStateForError(stateFile *statefile.File, err error, view views.Operation) tfdiags.Diagnostics {
   213  	var diags tfdiags.Diagnostics
   214  
   215  	diags = diags.Append(tfdiags.Sourceless(
   216  		tfdiags.Error,
   217  		"Failed to save state",
   218  		fmt.Sprintf("Error saving state: %s", err),
   219  	))
   220  
   221  	local := statemgr.NewFilesystem("errored.tfstate")
   222  	writeErr := local.WriteStateForMigration(stateFile, true)
   223  	if writeErr != nil {
   224  		diags = diags.Append(tfdiags.Sourceless(
   225  			tfdiags.Error,
   226  			"Failed to create local state file",
   227  			fmt.Sprintf("Error creating local state file for recovery: %s", writeErr),
   228  		))
   229  
   230  		// To avoid leaving the user with no state at all, our last resort
   231  		// is to print the JSON state out onto the terminal. This is an awful
   232  		// UX, so we should definitely avoid doing this if at all possible,
   233  		// but at least the user has _some_ path to recover if we end up
   234  		// here for some reason.
   235  		if dumpErr := view.EmergencyDumpState(stateFile); dumpErr != nil {
   236  			diags = diags.Append(tfdiags.Sourceless(
   237  				tfdiags.Error,
   238  				"Failed to serialize state",
   239  				fmt.Sprintf(stateWriteFatalErrorFmt, dumpErr),
   240  			))
   241  		}
   242  
   243  		diags = diags.Append(tfdiags.Sourceless(
   244  			tfdiags.Error,
   245  			"Failed to persist state to backend",
   246  			stateWriteConsoleFallbackError,
   247  		))
   248  		return diags
   249  	}
   250  
   251  	diags = diags.Append(tfdiags.Sourceless(
   252  		tfdiags.Error,
   253  		"Failed to persist state to backend",
   254  		stateWriteBackedUpError,
   255  	))
   256  
   257  	return diags
   258  }
   259  
   260  const stateWriteBackedUpError = `The error shown above has prevented Terraform 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.
   261  
   262  Running "terraform apply" again at this point will create a forked state, making it harder to recover.
   263  
   264  To retry writing this state, use the following command:
   265      terraform state push errored.tfstate
   266  `
   267  
   268  const stateWriteConsoleFallbackError = `The errors shown above prevented Terraform from writing the updated state to
   269  the configured backend and from creating a local backup file. As a fallback,
   270  the raw state data is printed above as a JSON object.
   271  
   272  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:
   273      terraform state push errored.tfstate
   274  `
   275  
   276  const stateWriteFatalErrorFmt = `Failed to save state after apply.
   277  
   278  Error serializing state: %s
   279  
   280  A catastrophic error has prevented Terraform 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 Terraform's management.
   281  
   282  For resources that support import, it is possible to recover by manually importing each resource using its id from the target system.
   283  
   284  This is a serious bug in Terraform and should be reported.
   285  `