github.com/chentex/terraform@v0.11.2-0.20171208003256-252e8145842e/backend/local/backend_plan.go (about)

     1  package local
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"log"
     8  	"os"
     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) opPlan(
    22  	ctx context.Context,
    23  	op *backend.Operation,
    24  	runningOp *backend.RunningOperation) {
    25  	log.Printf("[INFO] backend/local: starting Plan operation")
    26  
    27  	if b.CLI != nil && op.Plan != nil {
    28  		b.CLI.Output(b.Colorize().Color(
    29  			"[reset][bold][yellow]" +
    30  				"The plan command received a saved plan file as input. This command\n" +
    31  				"will output the saved plan. This will not modify the already-existing\n" +
    32  				"plan. If you wish to generate a new plan, please pass in a configuration\n" +
    33  				"directory as an argument.\n\n"))
    34  	}
    35  
    36  	// A local plan requires either a plan or a module
    37  	if op.Plan == nil && op.Module == nil && !op.Destroy {
    38  		runningOp.Err = fmt.Errorf(strings.TrimSpace(planErrNoConfig))
    39  		return
    40  	}
    41  
    42  	// If we have a nil module at this point, then set it to an empty tree
    43  	// to avoid any potential crashes.
    44  	if op.Module == nil {
    45  		op.Module = module.NewEmptyTree()
    46  	}
    47  
    48  	// Setup our count hook that keeps track of resource changes
    49  	countHook := new(CountHook)
    50  	if b.ContextOpts == nil {
    51  		b.ContextOpts = new(terraform.ContextOpts)
    52  	}
    53  	old := b.ContextOpts.Hooks
    54  	defer func() { b.ContextOpts.Hooks = old }()
    55  	b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, countHook)
    56  
    57  	// Get our context
    58  	tfCtx, opState, err := b.context(op)
    59  	if err != nil {
    60  		runningOp.Err = err
    61  		return
    62  	}
    63  
    64  	if op.LockState {
    65  		lockCtx, cancel := context.WithTimeout(ctx, op.StateLockTimeout)
    66  		defer cancel()
    67  
    68  		lockInfo := state.NewLockInfo()
    69  		lockInfo.Operation = op.Type.String()
    70  		lockID, err := clistate.Lock(lockCtx, opState, lockInfo, b.CLI, b.Colorize())
    71  		if err != nil {
    72  			runningOp.Err = errwrap.Wrapf("Error locking state: {{err}}", err)
    73  			return
    74  		}
    75  
    76  		defer func() {
    77  			if err := clistate.Unlock(opState, lockID, b.CLI, b.Colorize()); err != nil {
    78  				runningOp.Err = multierror.Append(runningOp.Err, err)
    79  			}
    80  		}()
    81  	}
    82  
    83  	// Setup the state
    84  	runningOp.State = tfCtx.State()
    85  
    86  	// If we're refreshing before plan, perform that
    87  	if op.PlanRefresh {
    88  		log.Printf("[INFO] backend/local: plan calling Refresh")
    89  
    90  		if b.CLI != nil {
    91  			b.CLI.Output(b.Colorize().Color(strings.TrimSpace(planRefreshing) + "\n"))
    92  		}
    93  
    94  		_, err := tfCtx.Refresh()
    95  		if err != nil {
    96  			runningOp.Err = errwrap.Wrapf("Error refreshing state: {{err}}", err)
    97  			return
    98  		}
    99  		if b.CLI != nil {
   100  			b.CLI.Output("\n------------------------------------------------------------------------")
   101  		}
   102  	}
   103  
   104  	// Perform the plan in a goroutine so we can be interrupted
   105  	var plan *terraform.Plan
   106  	var planErr error
   107  	doneCh := make(chan struct{})
   108  	go func() {
   109  		defer close(doneCh)
   110  		log.Printf("[INFO] backend/local: plan calling Plan")
   111  		plan, planErr = tfCtx.Plan()
   112  	}()
   113  
   114  	select {
   115  	case <-ctx.Done():
   116  		if b.CLI != nil {
   117  			b.CLI.Output("stopping plan operation...")
   118  		}
   119  
   120  		// Stop execution
   121  		go tfCtx.Stop()
   122  
   123  		// Wait for completion still
   124  		<-doneCh
   125  	case <-doneCh:
   126  	}
   127  
   128  	if planErr != nil {
   129  		runningOp.Err = errwrap.Wrapf("Error running plan: {{err}}", planErr)
   130  		return
   131  	}
   132  	// Record state
   133  	runningOp.PlanEmpty = plan.Diff.Empty()
   134  
   135  	// Save the plan to disk
   136  	if path := op.PlanOutPath; path != "" {
   137  		// Write the backend if we have one
   138  		plan.Backend = op.PlanOutBackend
   139  
   140  		// This works around a bug (#12871) which is no longer possible to
   141  		// trigger but will exist for already corrupted upgrades.
   142  		if plan.Backend != nil && plan.State != nil {
   143  			plan.State.Remote = nil
   144  		}
   145  
   146  		log.Printf("[INFO] backend/local: writing plan output to: %s", path)
   147  		f, err := os.Create(path)
   148  		if err == nil {
   149  			err = terraform.WritePlan(plan, f)
   150  		}
   151  		f.Close()
   152  		if err != nil {
   153  			runningOp.Err = fmt.Errorf("Error writing plan file: %s", err)
   154  			return
   155  		}
   156  	}
   157  
   158  	// Perform some output tasks if we have a CLI to output to.
   159  	if b.CLI != nil {
   160  		dispPlan := format.NewPlan(plan)
   161  		if dispPlan.Empty() {
   162  			b.CLI.Output("\n" + b.Colorize().Color(strings.TrimSpace(planNoChanges)))
   163  			return
   164  		}
   165  
   166  		b.renderPlan(dispPlan)
   167  
   168  		// Give the user some next-steps, unless we're running in an automation
   169  		// tool which is presumed to provide its own UI for further actions.
   170  		if !b.RunningInAutomation {
   171  
   172  			b.CLI.Output("\n------------------------------------------------------------------------")
   173  
   174  			if path := op.PlanOutPath; path == "" {
   175  				b.CLI.Output(fmt.Sprintf(
   176  					"\n" + strings.TrimSpace(planHeaderNoOutput) + "\n",
   177  				))
   178  			} else {
   179  				b.CLI.Output(fmt.Sprintf(
   180  					"\n"+strings.TrimSpace(planHeaderYesOutput)+"\n",
   181  					path, path,
   182  				))
   183  			}
   184  
   185  		}
   186  	}
   187  }
   188  
   189  func (b *Local) renderPlan(dispPlan *format.Plan) {
   190  
   191  	headerBuf := &bytes.Buffer{}
   192  	fmt.Fprintf(headerBuf, "\n%s\n", strings.TrimSpace(planHeaderIntro))
   193  	counts := dispPlan.ActionCounts()
   194  	if counts[terraform.DiffCreate] > 0 {
   195  		fmt.Fprintf(headerBuf, "%s create\n", format.DiffActionSymbol(terraform.DiffCreate))
   196  	}
   197  	if counts[terraform.DiffUpdate] > 0 {
   198  		fmt.Fprintf(headerBuf, "%s update in-place\n", format.DiffActionSymbol(terraform.DiffUpdate))
   199  	}
   200  	if counts[terraform.DiffDestroy] > 0 {
   201  		fmt.Fprintf(headerBuf, "%s destroy\n", format.DiffActionSymbol(terraform.DiffDestroy))
   202  	}
   203  	if counts[terraform.DiffDestroyCreate] > 0 {
   204  		fmt.Fprintf(headerBuf, "%s destroy and then create replacement\n", format.DiffActionSymbol(terraform.DiffDestroyCreate))
   205  	}
   206  	if counts[terraform.DiffRefresh] > 0 {
   207  		fmt.Fprintf(headerBuf, "%s read (data resources)\n", format.DiffActionSymbol(terraform.DiffRefresh))
   208  	}
   209  
   210  	b.CLI.Output(b.Colorize().Color(headerBuf.String()))
   211  
   212  	b.CLI.Output("Terraform will perform the following actions:\n")
   213  
   214  	b.CLI.Output(dispPlan.Format(b.Colorize()))
   215  
   216  	stats := dispPlan.Stats()
   217  	b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
   218  		"[reset][bold]Plan:[reset] "+
   219  			"%d to add, %d to change, %d to destroy.",
   220  		stats.ToAdd, stats.ToChange, stats.ToDestroy,
   221  	)))
   222  }
   223  
   224  const planErrNoConfig = `
   225  No configuration files found!
   226  
   227  Plan requires configuration to be present. Planning without a configuration
   228  would mark everything for destruction, which is normally not what is desired.
   229  If you would like to destroy everything, please run plan with the "-destroy"
   230  flag or create a single empty configuration file. Otherwise, please create
   231  a Terraform configuration file in the path being executed and try again.
   232  `
   233  
   234  const planHeaderIntro = `
   235  An execution plan has been generated and is shown below.
   236  Resource actions are indicated with the following symbols:
   237  `
   238  
   239  const planHeaderNoOutput = `
   240  Note: You didn't specify an "-out" parameter to save this plan, so Terraform
   241  can't guarantee that exactly these actions will be performed if
   242  "terraform apply" is subsequently run.
   243  `
   244  
   245  const planHeaderYesOutput = `
   246  This plan was saved to: %s
   247  
   248  To perform exactly these actions, run the following command to apply:
   249      terraform apply %q
   250  `
   251  
   252  const planNoChanges = `
   253  [reset][bold][green]No changes. Infrastructure is up-to-date.[reset][green]
   254  
   255  This means that Terraform did not detect any differences between your
   256  configuration and real physical resources that exist. As a result, no
   257  actions need to be performed.
   258  `
   259  
   260  const planRefreshing = `
   261  [reset][bold]Refreshing Terraform state in-memory prior to plan...[reset]
   262  The refreshed state will be used to calculate this plan, but will not be
   263  persisted to local or remote state storage.
   264  `