1package terraform 2 3import ( 4 "fmt" 5 "log" 6 7 "github.com/hashicorp/terraform/internal/addrs" 8 "github.com/hashicorp/terraform/internal/configs" 9 "github.com/hashicorp/terraform/internal/plans" 10 "github.com/hashicorp/terraform/internal/plans/objchange" 11 "github.com/hashicorp/terraform/internal/states" 12 "github.com/hashicorp/terraform/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. 22type 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 38var ( 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. 49func (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 61func (n *NodeApplyableResourceInstance) ModifyCreateBeforeDestroy(v bool) error { 62 n.ForceCreateBeforeDestroy = v 63 return nil 64} 65 66// GraphNodeCreator 67func (n *NodeApplyableResourceInstance) CreateAddr() *addrs.AbsResourceInstance { 68 addr := n.ResourceInstanceAddr() 69 return &addr 70} 71 72// GraphNodeReferencer, overriding NodeAbstractResourceInstance 73func (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 107func (n *NodeApplyableResourceInstance) AttachDependencies(deps []addrs.ConfigResource) { 108 n.Dependencies = deps 109} 110 111// GraphNodeExecutable 112func (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/hashicorp/terraform/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 Terraform'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 143func (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 160 // In this particular call to applyDataSource we include our planned 161 // change, which signals that we expect this read to complete fully 162 // with no unknown values; it'll produce an error if not. 163 state, applyDiags := n.applyDataSource(ctx, change) 164 diags = diags.Append(applyDiags) 165 if diags.HasErrors() { 166 return diags 167 } 168 169 diags = diags.Append(n.writeResourceInstanceState(ctx, state, workingState)) 170 if diags.HasErrors() { 171 return diags 172 } 173 174 diags = diags.Append(n.writeChange(ctx, nil, "")) 175 176 diags = diags.Append(updateStateHook(ctx)) 177 return diags 178} 179 180func (n *NodeApplyableResourceInstance) managedResourceExecute(ctx EvalContext) (diags tfdiags.Diagnostics) { 181 // Declare a bunch of variables that are used for state during 182 // evaluation. Most of this are written to by-address below. 183 var state *states.ResourceInstanceObject 184 var createBeforeDestroyEnabled bool 185 var deposedKey states.DeposedKey 186 187 addr := n.ResourceInstanceAddr().Resource 188 _, providerSchema, err := getProvider(ctx, n.ResolvedProvider) 189 diags = diags.Append(err) 190 if diags.HasErrors() { 191 return diags 192 } 193 194 // Get the saved diff for apply 195 diffApply, err := n.readDiff(ctx, providerSchema) 196 diags = diags.Append(err) 197 if diags.HasErrors() { 198 return diags 199 } 200 201 // We don't want to do any destroys 202 // (these are handled by NodeDestroyResourceInstance instead) 203 if diffApply == nil || diffApply.Action == plans.Delete { 204 return diags 205 } 206 207 destroy := (diffApply.Action == plans.Delete || diffApply.Action.IsReplace()) 208 // Get the stored action for CBD if we have a plan already 209 createBeforeDestroyEnabled = diffApply.Change.Action == plans.CreateThenDelete 210 211 if destroy && n.CreateBeforeDestroy() { 212 createBeforeDestroyEnabled = true 213 } 214 215 if createBeforeDestroyEnabled { 216 state := ctx.State() 217 if n.PreallocatedDeposedKey == states.NotDeposed { 218 deposedKey = state.DeposeResourceInstanceObject(n.Addr) 219 } else { 220 deposedKey = n.PreallocatedDeposedKey 221 state.DeposeResourceInstanceObjectForceKey(n.Addr, deposedKey) 222 } 223 log.Printf("[TRACE] managedResourceExecute: prior object for %s now deposed with key %s", n.Addr, deposedKey) 224 } 225 226 state, readDiags := n.readResourceInstanceState(ctx, n.ResourceInstanceAddr()) 227 diags = diags.Append(readDiags) 228 if diags.HasErrors() { 229 return diags 230 } 231 232 // Get the saved diff 233 diff, err := n.readDiff(ctx, providerSchema) 234 diags = diags.Append(err) 235 if diags.HasErrors() { 236 return diags 237 } 238 239 // Make a new diff, in case we've learned new values in the state 240 // during apply which we can now incorporate. 241 diffApply, _, planDiags := n.plan(ctx, diff, state, false, n.forceReplace) 242 diags = diags.Append(planDiags) 243 if diags.HasErrors() { 244 return diags 245 } 246 247 // Compare the diffs 248 diags = diags.Append(n.checkPlannedChange(ctx, diff, diffApply, providerSchema)) 249 if diags.HasErrors() { 250 return diags 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 diffApply = reducePlan(addr, diffApply, false) 260 // reducePlan may have simplified our planned change 261 // into a NoOp if it only requires destroying, since destroying 262 // is handled by NodeDestroyResourceInstance. 263 if diffApply == nil || diffApply.Action == plans.NoOp { 264 return diags 265 } 266 267 diags = diags.Append(n.preApplyHook(ctx, diffApply)) 268 if diags.HasErrors() { 269 return diags 270 } 271 272 state, applyDiags := n.apply(ctx, state, diffApply, n.Config, n.CreateBeforeDestroy()) 273 diags = diags.Append(applyDiags) 274 275 // We clear the change out here so that future nodes don't see a change 276 // that is already complete. 277 err = n.writeChange(ctx, nil, "") 278 if err != nil { 279 return diags.Append(err) 280 } 281 282 state = maybeTainted(addr.Absolute(ctx.Path()), state, diffApply, diags.Err()) 283 284 if state != nil { 285 // dependencies are always updated to match the configuration during apply 286 state.Dependencies = n.Dependencies 287 } 288 err = n.writeResourceInstanceState(ctx, state, workingState) 289 if err != nil { 290 return diags.Append(err) 291 } 292 293 // Run Provisioners 294 createNew := (diffApply.Action == plans.Create || diffApply.Action.IsReplace()) 295 applyProvisionersDiags := n.evalApplyProvisioners(ctx, state, createNew, configs.ProvisionerWhenCreate) 296 // the provisioner errors count as port of the apply error, so we can bundle the diags 297 diags = diags.Append(applyProvisionersDiags) 298 299 state = maybeTainted(addr.Absolute(ctx.Path()), state, diffApply, diags.Err()) 300 301 err = n.writeResourceInstanceState(ctx, state, workingState) 302 if err != nil { 303 return diags.Append(err) 304 } 305 306 if createBeforeDestroyEnabled && diags.HasErrors() { 307 if deposedKey == states.NotDeposed { 308 // This should never happen, and so it always indicates a bug. 309 // We should evaluate this node only if we've previously deposed 310 // an object as part of the same operation. 311 if diffApply != nil { 312 diags = diags.Append(tfdiags.Sourceless( 313 tfdiags.Error, 314 "Attempt to restore non-existent deposed object", 315 fmt.Sprintf( 316 "Terraform 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!", 317 addr, diffApply.Action, 318 ), 319 )) 320 } else { 321 diags = diags.Append(tfdiags.Sourceless( 322 tfdiags.Error, 323 "Attempt to restore non-existent deposed object", 324 fmt.Sprintf( 325 "Terraform 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!", 326 addr, 327 ), 328 )) 329 } 330 } else { 331 restored := ctx.State().MaybeRestoreResourceInstanceDeposed(addr.Absolute(ctx.Path()), deposedKey) 332 if restored { 333 log.Printf("[TRACE] managedResourceExecute: %s deposed object %s was restored as the current object", addr, deposedKey) 334 } else { 335 log.Printf("[TRACE] managedResourceExecute: %s deposed object %s remains deposed", addr, deposedKey) 336 } 337 } 338 } 339 340 diags = diags.Append(n.postApplyHook(ctx, state, diags.Err())) 341 diags = diags.Append(updateStateHook(ctx)) 342 return diags 343} 344 345// checkPlannedChange produces errors if the _actual_ expected value is not 346// compatible with what was recorded in the plan. 347// 348// Errors here are most often indicative of a bug in the provider, so our error 349// messages will report with that in mind. It's also possible that there's a bug 350// in Terraform's Core's own "proposed new value" code in EvalDiff. 351func (n *NodeApplyableResourceInstance) checkPlannedChange(ctx EvalContext, plannedChange, actualChange *plans.ResourceInstanceChange, providerSchema *ProviderSchema) tfdiags.Diagnostics { 352 var diags tfdiags.Diagnostics 353 addr := n.ResourceInstanceAddr().Resource 354 355 schema, _ := providerSchema.SchemaForResourceAddr(addr.ContainingResource()) 356 if schema == nil { 357 // Should be caught during validation, so we don't bother with a pretty error here 358 diags = diags.Append(fmt.Errorf("provider does not support %q", addr.Resource.Type)) 359 return diags 360 } 361 362 absAddr := addr.Absolute(ctx.Path()) 363 364 log.Printf("[TRACE] checkPlannedChange: Verifying that actual change (action %s) matches planned change (action %s)", actualChange.Action, plannedChange.Action) 365 366 if plannedChange.Action != actualChange.Action { 367 switch { 368 case plannedChange.Action == plans.Update && actualChange.Action == plans.NoOp: 369 // It's okay for an update to become a NoOp once we've filled in 370 // all of the unknown values, since the final values might actually 371 // match what was there before after all. 372 log.Printf("[DEBUG] After incorporating new values learned so far during apply, %s change has become NoOp", absAddr) 373 374 case (plannedChange.Action == plans.CreateThenDelete && actualChange.Action == plans.DeleteThenCreate) || 375 (plannedChange.Action == plans.DeleteThenCreate && actualChange.Action == plans.CreateThenDelete): 376 // If the order of replacement changed, then that is a bug in terraform 377 diags = diags.Append(tfdiags.Sourceless( 378 tfdiags.Error, 379 "Terraform produced inconsistent final plan", 380 fmt.Sprintf( 381 "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 Terraform and should be reported.", 382 absAddr, plannedChange.Action, actualChange.Action, 383 ), 384 )) 385 default: 386 diags = diags.Append(tfdiags.Sourceless( 387 tfdiags.Error, 388 "Provider produced inconsistent final plan", 389 fmt.Sprintf( 390 "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.", 391 absAddr, n.ResolvedProvider.Provider.String(), 392 plannedChange.Action, actualChange.Action, 393 ), 394 )) 395 } 396 } 397 398 errs := objchange.AssertObjectCompatible(schema, plannedChange.After, actualChange.After) 399 for _, err := range errs { 400 diags = diags.Append(tfdiags.Sourceless( 401 tfdiags.Error, 402 "Provider produced inconsistent final plan", 403 fmt.Sprintf( 404 "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.", 405 absAddr, n.ResolvedProvider.Provider.String(), tfdiags.FormatError(err), 406 ), 407 )) 408 } 409 return diags 410} 411 412// maybeTainted takes the resource addr, new value, planned change, and possible 413// error from an apply operation and return a new instance object marked as 414// tainted if it appears that a create operation has failed. 415func maybeTainted(addr addrs.AbsResourceInstance, state *states.ResourceInstanceObject, change *plans.ResourceInstanceChange, err error) *states.ResourceInstanceObject { 416 if state == nil || change == nil || err == nil { 417 return state 418 } 419 if state.Status == states.ObjectTainted { 420 log.Printf("[TRACE] maybeTainted: %s was already tainted, so nothing to do", addr) 421 return state 422 } 423 if change.Action == plans.Create { 424 // If there are errors during a _create_ then the object is 425 // in an undefined state, and so we'll mark it as tainted so 426 // we can try again on the next run. 427 // 428 // We don't do this for other change actions because errors 429 // during updates will often not change the remote object at all. 430 // If there _were_ changes prior to the error, it's the provider's 431 // responsibility to record the effect of those changes in the 432 // object value it returned. 433 log.Printf("[TRACE] maybeTainted: %s encountered an error during creation, so it is now marked as tainted", addr) 434 return state.AsTainted() 435 } 436 return state 437} 438