github.com/opentofu/opentofu@v1.7.1/internal/backend/local/backend_apply.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package local 7 8 import ( 9 "context" 10 "errors" 11 "fmt" 12 "log" 13 "time" 14 15 "github.com/opentofu/opentofu/internal/addrs" 16 "github.com/opentofu/opentofu/internal/backend" 17 "github.com/opentofu/opentofu/internal/command/views" 18 "github.com/opentofu/opentofu/internal/logging" 19 "github.com/opentofu/opentofu/internal/plans" 20 "github.com/opentofu/opentofu/internal/states" 21 "github.com/opentofu/opentofu/internal/states/statefile" 22 "github.com/opentofu/opentofu/internal/states/statemgr" 23 "github.com/opentofu/opentofu/internal/tfdiags" 24 "github.com/opentofu/opentofu/internal/tofu" 25 ) 26 27 // test hook called between plan+apply during opApply 28 var testHookStopPlanApply func() 29 30 func (b *Local) opApply( 31 stopCtx context.Context, 32 cancelCtx context.Context, 33 op *backend.Operation, 34 runningOp *backend.RunningOperation) { 35 log.Printf("[INFO] backend/local: starting Apply operation") 36 37 var diags, moreDiags tfdiags.Diagnostics 38 39 // If we have a nil module at this point, then set it to an empty tree 40 // to avoid any potential crashes. 41 if op.PlanFile == nil && op.PlanMode != plans.DestroyMode && !op.HasConfig() { 42 diags = diags.Append(tfdiags.Sourceless( 43 tfdiags.Error, 44 "No configuration files", 45 "Apply requires configuration to be present. Applying without a configuration "+ 46 "would mark everything for destruction, which is normally not what is desired. "+ 47 "If you would like to destroy everything, run 'tofu destroy' instead.", 48 )) 49 op.ReportResult(runningOp, diags) 50 return 51 } 52 53 stateHook := new(StateHook) 54 op.Hooks = append(op.Hooks, stateHook) 55 56 // Get our context 57 lr, _, opState, contextDiags := b.localRun(op) 58 diags = diags.Append(contextDiags) 59 if contextDiags.HasErrors() { 60 op.ReportResult(runningOp, diags) 61 return 62 } 63 // the state was locked during successful context creation; unlock the state 64 // when the operation completes 65 defer func() { 66 diags := op.StateLocker.Unlock() 67 if diags.HasErrors() { 68 op.View.Diagnostics(diags) 69 runningOp.Result = backend.OperationFailure 70 } 71 }() 72 73 // We'll start off with our result being the input state, and replace it 74 // with the result state only if we eventually complete the apply 75 // operation. 76 runningOp.State = lr.InputState 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 // stateHook uses schemas for when it periodically persists state to the 85 // persistent storage backend. 86 stateHook.Schemas = schemas 87 stateHook.PersistInterval = 20 * time.Second // arbitrary interval that's hopefully a sweet spot 88 89 var plan *plans.Plan 90 // If we weren't given a plan, then we refresh/plan 91 if op.PlanFile == nil { 92 // Perform the plan 93 log.Printf("[INFO] backend/local: apply calling Plan") 94 plan, moreDiags = lr.Core.Plan(lr.Config, lr.InputState, lr.PlanOpts) 95 diags = diags.Append(moreDiags) 96 if moreDiags.HasErrors() { 97 // If OpenTofu Core generated a partial plan despite the errors 98 // then we'll make the best effort to render it. OpenTofu Core 99 // promises that if it returns a non-nil plan along with errors 100 // then the plan won't necessarily contain all the needed 101 // actions but that any it does include will be properly-formed. 102 // plan.Errored will be true in this case, which our plan 103 // renderer can rely on to tailor its messaging. 104 if plan != nil && (len(plan.Changes.Resources) != 0 || len(plan.Changes.Outputs) != 0) { 105 op.View.Plan(plan, schemas) 106 } 107 op.ReportResult(runningOp, diags) 108 return 109 } 110 111 trivialPlan := !plan.CanApply() 112 hasUI := op.UIOut != nil && op.UIIn != nil 113 mustConfirm := hasUI && !op.AutoApprove && !trivialPlan 114 op.View.Plan(plan, schemas) 115 116 if testHookStopPlanApply != nil { 117 testHookStopPlanApply() 118 } 119 120 // Check if we've been stopped before going through confirmation, or 121 // skipping confirmation in the case of -auto-approve. 122 // This can currently happen if a single stop request was received 123 // during the final batch of resource plan calls, so no operations were 124 // forced to abort, and no errors were returned from Plan. 125 if stopCtx.Err() != nil { 126 diags = diags.Append(errors.New("execution halted")) 127 runningOp.Result = backend.OperationFailure 128 op.ReportResult(runningOp, diags) 129 return 130 } 131 132 if mustConfirm { 133 var desc, query string 134 switch op.PlanMode { 135 case plans.DestroyMode: 136 if op.Workspace != "default" { 137 query = "Do you really want to destroy all resources in workspace \"" + op.Workspace + "\"?" 138 } else { 139 query = "Do you really want to destroy all resources?" 140 } 141 desc = "OpenTofu will destroy all your managed infrastructure, as shown above.\n" + 142 "There is no undo. Only 'yes' will be accepted to confirm." 143 case plans.RefreshOnlyMode: 144 if op.Workspace != "default" { 145 query = "Would you like to update the OpenTofu state for \"" + op.Workspace + "\" to reflect these detected changes?" 146 } else { 147 query = "Would you like to update the OpenTofu state to reflect these detected changes?" 148 } 149 desc = "OpenTofu will write these changes to the state without modifying any real infrastructure.\n" + 150 "There is no undo. Only 'yes' will be accepted to confirm." 151 default: 152 if op.Workspace != "default" { 153 query = "Do you want to perform these actions in workspace \"" + op.Workspace + "\"?" 154 } else { 155 query = "Do you want to perform these actions?" 156 } 157 desc = "OpenTofu will perform the actions described above.\n" + 158 "Only 'yes' will be accepted to approve." 159 } 160 161 // We'll show any accumulated warnings before we display the prompt, 162 // so the user can consider them when deciding how to answer. 163 if len(diags) > 0 { 164 op.View.Diagnostics(diags) 165 diags = nil // reset so we won't show the same diagnostics again later 166 } 167 168 v, err := op.UIIn.Input(stopCtx, &tofu.InputOpts{ 169 Id: "approve", 170 Query: "\n" + query, 171 Description: desc, 172 }) 173 if err != nil { 174 diags = diags.Append(fmt.Errorf("error asking for approval: %w", err)) 175 op.ReportResult(runningOp, diags) 176 return 177 } 178 if v != "yes" { 179 op.View.Cancelled(op.PlanMode) 180 runningOp.Result = backend.OperationFailure 181 return 182 } 183 } else { 184 // If we didn't ask for confirmation from the user, and they have 185 // included any failing checks in their configuration, then they 186 // will see a very confusing output after the apply operation 187 // completes. This is because all the diagnostics from the plan 188 // operation will now be shown alongside the diagnostics from the 189 // apply operation. For check diagnostics, the plan output is 190 // irrelevant and simple noise after the same set of checks have 191 // been executed again during the apply stage. As such, we are going 192 // to remove all diagnostics marked as check diagnostics at this 193 // stage, so we will only show the user the check results from the 194 // apply operation. 195 // 196 // Note, if we did ask for approval then we would have displayed the 197 // plan check results at that point which is useful as the user can 198 // use them to make a decision about whether to apply the changes. 199 // It's just that if we didn't ask for approval then showing the 200 // user the checks from the plan alongside the checks from the apply 201 // is needlessly confusing. 202 var filteredDiags tfdiags.Diagnostics 203 for _, diag := range diags { 204 if rule, ok := addrs.DiagnosticOriginatesFromCheckRule(diag); ok && rule.Container.CheckableKind() == addrs.CheckableCheck { 205 continue 206 } 207 filteredDiags = filteredDiags.Append(diag) 208 } 209 diags = filteredDiags 210 } 211 } else { 212 plan = lr.Plan 213 if plan.Errored { 214 diags = diags.Append(tfdiags.Sourceless( 215 tfdiags.Error, 216 "Cannot apply incomplete plan", 217 "OpenTofu encountered an error when generating this plan, so it cannot be applied.", 218 )) 219 op.ReportResult(runningOp, diags) 220 return 221 } 222 for _, change := range plan.Changes.Resources { 223 if change.Action != plans.NoOp { 224 op.View.PlannedChange(change) 225 } 226 } 227 } 228 229 // Set up our hook for continuous state updates 230 stateHook.StateMgr = opState 231 232 // Start to apply in a goroutine so that we can be interrupted. 233 var applyState *states.State 234 var applyDiags tfdiags.Diagnostics 235 doneCh := make(chan struct{}) 236 panicHandler := logging.PanicHandlerWithTraceFn() 237 go func() { 238 defer panicHandler() 239 defer close(doneCh) 240 log.Printf("[INFO] backend/local: apply calling Apply") 241 applyState, applyDiags = lr.Core.Apply(plan, lr.Config) 242 }() 243 244 if b.opWait(doneCh, stopCtx, cancelCtx, lr.Core, opState, op.View) { 245 return 246 } 247 diags = diags.Append(applyDiags) 248 249 // Even on error with an empty state, the state value should not be nil. 250 // Return early here to prevent corrupting any existing state. 251 if diags.HasErrors() && applyState == nil { 252 log.Printf("[ERROR] backend/local: apply returned nil state") 253 op.ReportResult(runningOp, diags) 254 return 255 } 256 257 // Store the final state 258 runningOp.State = applyState 259 err := statemgr.WriteAndPersist(opState, applyState, schemas) 260 if err != nil { 261 // Export the state file from the state manager and assign the new 262 // state. This is needed to preserve the existing serial and lineage. 263 stateFile := statemgr.Export(opState) 264 if stateFile == nil { 265 stateFile = &statefile.File{} 266 } 267 stateFile.State = applyState 268 269 diags = diags.Append(b.backupStateForError(stateFile, err, op.View)) 270 op.ReportResult(runningOp, diags) 271 return 272 } 273 274 if applyDiags.HasErrors() { 275 op.ReportResult(runningOp, diags) 276 return 277 } 278 279 // If we've accumulated any warnings along the way then we'll show them 280 // here just before we show the summary and next steps. If we encountered 281 // errors then we would've returned early at some other point above. 282 op.View.Diagnostics(diags) 283 } 284 285 // backupStateForError is called in a scenario where we're unable to persist the 286 // state for some reason, and will attempt to save a backup copy of the state 287 // to local disk to help the user recover. This is a "last ditch effort" sort 288 // of thing, so we really don't want to end up in this codepath; we should do 289 // everything we possibly can to get the state saved _somewhere_. 290 func (b *Local) backupStateForError(stateFile *statefile.File, err error, view views.Operation) tfdiags.Diagnostics { 291 var diags tfdiags.Diagnostics 292 293 diags = diags.Append(tfdiags.Sourceless( 294 tfdiags.Error, 295 "Failed to save state", 296 fmt.Sprintf("Error saving state: %s", err), 297 )) 298 299 local := statemgr.NewFilesystem("errored.tfstate", b.encryption) 300 writeErr := local.WriteStateForMigration(stateFile, true) 301 if writeErr != nil { 302 diags = diags.Append(tfdiags.Sourceless( 303 tfdiags.Error, 304 "Failed to create local state file", 305 fmt.Sprintf("Error creating local state file for recovery: %s", writeErr), 306 )) 307 308 // To avoid leaving the user with no state at all, our last resort 309 // is to print the JSON state out onto the terminal. This is an awful 310 // UX, so we should definitely avoid doing this if at all possible, 311 // but at least the user has _some_ path to recover if we end up 312 // here for some reason. 313 if dumpErr := view.EmergencyDumpState(stateFile, b.encryption); dumpErr != nil { 314 diags = diags.Append(tfdiags.Sourceless( 315 tfdiags.Error, 316 "Failed to serialize state", 317 fmt.Sprintf(stateWriteFatalErrorFmt, dumpErr), 318 )) 319 } 320 321 diags = diags.Append(tfdiags.Sourceless( 322 tfdiags.Error, 323 "Failed to persist state to backend", 324 stateWriteConsoleFallbackError, 325 )) 326 return diags 327 } 328 329 diags = diags.Append(tfdiags.Sourceless( 330 tfdiags.Error, 331 "Failed to persist state to backend", 332 stateWriteBackedUpError, 333 )) 334 335 return diags 336 } 337 338 const stateWriteBackedUpError = `The error shown above has prevented OpenTofu 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. 339 340 Running "tofu apply" again at this point will create a forked state, making it harder to recover. 341 342 To retry writing this state, use the following command: 343 tofu state push errored.tfstate 344 ` 345 346 const stateWriteConsoleFallbackError = `The errors shown above prevented OpenTofu from writing the updated state to 347 the configured backend and from creating a local backup file. As a fallback, 348 the raw state data is printed above as a JSON object. 349 350 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: 351 tofu state push errored.tfstate 352 ` 353 354 const stateWriteFatalErrorFmt = `Failed to save state after apply. 355 356 Error serializing state: %s 357 358 A catastrophic error has prevented OpenTofu 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 OpenTofu's management. 359 360 For resources that support import, it is possible to recover by manually importing each resource using its id from the target system. 361 362 This is a serious bug in OpenTofu and should be reported. 363 `