github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/backend/local/backend_apply.go (about)

     1  package local
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"log"
     9  
    10  	"github.com/hashicorp/errwrap"
    11  	"github.com/hashicorp/terraform/backend"
    12  	"github.com/hashicorp/terraform/states"
    13  	"github.com/hashicorp/terraform/states/statefile"
    14  	"github.com/hashicorp/terraform/states/statemgr"
    15  	"github.com/hashicorp/terraform/terraform"
    16  	"github.com/hashicorp/terraform/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.Destroy && !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  		b.ReportResult(runningOp, diags)
    39  		return
    40  	}
    41  
    42  	// Setup our count hook that keeps track of resource changes
    43  	countHook := new(CountHook)
    44  	stateHook := new(StateHook)
    45  	if b.ContextOpts == nil {
    46  		b.ContextOpts = new(terraform.ContextOpts)
    47  	}
    48  	old := b.ContextOpts.Hooks
    49  	defer func() { b.ContextOpts.Hooks = old }()
    50  	b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, countHook, stateHook)
    51  
    52  	// Get our context
    53  	tfCtx, _, opState, contextDiags := b.context(op)
    54  	diags = diags.Append(contextDiags)
    55  	if contextDiags.HasErrors() {
    56  		b.ReportResult(runningOp, diags)
    57  		return
    58  	}
    59  
    60  	// Setup the state
    61  	runningOp.State = tfCtx.State()
    62  
    63  	// If we weren't given a plan, then we refresh/plan
    64  	if op.PlanFile == nil {
    65  		// If we're refreshing before apply, perform that
    66  		if op.PlanRefresh {
    67  			log.Printf("[INFO] backend/local: apply calling Refresh")
    68  			_, refreshDiags := tfCtx.Refresh()
    69  			diags = diags.Append(refreshDiags)
    70  			if diags.HasErrors() {
    71  				runningOp.Result = backend.OperationFailure
    72  				b.ShowDiagnostics(diags)
    73  				return
    74  			}
    75  		}
    76  
    77  		// Perform the plan
    78  		log.Printf("[INFO] backend/local: apply calling Plan")
    79  		plan, planDiags := tfCtx.Plan()
    80  		diags = diags.Append(planDiags)
    81  		if planDiags.HasErrors() {
    82  			b.ReportResult(runningOp, diags)
    83  			return
    84  		}
    85  
    86  		trivialPlan := plan.Changes.Empty()
    87  		hasUI := op.UIOut != nil && op.UIIn != nil
    88  		mustConfirm := hasUI && ((op.Destroy && (!op.DestroyForce && !op.AutoApprove)) || (!op.Destroy && !op.AutoApprove && !trivialPlan))
    89  		if mustConfirm {
    90  			var desc, query string
    91  			if op.Destroy {
    92  				if op.Workspace != "default" {
    93  					query = "Do you really want to destroy all resources in workspace \"" + op.Workspace + "\"?"
    94  				} else {
    95  					query = "Do you really want to destroy all resources?"
    96  				}
    97  				desc = "Terraform will destroy all your managed infrastructure, as shown above.\n" +
    98  					"There is no undo. Only 'yes' will be accepted to confirm."
    99  			} else {
   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 = "Terraform will perform the actions described above.\n" +
   106  					"Only 'yes' will be accepted to approve."
   107  			}
   108  
   109  			if !trivialPlan {
   110  				// Display the plan of what we are going to apply/destroy.
   111  				b.renderPlan(plan, runningOp.State, tfCtx.Schemas())
   112  				b.CLI.Output("")
   113  			}
   114  
   115  			// We'll show any accumulated warnings before we display the prompt,
   116  			// so the user can consider them when deciding how to answer.
   117  			if len(diags) > 0 {
   118  				b.ShowDiagnostics(diags)
   119  				diags = nil // reset so we won't show the same diagnostics again later
   120  			}
   121  
   122  			v, err := op.UIIn.Input(stopCtx, &terraform.InputOpts{
   123  				Id:          "approve",
   124  				Query:       query,
   125  				Description: desc,
   126  			})
   127  			if err != nil {
   128  				diags = diags.Append(errwrap.Wrapf("Error asking for approval: {{err}}", err))
   129  				b.ReportResult(runningOp, diags)
   130  				return
   131  			}
   132  			if v != "yes" {
   133  				if op.Destroy {
   134  					b.CLI.Info("Destroy cancelled.")
   135  				} else {
   136  					b.CLI.Info("Apply cancelled.")
   137  				}
   138  				runningOp.Result = backend.OperationFailure
   139  				return
   140  			}
   141  		}
   142  	}
   143  
   144  	// Setup our hook for continuous state updates
   145  	stateHook.StateMgr = opState
   146  
   147  	// Start the apply in a goroutine so that we can be interrupted.
   148  	var applyState *states.State
   149  	var applyDiags tfdiags.Diagnostics
   150  	doneCh := make(chan struct{})
   151  	go func() {
   152  		defer close(doneCh)
   153  		log.Printf("[INFO] backend/local: apply calling Apply")
   154  		_, applyDiags = tfCtx.Apply()
   155  		// we always want the state, even if apply failed
   156  		applyState = tfCtx.State()
   157  	}()
   158  
   159  	if b.opWait(doneCh, stopCtx, cancelCtx, tfCtx, opState) {
   160  		return
   161  	}
   162  
   163  	// Store the final state
   164  	runningOp.State = applyState
   165  	err := statemgr.WriteAndPersist(opState, applyState)
   166  	if err != nil {
   167  		// Export the state file from the state manager and assign the new
   168  		// state. This is needed to preserve the existing serial and lineage.
   169  		stateFile := statemgr.Export(opState)
   170  		if stateFile == nil {
   171  			stateFile = &statefile.File{}
   172  		}
   173  		stateFile.State = applyState
   174  
   175  		diags = diags.Append(b.backupStateForError(stateFile, err))
   176  		b.ReportResult(runningOp, diags)
   177  		return
   178  	}
   179  
   180  	diags = diags.Append(applyDiags)
   181  	if applyDiags.HasErrors() {
   182  		b.ReportResult(runningOp, diags)
   183  		return
   184  	}
   185  
   186  	// If we've accumulated any warnings along the way then we'll show them
   187  	// here just before we show the summary and next steps. If we encountered
   188  	// errors then we would've returned early at some other point above.
   189  	b.ShowDiagnostics(diags)
   190  
   191  	// If we have a UI, output the results
   192  	if b.CLI != nil {
   193  		if op.Destroy {
   194  			b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
   195  				"[reset][bold][green]\n"+
   196  					"Destroy complete! Resources: %d destroyed.",
   197  				countHook.Removed)))
   198  		} else {
   199  			b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
   200  				"[reset][bold][green]\n"+
   201  					"Apply complete! Resources: %d added, %d changed, %d destroyed.",
   202  				countHook.Added,
   203  				countHook.Changed,
   204  				countHook.Removed)))
   205  		}
   206  
   207  		// only show the state file help message if the state is local.
   208  		if (countHook.Added > 0 || countHook.Changed > 0) && b.StateOutPath != "" {
   209  			b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
   210  				"[reset]\n"+
   211  					"The state of your infrastructure has been saved to the path\n"+
   212  					"below. This state is required to modify and destroy your\n"+
   213  					"infrastructure, so keep it safe. To inspect the complete state\n"+
   214  					"use the `terraform show` command.\n\n"+
   215  					"State path: %s",
   216  				b.StateOutPath)))
   217  		}
   218  	}
   219  }
   220  
   221  // backupStateForError is called in a scenario where we're unable to persist the
   222  // state for some reason, and will attempt to save a backup copy of the state
   223  // to local disk to help the user recover. This is a "last ditch effort" sort
   224  // of thing, so we really don't want to end up in this codepath; we should do
   225  // everything we possibly can to get the state saved _somewhere_.
   226  func (b *Local) backupStateForError(stateFile *statefile.File, err error) error {
   227  	b.CLI.Error(fmt.Sprintf("Failed to save state: %s\n", err))
   228  
   229  	local := statemgr.NewFilesystem("errored.tfstate")
   230  	writeErr := local.WriteStateForMigration(stateFile, true)
   231  	if writeErr != nil {
   232  		b.CLI.Error(fmt.Sprintf(
   233  			"Also failed to create local state file for recovery: %s\n\n", writeErr,
   234  		))
   235  		// To avoid leaving the user with no state at all, our last resort
   236  		// is to print the JSON state out onto the terminal. This is an awful
   237  		// UX, so we should definitely avoid doing this if at all possible,
   238  		// but at least the user has _some_ path to recover if we end up
   239  		// here for some reason.
   240  		stateBuf := new(bytes.Buffer)
   241  		jsonErr := statefile.Write(stateFile, stateBuf)
   242  		if jsonErr != nil {
   243  			b.CLI.Error(fmt.Sprintf(
   244  				"Also failed to JSON-serialize the state to print it: %s\n\n", jsonErr,
   245  			))
   246  			return errors.New(stateWriteFatalError)
   247  		}
   248  
   249  		b.CLI.Output(stateBuf.String())
   250  
   251  		return errors.New(stateWriteConsoleFallbackError)
   252  	}
   253  
   254  	return errors.New(stateWriteBackedUpError)
   255  }
   256  
   257  const stateWriteBackedUpError = `Failed to persist state to backend.
   258  
   259  The error shown above has prevented Terraform from writing the updated state
   260  to the configured backend. To allow for recovery, the state has been written
   261  to the file "errored.tfstate" in the current working directory.
   262  
   263  Running "terraform apply" again at this point will create a forked state,
   264  making it harder to recover.
   265  
   266  To retry writing this state, use the following command:
   267      terraform state push errored.tfstate
   268  `
   269  
   270  const stateWriteConsoleFallbackError = `Failed to persist state to backend.
   271  
   272  The errors shown above prevented Terraform from writing the updated state to
   273  the configured backend and from creating a local backup file. As a fallback,
   274  the raw state data is printed above as a JSON object.
   275  
   276  To retry writing this state, copy the state data (from the first { to the
   277  last } inclusive) and save it into a local file called errored.tfstate, then
   278  run the following command:
   279      terraform state push errored.tfstate
   280  `
   281  
   282  const stateWriteFatalError = `Failed to save state after apply.
   283  
   284  A catastrophic error has prevented Terraform from persisting the state file
   285  or creating a backup. Unfortunately this means that the record of any resources
   286  created during this apply has been lost, and such resources may exist outside
   287  of Terraform's management.
   288  
   289  For resources that support import, it is possible to recover by manually
   290  importing each resource using its id from the target system.
   291  
   292  This is a serious bug in Terraform and should be reported.
   293  `
   294  
   295  const earlyStateWriteErrorFmt = `Error saving current state: %s
   296  
   297  Terraform encountered an error attempting to save the state before cancelling
   298  the current operation. Once the operation is complete another attempt will be
   299  made to save the final state.
   300  `