github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/internal/backend/local/backend_apply.go (about)

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