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