github.com/gerbenjacobs/terraform@v0.9.5-0.20170630130047-e6ddd62583d8/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  		trivialPlan := plan.Diff == nil || plan.Diff.Empty()
   100  		hasUI := op.UIOut != nil && op.UIIn != nil
   101  		if hasUI && ((op.Destroy && !op.DestroyForce) ||
   102  			(!op.Destroy && !op.AutoApprove && !trivialPlan)) {
   103  			var desc, query string
   104  			if op.Destroy {
   105  				// Default destroy message
   106  				desc = "Terraform will delete all your managed infrastructure, as shown above.\n" +
   107  					"There is no undo. Only 'yes' will be accepted to confirm."
   108  
   109  				// If targets are specified, list those to user
   110  				if op.Targets != nil {
   111  					var descBuffer bytes.Buffer
   112  					descBuffer.WriteString("Terraform will delete the following infrastructure:\n")
   113  					for _, target := range op.Targets {
   114  						descBuffer.WriteString("\t")
   115  						descBuffer.WriteString(target)
   116  						descBuffer.WriteString("\n")
   117  					}
   118  					descBuffer.WriteString("There is no undo. Only 'yes' will be accepted to confirm")
   119  					desc = descBuffer.String()
   120  				}
   121  				query = "Do you really want to destroy?"
   122  			} else {
   123  				desc = "Terraform will apply the changes described above.\n" +
   124  					"Only 'yes' will be accepted to approve."
   125  				query = "Do you want to apply these changes?"
   126  			}
   127  
   128  			if !trivialPlan {
   129  				// Display the plan of what we are going to apply/destroy.
   130  				if op.Destroy {
   131  					op.UIOut.Output("\n" + strings.TrimSpace(approveDestroyPlanHeader) + "\n")
   132  				} else {
   133  					op.UIOut.Output("\n" + strings.TrimSpace(approvePlanHeader) + "\n")
   134  				}
   135  				op.UIOut.Output(format.Plan(&format.PlanOpts{
   136  					Plan:        plan,
   137  					Color:       b.Colorize(),
   138  					ModuleDepth: -1,
   139  				}))
   140  			}
   141  
   142  			v, err := op.UIIn.Input(&terraform.InputOpts{
   143  				Id:          "approve",
   144  				Query:       query,
   145  				Description: desc,
   146  			})
   147  			if err != nil {
   148  				runningOp.Err = errwrap.Wrapf("Error asking for approval: {{err}}", err)
   149  				return
   150  			}
   151  			if v != "yes" {
   152  				if op.Destroy {
   153  					runningOp.Err = errors.New("Destroy cancelled.")
   154  				} else {
   155  					runningOp.Err = errors.New("Apply cancelled.")
   156  				}
   157  				return
   158  			}
   159  		}
   160  	}
   161  
   162  	// Setup our hook for continuous state updates
   163  	stateHook.State = opState
   164  
   165  	// Start the apply in a goroutine so that we can be interrupted.
   166  	var applyState *terraform.State
   167  	var applyErr error
   168  	doneCh := make(chan struct{})
   169  	go func() {
   170  		defer close(doneCh)
   171  		_, applyErr = tfCtx.Apply()
   172  		// we always want the state, even if apply failed
   173  		applyState = tfCtx.State()
   174  
   175  		/*
   176  			// Record any shadow errors for later
   177  			if err := ctx.ShadowError(); err != nil {
   178  				shadowErr = multierror.Append(shadowErr, multierror.Prefix(
   179  					err, "apply operation:"))
   180  			}
   181  		*/
   182  	}()
   183  
   184  	// Wait for the apply to finish or for us to be interrupted so
   185  	// we can handle it properly.
   186  	err = nil
   187  	select {
   188  	case <-ctx.Done():
   189  		if b.CLI != nil {
   190  			b.CLI.Output("stopping apply operation...")
   191  		}
   192  
   193  		// try to force a PersistState just in case the process is terminated
   194  		// before we can complete.
   195  		if err := opState.PersistState(); err != nil {
   196  			// We can't error out from here, but warn the user if there was an error.
   197  			// If this isn't transient, we will catch it again below, and
   198  			// attempt to save the state another way.
   199  			if b.CLI != nil {
   200  				b.CLI.Error(fmt.Sprintf(earlyStateWriteErrorFmt, err))
   201  			}
   202  		}
   203  
   204  		// Stop execution
   205  		go tfCtx.Stop()
   206  
   207  		// Wait for completion still
   208  		<-doneCh
   209  	case <-doneCh:
   210  	}
   211  
   212  	// Store the final state
   213  	runningOp.State = applyState
   214  
   215  	// Persist the state
   216  	if err := opState.WriteState(applyState); err != nil {
   217  		runningOp.Err = b.backupStateForError(applyState, err)
   218  		return
   219  	}
   220  	if err := opState.PersistState(); err != nil {
   221  		runningOp.Err = b.backupStateForError(applyState, err)
   222  		return
   223  	}
   224  
   225  	if applyErr != nil {
   226  		runningOp.Err = fmt.Errorf(
   227  			"Error applying plan:\n\n"+
   228  				"%s\n\n"+
   229  				"Terraform does not automatically rollback in the face of errors.\n"+
   230  				"Instead, your Terraform state file has been partially updated with\n"+
   231  				"any resources that successfully completed. Please address the error\n"+
   232  				"above and apply again to incrementally change your infrastructure.",
   233  			multierror.Flatten(applyErr))
   234  		return
   235  	}
   236  
   237  	// If we have a UI, output the results
   238  	if b.CLI != nil {
   239  		if op.Destroy {
   240  			b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
   241  				"[reset][bold][green]\n"+
   242  					"Destroy complete! Resources: %d destroyed.",
   243  				countHook.Removed)))
   244  		} else {
   245  			b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
   246  				"[reset][bold][green]\n"+
   247  					"Apply complete! Resources: %d added, %d changed, %d destroyed.",
   248  				countHook.Added,
   249  				countHook.Changed,
   250  				countHook.Removed)))
   251  		}
   252  
   253  		if countHook.Added > 0 || countHook.Changed > 0 {
   254  			b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
   255  				"[reset]\n"+
   256  					"The state of your infrastructure has been saved to the path\n"+
   257  					"below. This state is required to modify and destroy your\n"+
   258  					"infrastructure, so keep it safe. To inspect the complete state\n"+
   259  					"use the `terraform show` command.\n\n"+
   260  					"State path: %s",
   261  				b.StateOutPath)))
   262  		}
   263  	}
   264  }
   265  
   266  // backupStateForError is called in a scenario where we're unable to persist the
   267  // state for some reason, and will attempt to save a backup copy of the state
   268  // to local disk to help the user recover. This is a "last ditch effort" sort
   269  // of thing, so we really don't want to end up in this codepath; we should do
   270  // everything we possibly can to get the state saved _somewhere_.
   271  func (b *Local) backupStateForError(applyState *terraform.State, err error) error {
   272  	b.CLI.Error(fmt.Sprintf("Failed to save state: %s\n", err))
   273  
   274  	local := &state.LocalState{Path: "errored.tfstate"}
   275  	writeErr := local.WriteState(applyState)
   276  	if writeErr != nil {
   277  		b.CLI.Error(fmt.Sprintf(
   278  			"Also failed to create local state file for recovery: %s\n\n", writeErr,
   279  		))
   280  		// To avoid leaving the user with no state at all, our last resort
   281  		// is to print the JSON state out onto the terminal. This is an awful
   282  		// UX, so we should definitely avoid doing this if at all possible,
   283  		// but at least the user has _some_ path to recover if we end up
   284  		// here for some reason.
   285  		stateBuf := new(bytes.Buffer)
   286  		jsonErr := terraform.WriteState(applyState, stateBuf)
   287  		if jsonErr != nil {
   288  			b.CLI.Error(fmt.Sprintf(
   289  				"Also failed to JSON-serialize the state to print it: %s\n\n", jsonErr,
   290  			))
   291  			return errors.New(stateWriteFatalError)
   292  		}
   293  
   294  		b.CLI.Output(stateBuf.String())
   295  
   296  		return errors.New(stateWriteConsoleFallbackError)
   297  	}
   298  
   299  	return errors.New(stateWriteBackedUpError)
   300  }
   301  
   302  const applyErrNoConfig = `
   303  No configuration files found!
   304  
   305  Apply requires configuration to be present. Applying without a configuration
   306  would mark everything for destruction, which is normally not what is desired.
   307  If you would like to destroy everything, please run 'terraform destroy' instead
   308  which does not require any configuration files.
   309  `
   310  
   311  const stateWriteBackedUpError = `Failed to persist state to backend.
   312  
   313  The error shown above has prevented Terraform from writing the updated state
   314  to the configured backend. To allow for recovery, the state has been written
   315  to the file "errored.tfstate" in the current working directory.
   316  
   317  Running "terraform apply" again at this point will create a forked state,
   318  making it harder to recover.
   319  
   320  To retry writing this state, use the following command:
   321      terraform state push errored.tfstate
   322  `
   323  
   324  const stateWriteConsoleFallbackError = `Failed to persist state to backend.
   325  
   326  The errors shown above prevented Terraform from writing the updated state to
   327  the configured backend and from creating a local backup file. As a fallback,
   328  the raw state data is printed above as a JSON object.
   329  
   330  To retry writing this state, copy the state data (from the first { to the
   331  last } inclusive) and save it into a local file called errored.tfstate, then
   332  run the following command:
   333      terraform state push errored.tfstate
   334  `
   335  
   336  const stateWriteFatalError = `Failed to save state after apply.
   337  
   338  A catastrophic error has prevented Terraform from persisting the state file
   339  or creating a backup. Unfortunately this means that the record of any resources
   340  created during this apply has been lost, and such resources may exist outside
   341  of Terraform's management.
   342  
   343  For resources that support import, it is possible to recover by manually
   344  importing each resource using its id from the target system.
   345  
   346  This is a serious bug in Terraform and should be reported.
   347  `
   348  
   349  const earlyStateWriteErrorFmt = `Error saving current state: %s
   350  
   351  Terraform encountered an error attempting to save the state before canceling
   352  the current operation. Once the operation is complete another attempt will be
   353  made to save the final state.
   354  `
   355  
   356  const approvePlanHeader = `
   357  The Terraform execution plan has been generated and is shown below.
   358  Resources are shown in alphabetical order for quick scanning. Green resources
   359  will be created (or destroyed and then created if an existing resource
   360  exists), yellow resources are being changed in-place, and red resources
   361  will be destroyed. Cyan entries are data sources to be read.
   362  `
   363  
   364  const approveDestroyPlanHeader = `
   365  The Terraform destroy plan has been generated and is shown below.
   366  Resources are shown in alphabetical order for quick scanning.
   367  Resources shown in red will be destroyed.
   368  `