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