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 `