github.com/justnom/terraform@v0.5.4-0.20180524223219-170a1530d1ca/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/format" 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 stopCtx context.Context, 22 cancelCtx 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 // Setup the state 58 runningOp.State = tfCtx.State() 59 60 // If we weren't given a plan, then we refresh/plan 61 if op.Plan == nil { 62 // If we're refreshing before apply, perform that 63 if op.PlanRefresh { 64 log.Printf("[INFO] backend/local: apply calling Refresh") 65 _, err := tfCtx.Refresh() 66 if err != nil { 67 runningOp.Err = errwrap.Wrapf("Error refreshing state: {{err}}", err) 68 return 69 } 70 } 71 72 // Perform the plan 73 log.Printf("[INFO] backend/local: apply calling Plan") 74 plan, err := tfCtx.Plan() 75 if err != nil { 76 runningOp.Err = errwrap.Wrapf("Error running plan: {{err}}", err) 77 return 78 } 79 80 dispPlan := format.NewPlan(plan) 81 trivialPlan := dispPlan.Empty() 82 hasUI := op.UIOut != nil && op.UIIn != nil 83 mustConfirm := hasUI && ((op.Destroy && (!op.DestroyForce && !op.AutoApprove)) || (!op.Destroy && !op.AutoApprove && !trivialPlan)) 84 if mustConfirm { 85 var desc, query string 86 if op.Destroy { 87 // Default destroy message 88 desc = "Terraform will destroy all your managed infrastructure, as shown above.\n" + 89 "There is no undo. Only 'yes' will be accepted to confirm." 90 query = "Do you really want to destroy?" 91 } else { 92 desc = "Terraform will perform the actions described above.\n" + 93 "Only 'yes' will be accepted to approve." 94 query = "Do you want to perform these actions?" 95 } 96 97 if !trivialPlan { 98 // Display the plan of what we are going to apply/destroy. 99 b.renderPlan(dispPlan) 100 b.CLI.Output("") 101 } 102 103 v, err := op.UIIn.Input(&terraform.InputOpts{ 104 Id: "approve", 105 Query: query, 106 Description: desc, 107 }) 108 if err != nil { 109 runningOp.Err = errwrap.Wrapf("Error asking for approval: {{err}}", err) 110 return 111 } 112 if v != "yes" { 113 if op.Destroy { 114 runningOp.Err = errors.New("Destroy cancelled.") 115 } else { 116 runningOp.Err = errors.New("Apply cancelled.") 117 } 118 return 119 } 120 } 121 } 122 123 // Setup our hook for continuous state updates 124 stateHook.State = opState 125 126 // Start the apply in a goroutine so that we can be interrupted. 127 var applyState *terraform.State 128 var applyErr error 129 doneCh := make(chan struct{}) 130 go func() { 131 defer close(doneCh) 132 _, applyErr = tfCtx.Apply() 133 // we always want the state, even if apply failed 134 applyState = tfCtx.State() 135 }() 136 137 if b.opWait(doneCh, stopCtx, cancelCtx, tfCtx, opState) { 138 return 139 } 140 141 // Store the final state 142 runningOp.State = applyState 143 144 // Persist the state 145 if err := opState.WriteState(applyState); err != nil { 146 runningOp.Err = b.backupStateForError(applyState, err) 147 return 148 } 149 if err := opState.PersistState(); err != nil { 150 runningOp.Err = b.backupStateForError(applyState, err) 151 return 152 } 153 154 if applyErr != nil { 155 runningOp.Err = fmt.Errorf( 156 "Error applying plan:\n\n"+ 157 "%s\n\n"+ 158 "Terraform does not automatically rollback in the face of errors.\n"+ 159 "Instead, your Terraform state file has been partially updated with\n"+ 160 "any resources that successfully completed. Please address the error\n"+ 161 "above and apply again to incrementally change your infrastructure.", 162 multierror.Flatten(applyErr)) 163 return 164 } 165 166 // If we have a UI, output the results 167 if b.CLI != nil { 168 if op.Destroy { 169 b.CLI.Output(b.Colorize().Color(fmt.Sprintf( 170 "[reset][bold][green]\n"+ 171 "Destroy complete! Resources: %d destroyed.", 172 countHook.Removed))) 173 } else { 174 b.CLI.Output(b.Colorize().Color(fmt.Sprintf( 175 "[reset][bold][green]\n"+ 176 "Apply complete! Resources: %d added, %d changed, %d destroyed.", 177 countHook.Added, 178 countHook.Changed, 179 countHook.Removed))) 180 } 181 182 // only show the state file help message if the state is local. 183 if (countHook.Added > 0 || countHook.Changed > 0) && b.StateOutPath != "" { 184 b.CLI.Output(b.Colorize().Color(fmt.Sprintf( 185 "[reset]\n"+ 186 "The state of your infrastructure has been saved to the path\n"+ 187 "below. This state is required to modify and destroy your\n"+ 188 "infrastructure, so keep it safe. To inspect the complete state\n"+ 189 "use the `terraform show` command.\n\n"+ 190 "State path: %s", 191 b.StateOutPath))) 192 } 193 } 194 } 195 196 // backupStateForError is called in a scenario where we're unable to persist the 197 // state for some reason, and will attempt to save a backup copy of the state 198 // to local disk to help the user recover. This is a "last ditch effort" sort 199 // of thing, so we really don't want to end up in this codepath; we should do 200 // everything we possibly can to get the state saved _somewhere_. 201 func (b *Local) backupStateForError(applyState *terraform.State, err error) error { 202 b.CLI.Error(fmt.Sprintf("Failed to save state: %s\n", err)) 203 204 local := &state.LocalState{Path: "errored.tfstate"} 205 writeErr := local.WriteState(applyState) 206 if writeErr != nil { 207 b.CLI.Error(fmt.Sprintf( 208 "Also failed to create local state file for recovery: %s\n\n", writeErr, 209 )) 210 // To avoid leaving the user with no state at all, our last resort 211 // is to print the JSON state out onto the terminal. This is an awful 212 // UX, so we should definitely avoid doing this if at all possible, 213 // but at least the user has _some_ path to recover if we end up 214 // here for some reason. 215 stateBuf := new(bytes.Buffer) 216 jsonErr := terraform.WriteState(applyState, stateBuf) 217 if jsonErr != nil { 218 b.CLI.Error(fmt.Sprintf( 219 "Also failed to JSON-serialize the state to print it: %s\n\n", jsonErr, 220 )) 221 return errors.New(stateWriteFatalError) 222 } 223 224 b.CLI.Output(stateBuf.String()) 225 226 return errors.New(stateWriteConsoleFallbackError) 227 } 228 229 return errors.New(stateWriteBackedUpError) 230 } 231 232 const applyErrNoConfig = ` 233 No configuration files found! 234 235 Apply requires configuration to be present. Applying without a configuration 236 would mark everything for destruction, which is normally not what is desired. 237 If you would like to destroy everything, please run 'terraform destroy' instead 238 which does not require any configuration files. 239 ` 240 241 const stateWriteBackedUpError = `Failed to persist state to backend. 242 243 The error shown above has prevented Terraform from writing the updated state 244 to the configured backend. To allow for recovery, the state has been written 245 to the file "errored.tfstate" in the current working directory. 246 247 Running "terraform apply" again at this point will create a forked state, 248 making it harder to recover. 249 250 To retry writing this state, use the following command: 251 terraform state push errored.tfstate 252 ` 253 254 const stateWriteConsoleFallbackError = `Failed to persist state to backend. 255 256 The errors shown above prevented Terraform from writing the updated state to 257 the configured backend and from creating a local backup file. As a fallback, 258 the raw state data is printed above as a JSON object. 259 260 To retry writing this state, copy the state data (from the first { to the 261 last } inclusive) and save it into a local file called errored.tfstate, then 262 run the following command: 263 terraform state push errored.tfstate 264 ` 265 266 const stateWriteFatalError = `Failed to save state after apply. 267 268 A catastrophic error has prevented Terraform from persisting the state file 269 or creating a backup. Unfortunately this means that the record of any resources 270 created during this apply has been lost, and such resources may exist outside 271 of Terraform's management. 272 273 For resources that support import, it is possible to recover by manually 274 importing each resource using its id from the target system. 275 276 This is a serious bug in Terraform and should be reported. 277 ` 278 279 const earlyStateWriteErrorFmt = `Error saving current state: %s 280 281 Terraform encountered an error attempting to save the state before canceling 282 the current operation. Once the operation is complete another attempt will be 283 made to save the final state. 284 `