github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/durgaform/node_resource_apply_instance.go (about) 1 package durgaform 2 3 import ( 4 "fmt" 5 "log" 6 7 "github.com/eliastor/durgaform/internal/addrs" 8 "github.com/eliastor/durgaform/internal/configs" 9 "github.com/eliastor/durgaform/internal/plans" 10 "github.com/eliastor/durgaform/internal/plans/objchange" 11 "github.com/eliastor/durgaform/internal/states" 12 "github.com/eliastor/durgaform/internal/tfdiags" 13 ) 14 15 // NodeApplyableResourceInstance represents a resource instance that is 16 // "applyable": it is ready to be applied and is represented by a diff. 17 // 18 // This node is for a specific instance of a resource. It will usually be 19 // accompanied in the graph by a NodeApplyableResource representing its 20 // containing resource, and should depend on that node to ensure that the 21 // state is properly prepared to receive changes to instances. 22 type NodeApplyableResourceInstance struct { 23 *NodeAbstractResourceInstance 24 25 graphNodeDeposer // implementation of GraphNodeDeposerConfig 26 27 // If this node is forced to be CreateBeforeDestroy, we need to record that 28 // in the state to. 29 ForceCreateBeforeDestroy bool 30 31 // forceReplace are resource instance addresses where the user wants to 32 // force generating a replace action. This set isn't pre-filtered, so 33 // it might contain addresses that have nothing to do with the resource 34 // that this node represents, which the node itself must therefore ignore. 35 forceReplace []addrs.AbsResourceInstance 36 } 37 38 var ( 39 _ GraphNodeConfigResource = (*NodeApplyableResourceInstance)(nil) 40 _ GraphNodeResourceInstance = (*NodeApplyableResourceInstance)(nil) 41 _ GraphNodeCreator = (*NodeApplyableResourceInstance)(nil) 42 _ GraphNodeReferencer = (*NodeApplyableResourceInstance)(nil) 43 _ GraphNodeDeposer = (*NodeApplyableResourceInstance)(nil) 44 _ GraphNodeExecutable = (*NodeApplyableResourceInstance)(nil) 45 _ GraphNodeAttachDependencies = (*NodeApplyableResourceInstance)(nil) 46 ) 47 48 // CreateBeforeDestroy returns this node's CreateBeforeDestroy status. 49 func (n *NodeApplyableResourceInstance) CreateBeforeDestroy() bool { 50 if n.ForceCreateBeforeDestroy { 51 return n.ForceCreateBeforeDestroy 52 } 53 54 if n.Config != nil && n.Config.Managed != nil { 55 return n.Config.Managed.CreateBeforeDestroy 56 } 57 58 return false 59 } 60 61 func (n *NodeApplyableResourceInstance) ModifyCreateBeforeDestroy(v bool) error { 62 n.ForceCreateBeforeDestroy = v 63 return nil 64 } 65 66 // GraphNodeCreator 67 func (n *NodeApplyableResourceInstance) CreateAddr() *addrs.AbsResourceInstance { 68 addr := n.ResourceInstanceAddr() 69 return &addr 70 } 71 72 // GraphNodeReferencer, overriding NodeAbstractResourceInstance 73 func (n *NodeApplyableResourceInstance) References() []*addrs.Reference { 74 // Start with the usual resource instance implementation 75 ret := n.NodeAbstractResourceInstance.References() 76 77 // Applying a resource must also depend on the destruction of any of its 78 // dependencies, since this may for example affect the outcome of 79 // evaluating an entire list of resources with "count" set (by reducing 80 // the count). 81 // 82 // However, we can't do this in create_before_destroy mode because that 83 // would create a dependency cycle. We make a compromise here of requiring 84 // changes to be updated across two applies in this case, since the first 85 // plan will use the old values. 86 if !n.CreateBeforeDestroy() { 87 for _, ref := range ret { 88 switch tr := ref.Subject.(type) { 89 case addrs.ResourceInstance: 90 newRef := *ref // shallow copy so we can mutate 91 newRef.Subject = tr.Phase(addrs.ResourceInstancePhaseDestroy) 92 newRef.Remaining = nil // can't access attributes of something being destroyed 93 ret = append(ret, &newRef) 94 case addrs.Resource: 95 newRef := *ref // shallow copy so we can mutate 96 newRef.Subject = tr.Phase(addrs.ResourceInstancePhaseDestroy) 97 newRef.Remaining = nil // can't access attributes of something being destroyed 98 ret = append(ret, &newRef) 99 } 100 } 101 } 102 103 return ret 104 } 105 106 // GraphNodeAttachDependencies 107 func (n *NodeApplyableResourceInstance) AttachDependencies(deps []addrs.ConfigResource) { 108 n.Dependencies = deps 109 } 110 111 // GraphNodeExecutable 112 func (n *NodeApplyableResourceInstance) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) { 113 addr := n.ResourceInstanceAddr() 114 115 if n.Config == nil { 116 // This should not be possible, but we've got here in at least one 117 // case as discussed in the following issue: 118 // https://github.com/eliastor/durgaform/issues/21258 119 // To avoid an outright crash here, we'll instead return an explicit 120 // error. 121 diags = diags.Append(tfdiags.Sourceless( 122 tfdiags.Error, 123 "Resource node has no configuration attached", 124 fmt.Sprintf( 125 "The graph node for %s has no configuration attached to it. This suggests a bug in Durgaform's apply graph builder; please report it!", 126 addr, 127 ), 128 )) 129 return diags 130 } 131 132 // Eval info is different depending on what kind of resource this is 133 switch n.Config.Mode { 134 case addrs.ManagedResourceMode: 135 return n.managedResourceExecute(ctx) 136 case addrs.DataResourceMode: 137 return n.dataResourceExecute(ctx) 138 default: 139 panic(fmt.Errorf("unsupported resource mode %s", n.Config.Mode)) 140 } 141 } 142 143 func (n *NodeApplyableResourceInstance) dataResourceExecute(ctx EvalContext) (diags tfdiags.Diagnostics) { 144 _, providerSchema, err := getProvider(ctx, n.ResolvedProvider) 145 diags = diags.Append(err) 146 if diags.HasErrors() { 147 return diags 148 } 149 150 change, err := n.readDiff(ctx, providerSchema) 151 diags = diags.Append(err) 152 if diags.HasErrors() { 153 return diags 154 } 155 // Stop early if we don't actually have a diff 156 if change == nil { 157 return diags 158 } 159 if change.Action != plans.Read && change.Action != plans.NoOp { 160 diags = diags.Append(fmt.Errorf("nonsensical planned action %#v for %s; this is a bug in Durgaform", change.Action, n.Addr)) 161 } 162 163 // In this particular call to applyDataSource we include our planned 164 // change, which signals that we expect this read to complete fully 165 // with no unknown values; it'll produce an error if not. 166 state, repeatData, applyDiags := n.applyDataSource(ctx, change) 167 diags = diags.Append(applyDiags) 168 if diags.HasErrors() { 169 return diags 170 } 171 172 if state != nil { 173 // If n.applyDataSource returned a nil state object with no accompanying 174 // errors then it determined that the given change doesn't require 175 // actually reading the data (e.g. because it was already read during 176 // the plan phase) and so we're only running through here to get the 177 // extra details like precondition/postcondition checks. 178 diags = diags.Append(n.writeResourceInstanceState(ctx, state, workingState)) 179 if diags.HasErrors() { 180 return diags 181 } 182 } 183 184 diags = diags.Append(n.writeChange(ctx, nil, "")) 185 186 diags = diags.Append(updateStateHook(ctx)) 187 188 // Post-conditions might block further progress. We intentionally do this 189 // _after_ writing the state/diff because we want to check against 190 // the result of the operation, and to fail on future operations 191 // until the user makes the condition succeed. 192 checkDiags := evalCheckRules( 193 addrs.ResourcePostcondition, 194 n.Config.Postconditions, 195 ctx, n.ResourceInstanceAddr(), 196 repeatData, 197 tfdiags.Error, 198 ) 199 diags = diags.Append(checkDiags) 200 201 return diags 202 } 203 204 func (n *NodeApplyableResourceInstance) managedResourceExecute(ctx EvalContext) (diags tfdiags.Diagnostics) { 205 // Declare a bunch of variables that are used for state during 206 // evaluation. Most of this are written to by-address below. 207 var state *states.ResourceInstanceObject 208 var createBeforeDestroyEnabled bool 209 var deposedKey states.DeposedKey 210 211 addr := n.ResourceInstanceAddr().Resource 212 _, providerSchema, err := getProvider(ctx, n.ResolvedProvider) 213 diags = diags.Append(err) 214 if diags.HasErrors() { 215 return diags 216 } 217 218 // Get the saved diff for apply 219 diffApply, err := n.readDiff(ctx, providerSchema) 220 diags = diags.Append(err) 221 if diags.HasErrors() { 222 return diags 223 } 224 225 // We don't want to do any destroys 226 // (these are handled by NodeDestroyResourceInstance instead) 227 if diffApply == nil || diffApply.Action == plans.Delete { 228 return diags 229 } 230 if diffApply.Action == plans.Read { 231 diags = diags.Append(fmt.Errorf("nonsensical planned action %#v for %s; this is a bug in Durgaform", diffApply.Action, n.Addr)) 232 } 233 234 destroy := (diffApply.Action == plans.Delete || diffApply.Action.IsReplace()) 235 // Get the stored action for CBD if we have a plan already 236 createBeforeDestroyEnabled = diffApply.Change.Action == plans.CreateThenDelete 237 238 if destroy && n.CreateBeforeDestroy() { 239 createBeforeDestroyEnabled = true 240 } 241 242 if createBeforeDestroyEnabled { 243 state := ctx.State() 244 if n.PreallocatedDeposedKey == states.NotDeposed { 245 deposedKey = state.DeposeResourceInstanceObject(n.Addr) 246 } else { 247 deposedKey = n.PreallocatedDeposedKey 248 state.DeposeResourceInstanceObjectForceKey(n.Addr, deposedKey) 249 } 250 log.Printf("[TRACE] managedResourceExecute: prior object for %s now deposed with key %s", n.Addr, deposedKey) 251 } 252 253 state, readDiags := n.readResourceInstanceState(ctx, n.ResourceInstanceAddr()) 254 diags = diags.Append(readDiags) 255 if diags.HasErrors() { 256 return diags 257 } 258 259 // Get the saved diff 260 diff, err := n.readDiff(ctx, providerSchema) 261 diags = diags.Append(err) 262 if diags.HasErrors() { 263 return diags 264 } 265 266 // Make a new diff, in case we've learned new values in the state 267 // during apply which we can now incorporate. 268 diffApply, _, _, planDiags := n.plan(ctx, diff, state, false, n.forceReplace) 269 diags = diags.Append(planDiags) 270 if diags.HasErrors() { 271 return diags 272 } 273 274 // Compare the diffs 275 diags = diags.Append(n.checkPlannedChange(ctx, diff, diffApply, providerSchema)) 276 if diags.HasErrors() { 277 return diags 278 } 279 280 diffApply = reducePlan(addr, diffApply, false) 281 // reducePlan may have simplified our planned change 282 // into a NoOp if it only requires destroying, since destroying 283 // is handled by NodeDestroyResourceInstance. If so, we'll 284 // still run through most of the logic here because we do still 285 // need to deal with other book-keeping such as marking the 286 // change as "complete", and running the author's postconditions. 287 288 diags = diags.Append(n.preApplyHook(ctx, diffApply)) 289 if diags.HasErrors() { 290 return diags 291 } 292 293 state, repeatData, applyDiags := n.apply(ctx, state, diffApply, n.Config, n.CreateBeforeDestroy()) 294 diags = diags.Append(applyDiags) 295 296 // We clear the change out here so that future nodes don't see a change 297 // that is already complete. 298 err = n.writeChange(ctx, nil, "") 299 if err != nil { 300 return diags.Append(err) 301 } 302 303 state = maybeTainted(addr.Absolute(ctx.Path()), state, diffApply, diags.Err()) 304 305 if state != nil { 306 // dependencies are always updated to match the configuration during apply 307 state.Dependencies = n.Dependencies 308 } 309 err = n.writeResourceInstanceState(ctx, state, workingState) 310 if err != nil { 311 return diags.Append(err) 312 } 313 314 // Run Provisioners 315 createNew := (diffApply.Action == plans.Create || diffApply.Action.IsReplace()) 316 applyProvisionersDiags := n.evalApplyProvisioners(ctx, state, createNew, configs.ProvisionerWhenCreate) 317 // the provisioner errors count as port of the apply error, so we can bundle the diags 318 diags = diags.Append(applyProvisionersDiags) 319 320 state = maybeTainted(addr.Absolute(ctx.Path()), state, diffApply, diags.Err()) 321 322 err = n.writeResourceInstanceState(ctx, state, workingState) 323 if err != nil { 324 return diags.Append(err) 325 } 326 327 if createBeforeDestroyEnabled && diags.HasErrors() { 328 if deposedKey == states.NotDeposed { 329 // This should never happen, and so it always indicates a bug. 330 // We should evaluate this node only if we've previously deposed 331 // an object as part of the same operation. 332 if diffApply != nil { 333 diags = diags.Append(tfdiags.Sourceless( 334 tfdiags.Error, 335 "Attempt to restore non-existent deposed object", 336 fmt.Sprintf( 337 "Durgaform has encountered a bug where it would need to restore a deposed object for %s without knowing a deposed object key for that object. This occurred during a %s action. This is a bug in Terraform; please report it!", 338 addr, diffApply.Action, 339 ), 340 )) 341 } else { 342 diags = diags.Append(tfdiags.Sourceless( 343 tfdiags.Error, 344 "Attempt to restore non-existent deposed object", 345 fmt.Sprintf( 346 "Durgaform has encountered a bug where it would need to restore a deposed object for %s without knowing a deposed object key for that object. This is a bug in Terraform; please report it!", 347 addr, 348 ), 349 )) 350 } 351 } else { 352 restored := ctx.State().MaybeRestoreResourceInstanceDeposed(addr.Absolute(ctx.Path()), deposedKey) 353 if restored { 354 log.Printf("[TRACE] managedResourceExecute: %s deposed object %s was restored as the current object", addr, deposedKey) 355 } else { 356 log.Printf("[TRACE] managedResourceExecute: %s deposed object %s remains deposed", addr, deposedKey) 357 } 358 } 359 } 360 361 diags = diags.Append(n.postApplyHook(ctx, state, diags.Err())) 362 diags = diags.Append(updateStateHook(ctx)) 363 364 // Post-conditions might block further progress. We intentionally do this 365 // _after_ writing the state because we want to check against 366 // the result of the operation, and to fail on future operations 367 // until the user makes the condition succeed. 368 checkDiags := evalCheckRules( 369 addrs.ResourcePostcondition, 370 n.Config.Postconditions, 371 ctx, n.ResourceInstanceAddr(), repeatData, 372 tfdiags.Error, 373 ) 374 diags = diags.Append(checkDiags) 375 376 return diags 377 } 378 379 // checkPlannedChange produces errors if the _actual_ expected value is not 380 // compatible with what was recorded in the plan. 381 // 382 // Errors here are most often indicative of a bug in the provider, so our error 383 // messages will report with that in mind. It's also possible that there's a bug 384 // in Durgaform's Core's own "proposed new value" code in EvalDiff. 385 func (n *NodeApplyableResourceInstance) checkPlannedChange(ctx EvalContext, plannedChange, actualChange *plans.ResourceInstanceChange, providerSchema *ProviderSchema) tfdiags.Diagnostics { 386 var diags tfdiags.Diagnostics 387 addr := n.ResourceInstanceAddr().Resource 388 389 schema, _ := providerSchema.SchemaForResourceAddr(addr.ContainingResource()) 390 if schema == nil { 391 // Should be caught during validation, so we don't bother with a pretty error here 392 diags = diags.Append(fmt.Errorf("provider does not support %q", addr.Resource.Type)) 393 return diags 394 } 395 396 absAddr := addr.Absolute(ctx.Path()) 397 398 log.Printf("[TRACE] checkPlannedChange: Verifying that actual change (action %s) matches planned change (action %s)", actualChange.Action, plannedChange.Action) 399 400 if plannedChange.Action != actualChange.Action { 401 switch { 402 case plannedChange.Action == plans.Update && actualChange.Action == plans.NoOp: 403 // It's okay for an update to become a NoOp once we've filled in 404 // all of the unknown values, since the final values might actually 405 // match what was there before after all. 406 log.Printf("[DEBUG] After incorporating new values learned so far during apply, %s change has become NoOp", absAddr) 407 408 case (plannedChange.Action == plans.CreateThenDelete && actualChange.Action == plans.DeleteThenCreate) || 409 (plannedChange.Action == plans.DeleteThenCreate && actualChange.Action == plans.CreateThenDelete): 410 // If the order of replacement changed, then that is a bug in durgaform 411 diags = diags.Append(tfdiags.Sourceless( 412 tfdiags.Error, 413 "Durgaform produced inconsistent final plan", 414 fmt.Sprintf( 415 "When expanding the plan for %s to include new values learned so far during apply, the planned action changed from %s to %s.\n\nThis is a bug in Durgaform and should be reported.", 416 absAddr, plannedChange.Action, actualChange.Action, 417 ), 418 )) 419 default: 420 diags = diags.Append(tfdiags.Sourceless( 421 tfdiags.Error, 422 "Provider produced inconsistent final plan", 423 fmt.Sprintf( 424 "When expanding the plan for %s to include new values learned so far during apply, provider %q changed the planned action from %s to %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", 425 absAddr, n.ResolvedProvider.Provider.String(), 426 plannedChange.Action, actualChange.Action, 427 ), 428 )) 429 } 430 } 431 432 errs := objchange.AssertObjectCompatible(schema, plannedChange.After, actualChange.After) 433 for _, err := range errs { 434 diags = diags.Append(tfdiags.Sourceless( 435 tfdiags.Error, 436 "Provider produced inconsistent final plan", 437 fmt.Sprintf( 438 "When expanding the plan for %s to include new values learned so far during apply, provider %q produced an invalid new value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", 439 absAddr, n.ResolvedProvider.Provider.String(), tfdiags.FormatError(err), 440 ), 441 )) 442 } 443 return diags 444 } 445 446 // maybeTainted takes the resource addr, new value, planned change, and possible 447 // error from an apply operation and return a new instance object marked as 448 // tainted if it appears that a create operation has failed. 449 func maybeTainted(addr addrs.AbsResourceInstance, state *states.ResourceInstanceObject, change *plans.ResourceInstanceChange, err error) *states.ResourceInstanceObject { 450 if state == nil || change == nil || err == nil { 451 return state 452 } 453 if state.Status == states.ObjectTainted { 454 log.Printf("[TRACE] maybeTainted: %s was already tainted, so nothing to do", addr) 455 return state 456 } 457 if change.Action == plans.Create { 458 // If there are errors during a _create_ then the object is 459 // in an undefined state, and so we'll mark it as tainted so 460 // we can try again on the next run. 461 // 462 // We don't do this for other change actions because errors 463 // during updates will often not change the remote object at all. 464 // If there _were_ changes prior to the error, it's the provider's 465 // responsibility to record the effect of those changes in the 466 // object value it returned. 467 log.Printf("[TRACE] maybeTainted: %s encountered an error during creation, so it is now marked as tainted", addr) 468 return state.AsTainted() 469 } 470 return state 471 }