github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/terraform/eval_diff.go (about) 1 package terraform 2 3 import ( 4 "fmt" 5 "log" 6 "strings" 7 8 "github.com/hashicorp/hcl/v2" 9 "github.com/zclconf/go-cty/cty" 10 11 "github.com/hashicorp/terraform/addrs" 12 "github.com/hashicorp/terraform/configs" 13 "github.com/hashicorp/terraform/plans" 14 "github.com/hashicorp/terraform/plans/objchange" 15 "github.com/hashicorp/terraform/providers" 16 "github.com/hashicorp/terraform/states" 17 "github.com/hashicorp/terraform/tfdiags" 18 ) 19 20 // EvalCheckPlannedChange is an EvalNode implementation that produces errors 21 // if the _actual_ expected value is not compatible with what was recorded 22 // in the plan. 23 // 24 // Errors here are most often indicative of a bug in the provider, so our 25 // error messages will report with that in mind. It's also possible that 26 // there's a bug in Terraform's Core's own "proposed new value" code in 27 // EvalDiff. 28 type EvalCheckPlannedChange struct { 29 Addr addrs.ResourceInstance 30 ProviderAddr addrs.AbsProviderConfig 31 ProviderSchema **ProviderSchema 32 33 // We take ResourceInstanceChange objects here just because that's what's 34 // convenient to pass in from the evaltree implementation, but we really 35 // only look at the "After" value of each change. 36 Planned, Actual **plans.ResourceInstanceChange 37 } 38 39 func (n *EvalCheckPlannedChange) Eval(ctx EvalContext) (interface{}, error) { 40 providerSchema := *n.ProviderSchema 41 plannedChange := *n.Planned 42 actualChange := *n.Actual 43 44 schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource()) 45 if schema == nil { 46 // Should be caught during validation, so we don't bother with a pretty error here 47 return nil, fmt.Errorf("provider does not support %q", n.Addr.Resource.Type) 48 } 49 50 var diags tfdiags.Diagnostics 51 absAddr := n.Addr.Absolute(ctx.Path()) 52 53 log.Printf("[TRACE] EvalCheckPlannedChange: Verifying that actual change (action %s) matches planned change (action %s)", actualChange.Action, plannedChange.Action) 54 55 if plannedChange.Action != actualChange.Action { 56 switch { 57 case plannedChange.Action == plans.Update && actualChange.Action == plans.NoOp: 58 // It's okay for an update to become a NoOp once we've filled in 59 // all of the unknown values, since the final values might actually 60 // match what was there before after all. 61 log.Printf("[DEBUG] After incorporating new values learned so far during apply, %s change has become NoOp", absAddr) 62 default: 63 diags = diags.Append(tfdiags.Sourceless( 64 tfdiags.Error, 65 "Provider produced inconsistent final plan", 66 fmt.Sprintf( 67 "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.", 68 absAddr, n.ProviderAddr.ProviderConfig.Type, 69 plannedChange.Action, actualChange.Action, 70 ), 71 )) 72 } 73 } 74 75 errs := objchange.AssertObjectCompatible(schema, plannedChange.After, actualChange.After) 76 for _, err := range errs { 77 diags = diags.Append(tfdiags.Sourceless( 78 tfdiags.Error, 79 "Provider produced inconsistent final plan", 80 fmt.Sprintf( 81 "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.", 82 absAddr, n.ProviderAddr.ProviderConfig.Type, tfdiags.FormatError(err), 83 ), 84 )) 85 } 86 return nil, diags.Err() 87 } 88 89 // EvalDiff is an EvalNode implementation that detects changes for a given 90 // resource instance. 91 type EvalDiff struct { 92 Addr addrs.ResourceInstance 93 Config *configs.Resource 94 Provider *providers.Interface 95 ProviderAddr addrs.AbsProviderConfig 96 ProviderSchema **ProviderSchema 97 State **states.ResourceInstanceObject 98 PreviousDiff **plans.ResourceInstanceChange 99 100 // CreateBeforeDestroy is set if either the resource's own config sets 101 // create_before_destroy explicitly or if dependencies have forced the 102 // resource to be handled as create_before_destroy in order to avoid 103 // a dependency cycle. 104 CreateBeforeDestroy bool 105 106 OutputChange **plans.ResourceInstanceChange 107 OutputValue *cty.Value 108 OutputState **states.ResourceInstanceObject 109 110 Stub bool 111 } 112 113 // TODO: test 114 func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) { 115 state := *n.State 116 config := *n.Config 117 provider := *n.Provider 118 providerSchema := *n.ProviderSchema 119 120 if providerSchema == nil { 121 return nil, fmt.Errorf("provider schema is unavailable for %s", n.Addr) 122 } 123 if n.ProviderAddr.ProviderConfig.Type.LegacyString() == "" { 124 panic(fmt.Sprintf("EvalDiff for %s does not have ProviderAddr set", n.Addr.Absolute(ctx.Path()))) 125 } 126 127 var diags tfdiags.Diagnostics 128 129 // Evaluate the configuration 130 schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource()) 131 if schema == nil { 132 // Should be caught during validation, so we don't bother with a pretty error here 133 return nil, fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Type) 134 } 135 forEach, _ := evaluateResourceForEachExpression(n.Config.ForEach, ctx) 136 keyData := EvalDataForInstanceKey(n.Addr.Key, forEach) 137 configVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema, nil, keyData) 138 diags = diags.Append(configDiags) 139 if configDiags.HasErrors() { 140 return nil, diags.Err() 141 } 142 143 absAddr := n.Addr.Absolute(ctx.Path()) 144 var priorVal cty.Value 145 var priorValTainted cty.Value 146 var priorPrivate []byte 147 if state != nil { 148 if state.Status != states.ObjectTainted { 149 priorVal = state.Value 150 priorPrivate = state.Private 151 } else { 152 // If the prior state is tainted then we'll proceed below like 153 // we're creating an entirely new object, but then turn it into 154 // a synthetic "Replace" change at the end, creating the same 155 // result as if the provider had marked at least one argument 156 // change as "requires replacement". 157 priorValTainted = state.Value 158 priorVal = cty.NullVal(schema.ImpliedType()) 159 } 160 } else { 161 priorVal = cty.NullVal(schema.ImpliedType()) 162 } 163 164 proposedNewVal := objchange.ProposedNewObject(schema, priorVal, configVal) 165 166 // Call pre-diff hook 167 if !n.Stub { 168 err := ctx.Hook(func(h Hook) (HookAction, error) { 169 return h.PreDiff(absAddr, states.CurrentGen, priorVal, proposedNewVal) 170 }) 171 if err != nil { 172 return nil, err 173 } 174 } 175 176 log.Printf("[TRACE] Re-validating config for %q", n.Addr.Absolute(ctx.Path())) 177 // Allow the provider to validate the final set of values. 178 // The config was statically validated early on, but there may have been 179 // unknown values which the provider could not validate at the time. 180 validateResp := provider.ValidateResourceTypeConfig( 181 providers.ValidateResourceTypeConfigRequest{ 182 TypeName: n.Addr.Resource.Type, 183 Config: configVal, 184 }, 185 ) 186 if validateResp.Diagnostics.HasErrors() { 187 return nil, validateResp.Diagnostics.InConfigBody(config.Config).Err() 188 } 189 190 // The provider gets an opportunity to customize the proposed new value, 191 // which in turn produces the _planned_ new value. But before 192 // we send back this information, we need to process ignore_changes 193 // so that CustomizeDiff will not act on them 194 var ignoreChangeDiags tfdiags.Diagnostics 195 proposedNewVal, ignoreChangeDiags = n.processIgnoreChanges(priorVal, proposedNewVal) 196 diags = diags.Append(ignoreChangeDiags) 197 if ignoreChangeDiags.HasErrors() { 198 return nil, diags.Err() 199 } 200 201 resp := provider.PlanResourceChange(providers.PlanResourceChangeRequest{ 202 TypeName: n.Addr.Resource.Type, 203 Config: configVal, 204 PriorState: priorVal, 205 ProposedNewState: proposedNewVal, 206 PriorPrivate: priorPrivate, 207 }) 208 diags = diags.Append(resp.Diagnostics.InConfigBody(config.Config)) 209 if diags.HasErrors() { 210 return nil, diags.Err() 211 } 212 213 plannedNewVal := resp.PlannedState 214 plannedPrivate := resp.PlannedPrivate 215 216 if plannedNewVal == cty.NilVal { 217 // Should never happen. Since real-world providers return via RPC a nil 218 // is always a bug in the client-side stub. This is more likely caused 219 // by an incompletely-configured mock provider in tests, though. 220 panic(fmt.Sprintf("PlanResourceChange of %s produced nil value", absAddr.String())) 221 } 222 223 // We allow the planned new value to disagree with configuration _values_ 224 // here, since that allows the provider to do special logic like a 225 // DiffSuppressFunc, but we still require that the provider produces 226 // a value whose type conforms to the schema. 227 for _, err := range plannedNewVal.Type().TestConformance(schema.ImpliedType()) { 228 diags = diags.Append(tfdiags.Sourceless( 229 tfdiags.Error, 230 "Provider produced invalid plan", 231 fmt.Sprintf( 232 "Provider %q planned an invalid value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", 233 n.ProviderAddr.ProviderConfig.Type, tfdiags.FormatErrorPrefixed(err, absAddr.String()), 234 ), 235 )) 236 } 237 if diags.HasErrors() { 238 return nil, diags.Err() 239 } 240 241 if errs := objchange.AssertPlanValid(schema, priorVal, configVal, plannedNewVal); len(errs) > 0 { 242 if resp.LegacyTypeSystem { 243 // The shimming of the old type system in the legacy SDK is not precise 244 // enough to pass this consistency check, so we'll give it a pass here, 245 // but we will generate a warning about it so that we are more likely 246 // to notice in the logs if an inconsistency beyond the type system 247 // leads to a downstream provider failure. 248 var buf strings.Builder 249 fmt.Fprintf(&buf, "[WARN] Provider %q produced an invalid plan for %s, but we are tolerating it because it is using the legacy plugin SDK.\n The following problems may be the cause of any confusing errors from downstream operations:", n.ProviderAddr.ProviderConfig.Type, absAddr) 250 for _, err := range errs { 251 fmt.Fprintf(&buf, "\n - %s", tfdiags.FormatError(err)) 252 } 253 log.Print(buf.String()) 254 } else { 255 for _, err := range errs { 256 diags = diags.Append(tfdiags.Sourceless( 257 tfdiags.Error, 258 "Provider produced invalid plan", 259 fmt.Sprintf( 260 "Provider %q planned an invalid value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", 261 n.ProviderAddr.ProviderConfig.Type, tfdiags.FormatErrorPrefixed(err, absAddr.String()), 262 ), 263 )) 264 } 265 return nil, diags.Err() 266 } 267 } 268 269 // TODO: We should be able to remove this repeat of processing ignored changes 270 // after the plan, which helps providers relying on old behavior "just work" 271 // in the next major version, such that we can be stricter about ignore_changes 272 // values 273 plannedNewVal, ignoreChangeDiags = n.processIgnoreChanges(priorVal, plannedNewVal) 274 diags = diags.Append(ignoreChangeDiags) 275 if ignoreChangeDiags.HasErrors() { 276 return nil, diags.Err() 277 } 278 279 // The provider produces a list of paths to attributes whose changes mean 280 // that we must replace rather than update an existing remote object. 281 // However, we only need to do that if the identified attributes _have_ 282 // actually changed -- particularly after we may have undone some of the 283 // changes in processIgnoreChanges -- so now we'll filter that list to 284 // include only where changes are detected. 285 reqRep := cty.NewPathSet() 286 if len(resp.RequiresReplace) > 0 { 287 for _, path := range resp.RequiresReplace { 288 if priorVal.IsNull() { 289 // If prior is null then we don't expect any RequiresReplace at all, 290 // because this is a Create action. 291 continue 292 } 293 294 priorChangedVal, priorPathDiags := hcl.ApplyPath(priorVal, path, nil) 295 plannedChangedVal, plannedPathDiags := hcl.ApplyPath(plannedNewVal, path, nil) 296 if plannedPathDiags.HasErrors() && priorPathDiags.HasErrors() { 297 // This means the path was invalid in both the prior and new 298 // values, which is an error with the provider itself. 299 diags = diags.Append(tfdiags.Sourceless( 300 tfdiags.Error, 301 "Provider produced invalid plan", 302 fmt.Sprintf( 303 "Provider %q has indicated \"requires replacement\" on %s for a non-existent attribute path %#v.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", 304 n.ProviderAddr.ProviderConfig.Type, absAddr, path, 305 ), 306 )) 307 continue 308 } 309 310 // Make sure we have valid Values for both values. 311 // Note: if the opposing value was of the type 312 // cty.DynamicPseudoType, the type assigned here may not exactly 313 // match the schema. This is fine here, since we're only going to 314 // check for equality, but if the NullVal is to be used, we need to 315 // check the schema for th true type. 316 switch { 317 case priorChangedVal == cty.NilVal && plannedChangedVal == cty.NilVal: 318 // this should never happen without ApplyPath errors above 319 panic("requires replace path returned 2 nil values") 320 case priorChangedVal == cty.NilVal: 321 priorChangedVal = cty.NullVal(plannedChangedVal.Type()) 322 case plannedChangedVal == cty.NilVal: 323 plannedChangedVal = cty.NullVal(priorChangedVal.Type()) 324 } 325 326 eqV := plannedChangedVal.Equals(priorChangedVal) 327 if !eqV.IsKnown() || eqV.False() { 328 reqRep.Add(path) 329 } 330 } 331 if diags.HasErrors() { 332 return nil, diags.Err() 333 } 334 } 335 336 eqV := plannedNewVal.Equals(priorVal) 337 eq := eqV.IsKnown() && eqV.True() 338 339 var action plans.Action 340 switch { 341 case priorVal.IsNull(): 342 action = plans.Create 343 case eq: 344 action = plans.NoOp 345 case !reqRep.Empty(): 346 // If there are any "requires replace" paths left _after our filtering 347 // above_ then this is a replace action. 348 if n.CreateBeforeDestroy { 349 action = plans.CreateThenDelete 350 } else { 351 action = plans.DeleteThenCreate 352 } 353 default: 354 action = plans.Update 355 // "Delete" is never chosen here, because deletion plans are always 356 // created more directly elsewhere, such as in "orphan" handling. 357 } 358 359 if action.IsReplace() { 360 // In this strange situation we want to produce a change object that 361 // shows our real prior object but has a _new_ object that is built 362 // from a null prior object, since we're going to delete the one 363 // that has all the computed values on it. 364 // 365 // Therefore we'll ask the provider to plan again here, giving it 366 // a null object for the prior, and then we'll meld that with the 367 // _actual_ prior state to produce a correctly-shaped replace change. 368 // The resulting change should show any computed attributes changing 369 // from known prior values to unknown values, unless the provider is 370 // able to predict new values for any of these computed attributes. 371 nullPriorVal := cty.NullVal(schema.ImpliedType()) 372 373 // create a new proposed value from the null state and the config 374 proposedNewVal = objchange.ProposedNewObject(schema, nullPriorVal, configVal) 375 376 resp = provider.PlanResourceChange(providers.PlanResourceChangeRequest{ 377 TypeName: n.Addr.Resource.Type, 378 Config: configVal, 379 PriorState: nullPriorVal, 380 ProposedNewState: proposedNewVal, 381 PriorPrivate: plannedPrivate, 382 }) 383 // We need to tread carefully here, since if there are any warnings 384 // in here they probably also came out of our previous call to 385 // PlanResourceChange above, and so we don't want to repeat them. 386 // Consequently, we break from the usual pattern here and only 387 // append these new diagnostics if there's at least one error inside. 388 if resp.Diagnostics.HasErrors() { 389 diags = diags.Append(resp.Diagnostics.InConfigBody(config.Config)) 390 return nil, diags.Err() 391 } 392 plannedNewVal = resp.PlannedState 393 plannedPrivate = resp.PlannedPrivate 394 for _, err := range plannedNewVal.Type().TestConformance(schema.ImpliedType()) { 395 diags = diags.Append(tfdiags.Sourceless( 396 tfdiags.Error, 397 "Provider produced invalid plan", 398 fmt.Sprintf( 399 "Provider %q planned an invalid value for %s%s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", 400 n.ProviderAddr.ProviderConfig.Type, absAddr, tfdiags.FormatError(err), 401 ), 402 )) 403 } 404 if diags.HasErrors() { 405 return nil, diags.Err() 406 } 407 } 408 409 // If our prior value was tainted then we actually want this to appear 410 // as a replace change, even though so far we've been treating it as a 411 // create. 412 if action == plans.Create && priorValTainted != cty.NilVal { 413 if n.CreateBeforeDestroy { 414 action = plans.CreateThenDelete 415 } else { 416 action = plans.DeleteThenCreate 417 } 418 priorVal = priorValTainted 419 } 420 421 // As a special case, if we have a previous diff (presumably from the plan 422 // phases, whereas we're now in the apply phase) and it was for a replace, 423 // we've already deleted the original object from state by the time we 424 // get here and so we would've ended up with a _create_ action this time, 425 // which we now need to paper over to get a result consistent with what 426 // we originally intended. 427 if n.PreviousDiff != nil { 428 prevChange := *n.PreviousDiff 429 if prevChange.Action.IsReplace() && action == plans.Create { 430 log.Printf("[TRACE] EvalDiff: %s treating Create change as %s change to match with earlier plan", absAddr, prevChange.Action) 431 action = prevChange.Action 432 priorVal = prevChange.Before 433 } 434 } 435 436 // Call post-refresh hook 437 if !n.Stub { 438 err := ctx.Hook(func(h Hook) (HookAction, error) { 439 return h.PostDiff(absAddr, states.CurrentGen, action, priorVal, plannedNewVal) 440 }) 441 if err != nil { 442 return nil, err 443 } 444 } 445 446 // Update our output if we care 447 if n.OutputChange != nil { 448 *n.OutputChange = &plans.ResourceInstanceChange{ 449 Addr: absAddr, 450 Private: plannedPrivate, 451 ProviderAddr: n.ProviderAddr, 452 Change: plans.Change{ 453 Action: action, 454 Before: priorVal, 455 After: plannedNewVal, 456 }, 457 RequiredReplace: reqRep, 458 } 459 } 460 461 if n.OutputValue != nil { 462 *n.OutputValue = configVal 463 } 464 465 // Update the state if we care 466 if n.OutputState != nil { 467 *n.OutputState = &states.ResourceInstanceObject{ 468 // We use the special "planned" status here to note that this 469 // object's value is not yet complete. Objects with this status 470 // cannot be used during expression evaluation, so the caller 471 // must _also_ record the returned change in the active plan, 472 // which the expression evaluator will use in preference to this 473 // incomplete value recorded in the state. 474 Status: states.ObjectPlanned, 475 Value: plannedNewVal, 476 Private: plannedPrivate, 477 } 478 } 479 480 return nil, nil 481 } 482 483 func (n *EvalDiff) processIgnoreChanges(prior, proposed cty.Value) (cty.Value, tfdiags.Diagnostics) { 484 // ignore_changes only applies when an object already exists, since we 485 // can't ignore changes to a thing we've not created yet. 486 if prior.IsNull() { 487 return proposed, nil 488 } 489 490 ignoreChanges := n.Config.Managed.IgnoreChanges 491 ignoreAll := n.Config.Managed.IgnoreAllChanges 492 493 if len(ignoreChanges) == 0 && !ignoreAll { 494 return proposed, nil 495 } 496 if ignoreAll { 497 return prior, nil 498 } 499 if prior.IsNull() || proposed.IsNull() { 500 // Ignore changes doesn't apply when we're creating for the first time. 501 // Proposed should never be null here, but if it is then we'll just let it be. 502 return proposed, nil 503 } 504 505 return processIgnoreChangesIndividual(prior, proposed, ignoreChanges) 506 } 507 508 func processIgnoreChangesIndividual(prior, proposed cty.Value, ignoreChanges []hcl.Traversal) (cty.Value, tfdiags.Diagnostics) { 509 // When we walk below we will be using cty.Path values for comparison, so 510 // we'll convert our traversals here so we can compare more easily. 511 ignoreChangesPath := make([]cty.Path, len(ignoreChanges)) 512 for i, traversal := range ignoreChanges { 513 path := make(cty.Path, len(traversal)) 514 for si, step := range traversal { 515 switch ts := step.(type) { 516 case hcl.TraverseRoot: 517 path[si] = cty.GetAttrStep{ 518 Name: ts.Name, 519 } 520 case hcl.TraverseAttr: 521 path[si] = cty.GetAttrStep{ 522 Name: ts.Name, 523 } 524 case hcl.TraverseIndex: 525 path[si] = cty.IndexStep{ 526 Key: ts.Key, 527 } 528 default: 529 panic(fmt.Sprintf("unsupported traversal step %#v", step)) 530 } 531 } 532 ignoreChangesPath[i] = path 533 } 534 535 var diags tfdiags.Diagnostics 536 ret, _ := cty.Transform(proposed, func(path cty.Path, v cty.Value) (cty.Value, error) { 537 // First we must see if this is a path that's being ignored at all. 538 // We're looking for an exact match here because this walk will visit 539 // leaf values first and then their containers, and we want to do 540 // the "ignore" transform once we reach the point indicated, throwing 541 // away any deeper values we already produced at that point. 542 var ignoreTraversal hcl.Traversal 543 for i, candidate := range ignoreChangesPath { 544 if path.Equals(candidate) { 545 ignoreTraversal = ignoreChanges[i] 546 } 547 } 548 if ignoreTraversal == nil { 549 return v, nil 550 } 551 552 // If we're able to follow the same path through the prior value, 553 // we'll take the value there instead, effectively undoing the 554 // change that was planned. 555 priorV, diags := hcl.ApplyPath(prior, path, nil) 556 if diags.HasErrors() { 557 // We just ignore the errors and move on here, since we assume it's 558 // just because the prior value was a slightly-different shape. 559 // It could potentially also be that the traversal doesn't match 560 // the schema, but we should've caught that during the validate 561 // walk if so. 562 return v, nil 563 } 564 return priorV, nil 565 }) 566 return ret, diags 567 } 568 569 // a group of key-*ResourceAttrDiff pairs from the same flatmapped container 570 type flatAttrDiff map[string]*ResourceAttrDiff 571 572 // we need to keep all keys if any of them have a diff that's not ignored 573 func (f flatAttrDiff) keepDiff(ignoreChanges map[string]bool) bool { 574 for k, v := range f { 575 ignore := false 576 for attr := range ignoreChanges { 577 if strings.HasPrefix(k, attr) { 578 ignore = true 579 } 580 } 581 582 if !v.Empty() && !v.NewComputed && !ignore { 583 return true 584 } 585 } 586 return false 587 } 588 589 // EvalDiffDestroy is an EvalNode implementation that returns a plain 590 // destroy diff. 591 type EvalDiffDestroy struct { 592 Addr addrs.ResourceInstance 593 DeposedKey states.DeposedKey 594 State **states.ResourceInstanceObject 595 ProviderAddr addrs.AbsProviderConfig 596 597 Output **plans.ResourceInstanceChange 598 OutputState **states.ResourceInstanceObject 599 } 600 601 // TODO: test 602 func (n *EvalDiffDestroy) Eval(ctx EvalContext) (interface{}, error) { 603 absAddr := n.Addr.Absolute(ctx.Path()) 604 state := *n.State 605 606 if n.ProviderAddr.ProviderConfig.Type.LegacyString() == "" { 607 if n.DeposedKey == "" { 608 panic(fmt.Sprintf("EvalDiffDestroy for %s does not have ProviderAddr set", absAddr)) 609 } else { 610 panic(fmt.Sprintf("EvalDiffDestroy for %s (deposed %s) does not have ProviderAddr set", absAddr, n.DeposedKey)) 611 } 612 } 613 614 // If there is no state or our attributes object is null then we're already 615 // destroyed. 616 if state == nil || state.Value.IsNull() { 617 return nil, nil 618 } 619 620 // Call pre-diff hook 621 err := ctx.Hook(func(h Hook) (HookAction, error) { 622 return h.PreDiff( 623 absAddr, n.DeposedKey.Generation(), 624 state.Value, 625 cty.NullVal(cty.DynamicPseudoType), 626 ) 627 }) 628 if err != nil { 629 return nil, err 630 } 631 632 // Change is always the same for a destroy. We don't need the provider's 633 // help for this one. 634 // TODO: Should we give the provider an opportunity to veto this? 635 change := &plans.ResourceInstanceChange{ 636 Addr: absAddr, 637 DeposedKey: n.DeposedKey, 638 Change: plans.Change{ 639 Action: plans.Delete, 640 Before: state.Value, 641 After: cty.NullVal(cty.DynamicPseudoType), 642 }, 643 Private: state.Private, 644 ProviderAddr: n.ProviderAddr, 645 } 646 647 // Call post-diff hook 648 err = ctx.Hook(func(h Hook) (HookAction, error) { 649 return h.PostDiff( 650 absAddr, 651 n.DeposedKey.Generation(), 652 change.Action, 653 change.Before, 654 change.After, 655 ) 656 }) 657 if err != nil { 658 return nil, err 659 } 660 661 // Update our output 662 *n.Output = change 663 664 if n.OutputState != nil { 665 // Record our proposed new state, which is nil because we're destroying. 666 *n.OutputState = nil 667 } 668 669 return nil, nil 670 } 671 672 // EvalReduceDiff is an EvalNode implementation that takes a planned resource 673 // instance change as might be produced by EvalDiff or EvalDiffDestroy and 674 // "simplifies" it to a single atomic action to be performed by a specific 675 // graph node. 676 // 677 // Callers must specify whether they are a destroy node or a regular apply 678 // node. If the result is NoOp then the given change requires no action for 679 // the specific graph node calling this and so evaluation of the that graph 680 // node should exit early and take no action. 681 // 682 // The object written to OutChange may either be identical to InChange or 683 // a new change object derived from InChange. Because of the former case, the 684 // caller must not mutate the object returned in OutChange. 685 type EvalReduceDiff struct { 686 Addr addrs.ResourceInstance 687 InChange **plans.ResourceInstanceChange 688 Destroy bool 689 OutChange **plans.ResourceInstanceChange 690 } 691 692 // TODO: test 693 func (n *EvalReduceDiff) Eval(ctx EvalContext) (interface{}, error) { 694 in := *n.InChange 695 out := in.Simplify(n.Destroy) 696 if n.OutChange != nil { 697 *n.OutChange = out 698 } 699 if out.Action != in.Action { 700 if n.Destroy { 701 log.Printf("[TRACE] EvalReduceDiff: %s change simplified from %s to %s for destroy node", n.Addr, in.Action, out.Action) 702 } else { 703 log.Printf("[TRACE] EvalReduceDiff: %s change simplified from %s to %s for apply node", n.Addr, in.Action, out.Action) 704 } 705 } 706 return nil, nil 707 } 708 709 // EvalReadDiff is an EvalNode implementation that retrieves the planned 710 // change for a particular resource instance object. 711 type EvalReadDiff struct { 712 Addr addrs.ResourceInstance 713 DeposedKey states.DeposedKey 714 ProviderSchema **ProviderSchema 715 Change **plans.ResourceInstanceChange 716 } 717 718 func (n *EvalReadDiff) Eval(ctx EvalContext) (interface{}, error) { 719 providerSchema := *n.ProviderSchema 720 changes := ctx.Changes() 721 addr := n.Addr.Absolute(ctx.Path()) 722 723 schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource()) 724 if schema == nil { 725 // Should be caught during validation, so we don't bother with a pretty error here 726 return nil, fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Type) 727 } 728 729 gen := states.CurrentGen 730 if n.DeposedKey != states.NotDeposed { 731 gen = n.DeposedKey 732 } 733 csrc := changes.GetResourceInstanceChange(addr, gen) 734 if csrc == nil { 735 log.Printf("[TRACE] EvalReadDiff: No planned change recorded for %s", addr) 736 return nil, nil 737 } 738 739 change, err := csrc.Decode(schema.ImpliedType()) 740 if err != nil { 741 return nil, fmt.Errorf("failed to decode planned changes for %s: %s", addr, err) 742 } 743 if n.Change != nil { 744 *n.Change = change 745 } 746 747 log.Printf("[TRACE] EvalReadDiff: Read %s change from plan for %s", change.Action, addr) 748 749 return nil, nil 750 } 751 752 // EvalWriteDiff is an EvalNode implementation that saves a planned change 753 // for an instance object into the set of global planned changes. 754 type EvalWriteDiff struct { 755 Addr addrs.ResourceInstance 756 DeposedKey states.DeposedKey 757 ProviderSchema **ProviderSchema 758 Change **plans.ResourceInstanceChange 759 } 760 761 // TODO: test 762 func (n *EvalWriteDiff) Eval(ctx EvalContext) (interface{}, error) { 763 changes := ctx.Changes() 764 addr := n.Addr.Absolute(ctx.Path()) 765 if n.Change == nil || *n.Change == nil { 766 // Caller sets nil to indicate that we need to remove a change from 767 // the set of changes. 768 gen := states.CurrentGen 769 if n.DeposedKey != states.NotDeposed { 770 gen = n.DeposedKey 771 } 772 changes.RemoveResourceInstanceChange(addr, gen) 773 return nil, nil 774 } 775 776 providerSchema := *n.ProviderSchema 777 change := *n.Change 778 779 if change.Addr.String() != addr.String() || change.DeposedKey != n.DeposedKey { 780 // Should never happen, and indicates a bug in the caller. 781 panic("inconsistent address and/or deposed key in EvalWriteDiff") 782 } 783 784 schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource()) 785 if schema == nil { 786 // Should be caught during validation, so we don't bother with a pretty error here 787 return nil, fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Type) 788 } 789 790 csrc, err := change.Encode(schema.ImpliedType()) 791 if err != nil { 792 return nil, fmt.Errorf("failed to encode planned changes for %s: %s", addr, err) 793 } 794 795 changes.AppendResourceInstanceChange(csrc) 796 if n.DeposedKey == states.NotDeposed { 797 log.Printf("[TRACE] EvalWriteDiff: recorded %s change for %s", change.Action, addr) 798 } else { 799 log.Printf("[TRACE] EvalWriteDiff: recorded %s change for %s deposed object %s", change.Action, addr, n.DeposedKey) 800 } 801 802 return nil, nil 803 }