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