github.com/trawler/terraform@v0.10.8-0.20171106022149-4b1c7a1d9b48/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 dispPlan := format.NewPlan(plan) 100 trivialPlan := dispPlan.Empty() 101 hasUI := op.UIOut != nil && op.UIIn != nil 102 mustConfirm := hasUI && ((op.Destroy && !op.DestroyForce) || (!op.Destroy && !op.AutoApprove && !trivialPlan)) 103 if mustConfirm { 104 var desc, query string 105 if op.Destroy { 106 // Default destroy message 107 desc = "Terraform will destroy all your managed infrastructure, as shown above.\n" + 108 "There is no undo. Only 'yes' will be accepted to confirm." 109 query = "Do you really want to destroy?" 110 } else { 111 desc = "Terraform will perform the actions described above.\n" + 112 "Only 'yes' will be accepted to approve." 113 query = "Do you want to perform these actions?" 114 } 115 116 if !trivialPlan { 117 // Display the plan of what we are going to apply/destroy. 118 b.renderPlan(dispPlan) 119 b.CLI.Output("") 120 } 121 122 v, err := op.UIIn.Input(&terraform.InputOpts{ 123 Id: "approve", 124 Query: query, 125 Description: desc, 126 }) 127 if err != nil { 128 runningOp.Err = errwrap.Wrapf("Error asking for approval: {{err}}", err) 129 return 130 } 131 if v != "yes" { 132 if op.Destroy { 133 runningOp.Err = errors.New("Destroy cancelled.") 134 } else { 135 runningOp.Err = errors.New("Apply cancelled.") 136 } 137 return 138 } 139 } 140 } 141 142 // Setup our hook for continuous state updates 143 stateHook.State = opState 144 145 // Start the apply in a goroutine so that we can be interrupted. 146 var applyState *terraform.State 147 var applyErr error 148 doneCh := make(chan struct{}) 149 go func() { 150 defer close(doneCh) 151 _, applyErr = tfCtx.Apply() 152 // we always want the state, even if apply failed 153 applyState = tfCtx.State() 154 155 /* 156 // Record any shadow errors for later 157 if err := ctx.ShadowError(); err != nil { 158 shadowErr = multierror.Append(shadowErr, multierror.Prefix( 159 err, "apply operation:")) 160 } 161 */ 162 }() 163 164 // Wait for the apply to finish or for us to be interrupted so 165 // we can handle it properly. 166 err = nil 167 select { 168 case <-ctx.Done(): 169 if b.CLI != nil { 170 b.CLI.Output("stopping apply operation...") 171 } 172 173 // try to force a PersistState just in case the process is terminated 174 // before we can complete. 175 if err := opState.PersistState(); err != nil { 176 // We can't error out from here, but warn the user if there was an error. 177 // If this isn't transient, we will catch it again below, and 178 // attempt to save the state another way. 179 if b.CLI != nil { 180 b.CLI.Error(fmt.Sprintf(earlyStateWriteErrorFmt, err)) 181 } 182 } 183 184 // Stop execution 185 go tfCtx.Stop() 186 187 // Wait for completion still 188 <-doneCh 189 case <-doneCh: 190 } 191 192 // Store the final state 193 runningOp.State = applyState 194 195 // Persist the state 196 if err := opState.WriteState(applyState); err != nil { 197 runningOp.Err = b.backupStateForError(applyState, err) 198 return 199 } 200 if err := opState.PersistState(); err != nil { 201 runningOp.Err = b.backupStateForError(applyState, err) 202 return 203 } 204 205 if applyErr != nil { 206 runningOp.Err = fmt.Errorf( 207 "Error applying plan:\n\n"+ 208 "%s\n\n"+ 209 "Terraform does not automatically rollback in the face of errors.\n"+ 210 "Instead, your Terraform state file has been partially updated with\n"+ 211 "any resources that successfully completed. Please address the error\n"+ 212 "above and apply again to incrementally change your infrastructure.", 213 multierror.Flatten(applyErr)) 214 return 215 } 216 217 // If we have a UI, output the results 218 if b.CLI != nil { 219 if op.Destroy { 220 b.CLI.Output(b.Colorize().Color(fmt.Sprintf( 221 "[reset][bold][green]\n"+ 222 "Destroy complete! Resources: %d destroyed.", 223 countHook.Removed))) 224 } else { 225 b.CLI.Output(b.Colorize().Color(fmt.Sprintf( 226 "[reset][bold][green]\n"+ 227 "Apply complete! Resources: %d added, %d changed, %d destroyed.", 228 countHook.Added, 229 countHook.Changed, 230 countHook.Removed))) 231 } 232 233 // only show the state file help message if the state is local. 234 if (countHook.Added > 0 || countHook.Changed > 0) && b.StateOutPath != "" { 235 b.CLI.Output(b.Colorize().Color(fmt.Sprintf( 236 "[reset]\n"+ 237 "The state of your infrastructure has been saved to the path\n"+ 238 "below. This state is required to modify and destroy your\n"+ 239 "infrastructure, so keep it safe. To inspect the complete state\n"+ 240 "use the `terraform show` command.\n\n"+ 241 "State path: %s", 242 b.StateOutPath))) 243 } 244 } 245 } 246 247 // backupStateForError is called in a scenario where we're unable to persist the 248 // state for some reason, and will attempt to save a backup copy of the state 249 // to local disk to help the user recover. This is a "last ditch effort" sort 250 // of thing, so we really don't want to end up in this codepath; we should do 251 // everything we possibly can to get the state saved _somewhere_. 252 func (b *Local) backupStateForError(applyState *terraform.State, err error) error { 253 b.CLI.Error(fmt.Sprintf("Failed to save state: %s\n", err)) 254 255 local := &state.LocalState{Path: "errored.tfstate"} 256 writeErr := local.WriteState(applyState) 257 if writeErr != nil { 258 b.CLI.Error(fmt.Sprintf( 259 "Also failed to create local state file for recovery: %s\n\n", writeErr, 260 )) 261 // To avoid leaving the user with no state at all, our last resort 262 // is to print the JSON state out onto the terminal. This is an awful 263 // UX, so we should definitely avoid doing this if at all possible, 264 // but at least the user has _some_ path to recover if we end up 265 // here for some reason. 266 stateBuf := new(bytes.Buffer) 267 jsonErr := terraform.WriteState(applyState, stateBuf) 268 if jsonErr != nil { 269 b.CLI.Error(fmt.Sprintf( 270 "Also failed to JSON-serialize the state to print it: %s\n\n", jsonErr, 271 )) 272 return errors.New(stateWriteFatalError) 273 } 274 275 b.CLI.Output(stateBuf.String()) 276 277 return errors.New(stateWriteConsoleFallbackError) 278 } 279 280 return errors.New(stateWriteBackedUpError) 281 } 282 283 const applyErrNoConfig = ` 284 No configuration files found! 285 286 Apply requires configuration to be present. Applying without a configuration 287 would mark everything for destruction, which is normally not what is desired. 288 If you would like to destroy everything, please run 'terraform destroy' instead 289 which does not require any configuration files. 290 ` 291 292 const stateWriteBackedUpError = `Failed to persist state to backend. 293 294 The error shown above has prevented Terraform from writing the updated state 295 to the configured backend. To allow for recovery, the state has been written 296 to the file "errored.tfstate" in the current working directory. 297 298 Running "terraform apply" again at this point will create a forked state, 299 making it harder to recover. 300 301 To retry writing this state, use the following command: 302 terraform state push errored.tfstate 303 ` 304 305 const stateWriteConsoleFallbackError = `Failed to persist state to backend. 306 307 The errors shown above prevented Terraform from writing the updated state to 308 the configured backend and from creating a local backup file. As a fallback, 309 the raw state data is printed above as a JSON object. 310 311 To retry writing this state, copy the state data (from the first { to the 312 last } inclusive) and save it into a local file called errored.tfstate, then 313 run the following command: 314 terraform state push errored.tfstate 315 ` 316 317 const stateWriteFatalError = `Failed to save state after apply. 318 319 A catastrophic error has prevented Terraform from persisting the state file 320 or creating a backup. Unfortunately this means that the record of any resources 321 created during this apply has been lost, and such resources may exist outside 322 of Terraform's management. 323 324 For resources that support import, it is possible to recover by manually 325 importing each resource using its id from the target system. 326 327 This is a serious bug in Terraform and should be reported. 328 ` 329 330 const earlyStateWriteErrorFmt = `Error saving current state: %s 331 332 Terraform encountered an error attempting to save the state before canceling 333 the current operation. Once the operation is complete another attempt will be 334 made to save the final state. 335 `