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