github.com/trawler/terraform@v0.10.8-0.20171106022149-4b1c7a1d9b48/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  			// Record any shadow errors for later
   157  			if err := ctx.ShadowError(); err != nil {
   158  				shadowErr = multierror.Append(shadowErr, multierror.Prefix(
   159  					err, "apply operation:"))
   160  			}
   161  		*/
   162  	}()
   163  
   164  	// Wait for the apply to finish or for us to be interrupted so
   165  	// we can handle it properly.
   166  	err = nil
   167  	select {
   168  	case <-ctx.Done():
   169  		if b.CLI != nil {
   170  			b.CLI.Output("stopping apply operation...")
   171  		}
   172  
   173  		// try to force a PersistState just in case the process is terminated
   174  		// before we can complete.
   175  		if err := opState.PersistState(); err != nil {
   176  			// We can't error out from here, but warn the user if there was an error.
   177  			// If this isn't transient, we will catch it again below, and
   178  			// attempt to save the state another way.
   179  			if b.CLI != nil {
   180  				b.CLI.Error(fmt.Sprintf(earlyStateWriteErrorFmt, err))
   181  			}
   182  		}
   183  
   184  		// Stop execution
   185  		go tfCtx.Stop()
   186  
   187  		// Wait for completion still
   188  		<-doneCh
   189  	case <-doneCh:
   190  	}
   191  
   192  	// Store the final state
   193  	runningOp.State = applyState
   194  
   195  	// Persist the state
   196  	if err := opState.WriteState(applyState); err != nil {
   197  		runningOp.Err = b.backupStateForError(applyState, err)
   198  		return
   199  	}
   200  	if err := opState.PersistState(); err != nil {
   201  		runningOp.Err = b.backupStateForError(applyState, err)
   202  		return
   203  	}
   204  
   205  	if applyErr != nil {
   206  		runningOp.Err = fmt.Errorf(
   207  			"Error applying plan:\n\n"+
   208  				"%s\n\n"+
   209  				"Terraform does not automatically rollback in the face of errors.\n"+
   210  				"Instead, your Terraform state file has been partially updated with\n"+
   211  				"any resources that successfully completed. Please address the error\n"+
   212  				"above and apply again to incrementally change your infrastructure.",
   213  			multierror.Flatten(applyErr))
   214  		return
   215  	}
   216  
   217  	// If we have a UI, output the results
   218  	if b.CLI != nil {
   219  		if op.Destroy {
   220  			b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
   221  				"[reset][bold][green]\n"+
   222  					"Destroy complete! Resources: %d destroyed.",
   223  				countHook.Removed)))
   224  		} else {
   225  			b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
   226  				"[reset][bold][green]\n"+
   227  					"Apply complete! Resources: %d added, %d changed, %d destroyed.",
   228  				countHook.Added,
   229  				countHook.Changed,
   230  				countHook.Removed)))
   231  		}
   232  
   233  		// only show the state file help message if the state is local.
   234  		if (countHook.Added > 0 || countHook.Changed > 0) && b.StateOutPath != "" {
   235  			b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
   236  				"[reset]\n"+
   237  					"The state of your infrastructure has been saved to the path\n"+
   238  					"below. This state is required to modify and destroy your\n"+
   239  					"infrastructure, so keep it safe. To inspect the complete state\n"+
   240  					"use the `terraform show` command.\n\n"+
   241  					"State path: %s",
   242  				b.StateOutPath)))
   243  		}
   244  	}
   245  }
   246  
   247  // backupStateForError is called in a scenario where we're unable to persist the
   248  // state for some reason, and will attempt to save a backup copy of the state
   249  // to local disk to help the user recover. This is a "last ditch effort" sort
   250  // of thing, so we really don't want to end up in this codepath; we should do
   251  // everything we possibly can to get the state saved _somewhere_.
   252  func (b *Local) backupStateForError(applyState *terraform.State, err error) error {
   253  	b.CLI.Error(fmt.Sprintf("Failed to save state: %s\n", err))
   254  
   255  	local := &state.LocalState{Path: "errored.tfstate"}
   256  	writeErr := local.WriteState(applyState)
   257  	if writeErr != nil {
   258  		b.CLI.Error(fmt.Sprintf(
   259  			"Also failed to create local state file for recovery: %s\n\n", writeErr,
   260  		))
   261  		// To avoid leaving the user with no state at all, our last resort
   262  		// is to print the JSON state out onto the terminal. This is an awful
   263  		// UX, so we should definitely avoid doing this if at all possible,
   264  		// but at least the user has _some_ path to recover if we end up
   265  		// here for some reason.
   266  		stateBuf := new(bytes.Buffer)
   267  		jsonErr := terraform.WriteState(applyState, stateBuf)
   268  		if jsonErr != nil {
   269  			b.CLI.Error(fmt.Sprintf(
   270  				"Also failed to JSON-serialize the state to print it: %s\n\n", jsonErr,
   271  			))
   272  			return errors.New(stateWriteFatalError)
   273  		}
   274  
   275  		b.CLI.Output(stateBuf.String())
   276  
   277  		return errors.New(stateWriteConsoleFallbackError)
   278  	}
   279  
   280  	return errors.New(stateWriteBackedUpError)
   281  }
   282  
   283  const applyErrNoConfig = `
   284  No configuration files found!
   285  
   286  Apply requires configuration to be present. Applying without a configuration
   287  would mark everything for destruction, which is normally not what is desired.
   288  If you would like to destroy everything, please run 'terraform destroy' instead
   289  which does not require any configuration files.
   290  `
   291  
   292  const stateWriteBackedUpError = `Failed to persist state to backend.
   293  
   294  The error shown above has prevented Terraform from writing the updated state
   295  to the configured backend. To allow for recovery, the state has been written
   296  to the file "errored.tfstate" in the current working directory.
   297  
   298  Running "terraform apply" again at this point will create a forked state,
   299  making it harder to recover.
   300  
   301  To retry writing this state, use the following command:
   302      terraform state push errored.tfstate
   303  `
   304  
   305  const stateWriteConsoleFallbackError = `Failed to persist state to backend.
   306  
   307  The errors shown above prevented Terraform from writing the updated state to
   308  the configured backend and from creating a local backup file. As a fallback,
   309  the raw state data is printed above as a JSON object.
   310  
   311  To retry writing this state, copy the state data (from the first { to the
   312  last } inclusive) and save it into a local file called errored.tfstate, then
   313  run the following command:
   314      terraform state push errored.tfstate
   315  `
   316  
   317  const stateWriteFatalError = `Failed to save state after apply.
   318  
   319  A catastrophic error has prevented Terraform from persisting the state file
   320  or creating a backup. Unfortunately this means that the record of any resources
   321  created during this apply has been lost, and such resources may exist outside
   322  of Terraform's management.
   323  
   324  For resources that support import, it is possible to recover by manually
   325  importing each resource using its id from the target system.
   326  
   327  This is a serious bug in Terraform and should be reported.
   328  `
   329  
   330  const earlyStateWriteErrorFmt = `Error saving current state: %s
   331  
   332  Terraform encountered an error attempting to save the state before canceling
   333  the current operation. Once the operation is complete another attempt will be
   334  made to save the final state.
   335  `