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