github.com/chentex/terraform@v0.11.2-0.20171208003256-252e8145842e/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 // Wait for the apply to finish or for us to be interrupted so 157 // we can handle it properly. 158 err = nil 159 select { 160 case <-ctx.Done(): 161 if b.CLI != nil { 162 b.CLI.Output("stopping apply operation...") 163 } 164 165 // try to force a PersistState just in case the process is terminated 166 // before we can complete. 167 if err := opState.PersistState(); err != nil { 168 // We can't error out from here, but warn the user if there was an error. 169 // If this isn't transient, we will catch it again below, and 170 // attempt to save the state another way. 171 if b.CLI != nil { 172 b.CLI.Error(fmt.Sprintf(earlyStateWriteErrorFmt, err)) 173 } 174 } 175 176 // Stop execution 177 go tfCtx.Stop() 178 179 // Wait for completion still 180 <-doneCh 181 case <-doneCh: 182 } 183 184 // Store the final state 185 runningOp.State = applyState 186 187 // Persist the state 188 if err := opState.WriteState(applyState); err != nil { 189 runningOp.Err = b.backupStateForError(applyState, err) 190 return 191 } 192 if err := opState.PersistState(); err != nil { 193 runningOp.Err = b.backupStateForError(applyState, err) 194 return 195 } 196 197 if applyErr != nil { 198 runningOp.Err = fmt.Errorf( 199 "Error applying plan:\n\n"+ 200 "%s\n\n"+ 201 "Terraform does not automatically rollback in the face of errors.\n"+ 202 "Instead, your Terraform state file has been partially updated with\n"+ 203 "any resources that successfully completed. Please address the error\n"+ 204 "above and apply again to incrementally change your infrastructure.", 205 multierror.Flatten(applyErr)) 206 return 207 } 208 209 // If we have a UI, output the results 210 if b.CLI != nil { 211 if op.Destroy { 212 b.CLI.Output(b.Colorize().Color(fmt.Sprintf( 213 "[reset][bold][green]\n"+ 214 "Destroy complete! Resources: %d destroyed.", 215 countHook.Removed))) 216 } else { 217 b.CLI.Output(b.Colorize().Color(fmt.Sprintf( 218 "[reset][bold][green]\n"+ 219 "Apply complete! Resources: %d added, %d changed, %d destroyed.", 220 countHook.Added, 221 countHook.Changed, 222 countHook.Removed))) 223 } 224 225 // only show the state file help message if the state is local. 226 if (countHook.Added > 0 || countHook.Changed > 0) && b.StateOutPath != "" { 227 b.CLI.Output(b.Colorize().Color(fmt.Sprintf( 228 "[reset]\n"+ 229 "The state of your infrastructure has been saved to the path\n"+ 230 "below. This state is required to modify and destroy your\n"+ 231 "infrastructure, so keep it safe. To inspect the complete state\n"+ 232 "use the `terraform show` command.\n\n"+ 233 "State path: %s", 234 b.StateOutPath))) 235 } 236 } 237 } 238 239 // backupStateForError is called in a scenario where we're unable to persist the 240 // state for some reason, and will attempt to save a backup copy of the state 241 // to local disk to help the user recover. This is a "last ditch effort" sort 242 // of thing, so we really don't want to end up in this codepath; we should do 243 // everything we possibly can to get the state saved _somewhere_. 244 func (b *Local) backupStateForError(applyState *terraform.State, err error) error { 245 b.CLI.Error(fmt.Sprintf("Failed to save state: %s\n", err)) 246 247 local := &state.LocalState{Path: "errored.tfstate"} 248 writeErr := local.WriteState(applyState) 249 if writeErr != nil { 250 b.CLI.Error(fmt.Sprintf( 251 "Also failed to create local state file for recovery: %s\n\n", writeErr, 252 )) 253 // To avoid leaving the user with no state at all, our last resort 254 // is to print the JSON state out onto the terminal. This is an awful 255 // UX, so we should definitely avoid doing this if at all possible, 256 // but at least the user has _some_ path to recover if we end up 257 // here for some reason. 258 stateBuf := new(bytes.Buffer) 259 jsonErr := terraform.WriteState(applyState, stateBuf) 260 if jsonErr != nil { 261 b.CLI.Error(fmt.Sprintf( 262 "Also failed to JSON-serialize the state to print it: %s\n\n", jsonErr, 263 )) 264 return errors.New(stateWriteFatalError) 265 } 266 267 b.CLI.Output(stateBuf.String()) 268 269 return errors.New(stateWriteConsoleFallbackError) 270 } 271 272 return errors.New(stateWriteBackedUpError) 273 } 274 275 const applyErrNoConfig = ` 276 No configuration files found! 277 278 Apply requires configuration to be present. Applying without a configuration 279 would mark everything for destruction, which is normally not what is desired. 280 If you would like to destroy everything, please run 'terraform destroy' instead 281 which does not require any configuration files. 282 ` 283 284 const stateWriteBackedUpError = `Failed to persist state to backend. 285 286 The error shown above has prevented Terraform from writing the updated state 287 to the configured backend. To allow for recovery, the state has been written 288 to the file "errored.tfstate" in the current working directory. 289 290 Running "terraform apply" again at this point will create a forked state, 291 making it harder to recover. 292 293 To retry writing this state, use the following command: 294 terraform state push errored.tfstate 295 ` 296 297 const stateWriteConsoleFallbackError = `Failed to persist state to backend. 298 299 The errors shown above prevented Terraform from writing the updated state to 300 the configured backend and from creating a local backup file. As a fallback, 301 the raw state data is printed above as a JSON object. 302 303 To retry writing this state, copy the state data (from the first { to the 304 last } inclusive) and save it into a local file called errored.tfstate, then 305 run the following command: 306 terraform state push errored.tfstate 307 ` 308 309 const stateWriteFatalError = `Failed to save state after apply. 310 311 A catastrophic error has prevented Terraform from persisting the state file 312 or creating a backup. Unfortunately this means that the record of any resources 313 created during this apply has been lost, and such resources may exist outside 314 of Terraform's management. 315 316 For resources that support import, it is possible to recover by manually 317 importing each resource using its id from the target system. 318 319 This is a serious bug in Terraform and should be reported. 320 ` 321 322 const earlyStateWriteErrorFmt = `Error saving current state: %s 323 324 Terraform encountered an error attempting to save the state before canceling 325 the current operation. Once the operation is complete another attempt will be 326 made to save the final state. 327 `