github.com/kevholditch/terraform@v0.9.7-0.20170613192930-9706042ddd51/backend/local/backend_apply.go (about)

     1  package local
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"log"
     9  	"strings"
    10  
    11  	"github.com/hashicorp/errwrap"
    12  	"github.com/hashicorp/go-multierror"
    13  	"github.com/hashicorp/terraform/backend"
    14  	"github.com/hashicorp/terraform/command/clistate"
    15  	"github.com/hashicorp/terraform/config/module"
    16  	"github.com/hashicorp/terraform/state"
    17  	"github.com/hashicorp/terraform/terraform"
    18  )
    19  
    20  func (b *Local) opApply(
    21  	ctx context.Context,
    22  	op *backend.Operation,
    23  	runningOp *backend.RunningOperation) {
    24  	log.Printf("[INFO] backend/local: starting Apply operation")
    25  
    26  	// If we have a nil module at this point, then set it to an empty tree
    27  	// to avoid any potential crashes.
    28  	if op.Plan == nil && op.Module == nil && !op.Destroy {
    29  		runningOp.Err = fmt.Errorf(strings.TrimSpace(applyErrNoConfig))
    30  		return
    31  	}
    32  
    33  	// If we have a nil module at this point, then set it to an empty tree
    34  	// to avoid any potential crashes.
    35  	if op.Module == nil {
    36  		op.Module = module.NewEmptyTree()
    37  	}
    38  
    39  	// Setup our count hook that keeps track of resource changes
    40  	countHook := new(CountHook)
    41  	stateHook := new(StateHook)
    42  	if b.ContextOpts == nil {
    43  		b.ContextOpts = new(terraform.ContextOpts)
    44  	}
    45  	old := b.ContextOpts.Hooks
    46  	defer func() { b.ContextOpts.Hooks = old }()
    47  	b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, countHook, stateHook)
    48  
    49  	// Get our context
    50  	tfCtx, opState, err := b.context(op)
    51  	if err != nil {
    52  		runningOp.Err = err
    53  		return
    54  	}
    55  
    56  	if op.LockState {
    57  		lockCtx, cancel := context.WithTimeout(ctx, op.StateLockTimeout)
    58  		defer cancel()
    59  
    60  		lockInfo := state.NewLockInfo()
    61  		lockInfo.Operation = op.Type.String()
    62  		lockID, err := clistate.Lock(lockCtx, opState, lockInfo, b.CLI, b.Colorize())
    63  		if err != nil {
    64  			runningOp.Err = errwrap.Wrapf("Error locking state: {{err}}", err)
    65  			return
    66  		}
    67  
    68  		defer func() {
    69  			if err := clistate.Unlock(opState, lockID, b.CLI, b.Colorize()); err != nil {
    70  				runningOp.Err = multierror.Append(runningOp.Err, err)
    71  			}
    72  		}()
    73  	}
    74  
    75  	// Setup the state
    76  	runningOp.State = tfCtx.State()
    77  
    78  	// If we weren't given a plan, then we refresh/plan
    79  	if op.Plan == nil {
    80  		// If we're refreshing before apply, perform that
    81  		if op.PlanRefresh {
    82  			log.Printf("[INFO] backend/local: apply calling Refresh")
    83  			_, err := tfCtx.Refresh()
    84  			if err != nil {
    85  				runningOp.Err = errwrap.Wrapf("Error refreshing state: {{err}}", err)
    86  				return
    87  			}
    88  		}
    89  
    90  		// Perform the plan
    91  		log.Printf("[INFO] backend/local: apply calling Plan")
    92  		if _, err := tfCtx.Plan(); err != nil {
    93  			runningOp.Err = errwrap.Wrapf("Error running plan: {{err}}", err)
    94  			return
    95  		}
    96  	}
    97  
    98  	// Setup our hook for continuous state updates
    99  	stateHook.State = opState
   100  
   101  	// Start the apply in a goroutine so that we can be interrupted.
   102  	var applyState *terraform.State
   103  	var applyErr error
   104  	doneCh := make(chan struct{})
   105  	go func() {
   106  		defer close(doneCh)
   107  		_, applyErr = tfCtx.Apply()
   108  		// we always want the state, even if apply failed
   109  		applyState = tfCtx.State()
   110  
   111  		/*
   112  			// Record any shadow errors for later
   113  			if err := ctx.ShadowError(); err != nil {
   114  				shadowErr = multierror.Append(shadowErr, multierror.Prefix(
   115  					err, "apply operation:"))
   116  			}
   117  		*/
   118  	}()
   119  
   120  	// Wait for the apply to finish or for us to be interrupted so
   121  	// we can handle it properly.
   122  	err = nil
   123  	select {
   124  	case <-ctx.Done():
   125  		if b.CLI != nil {
   126  			b.CLI.Output("stopping apply operation...")
   127  		}
   128  
   129  		// try to force a PersistState just in case the process is terminated
   130  		// before we can complete.
   131  		if err := opState.PersistState(); err != nil {
   132  			// We can't error out from here, but warn the user if there was an error.
   133  			// If this isn't transient, we will catch it again below, and
   134  			// attempt to save the state another way.
   135  			if b.CLI != nil {
   136  				b.CLI.Error(fmt.Sprintf(earlyStateWriteErrorFmt, err))
   137  			}
   138  		}
   139  
   140  		// Stop execution
   141  		go tfCtx.Stop()
   142  
   143  		// Wait for completion still
   144  		<-doneCh
   145  	case <-doneCh:
   146  	}
   147  
   148  	// Store the final state
   149  	runningOp.State = applyState
   150  
   151  	// Persist the state
   152  	if err := opState.WriteState(applyState); err != nil {
   153  		runningOp.Err = b.backupStateForError(applyState, err)
   154  		return
   155  	}
   156  	if err := opState.PersistState(); err != nil {
   157  		runningOp.Err = b.backupStateForError(applyState, err)
   158  		return
   159  	}
   160  
   161  	if applyErr != nil {
   162  		runningOp.Err = fmt.Errorf(
   163  			"Error applying plan:\n\n"+
   164  				"%s\n\n"+
   165  				"Terraform does not automatically rollback in the face of errors.\n"+
   166  				"Instead, your Terraform state file has been partially updated with\n"+
   167  				"any resources that successfully completed. Please address the error\n"+
   168  				"above and apply again to incrementally change your infrastructure.",
   169  			multierror.Flatten(applyErr))
   170  		return
   171  	}
   172  
   173  	// If we have a UI, output the results
   174  	if b.CLI != nil {
   175  		if op.Destroy {
   176  			b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
   177  				"[reset][bold][green]\n"+
   178  					"Destroy complete! Resources: %d destroyed.",
   179  				countHook.Removed)))
   180  		} else {
   181  			b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
   182  				"[reset][bold][green]\n"+
   183  					"Apply complete! Resources: %d added, %d changed, %d destroyed.",
   184  				countHook.Added,
   185  				countHook.Changed,
   186  				countHook.Removed)))
   187  		}
   188  
   189  		if countHook.Added > 0 || countHook.Changed > 0 {
   190  			b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
   191  				"[reset]\n"+
   192  					"The state of your infrastructure has been saved to the path\n"+
   193  					"below. This state is required to modify and destroy your\n"+
   194  					"infrastructure, so keep it safe. To inspect the complete state\n"+
   195  					"use the `terraform show` command.\n\n"+
   196  					"State path: %s",
   197  				b.StateOutPath)))
   198  		}
   199  	}
   200  }
   201  
   202  // backupStateForError is called in a scenario where we're unable to persist the
   203  // state for some reason, and will attempt to save a backup copy of the state
   204  // to local disk to help the user recover. This is a "last ditch effort" sort
   205  // of thing, so we really don't want to end up in this codepath; we should do
   206  // everything we possibly can to get the state saved _somewhere_.
   207  func (b *Local) backupStateForError(applyState *terraform.State, err error) error {
   208  	b.CLI.Error(fmt.Sprintf("Failed to save state: %s\n", err))
   209  
   210  	local := &state.LocalState{Path: "errored.tfstate"}
   211  	writeErr := local.WriteState(applyState)
   212  	if writeErr != nil {
   213  		b.CLI.Error(fmt.Sprintf(
   214  			"Also failed to create local state file for recovery: %s\n\n", writeErr,
   215  		))
   216  		// To avoid leaving the user with no state at all, our last resort
   217  		// is to print the JSON state out onto the terminal. This is an awful
   218  		// UX, so we should definitely avoid doing this if at all possible,
   219  		// but at least the user has _some_ path to recover if we end up
   220  		// here for some reason.
   221  		stateBuf := new(bytes.Buffer)
   222  		jsonErr := terraform.WriteState(applyState, stateBuf)
   223  		if jsonErr != nil {
   224  			b.CLI.Error(fmt.Sprintf(
   225  				"Also failed to JSON-serialize the state to print it: %s\n\n", jsonErr,
   226  			))
   227  			return errors.New(stateWriteFatalError)
   228  		}
   229  
   230  		b.CLI.Output(stateBuf.String())
   231  
   232  		return errors.New(stateWriteConsoleFallbackError)
   233  	}
   234  
   235  	return errors.New(stateWriteBackedUpError)
   236  }
   237  
   238  const applyErrNoConfig = `
   239  No configuration files found!
   240  
   241  Apply requires configuration to be present. Applying without a configuration
   242  would mark everything for destruction, which is normally not what is desired.
   243  If you would like to destroy everything, please run 'terraform destroy' instead
   244  which does not require any configuration files.
   245  `
   246  
   247  const stateWriteBackedUpError = `Failed to persist state to backend.
   248  
   249  The error shown above has prevented Terraform from writing the updated state
   250  to the configured backend. To allow for recovery, the state has been written
   251  to the file "errored.tfstate" in the current working directory.
   252  
   253  Running "terraform apply" again at this point will create a forked state,
   254  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 = `Failed to persist state to backend.
   261  
   262  The errors shown above prevented Terraform from writing the updated state to
   263  the configured backend and from creating a local backup file. As a fallback,
   264  the raw state data is printed above as a JSON object.
   265  
   266  To retry writing this state, copy the state data (from the first { to the
   267  last } inclusive) and save it into a local file called errored.tfstate, then
   268  run the following command:
   269      terraform state push errored.tfstate
   270  `
   271  
   272  const stateWriteFatalError = `Failed to save state after apply.
   273  
   274  A catastrophic error has prevented Terraform from persisting the state file
   275  or creating a backup. Unfortunately this means that the record of any resources
   276  created during this apply has been lost, and such resources may exist outside
   277  of Terraform's management.
   278  
   279  For resources that support import, it is possible to recover by manually
   280  importing each resource using its id from the target system.
   281  
   282  This is a serious bug in Terraform and should be reported.
   283  `
   284  
   285  const earlyStateWriteErrorFmt = `Error saving current state: %s
   286  
   287  Terraform encountered an error attempting to save the state before canceling
   288  the current operation. Once the operation is complete another attempt will be
   289  made to save the final state.
   290  `