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