github.com/meteor/terraform@v0.6.15-0.20210412225145-79ec4bc057c6/backend/local/backend_apply.go (about)

     1  package local
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"log"
     8  	"strings"
     9  
    10  	"github.com/hashicorp/errwrap"
    11  	"github.com/hashicorp/go-multierror"
    12  	"github.com/hashicorp/terraform/backend"
    13  	"github.com/hashicorp/terraform/command/clistate"
    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  	ctx context.Context,
    22  	op *backend.Operation,
    23  	runningOp *backend.RunningOperation) {
    24  	log.Printf("[INFO] backend/local: starting Apply operation")
    25  
    26  	// If we have a nil module at this point, then set it to an empty tree
    27  	// to avoid any potential crashes.
    28  	if op.Plan == nil && op.Module == nil && !op.Destroy {
    29  		runningOp.Err = fmt.Errorf(strings.TrimSpace(applyErrNoConfig))
    30  		return
    31  	}
    32  
    33  	// If we have a nil module at this point, then set it to an empty tree
    34  	// to avoid any potential crashes.
    35  	if op.Module == nil {
    36  		op.Module = module.NewEmptyTree()
    37  	}
    38  
    39  	// Setup our count hook that keeps track of resource changes
    40  	countHook := new(CountHook)
    41  	stateHook := new(StateHook)
    42  	if b.ContextOpts == nil {
    43  		b.ContextOpts = new(terraform.ContextOpts)
    44  	}
    45  	old := b.ContextOpts.Hooks
    46  	defer func() { b.ContextOpts.Hooks = old }()
    47  	b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, countHook, stateHook)
    48  
    49  	// Get our context
    50  	tfCtx, opState, err := b.context(op)
    51  	if err != nil {
    52  		runningOp.Err = err
    53  		return
    54  	}
    55  
    56  	if op.LockState {
    57  		lockCtx, cancel := context.WithTimeout(ctx, op.StateLockTimeout)
    58  		defer cancel()
    59  
    60  		lockInfo := state.NewLockInfo()
    61  		lockInfo.Operation = op.Type.String()
    62  		lockID, err := clistate.Lock(lockCtx, opState, lockInfo, b.CLI, b.Colorize())
    63  		if err != nil {
    64  			runningOp.Err = errwrap.Wrapf("Error locking state: {{err}}", err)
    65  			return
    66  		}
    67  
    68  		defer func() {
    69  			if err := clistate.Unlock(opState, lockID, b.CLI, b.Colorize()); err != nil {
    70  				runningOp.Err = multierror.Append(runningOp.Err, err)
    71  			}
    72  		}()
    73  	}
    74  
    75  	// Setup the state
    76  	runningOp.State = tfCtx.State()
    77  
    78  	// If we weren't given a plan, then we refresh/plan
    79  	if op.Plan == nil {
    80  		// If we're refreshing before apply, perform that
    81  		if op.PlanRefresh {
    82  			log.Printf("[INFO] backend/local: apply calling Refresh")
    83  			_, err := tfCtx.Refresh()
    84  			if err != nil {
    85  				runningOp.Err = errwrap.Wrapf("Error refreshing state: {{err}}", err)
    86  				return
    87  			}
    88  		}
    89  
    90  		// Perform the plan
    91  		log.Printf("[INFO] backend/local: apply calling Plan")
    92  		plan, err := tfCtx.Plan()
    93  		if err != nil {
    94  			runningOp.Err = errwrap.Wrapf("Error running plan: {{err}}", err)
    95  			return
    96  		}
    97  
    98  		trivialPlan := plan.Diff == nil || plan.Diff.Empty()
    99  		if op.ConfirmPlan && !trivialPlan {
   100  			op.UIOut.Output(strings.TrimSpace(confirmPlanHeader) + "\n")
   101  			op.UIOut.Output(format.Plan(&format.PlanOpts{
   102  				Plan:        plan,
   103  				Color:       b.Colorize(),
   104  				ModuleDepth: -1,
   105  			}))
   106  			desc := "Terraform will apply the plan described above.\n" +
   107  				"Only 'yes' will be accepted to confirm."
   108  			v, err := op.UIIn.Input(&terraform.InputOpts{
   109  				Id:          "confirm",
   110  				Query:       "Do you want to apply the plan above?",
   111  				Description: desc,
   112  			})
   113  			if err != nil {
   114  				runningOp.Err = errwrap.Wrapf("Error asking for confirmation: {{err}}", err)
   115  				return
   116  			}
   117  			if v != "yes" {
   118  				runningOp.Err = errors.New("Apply cancelled.")
   119  				return
   120  			}
   121  		}
   122  	}
   123  
   124  	// Setup our hook for continuous state updates
   125  	stateHook.State = opState
   126  
   127  	// Start the apply in a goroutine so that we can be interrupted.
   128  	var applyState *terraform.State
   129  	var applyErr error
   130  	doneCh := make(chan struct{})
   131  	go func() {
   132  		defer close(doneCh)
   133  		_, applyErr = tfCtx.Apply()
   134  		// we always want the state, even if apply failed
   135  		applyState = tfCtx.State()
   136  
   137  		/*
   138  			// Record any shadow errors for later
   139  			if err := ctx.ShadowError(); err != nil {
   140  				shadowErr = multierror.Append(shadowErr, multierror.Prefix(
   141  					err, "apply operation:"))
   142  			}
   143  		*/
   144  	}()
   145  
   146  	// Wait for the apply to finish or for us to be interrupted so
   147  	// we can handle it properly.
   148  	err = nil
   149  	select {
   150  	case <-ctx.Done():
   151  		if b.CLI != nil {
   152  			b.CLI.Output("stopping apply operation...")
   153  		}
   154  
   155  		// Stop execution
   156  		go tfCtx.Stop()
   157  
   158  		// Wait for completion still
   159  		<-doneCh
   160  	case <-doneCh:
   161  	}
   162  
   163  	// Store the final state
   164  	runningOp.State = applyState
   165  
   166  	// Persist the state
   167  	if err := opState.WriteState(applyState); err != nil {
   168  		runningOp.Err = fmt.Errorf("Failed to save state: %s", err)
   169  		return
   170  	}
   171  	if err := opState.PersistState(); err != nil {
   172  		runningOp.Err = fmt.Errorf("Failed to save state: %s", err)
   173  		return
   174  	}
   175  
   176  	if applyErr != nil {
   177  		runningOp.Err = fmt.Errorf(
   178  			"Error applying plan:\n\n"+
   179  				"%s\n\n"+
   180  				"Terraform does not automatically rollback in the face of errors.\n"+
   181  				"Instead, your Terraform state file has been partially updated with\n"+
   182  				"any resources that successfully completed. Please address the error\n"+
   183  				"above and apply again to incrementally change your infrastructure.",
   184  			multierror.Flatten(applyErr))
   185  		return
   186  	}
   187  
   188  	// If we have a UI, output the results
   189  	if b.CLI != nil {
   190  		if op.Destroy {
   191  			b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
   192  				"[reset][bold][green]\n"+
   193  					"Destroy complete! Resources: %d destroyed.",
   194  				countHook.Removed)))
   195  		} else {
   196  			b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
   197  				"[reset][bold][green]\n"+
   198  					"Apply complete! Resources: %d added, %d changed, %d destroyed.",
   199  				countHook.Added,
   200  				countHook.Changed,
   201  				countHook.Removed)))
   202  		}
   203  
   204  		if countHook.Added > 0 || countHook.Changed > 0 {
   205  			b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
   206  				"[reset]\n"+
   207  					"The state of your infrastructure has been saved to the path\n"+
   208  					"below. This state is required to modify and destroy your\n"+
   209  					"infrastructure, so keep it safe. To inspect the complete state\n"+
   210  					"use the `terraform show` command.\n\n"+
   211  					"State path: %s",
   212  				b.StateOutPath)))
   213  		}
   214  	}
   215  }
   216  
   217  const applyErrNoConfig = `
   218  No configuration files found!
   219  
   220  Apply requires configuration to be present. Applying without a configuration
   221  would mark everything for destruction, which is normally not what is desired.
   222  If you would like to destroy everything, please run 'terraform destroy' instead
   223  which does not require any configuration files.
   224  `
   225  
   226  const confirmPlanHeader = `
   227  The Terraform execution plan has been generated and is shown below.
   228  Resources are shown in alphabetical order for quick scanning. Green resources
   229  will be created (or destroyed and then created if an existing resource
   230  exists), yellow resources are being changed in-place, and red resources
   231  will be destroyed. Cyan entries are data sources to be read.
   232  `