kubeform.dev/terraform-backend-sdk@v0.0.0-20220310143633-45f07fe731c5/backend/local/backend_apply.go (about) 1 package local 2 3 import ( 4 "context" 5 "fmt" 6 "log" 7 8 "kubeform.dev/terraform-backend-sdk/backend" 9 "kubeform.dev/terraform-backend-sdk/command/views" 10 "kubeform.dev/terraform-backend-sdk/plans" 11 "kubeform.dev/terraform-backend-sdk/states" 12 "kubeform.dev/terraform-backend-sdk/states/statefile" 13 "kubeform.dev/terraform-backend-sdk/states/statemgr" 14 "kubeform.dev/terraform-backend-sdk/terraform" 15 "kubeform.dev/terraform-backend-sdk/tfdiags" 16 ) 17 18 func (b *Local) opApply( 19 stopCtx context.Context, 20 cancelCtx context.Context, 21 op *backend.Operation, 22 runningOp *backend.RunningOperation) { 23 log.Printf("[INFO] backend/local: starting Apply operation") 24 25 var diags, moreDiags tfdiags.Diagnostics 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.PlanFile == nil && op.PlanMode != plans.DestroyMode && !op.HasConfig() { 30 diags = diags.Append(tfdiags.Sourceless( 31 tfdiags.Error, 32 "No configuration files", 33 "Apply requires configuration to be present. Applying without a configuration "+ 34 "would mark everything for destruction, which is normally not what is desired. "+ 35 "If you would like to destroy everything, run 'terraform destroy' instead.", 36 )) 37 op.ReportResult(runningOp, diags) 38 return 39 } 40 41 stateHook := new(StateHook) 42 op.Hooks = append(op.Hooks, stateHook) 43 44 // Get our context 45 lr, _, opState, contextDiags := b.localRun(op) 46 diags = diags.Append(contextDiags) 47 if contextDiags.HasErrors() { 48 op.ReportResult(runningOp, diags) 49 return 50 } 51 // the state was locked during succesfull context creation; unlock the state 52 // when the operation completes 53 defer func() { 54 diags := op.StateLocker.Unlock() 55 if diags.HasErrors() { 56 op.View.Diagnostics(diags) 57 runningOp.Result = backend.OperationFailure 58 } 59 }() 60 61 // We'll start off with our result being the input state, and replace it 62 // with the result state only if we eventually complete the apply 63 // operation. 64 runningOp.State = lr.InputState 65 66 var plan *plans.Plan 67 // If we weren't given a plan, then we refresh/plan 68 if op.PlanFile == nil { 69 // Perform the plan 70 log.Printf("[INFO] backend/local: apply calling Plan") 71 plan, moreDiags = lr.Core.Plan(lr.Config, lr.InputState, lr.PlanOpts) 72 diags = diags.Append(moreDiags) 73 if moreDiags.HasErrors() { 74 op.ReportResult(runningOp, diags) 75 return 76 } 77 78 schemas, moreDiags := lr.Core.Schemas(lr.Config, lr.InputState) 79 diags = diags.Append(moreDiags) 80 if moreDiags.HasErrors() { 81 op.ReportResult(runningOp, diags) 82 return 83 } 84 85 trivialPlan := !plan.CanApply() 86 hasUI := op.UIOut != nil && op.UIIn != nil 87 mustConfirm := hasUI && !op.AutoApprove && !trivialPlan 88 op.View.Plan(plan, schemas) 89 90 if mustConfirm { 91 var desc, query string 92 switch op.PlanMode { 93 case plans.DestroyMode: 94 if op.Workspace != "default" { 95 query = "Do you really want to destroy all resources in workspace \"" + op.Workspace + "\"?" 96 } else { 97 query = "Do you really want to destroy all resources?" 98 } 99 desc = "Terraform will destroy all your managed infrastructure, as shown above.\n" + 100 "There is no undo. Only 'yes' will be accepted to confirm." 101 case plans.RefreshOnlyMode: 102 if op.Workspace != "default" { 103 query = "Would you like to update the Terraform state for \"" + op.Workspace + "\" to reflect these detected changes?" 104 } else { 105 query = "Would you like to update the Terraform state to reflect these detected changes?" 106 } 107 desc = "Terraform will write these changes to the state without modifying any real infrastructure.\n" + 108 "There is no undo. Only 'yes' will be accepted to confirm." 109 default: 110 if op.Workspace != "default" { 111 query = "Do you want to perform these actions in workspace \"" + op.Workspace + "\"?" 112 } else { 113 query = "Do you want to perform these actions?" 114 } 115 desc = "Terraform will perform the actions described above.\n" + 116 "Only 'yes' will be accepted to approve." 117 } 118 119 // We'll show any accumulated warnings before we display the prompt, 120 // so the user can consider them when deciding how to answer. 121 if len(diags) > 0 { 122 op.View.Diagnostics(diags) 123 diags = nil // reset so we won't show the same diagnostics again later 124 } 125 126 v, err := op.UIIn.Input(stopCtx, &terraform.InputOpts{ 127 Id: "approve", 128 Query: "\n" + query, 129 Description: desc, 130 }) 131 if err != nil { 132 diags = diags.Append(fmt.Errorf("error asking for approval: %w", err)) 133 op.ReportResult(runningOp, diags) 134 return 135 } 136 if v != "yes" { 137 op.View.Cancelled(op.PlanMode) 138 runningOp.Result = backend.OperationFailure 139 return 140 } 141 } 142 } else { 143 plan = lr.Plan 144 for _, change := range plan.Changes.Resources { 145 if change.Action != plans.NoOp { 146 op.View.PlannedChange(change) 147 } 148 } 149 } 150 151 // Set up our hook for continuous state updates 152 stateHook.StateMgr = opState 153 154 // Start the apply in a goroutine so that we can be interrupted. 155 var applyState *states.State 156 var applyDiags tfdiags.Diagnostics 157 doneCh := make(chan struct{}) 158 go func() { 159 defer close(doneCh) 160 log.Printf("[INFO] backend/local: apply calling Apply") 161 applyState, applyDiags = lr.Core.Apply(plan, lr.Config) 162 }() 163 164 if b.opWait(doneCh, stopCtx, cancelCtx, lr.Core, opState, op.View) { 165 return 166 } 167 diags = diags.Append(applyDiags) 168 169 // Store the final state 170 runningOp.State = applyState 171 err := statemgr.WriteAndPersist(opState, applyState) 172 if err != nil { 173 // Export the state file from the state manager and assign the new 174 // state. This is needed to preserve the existing serial and lineage. 175 stateFile := statemgr.Export(opState) 176 if stateFile == nil { 177 stateFile = &statefile.File{} 178 } 179 stateFile.State = applyState 180 181 diags = diags.Append(b.backupStateForError(stateFile, err, op.View)) 182 op.ReportResult(runningOp, diags) 183 return 184 } 185 186 if applyDiags.HasErrors() { 187 op.ReportResult(runningOp, diags) 188 return 189 } 190 191 // If we've accumulated any warnings along the way then we'll show them 192 // here just before we show the summary and next steps. If we encountered 193 // errors then we would've returned early at some other point above. 194 op.View.Diagnostics(diags) 195 } 196 197 // backupStateForError is called in a scenario where we're unable to persist the 198 // state for some reason, and will attempt to save a backup copy of the state 199 // to local disk to help the user recover. This is a "last ditch effort" sort 200 // of thing, so we really don't want to end up in this codepath; we should do 201 // everything we possibly can to get the state saved _somewhere_. 202 func (b *Local) backupStateForError(stateFile *statefile.File, err error, view views.Operation) tfdiags.Diagnostics { 203 var diags tfdiags.Diagnostics 204 205 diags = diags.Append(tfdiags.Sourceless( 206 tfdiags.Error, 207 "Failed to save state", 208 fmt.Sprintf("Error saving state: %s", err), 209 )) 210 211 local := statemgr.NewFilesystem("errored.tfstate") 212 writeErr := local.WriteStateForMigration(stateFile, true) 213 if writeErr != nil { 214 diags = diags.Append(tfdiags.Sourceless( 215 tfdiags.Error, 216 "Failed to create local state file", 217 fmt.Sprintf("Error creating local state file for recovery: %s", writeErr), 218 )) 219 220 // To avoid leaving the user with no state at all, our last resort 221 // is to print the JSON state out onto the terminal. This is an awful 222 // UX, so we should definitely avoid doing this if at all possible, 223 // but at least the user has _some_ path to recover if we end up 224 // here for some reason. 225 if dumpErr := view.EmergencyDumpState(stateFile); dumpErr != nil { 226 diags = diags.Append(tfdiags.Sourceless( 227 tfdiags.Error, 228 "Failed to serialize state", 229 fmt.Sprintf(stateWriteFatalErrorFmt, dumpErr), 230 )) 231 } 232 233 diags = diags.Append(tfdiags.Sourceless( 234 tfdiags.Error, 235 "Failed to persist state to backend", 236 stateWriteConsoleFallbackError, 237 )) 238 return diags 239 } 240 241 diags = diags.Append(tfdiags.Sourceless( 242 tfdiags.Error, 243 "Failed to persist state to backend", 244 stateWriteBackedUpError, 245 )) 246 247 return diags 248 } 249 250 const stateWriteBackedUpError = `The error shown above has prevented Terraform from writing the updated state to the configured backend. To allow for recovery, the state has been written to the file "errored.tfstate" in the current working directory. 251 252 Running "terraform apply" again at this point will create a forked state, making it harder to recover. 253 254 To retry writing this state, use the following command: 255 terraform state push errored.tfstate 256 ` 257 258 const stateWriteConsoleFallbackError = `The errors shown above prevented Terraform from writing the updated state to 259 the configured backend and from creating a local backup file. As a fallback, 260 the raw state data is printed above as a JSON object. 261 262 To retry writing this state, copy the state data (from the first { to the last } inclusive) and save it into a local file called errored.tfstate, then run the following command: 263 terraform state push errored.tfstate 264 ` 265 266 const stateWriteFatalErrorFmt = `Failed to save state after apply. 267 268 Error serializing state: %s 269 270 A catastrophic error has prevented Terraform from persisting the state file or creating a backup. Unfortunately this means that the record of any resources created during this apply has been lost, and such resources may exist outside of Terraform's management. 271 272 For resources that support import, it is possible to recover by manually importing each resource using its id from the target system. 273 274 This is a serious bug in Terraform and should be reported. 275 `