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 `