github.com/muratcelep/terraform@v1.1.0-beta2-not-internal-4/not-internal/backend/local/backend_apply.go (about)

     1  package local
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"log"
     7  
     8  	"github.com/muratcelep/terraform/not-internal/backend"
     9  	"github.com/muratcelep/terraform/not-internal/command/views"
    10  	"github.com/muratcelep/terraform/not-internal/logging"
    11  	"github.com/muratcelep/terraform/not-internal/plans"
    12  	"github.com/muratcelep/terraform/not-internal/states"
    13  	"github.com/muratcelep/terraform/not-internal/states/statefile"
    14  	"github.com/muratcelep/terraform/not-internal/states/statemgr"
    15  	"github.com/muratcelep/terraform/not-internal/terraform"
    16  	"github.com/muratcelep/terraform/not-internal/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  	// Store the final state
   172  	runningOp.State = applyState
   173  	err := statemgr.WriteAndPersist(opState, applyState)
   174  	if err != nil {
   175  		// Export the state file from the state manager and assign the new
   176  		// state. This is needed to preserve the existing serial and lineage.
   177  		stateFile := statemgr.Export(opState)
   178  		if stateFile == nil {
   179  			stateFile = &statefile.File{}
   180  		}
   181  		stateFile.State = applyState
   182  
   183  		diags = diags.Append(b.backupStateForError(stateFile, err, op.View))
   184  		op.ReportResult(runningOp, diags)
   185  		return
   186  	}
   187  
   188  	if applyDiags.HasErrors() {
   189  		op.ReportResult(runningOp, diags)
   190  		return
   191  	}
   192  
   193  	// If we've accumulated any warnings along the way then we'll show them
   194  	// here just before we show the summary and next steps. If we encountered
   195  	// errors then we would've returned early at some other point above.
   196  	op.View.Diagnostics(diags)
   197  }
   198  
   199  // backupStateForError is called in a scenario where we're unable to persist the
   200  // state for some reason, and will attempt to save a backup copy of the state
   201  // to local disk to help the user recover. This is a "last ditch effort" sort
   202  // of thing, so we really don't want to end up in this codepath; we should do
   203  // everything we possibly can to get the state saved _somewhere_.
   204  func (b *Local) backupStateForError(stateFile *statefile.File, err error, view views.Operation) tfdiags.Diagnostics {
   205  	var diags tfdiags.Diagnostics
   206  
   207  	diags = diags.Append(tfdiags.Sourceless(
   208  		tfdiags.Error,
   209  		"Failed to save state",
   210  		fmt.Sprintf("Error saving state: %s", err),
   211  	))
   212  
   213  	local := statemgr.NewFilesystem("errored.tfstate")
   214  	writeErr := local.WriteStateForMigration(stateFile, true)
   215  	if writeErr != nil {
   216  		diags = diags.Append(tfdiags.Sourceless(
   217  			tfdiags.Error,
   218  			"Failed to create local state file",
   219  			fmt.Sprintf("Error creating local state file for recovery: %s", writeErr),
   220  		))
   221  
   222  		// To avoid leaving the user with no state at all, our last resort
   223  		// is to print the JSON state out onto the terminal. This is an awful
   224  		// UX, so we should definitely avoid doing this if at all possible,
   225  		// but at least the user has _some_ path to recover if we end up
   226  		// here for some reason.
   227  		if dumpErr := view.EmergencyDumpState(stateFile); dumpErr != nil {
   228  			diags = diags.Append(tfdiags.Sourceless(
   229  				tfdiags.Error,
   230  				"Failed to serialize state",
   231  				fmt.Sprintf(stateWriteFatalErrorFmt, dumpErr),
   232  			))
   233  		}
   234  
   235  		diags = diags.Append(tfdiags.Sourceless(
   236  			tfdiags.Error,
   237  			"Failed to persist state to backend",
   238  			stateWriteConsoleFallbackError,
   239  		))
   240  		return diags
   241  	}
   242  
   243  	diags = diags.Append(tfdiags.Sourceless(
   244  		tfdiags.Error,
   245  		"Failed to persist state to backend",
   246  		stateWriteBackedUpError,
   247  	))
   248  
   249  	return diags
   250  }
   251  
   252  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.
   253  
   254  Running "terraform apply" again at this point will create a forked state, making it harder to recover.
   255  
   256  To retry writing this state, use the following command:
   257      terraform state push errored.tfstate
   258  `
   259  
   260  const stateWriteConsoleFallbackError = `The errors shown above prevented Terraform from writing the updated state to
   261  the configured backend and from creating a local backup file. As a fallback,
   262  the raw state data is printed above as a JSON object.
   263  
   264  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:
   265      terraform state push errored.tfstate
   266  `
   267  
   268  const stateWriteFatalErrorFmt = `Failed to save state after apply.
   269  
   270  Error serializing state: %s
   271  
   272  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.
   273  
   274  For resources that support import, it is possible to recover by manually importing each resource using its id from the target system.
   275  
   276  This is a serious bug in Terraform and should be reported.
   277  `