kubeform.dev/terraform-backend-sdk@v0.0.0-20220310143633-45f07fe731c5/backend/local/backend_apply.go (about)

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