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