github.com/hooklift/terraform@v0.11.0-beta1.0.20171117000744-6786c1361ffe/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
   105  	log.Printf("[INFO] backend/local: plan calling Plan")
   106  	plan, err := tfCtx.Plan()
   107  	if err != nil {
   108  		runningOp.Err = errwrap.Wrapf("Error running plan: {{err}}", err)
   109  		return
   110  	}
   111  
   112  	// Record state
   113  	runningOp.PlanEmpty = plan.Diff.Empty()
   114  
   115  	// Save the plan to disk
   116  	if path := op.PlanOutPath; path != "" {
   117  		// Write the backend if we have one
   118  		plan.Backend = op.PlanOutBackend
   119  
   120  		// This works around a bug (#12871) which is no longer possible to
   121  		// trigger but will exist for already corrupted upgrades.
   122  		if plan.Backend != nil && plan.State != nil {
   123  			plan.State.Remote = nil
   124  		}
   125  
   126  		log.Printf("[INFO] backend/local: writing plan output to: %s", path)
   127  		f, err := os.Create(path)
   128  		if err == nil {
   129  			err = terraform.WritePlan(plan, f)
   130  		}
   131  		f.Close()
   132  		if err != nil {
   133  			runningOp.Err = fmt.Errorf("Error writing plan file: %s", err)
   134  			return
   135  		}
   136  	}
   137  
   138  	// Perform some output tasks if we have a CLI to output to.
   139  	if b.CLI != nil {
   140  		dispPlan := format.NewPlan(plan)
   141  		if dispPlan.Empty() {
   142  			b.CLI.Output("\n" + b.Colorize().Color(strings.TrimSpace(planNoChanges)))
   143  			return
   144  		}
   145  
   146  		b.renderPlan(dispPlan)
   147  
   148  		// Give the user some next-steps, unless we're running in an automation
   149  		// tool which is presumed to provide its own UI for further actions.
   150  		if !b.RunningInAutomation {
   151  
   152  			b.CLI.Output("\n------------------------------------------------------------------------")
   153  
   154  			if path := op.PlanOutPath; path == "" {
   155  				b.CLI.Output(fmt.Sprintf(
   156  					"\n" + strings.TrimSpace(planHeaderNoOutput) + "\n",
   157  				))
   158  			} else {
   159  				b.CLI.Output(fmt.Sprintf(
   160  					"\n"+strings.TrimSpace(planHeaderYesOutput)+"\n",
   161  					path, path,
   162  				))
   163  			}
   164  
   165  		}
   166  	}
   167  }
   168  
   169  func (b *Local) renderPlan(dispPlan *format.Plan) {
   170  
   171  	headerBuf := &bytes.Buffer{}
   172  	fmt.Fprintf(headerBuf, "\n%s\n", strings.TrimSpace(planHeaderIntro))
   173  	counts := dispPlan.ActionCounts()
   174  	if counts[terraform.DiffCreate] > 0 {
   175  		fmt.Fprintf(headerBuf, "%s create\n", format.DiffActionSymbol(terraform.DiffCreate))
   176  	}
   177  	if counts[terraform.DiffUpdate] > 0 {
   178  		fmt.Fprintf(headerBuf, "%s update in-place\n", format.DiffActionSymbol(terraform.DiffUpdate))
   179  	}
   180  	if counts[terraform.DiffDestroy] > 0 {
   181  		fmt.Fprintf(headerBuf, "%s destroy\n", format.DiffActionSymbol(terraform.DiffDestroy))
   182  	}
   183  	if counts[terraform.DiffDestroyCreate] > 0 {
   184  		fmt.Fprintf(headerBuf, "%s destroy and then create replacement\n", format.DiffActionSymbol(terraform.DiffDestroyCreate))
   185  	}
   186  	if counts[terraform.DiffRefresh] > 0 {
   187  		fmt.Fprintf(headerBuf, "%s read (data resources)\n", format.DiffActionSymbol(terraform.DiffRefresh))
   188  	}
   189  
   190  	b.CLI.Output(b.Colorize().Color(headerBuf.String()))
   191  
   192  	b.CLI.Output("Terraform will perform the following actions:\n")
   193  
   194  	b.CLI.Output(dispPlan.Format(b.Colorize()))
   195  
   196  	stats := dispPlan.Stats()
   197  	b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
   198  		"[reset][bold]Plan:[reset] "+
   199  			"%d to add, %d to change, %d to destroy.",
   200  		stats.ToAdd, stats.ToChange, stats.ToDestroy,
   201  	)))
   202  }
   203  
   204  const planErrNoConfig = `
   205  No configuration files found!
   206  
   207  Plan requires configuration to be present. Planning without a configuration
   208  would mark everything for destruction, which is normally not what is desired.
   209  If you would like to destroy everything, please run plan with the "-destroy"
   210  flag or create a single empty configuration file. Otherwise, please create
   211  a Terraform configuration file in the path being executed and try again.
   212  `
   213  
   214  const planHeaderIntro = `
   215  An execution plan has been generated and is shown below.
   216  Resource actions are indicated with the following symbols:
   217  `
   218  
   219  const planHeaderNoOutput = `
   220  Note: You didn't specify an "-out" parameter to save this plan, so Terraform
   221  can't guarantee that exactly these actions will be performed if
   222  "terraform apply" is subsequently run.
   223  `
   224  
   225  const planHeaderYesOutput = `
   226  This plan was saved to: %s
   227  
   228  To perform exactly these actions, run the following command to apply:
   229      terraform apply %q
   230  `
   231  
   232  const planNoChanges = `
   233  [reset][bold][green]No changes. Infrastructure is up-to-date.[reset][green]
   234  
   235  This means that Terraform did not detect any differences between your
   236  configuration and real physical resources that exist. As a result, no
   237  actions need to be performed.
   238  `
   239  
   240  const planRefreshing = `
   241  [reset][bold]Refreshing Terraform state in-memory prior to plan...[reset]
   242  The refreshed state will be used to calculate this plan, but will not be
   243  persisted to local or remote state storage.
   244  `