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