github.com/gerbenjacobs/terraform@v0.9.5-0.20170630130047-e6ddd62583d8/backend/local/backend_apply.go (about) 1 package local 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "log" 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) opApply( 22 ctx context.Context, 23 op *backend.Operation, 24 runningOp *backend.RunningOperation) { 25 log.Printf("[INFO] backend/local: starting Apply operation") 26 27 // If we have a nil module at this point, then set it to an empty tree 28 // to avoid any potential crashes. 29 if op.Plan == nil && op.Module == nil && !op.Destroy { 30 runningOp.Err = fmt.Errorf(strings.TrimSpace(applyErrNoConfig)) 31 return 32 } 33 34 // If we have a nil module at this point, then set it to an empty tree 35 // to avoid any potential crashes. 36 if op.Module == nil { 37 op.Module = module.NewEmptyTree() 38 } 39 40 // Setup our count hook that keeps track of resource changes 41 countHook := new(CountHook) 42 stateHook := new(StateHook) 43 if b.ContextOpts == nil { 44 b.ContextOpts = new(terraform.ContextOpts) 45 } 46 old := b.ContextOpts.Hooks 47 defer func() { b.ContextOpts.Hooks = old }() 48 b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, countHook, stateHook) 49 50 // Get our context 51 tfCtx, opState, err := b.context(op) 52 if err != nil { 53 runningOp.Err = err 54 return 55 } 56 57 if op.LockState { 58 lockCtx, cancel := context.WithTimeout(ctx, op.StateLockTimeout) 59 defer cancel() 60 61 lockInfo := state.NewLockInfo() 62 lockInfo.Operation = op.Type.String() 63 lockID, err := clistate.Lock(lockCtx, opState, lockInfo, b.CLI, b.Colorize()) 64 if err != nil { 65 runningOp.Err = errwrap.Wrapf("Error locking state: {{err}}", err) 66 return 67 } 68 69 defer func() { 70 if err := clistate.Unlock(opState, lockID, b.CLI, b.Colorize()); err != nil { 71 runningOp.Err = multierror.Append(runningOp.Err, err) 72 } 73 }() 74 } 75 76 // Setup the state 77 runningOp.State = tfCtx.State() 78 79 // If we weren't given a plan, then we refresh/plan 80 if op.Plan == nil { 81 // If we're refreshing before apply, perform that 82 if op.PlanRefresh { 83 log.Printf("[INFO] backend/local: apply calling Refresh") 84 _, err := tfCtx.Refresh() 85 if err != nil { 86 runningOp.Err = errwrap.Wrapf("Error refreshing state: {{err}}", err) 87 return 88 } 89 } 90 91 // Perform the plan 92 log.Printf("[INFO] backend/local: apply calling Plan") 93 plan, err := tfCtx.Plan() 94 if err != nil { 95 runningOp.Err = errwrap.Wrapf("Error running plan: {{err}}", err) 96 return 97 } 98 99 trivialPlan := plan.Diff == nil || plan.Diff.Empty() 100 hasUI := op.UIOut != nil && op.UIIn != nil 101 if hasUI && ((op.Destroy && !op.DestroyForce) || 102 (!op.Destroy && !op.AutoApprove && !trivialPlan)) { 103 var desc, query string 104 if op.Destroy { 105 // Default destroy message 106 desc = "Terraform will delete all your managed infrastructure, as shown above.\n" + 107 "There is no undo. Only 'yes' will be accepted to confirm." 108 109 // If targets are specified, list those to user 110 if op.Targets != nil { 111 var descBuffer bytes.Buffer 112 descBuffer.WriteString("Terraform will delete the following infrastructure:\n") 113 for _, target := range op.Targets { 114 descBuffer.WriteString("\t") 115 descBuffer.WriteString(target) 116 descBuffer.WriteString("\n") 117 } 118 descBuffer.WriteString("There is no undo. Only 'yes' will be accepted to confirm") 119 desc = descBuffer.String() 120 } 121 query = "Do you really want to destroy?" 122 } else { 123 desc = "Terraform will apply the changes described above.\n" + 124 "Only 'yes' will be accepted to approve." 125 query = "Do you want to apply these changes?" 126 } 127 128 if !trivialPlan { 129 // Display the plan of what we are going to apply/destroy. 130 if op.Destroy { 131 op.UIOut.Output("\n" + strings.TrimSpace(approveDestroyPlanHeader) + "\n") 132 } else { 133 op.UIOut.Output("\n" + strings.TrimSpace(approvePlanHeader) + "\n") 134 } 135 op.UIOut.Output(format.Plan(&format.PlanOpts{ 136 Plan: plan, 137 Color: b.Colorize(), 138 ModuleDepth: -1, 139 })) 140 } 141 142 v, err := op.UIIn.Input(&terraform.InputOpts{ 143 Id: "approve", 144 Query: query, 145 Description: desc, 146 }) 147 if err != nil { 148 runningOp.Err = errwrap.Wrapf("Error asking for approval: {{err}}", err) 149 return 150 } 151 if v != "yes" { 152 if op.Destroy { 153 runningOp.Err = errors.New("Destroy cancelled.") 154 } else { 155 runningOp.Err = errors.New("Apply cancelled.") 156 } 157 return 158 } 159 } 160 } 161 162 // Setup our hook for continuous state updates 163 stateHook.State = opState 164 165 // Start the apply in a goroutine so that we can be interrupted. 166 var applyState *terraform.State 167 var applyErr error 168 doneCh := make(chan struct{}) 169 go func() { 170 defer close(doneCh) 171 _, applyErr = tfCtx.Apply() 172 // we always want the state, even if apply failed 173 applyState = tfCtx.State() 174 175 /* 176 // Record any shadow errors for later 177 if err := ctx.ShadowError(); err != nil { 178 shadowErr = multierror.Append(shadowErr, multierror.Prefix( 179 err, "apply operation:")) 180 } 181 */ 182 }() 183 184 // Wait for the apply to finish or for us to be interrupted so 185 // we can handle it properly. 186 err = nil 187 select { 188 case <-ctx.Done(): 189 if b.CLI != nil { 190 b.CLI.Output("stopping apply operation...") 191 } 192 193 // try to force a PersistState just in case the process is terminated 194 // before we can complete. 195 if err := opState.PersistState(); err != nil { 196 // We can't error out from here, but warn the user if there was an error. 197 // If this isn't transient, we will catch it again below, and 198 // attempt to save the state another way. 199 if b.CLI != nil { 200 b.CLI.Error(fmt.Sprintf(earlyStateWriteErrorFmt, err)) 201 } 202 } 203 204 // Stop execution 205 go tfCtx.Stop() 206 207 // Wait for completion still 208 <-doneCh 209 case <-doneCh: 210 } 211 212 // Store the final state 213 runningOp.State = applyState 214 215 // Persist the state 216 if err := opState.WriteState(applyState); err != nil { 217 runningOp.Err = b.backupStateForError(applyState, err) 218 return 219 } 220 if err := opState.PersistState(); err != nil { 221 runningOp.Err = b.backupStateForError(applyState, err) 222 return 223 } 224 225 if applyErr != nil { 226 runningOp.Err = fmt.Errorf( 227 "Error applying plan:\n\n"+ 228 "%s\n\n"+ 229 "Terraform does not automatically rollback in the face of errors.\n"+ 230 "Instead, your Terraform state file has been partially updated with\n"+ 231 "any resources that successfully completed. Please address the error\n"+ 232 "above and apply again to incrementally change your infrastructure.", 233 multierror.Flatten(applyErr)) 234 return 235 } 236 237 // If we have a UI, output the results 238 if b.CLI != nil { 239 if op.Destroy { 240 b.CLI.Output(b.Colorize().Color(fmt.Sprintf( 241 "[reset][bold][green]\n"+ 242 "Destroy complete! Resources: %d destroyed.", 243 countHook.Removed))) 244 } else { 245 b.CLI.Output(b.Colorize().Color(fmt.Sprintf( 246 "[reset][bold][green]\n"+ 247 "Apply complete! Resources: %d added, %d changed, %d destroyed.", 248 countHook.Added, 249 countHook.Changed, 250 countHook.Removed))) 251 } 252 253 if countHook.Added > 0 || countHook.Changed > 0 { 254 b.CLI.Output(b.Colorize().Color(fmt.Sprintf( 255 "[reset]\n"+ 256 "The state of your infrastructure has been saved to the path\n"+ 257 "below. This state is required to modify and destroy your\n"+ 258 "infrastructure, so keep it safe. To inspect the complete state\n"+ 259 "use the `terraform show` command.\n\n"+ 260 "State path: %s", 261 b.StateOutPath))) 262 } 263 } 264 } 265 266 // backupStateForError is called in a scenario where we're unable to persist the 267 // state for some reason, and will attempt to save a backup copy of the state 268 // to local disk to help the user recover. This is a "last ditch effort" sort 269 // of thing, so we really don't want to end up in this codepath; we should do 270 // everything we possibly can to get the state saved _somewhere_. 271 func (b *Local) backupStateForError(applyState *terraform.State, err error) error { 272 b.CLI.Error(fmt.Sprintf("Failed to save state: %s\n", err)) 273 274 local := &state.LocalState{Path: "errored.tfstate"} 275 writeErr := local.WriteState(applyState) 276 if writeErr != nil { 277 b.CLI.Error(fmt.Sprintf( 278 "Also failed to create local state file for recovery: %s\n\n", writeErr, 279 )) 280 // To avoid leaving the user with no state at all, our last resort 281 // is to print the JSON state out onto the terminal. This is an awful 282 // UX, so we should definitely avoid doing this if at all possible, 283 // but at least the user has _some_ path to recover if we end up 284 // here for some reason. 285 stateBuf := new(bytes.Buffer) 286 jsonErr := terraform.WriteState(applyState, stateBuf) 287 if jsonErr != nil { 288 b.CLI.Error(fmt.Sprintf( 289 "Also failed to JSON-serialize the state to print it: %s\n\n", jsonErr, 290 )) 291 return errors.New(stateWriteFatalError) 292 } 293 294 b.CLI.Output(stateBuf.String()) 295 296 return errors.New(stateWriteConsoleFallbackError) 297 } 298 299 return errors.New(stateWriteBackedUpError) 300 } 301 302 const applyErrNoConfig = ` 303 No configuration files found! 304 305 Apply requires configuration to be present. Applying without a configuration 306 would mark everything for destruction, which is normally not what is desired. 307 If you would like to destroy everything, please run 'terraform destroy' instead 308 which does not require any configuration files. 309 ` 310 311 const stateWriteBackedUpError = `Failed to persist state to backend. 312 313 The error shown above has prevented Terraform from writing the updated state 314 to the configured backend. To allow for recovery, the state has been written 315 to the file "errored.tfstate" in the current working directory. 316 317 Running "terraform apply" again at this point will create a forked state, 318 making it harder to recover. 319 320 To retry writing this state, use the following command: 321 terraform state push errored.tfstate 322 ` 323 324 const stateWriteConsoleFallbackError = `Failed to persist state to backend. 325 326 The errors shown above prevented Terraform from writing the updated state to 327 the configured backend and from creating a local backup file. As a fallback, 328 the raw state data is printed above as a JSON object. 329 330 To retry writing this state, copy the state data (from the first { to the 331 last } inclusive) and save it into a local file called errored.tfstate, then 332 run the following command: 333 terraform state push errored.tfstate 334 ` 335 336 const stateWriteFatalError = `Failed to save state after apply. 337 338 A catastrophic error has prevented Terraform from persisting the state file 339 or creating a backup. Unfortunately this means that the record of any resources 340 created during this apply has been lost, and such resources may exist outside 341 of Terraform's management. 342 343 For resources that support import, it is possible to recover by manually 344 importing each resource using its id from the target system. 345 346 This is a serious bug in Terraform and should be reported. 347 ` 348 349 const earlyStateWriteErrorFmt = `Error saving current state: %s 350 351 Terraform encountered an error attempting to save the state before canceling 352 the current operation. Once the operation is complete another attempt will be 353 made to save the final state. 354 ` 355 356 const approvePlanHeader = ` 357 The Terraform execution plan has been generated and is shown below. 358 Resources are shown in alphabetical order for quick scanning. Green resources 359 will be created (or destroyed and then created if an existing resource 360 exists), yellow resources are being changed in-place, and red resources 361 will be destroyed. Cyan entries are data sources to be read. 362 ` 363 364 const approveDestroyPlanHeader = ` 365 The Terraform destroy plan has been generated and is shown below. 366 Resources are shown in alphabetical order for quick scanning. 367 Resources shown in red will be destroyed. 368 `