]> git.immae.eu Git - github/fretlink/terraform-provider-statuscake.git/blobdiff - vendor/github.com/hashicorp/terraform/terraform/eval_diff.go
Upgrade to 0.12
[github/fretlink/terraform-provider-statuscake.git] / vendor / github.com / hashicorp / terraform / terraform / eval_diff.go
index 26205ce51ec3746be226117781e63d0e013ef1a8..b7acfb06dcbb400b5abf054d5f4b758fe750c01f 100644 (file)
 package terraform
 
 import (
+       "bytes"
        "fmt"
        "log"
+       "reflect"
        "strings"
 
-       "github.com/hashicorp/terraform/config"
-       "github.com/hashicorp/terraform/version"
+       "github.com/hashicorp/hcl2/hcl"
+       "github.com/zclconf/go-cty/cty"
+
+       "github.com/hashicorp/terraform/addrs"
+       "github.com/hashicorp/terraform/configs"
+       "github.com/hashicorp/terraform/plans"
+       "github.com/hashicorp/terraform/plans/objchange"
+       "github.com/hashicorp/terraform/providers"
+       "github.com/hashicorp/terraform/states"
+       "github.com/hashicorp/terraform/tfdiags"
 )
 
-// EvalCompareDiff is an EvalNode implementation that compares two diffs
-// and errors if the diffs are not equal.
-type EvalCompareDiff struct {
-       Info     *InstanceInfo
-       One, Two **InstanceDiff
+// EvalCheckPlannedChange is an EvalNode implementation that produces errors
+// if the _actual_ expected value is not compatible with what was recorded
+// in the plan.
+//
+// Errors here are most often indicative of a bug in the provider, so our
+// error messages will report with that in mind. It's also possible that
+// there's a bug in Terraform's Core's own "proposed new value" code in
+// EvalDiff.
+type EvalCheckPlannedChange struct {
+       Addr           addrs.ResourceInstance
+       ProviderAddr   addrs.AbsProviderConfig
+       ProviderSchema **ProviderSchema
+
+       // We take ResourceInstanceChange objects here just because that's what's
+       // convenient to pass in from the evaltree implementation, but we really
+       // only look at the "After" value of each change.
+       Planned, Actual **plans.ResourceInstanceChange
 }
 
-// TODO: test
-func (n *EvalCompareDiff) Eval(ctx EvalContext) (interface{}, error) {
-       one, two := *n.One, *n.Two
-
-       // If either are nil, let them be empty
-       if one == nil {
-               one = new(InstanceDiff)
-               one.init()
-       }
-       if two == nil {
-               two = new(InstanceDiff)
-               two.init()
-       }
-       oneId, _ := one.GetAttribute("id")
-       twoId, _ := two.GetAttribute("id")
-       one.DelAttribute("id")
-       two.DelAttribute("id")
-       defer func() {
-               if oneId != nil {
-                       one.SetAttribute("id", oneId)
-               }
-               if twoId != nil {
-                       two.SetAttribute("id", twoId)
-               }
-       }()
-
-       if same, reason := one.Same(two); !same {
-               log.Printf("[ERROR] %s: diffs didn't match", n.Info.Id)
-               log.Printf("[ERROR] %s: reason: %s", n.Info.Id, reason)
-               log.Printf("[ERROR] %s: diff one: %#v", n.Info.Id, one)
-               log.Printf("[ERROR] %s: diff two: %#v", n.Info.Id, two)
-               return nil, fmt.Errorf(
-                       "%s: diffs didn't match during apply. This is a bug with "+
-                               "Terraform and should be reported as a GitHub Issue.\n"+
-                               "\n"+
-                               "Please include the following information in your report:\n"+
-                               "\n"+
-                               "    Terraform Version: %s\n"+
-                               "    Resource ID: %s\n"+
-                               "    Mismatch reason: %s\n"+
-                               "    Diff One (usually from plan): %#v\n"+
-                               "    Diff Two (usually from apply): %#v\n"+
-                               "\n"+
-                               "Also include as much context as you can about your config, state, "+
-                               "and the steps you performed to trigger this error.\n",
-                       n.Info.Id, version.Version, n.Info.Id, reason, one, two)
+func (n *EvalCheckPlannedChange) Eval(ctx EvalContext) (interface{}, error) {
+       providerSchema := *n.ProviderSchema
+       plannedChange := *n.Planned
+       actualChange := *n.Actual
+
+       schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource())
+       if schema == nil {
+               // Should be caught during validation, so we don't bother with a pretty error here
+               return nil, fmt.Errorf("provider does not support %q", n.Addr.Resource.Type)
+       }
+
+       var diags tfdiags.Diagnostics
+       absAddr := n.Addr.Absolute(ctx.Path())
+
+       log.Printf("[TRACE] EvalCheckPlannedChange: Verifying that actual change (action %s) matches planned change (action %s)", actualChange.Action, plannedChange.Action)
+
+       if plannedChange.Action != actualChange.Action {
+               switch {
+               case plannedChange.Action == plans.Update && actualChange.Action == plans.NoOp:
+                       // It's okay for an update to become a NoOp once we've filled in
+                       // all of the unknown values, since the final values might actually
+                       // match what was there before after all.
+                       log.Printf("[DEBUG] After incorporating new values learned so far during apply, %s change has become NoOp", absAddr)
+               default:
+                       diags = diags.Append(tfdiags.Sourceless(
+                               tfdiags.Error,
+                               "Provider produced inconsistent final plan",
+                               fmt.Sprintf(
+                                       "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.",
+                                       absAddr, n.ProviderAddr.ProviderConfig.Type,
+                                       plannedChange.Action, actualChange.Action,
+                               ),
+                       ))
+               }
        }
 
-       return nil, nil
+       errs := objchange.AssertObjectCompatible(schema, plannedChange.After, actualChange.After)
+       for _, err := range errs {
+               diags = diags.Append(tfdiags.Sourceless(
+                       tfdiags.Error,
+                       "Provider produced inconsistent final plan",
+                       fmt.Sprintf(
+                               "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.",
+                               absAddr, n.ProviderAddr.ProviderConfig.Type, tfdiags.FormatError(err),
+                       ),
+               ))
+       }
+       return nil, diags.Err()
 }
 
-// EvalDiff is an EvalNode implementation that does a refresh for
-// a resource.
+// EvalDiff is an EvalNode implementation that detects changes for a given
+// resource instance.
 type EvalDiff struct {
-       Name        string
-       Info        *InstanceInfo
-       Config      **ResourceConfig
-       Provider    *ResourceProvider
-       Diff        **InstanceDiff
-       State       **InstanceState
-       OutputDiff  **InstanceDiff
-       OutputState **InstanceState
-
-       // Resource is needed to fetch the ignore_changes list so we can
-       // filter user-requested ignored attributes from the diff.
-       Resource *config.Resource
-
-       // Stub is used to flag the generated InstanceDiff as a stub. This is used to
-       // ensure that the node exists to perform interpolations and generate
-       // computed paths off of, but not as an actual diff where resouces should be
-       // counted, and not as a diff that should be acted on.
+       Addr           addrs.ResourceInstance
+       Config         *configs.Resource
+       Provider       *providers.Interface
+       ProviderAddr   addrs.AbsProviderConfig
+       ProviderSchema **ProviderSchema
+       State          **states.ResourceInstanceObject
+       PreviousDiff   **plans.ResourceInstanceChange
+
+       // CreateBeforeDestroy is set if either the resource's own config sets
+       // create_before_destroy explicitly or if dependencies have forced the
+       // resource to be handled as create_before_destroy in order to avoid
+       // a dependency cycle.
+       CreateBeforeDestroy bool
+
+       OutputChange **plans.ResourceInstanceChange
+       OutputValue  *cty.Value
+       OutputState  **states.ResourceInstanceObject
+
        Stub bool
 }
 
@@ -95,81 +117,303 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
        state := *n.State
        config := *n.Config
        provider := *n.Provider
+       providerSchema := *n.ProviderSchema
+
+       if providerSchema == nil {
+               return nil, fmt.Errorf("provider schema is unavailable for %s", n.Addr)
+       }
+       if n.ProviderAddr.ProviderConfig.Type == "" {
+               panic(fmt.Sprintf("EvalDiff for %s does not have ProviderAddr set", n.Addr.Absolute(ctx.Path())))
+       }
+
+       var diags tfdiags.Diagnostics
+
+       // Evaluate the configuration
+       schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource())
+       if schema == nil {
+               // Should be caught during validation, so we don't bother with a pretty error here
+               return nil, fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Type)
+       }
+       keyData := EvalDataForInstanceKey(n.Addr.Key)
+       configVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema, nil, keyData)
+       diags = diags.Append(configDiags)
+       if configDiags.HasErrors() {
+               return nil, diags.Err()
+       }
+
+       absAddr := n.Addr.Absolute(ctx.Path())
+       var priorVal cty.Value
+       var priorValTainted cty.Value
+       var priorPrivate []byte
+       if state != nil {
+               if state.Status != states.ObjectTainted {
+                       priorVal = state.Value
+                       priorPrivate = state.Private
+               } else {
+                       // If the prior state is tainted then we'll proceed below like
+                       // we're creating an entirely new object, but then turn it into
+                       // a synthetic "Replace" change at the end, creating the same
+                       // result as if the provider had marked at least one argument
+                       // change as "requires replacement".
+                       priorValTainted = state.Value
+                       priorVal = cty.NullVal(schema.ImpliedType())
+               }
+       } else {
+               priorVal = cty.NullVal(schema.ImpliedType())
+       }
+
+       proposedNewVal := objchange.ProposedNewObject(schema, priorVal, configVal)
 
        // Call pre-diff hook
        if !n.Stub {
                err := ctx.Hook(func(h Hook) (HookAction, error) {
-                       return h.PreDiff(n.Info, state)
+                       return h.PreDiff(absAddr, states.CurrentGen, priorVal, proposedNewVal)
                })
                if err != nil {
                        return nil, err
                }
        }
 
-       // The state for the diff must never be nil
-       diffState := state
-       if diffState == nil {
-               diffState = new(InstanceState)
+       // The provider gets an opportunity to customize the proposed new value,
+       // which in turn produces the _planned_ new value.
+       resp := provider.PlanResourceChange(providers.PlanResourceChangeRequest{
+               TypeName:         n.Addr.Resource.Type,
+               Config:           configVal,
+               PriorState:       priorVal,
+               ProposedNewState: proposedNewVal,
+               PriorPrivate:     priorPrivate,
+       })
+       diags = diags.Append(resp.Diagnostics.InConfigBody(config.Config))
+       if diags.HasErrors() {
+               return nil, diags.Err()
+       }
+
+       plannedNewVal := resp.PlannedState
+       plannedPrivate := resp.PlannedPrivate
+
+       if plannedNewVal == cty.NilVal {
+               // Should never happen. Since real-world providers return via RPC a nil
+               // is always a bug in the client-side stub. This is more likely caused
+               // by an incompletely-configured mock provider in tests, though.
+               panic(fmt.Sprintf("PlanResourceChange of %s produced nil value", absAddr.String()))
+       }
+
+       // We allow the planned new value to disagree with configuration _values_
+       // here, since that allows the provider to do special logic like a
+       // DiffSuppressFunc, but we still require that the provider produces
+       // a value whose type conforms to the schema.
+       for _, err := range plannedNewVal.Type().TestConformance(schema.ImpliedType()) {
+               diags = diags.Append(tfdiags.Sourceless(
+                       tfdiags.Error,
+                       "Provider produced invalid plan",
+                       fmt.Sprintf(
+                               "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.",
+                               n.ProviderAddr.ProviderConfig.Type, tfdiags.FormatErrorPrefixed(err, absAddr.String()),
+                       ),
+               ))
+       }
+       if diags.HasErrors() {
+               return nil, diags.Err()
+       }
+
+       if errs := objchange.AssertPlanValid(schema, priorVal, configVal, plannedNewVal); len(errs) > 0 {
+               if resp.LegacyTypeSystem {
+                       // The shimming of the old type system in the legacy SDK is not precise
+                       // enough to pass this consistency check, so we'll give it a pass here,
+                       // but we will generate a warning about it so that we are more likely
+                       // to notice in the logs if an inconsistency beyond the type system
+                       // leads to a downstream provider failure.
+                       var buf strings.Builder
+                       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)
+                       for _, err := range errs {
+                               fmt.Fprintf(&buf, "\n      - %s", tfdiags.FormatError(err))
+                       }
+                       log.Print(buf.String())
+               } else {
+                       for _, err := range errs {
+                               diags = diags.Append(tfdiags.Sourceless(
+                                       tfdiags.Error,
+                                       "Provider produced invalid plan",
+                                       fmt.Sprintf(
+                                               "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.",
+                                               n.ProviderAddr.ProviderConfig.Type, tfdiags.FormatErrorPrefixed(err, absAddr.String()),
+                                       ),
+                               ))
+                       }
+                       return nil, diags.Err()
+               }
        }
-       diffState.init()
 
-       // Diff!
-       diff, err := provider.Diff(n.Info, diffState, config)
-       if err != nil {
-               return nil, err
-       }
-       if diff == nil {
-               diff = new(InstanceDiff)
+       {
+               var moreDiags tfdiags.Diagnostics
+               plannedNewVal, moreDiags = n.processIgnoreChanges(priorVal, plannedNewVal)
+               diags = diags.Append(moreDiags)
+               if moreDiags.HasErrors() {
+                       return nil, diags.Err()
+               }
        }
 
-       // Set DestroyDeposed if we have deposed instances
-       _, err = readInstanceFromState(ctx, n.Name, nil, func(rs *ResourceState) (*InstanceState, error) {
-               if len(rs.Deposed) > 0 {
-                       diff.DestroyDeposed = true
-               }
+       // The provider produces a list of paths to attributes whose changes mean
+       // that we must replace rather than update an existing remote object.
+       // However, we only need to do that if the identified attributes _have_
+       // actually changed -- particularly after we may have undone some of the
+       // changes in processIgnoreChanges -- so now we'll filter that list to
+       // include only where changes are detected.
+       reqRep := cty.NewPathSet()
+       if len(resp.RequiresReplace) > 0 {
+               for _, path := range resp.RequiresReplace {
+                       if priorVal.IsNull() {
+                               // If prior is null then we don't expect any RequiresReplace at all,
+                               // because this is a Create action.
+                               continue
+                       }
 
-               return nil, nil
-       })
-       if err != nil {
-               return nil, err
-       }
+                       priorChangedVal, priorPathDiags := hcl.ApplyPath(priorVal, path, nil)
+                       plannedChangedVal, plannedPathDiags := hcl.ApplyPath(plannedNewVal, path, nil)
+                       if plannedPathDiags.HasErrors() && priorPathDiags.HasErrors() {
+                               // This means the path was invalid in both the prior and new
+                               // values, which is an error with the provider itself.
+                               diags = diags.Append(tfdiags.Sourceless(
+                                       tfdiags.Error,
+                                       "Provider produced invalid plan",
+                                       fmt.Sprintf(
+                                               "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.",
+                                               n.ProviderAddr.ProviderConfig.Type, absAddr, path,
+                                       ),
+                               ))
+                               continue
+                       }
 
-       // Preserve the DestroyTainted flag
-       if n.Diff != nil {
-               diff.SetTainted((*n.Diff).GetDestroyTainted())
-       }
+                       // Make sure we have valid Values for both values.
+                       // Note: if the opposing value was of the type
+                       // cty.DynamicPseudoType, the type assigned here may not exactly
+                       // match the schema. This is fine here, since we're only going to
+                       // check for equality, but if the NullVal is to be used, we need to
+                       // check the schema for th true type.
+                       switch {
+                       case priorChangedVal == cty.NilVal && plannedChangedVal == cty.NilVal:
+                               // this should never happen without ApplyPath errors above
+                               panic("requires replace path returned 2 nil values")
+                       case priorChangedVal == cty.NilVal:
+                               priorChangedVal = cty.NullVal(plannedChangedVal.Type())
+                       case plannedChangedVal == cty.NilVal:
+                               plannedChangedVal = cty.NullVal(priorChangedVal.Type())
+                       }
 
-       // Require a destroy if there is an ID and it requires new.
-       if diff.RequiresNew() && state != nil && state.ID != "" {
-               diff.SetDestroy(true)
+                       eqV := plannedChangedVal.Equals(priorChangedVal)
+                       if !eqV.IsKnown() || eqV.False() {
+                               reqRep.Add(path)
+                       }
+               }
+               if diags.HasErrors() {
+                       return nil, diags.Err()
+               }
        }
 
-       // If we're creating a new resource, compute its ID
-       if diff.RequiresNew() || state == nil || state.ID == "" {
-               var oldID string
-               if state != nil {
-                       oldID = state.Attributes["id"]
+       eqV := plannedNewVal.Equals(priorVal)
+       eq := eqV.IsKnown() && eqV.True()
+
+       var action plans.Action
+       switch {
+       case priorVal.IsNull():
+               action = plans.Create
+       case eq:
+               action = plans.NoOp
+       case !reqRep.Empty():
+               // If there are any "requires replace" paths left _after our filtering
+               // above_ then this is a replace action.
+               if n.CreateBeforeDestroy {
+                       action = plans.CreateThenDelete
+               } else {
+                       action = plans.DeleteThenCreate
                }
-
-               // Add diff to compute new ID
-               diff.init()
-               diff.SetAttribute("id", &ResourceAttrDiff{
-                       Old:         oldID,
-                       NewComputed: true,
-                       RequiresNew: true,
-                       Type:        DiffAttrOutput,
+       default:
+               action = plans.Update
+               // "Delete" is never chosen here, because deletion plans are always
+               // created more directly elsewhere, such as in "orphan" handling.
+       }
+
+       if action.IsReplace() {
+               // In this strange situation we want to produce a change object that
+               // shows our real prior object but has a _new_ object that is built
+               // from a null prior object, since we're going to delete the one
+               // that has all the computed values on it.
+               //
+               // Therefore we'll ask the provider to plan again here, giving it
+               // a null object for the prior, and then we'll meld that with the
+               // _actual_ prior state to produce a correctly-shaped replace change.
+               // The resulting change should show any computed attributes changing
+               // from known prior values to unknown values, unless the provider is
+               // able to predict new values for any of these computed attributes.
+               nullPriorVal := cty.NullVal(schema.ImpliedType())
+
+               // create a new proposed value from the null state and the config
+               proposedNewVal = objchange.ProposedNewObject(schema, nullPriorVal, configVal)
+
+               resp = provider.PlanResourceChange(providers.PlanResourceChangeRequest{
+                       TypeName:         n.Addr.Resource.Type,
+                       Config:           configVal,
+                       PriorState:       nullPriorVal,
+                       ProposedNewState: proposedNewVal,
+                       PriorPrivate:     plannedPrivate,
                })
+               // We need to tread carefully here, since if there are any warnings
+               // in here they probably also came out of our previous call to
+               // PlanResourceChange above, and so we don't want to repeat them.
+               // Consequently, we break from the usual pattern here and only
+               // append these new diagnostics if there's at least one error inside.
+               if resp.Diagnostics.HasErrors() {
+                       diags = diags.Append(resp.Diagnostics.InConfigBody(config.Config))
+                       return nil, diags.Err()
+               }
+               plannedNewVal = resp.PlannedState
+               plannedPrivate = resp.PlannedPrivate
+               for _, err := range plannedNewVal.Type().TestConformance(schema.ImpliedType()) {
+                       diags = diags.Append(tfdiags.Sourceless(
+                               tfdiags.Error,
+                               "Provider produced invalid plan",
+                               fmt.Sprintf(
+                                       "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.",
+                                       n.ProviderAddr.ProviderConfig.Type, absAddr, tfdiags.FormatError(err),
+                               ),
+                       ))
+               }
+               if diags.HasErrors() {
+                       return nil, diags.Err()
+               }
        }
 
-       // filter out ignored resources
-       if err := n.processIgnoreChanges(diff); err != nil {
-               return nil, err
+       // If our prior value was tainted then we actually want this to appear
+       // as a replace change, even though so far we've been treating it as a
+       // create.
+       if action == plans.Create && priorValTainted != cty.NilVal {
+               if n.CreateBeforeDestroy {
+                       action = plans.CreateThenDelete
+               } else {
+                       action = plans.DeleteThenCreate
+               }
+               priorVal = priorValTainted
+       }
+
+       // As a special case, if we have a previous diff (presumably from the plan
+       // phases, whereas we're now in the apply phase) and it was for a replace,
+       // we've already deleted the original object from state by the time we
+       // get here and so we would've ended up with a _create_ action this time,
+       // which we now need to paper over to get a result consistent with what
+       // we originally intended.
+       if n.PreviousDiff != nil {
+               prevChange := *n.PreviousDiff
+               if prevChange.Action.IsReplace() && action == plans.Create {
+                       log.Printf("[TRACE] EvalDiff: %s treating Create change as %s change to match with earlier plan", absAddr, prevChange.Action)
+                       action = prevChange.Action
+                       priorVal = prevChange.Before
+               }
        }
 
        // Call post-refresh hook
        if !n.Stub {
-               err = ctx.Hook(func(h Hook) (HookAction, error) {
-                       return h.PostDiff(n.Info, diff)
+               err := ctx.Hook(func(h Hook) (HookAction, error) {
+                       return h.PostDiff(absAddr, states.CurrentGen, action, priorVal, plannedNewVal)
                })
                if err != nil {
                        return nil, err
@@ -177,30 +421,135 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
        }
 
        // Update our output if we care
-       if n.OutputDiff != nil {
-               *n.OutputDiff = diff
+       if n.OutputChange != nil {
+               *n.OutputChange = &plans.ResourceInstanceChange{
+                       Addr:         absAddr,
+                       Private:      plannedPrivate,
+                       ProviderAddr: n.ProviderAddr,
+                       Change: plans.Change{
+                               Action: action,
+                               Before: priorVal,
+                               After:  plannedNewVal,
+                       },
+                       RequiredReplace: reqRep,
+               }
+       }
+
+       if n.OutputValue != nil {
+               *n.OutputValue = configVal
        }
 
        // Update the state if we care
        if n.OutputState != nil {
-               *n.OutputState = state
-
-               // Merge our state so that the state is updated with our plan
-               if !diff.Empty() && n.OutputState != nil {
-                       *n.OutputState = state.MergeDiff(diff)
+               *n.OutputState = &states.ResourceInstanceObject{
+                       // We use the special "planned" status here to note that this
+                       // object's value is not yet complete. Objects with this status
+                       // cannot be used during expression evaluation, so the caller
+                       // must _also_ record the returned change in the active plan,
+                       // which the expression evaluator will use in preference to this
+                       // incomplete value recorded in the state.
+                       Status: states.ObjectPlanned,
+                       Value:  plannedNewVal,
                }
        }
 
        return nil, nil
 }
 
-func (n *EvalDiff) processIgnoreChanges(diff *InstanceDiff) error {
-       if diff == nil || n.Resource == nil || n.Resource.Id() == "" {
+func (n *EvalDiff) processIgnoreChanges(prior, proposed cty.Value) (cty.Value, tfdiags.Diagnostics) {
+       // ignore_changes only applies when an object already exists, since we
+       // can't ignore changes to a thing we've not created yet.
+       if prior.IsNull() {
+               return proposed, nil
+       }
+
+       ignoreChanges := n.Config.Managed.IgnoreChanges
+       ignoreAll := n.Config.Managed.IgnoreAllChanges
+
+       if len(ignoreChanges) == 0 && !ignoreAll {
+               return proposed, nil
+       }
+       if ignoreAll {
+               return prior, nil
+       }
+       if prior.IsNull() || proposed.IsNull() {
+               // Ignore changes doesn't apply when we're creating for the first time.
+               // Proposed should never be null here, but if it is then we'll just let it be.
+               return proposed, nil
+       }
+
+       return processIgnoreChangesIndividual(prior, proposed, ignoreChanges)
+}
+
+func processIgnoreChangesIndividual(prior, proposed cty.Value, ignoreChanges []hcl.Traversal) (cty.Value, tfdiags.Diagnostics) {
+       // When we walk below we will be using cty.Path values for comparison, so
+       // we'll convert our traversals here so we can compare more easily.
+       ignoreChangesPath := make([]cty.Path, len(ignoreChanges))
+       for i, traversal := range ignoreChanges {
+               path := make(cty.Path, len(traversal))
+               for si, step := range traversal {
+                       switch ts := step.(type) {
+                       case hcl.TraverseRoot:
+                               path[si] = cty.GetAttrStep{
+                                       Name: ts.Name,
+                               }
+                       case hcl.TraverseAttr:
+                               path[si] = cty.GetAttrStep{
+                                       Name: ts.Name,
+                               }
+                       case hcl.TraverseIndex:
+                               path[si] = cty.IndexStep{
+                                       Key: ts.Key,
+                               }
+                       default:
+                               panic(fmt.Sprintf("unsupported traversal step %#v", step))
+                       }
+               }
+               ignoreChangesPath[i] = path
+       }
+
+       var diags tfdiags.Diagnostics
+       ret, _ := cty.Transform(proposed, func(path cty.Path, v cty.Value) (cty.Value, error) {
+               // First we must see if this is a path that's being ignored at all.
+               // We're looking for an exact match here because this walk will visit
+               // leaf values first and then their containers, and we want to do
+               // the "ignore" transform once we reach the point indicated, throwing
+               // away any deeper values we already produced at that point.
+               var ignoreTraversal hcl.Traversal
+               for i, candidate := range ignoreChangesPath {
+                       if reflect.DeepEqual(path, candidate) {
+                               ignoreTraversal = ignoreChanges[i]
+                       }
+               }
+               if ignoreTraversal == nil {
+                       return v, nil
+               }
+
+               // If we're able to follow the same path through the prior value,
+               // we'll take the value there instead, effectively undoing the
+               // change that was planned.
+               priorV, diags := hcl.ApplyPath(prior, path, nil)
+               if diags.HasErrors() {
+                       // We just ignore the errors and move on here, since we assume it's
+                       // just because the prior value was a slightly-different shape.
+                       // It could potentially also be that the traversal doesn't match
+                       // the schema, but we should've caught that during the validate
+                       // walk if so.
+                       return v, nil
+               }
+               return priorV, nil
+       })
+       return ret, diags
+}
+
+func (n *EvalDiff) processIgnoreChangesOld(diff *InstanceDiff) error {
+       if diff == nil || n.Config == nil || n.Config.Managed == nil {
                return nil
        }
-       ignoreChanges := n.Resource.Lifecycle.IgnoreChanges
+       ignoreChanges := n.Config.Managed.IgnoreChanges
+       ignoreAll := n.Config.Managed.IgnoreAllChanges
 
-       if len(ignoreChanges) == 0 {
+       if len(ignoreChanges) == 0 && !ignoreAll {
                return nil
        }
 
@@ -220,9 +569,14 @@ func (n *EvalDiff) processIgnoreChanges(diff *InstanceDiff) error {
 
        // get the complete set of keys we want to ignore
        ignorableAttrKeys := make(map[string]bool)
-       for _, ignoredKey := range ignoreChanges {
-               for k := range attrs {
-                       if ignoredKey == "*" || strings.HasPrefix(k, ignoredKey) {
+       for k := range attrs {
+               if ignoreAll {
+                       ignorableAttrKeys[k] = true
+                       continue
+               }
+               for _, ignoredTraversal := range ignoreChanges {
+                       ignoredKey := legacyFlatmapKeyForTraversal(ignoredTraversal)
+                       if k == ignoredKey || strings.HasPrefix(k, ignoredKey+".") {
                                ignorableAttrKeys[k] = true
                        }
                }
@@ -285,14 +639,56 @@ func (n *EvalDiff) processIgnoreChanges(diff *InstanceDiff) error {
 
        // If we didn't hit any of our early exit conditions, we can filter the diff.
        for k := range ignorableAttrKeys {
-               log.Printf("[DEBUG] [EvalIgnoreChanges] %s - Ignoring diff attribute: %s",
-                       n.Resource.Id(), k)
+               log.Printf("[DEBUG] [EvalIgnoreChanges] %s: Ignoring diff attribute: %s", n.Addr.String(), k)
                diff.DelAttribute(k)
        }
 
        return nil
 }
 
+// legacyFlagmapKeyForTraversal constructs a key string compatible with what
+// the flatmap package would generate for an attribute addressable by the given
+// traversal.
+//
+// This is used only to shim references to attributes within the diff and
+// state structures, which have not (at the time of writing) yet been updated
+// to use the newer HCL-based representations.
+func legacyFlatmapKeyForTraversal(traversal hcl.Traversal) string {
+       var buf bytes.Buffer
+       first := true
+       for _, step := range traversal {
+               if !first {
+                       buf.WriteByte('.')
+               }
+               switch ts := step.(type) {
+               case hcl.TraverseRoot:
+                       buf.WriteString(ts.Name)
+               case hcl.TraverseAttr:
+                       buf.WriteString(ts.Name)
+               case hcl.TraverseIndex:
+                       val := ts.Key
+                       switch val.Type() {
+                       case cty.Number:
+                               bf := val.AsBigFloat()
+                               buf.WriteString(bf.String())
+                       case cty.String:
+                               s := val.AsString()
+                               buf.WriteString(s)
+                       default:
+                               // should never happen, since no other types appear in
+                               // traversals in practice.
+                               buf.WriteByte('?')
+                       }
+               default:
+                       // should never happen, since we've covered all of the types
+                       // that show up in parsed traversals in practice.
+                       buf.WriteByte('?')
+               }
+               first = false
+       }
+       return buf.String()
+}
+
 // a group of key-*ResourceAttrDiff pairs from the same flatmapped container
 type flatAttrDiff map[string]*ResourceAttrDiff
 
@@ -343,159 +739,213 @@ func groupContainers(d *InstanceDiff) map[string]flatAttrDiff {
 // EvalDiffDestroy is an EvalNode implementation that returns a plain
 // destroy diff.
 type EvalDiffDestroy struct {
-       Info   *InstanceInfo
-       State  **InstanceState
-       Output **InstanceDiff
+       Addr         addrs.ResourceInstance
+       DeposedKey   states.DeposedKey
+       State        **states.ResourceInstanceObject
+       ProviderAddr addrs.AbsProviderConfig
+
+       Output      **plans.ResourceInstanceChange
+       OutputState **states.ResourceInstanceObject
 }
 
 // TODO: test
 func (n *EvalDiffDestroy) Eval(ctx EvalContext) (interface{}, error) {
+       absAddr := n.Addr.Absolute(ctx.Path())
        state := *n.State
 
-       // If there is no state or we don't have an ID, we're already destroyed
-       if state == nil || state.ID == "" {
+       if n.ProviderAddr.ProviderConfig.Type == "" {
+               if n.DeposedKey == "" {
+                       panic(fmt.Sprintf("EvalDiffDestroy for %s does not have ProviderAddr set", absAddr))
+               } else {
+                       panic(fmt.Sprintf("EvalDiffDestroy for %s (deposed %s) does not have ProviderAddr set", absAddr, n.DeposedKey))
+               }
+       }
+
+       // If there is no state or our attributes object is null then we're already
+       // destroyed.
+       if state == nil || state.Value.IsNull() {
                return nil, nil
        }
 
        // Call pre-diff hook
        err := ctx.Hook(func(h Hook) (HookAction, error) {
-               return h.PreDiff(n.Info, state)
+               return h.PreDiff(
+                       absAddr, n.DeposedKey.Generation(),
+                       state.Value,
+                       cty.NullVal(cty.DynamicPseudoType),
+               )
        })
        if err != nil {
                return nil, err
        }
 
-       // The diff
-       diff := &InstanceDiff{Destroy: true}
+       // Change is always the same for a destroy. We don't need the provider's
+       // help for this one.
+       // TODO: Should we give the provider an opportunity to veto this?
+       change := &plans.ResourceInstanceChange{
+               Addr:       absAddr,
+               DeposedKey: n.DeposedKey,
+               Change: plans.Change{
+                       Action: plans.Delete,
+                       Before: state.Value,
+                       After:  cty.NullVal(cty.DynamicPseudoType),
+               },
+               ProviderAddr: n.ProviderAddr,
+       }
 
        // Call post-diff hook
        err = ctx.Hook(func(h Hook) (HookAction, error) {
-               return h.PostDiff(n.Info, diff)
+               return h.PostDiff(
+                       absAddr,
+                       n.DeposedKey.Generation(),
+                       change.Action,
+                       change.Before,
+                       change.After,
+               )
        })
        if err != nil {
                return nil, err
        }
 
        // Update our output
-       *n.Output = diff
-
-       return nil, nil
-}
-
-// EvalDiffDestroyModule is an EvalNode implementation that writes the diff to
-// the full diff.
-type EvalDiffDestroyModule struct {
-       Path []string
-}
-
-// TODO: test
-func (n *EvalDiffDestroyModule) Eval(ctx EvalContext) (interface{}, error) {
-       diff, lock := ctx.Diff()
-
-       // Acquire the lock so that we can do this safely concurrently
-       lock.Lock()
-       defer lock.Unlock()
+       *n.Output = change
 
-       // Write the diff
-       modDiff := diff.ModuleByPath(n.Path)
-       if modDiff == nil {
-               modDiff = diff.AddModule(n.Path)
+       if n.OutputState != nil {
+               // Record our proposed new state, which is nil because we're destroying.
+               *n.OutputState = nil
        }
-       modDiff.Destroy = true
 
        return nil, nil
 }
 
-// EvalFilterDiff is an EvalNode implementation that filters the diff
-// according to some filter.
-type EvalFilterDiff struct {
-       // Input and output
-       Diff   **InstanceDiff
-       Output **InstanceDiff
-
-       // Destroy, if true, will only include a destroy diff if it is set.
-       Destroy bool
+// EvalReduceDiff is an EvalNode implementation that takes a planned resource
+// instance change as might be produced by EvalDiff or EvalDiffDestroy and
+// "simplifies" it to a single atomic action to be performed by a specific
+// graph node.
+//
+// Callers must specify whether they are a destroy node or a regular apply
+// node.  If the result is NoOp then the given change requires no action for
+// the specific graph node calling this and so evaluation of the that graph
+// node should exit early and take no action.
+//
+// The object written to OutChange may either be identical to InChange or
+// a new change object derived from InChange. Because of the former case, the
+// caller must not mutate the object returned in OutChange.
+type EvalReduceDiff struct {
+       Addr      addrs.ResourceInstance
+       InChange  **plans.ResourceInstanceChange
+       Destroy   bool
+       OutChange **plans.ResourceInstanceChange
 }
 
-func (n *EvalFilterDiff) Eval(ctx EvalContext) (interface{}, error) {
-       if *n.Diff == nil {
-               return nil, nil
-       }
-
-       input := *n.Diff
-       result := new(InstanceDiff)
-
-       if n.Destroy {
-               if input.GetDestroy() || input.RequiresNew() {
-                       result.SetDestroy(true)
+// TODO: test
+func (n *EvalReduceDiff) Eval(ctx EvalContext) (interface{}, error) {
+       in := *n.InChange
+       out := in.Simplify(n.Destroy)
+       if n.OutChange != nil {
+               *n.OutChange = out
+       }
+       if out.Action != in.Action {
+               if n.Destroy {
+                       log.Printf("[TRACE] EvalReduceDiff: %s change simplified from %s to %s for destroy node", n.Addr, in.Action, out.Action)
+               } else {
+                       log.Printf("[TRACE] EvalReduceDiff: %s change simplified from %s to %s for apply node", n.Addr, in.Action, out.Action)
                }
        }
-
-       if n.Output != nil {
-               *n.Output = result
-       }
-
        return nil, nil
 }
 
-// EvalReadDiff is an EvalNode implementation that writes the diff to
-// the full diff.
+// EvalReadDiff is an EvalNode implementation that retrieves the planned
+// change for a particular resource instance object.
 type EvalReadDiff struct {
-       Name string
-       Diff **InstanceDiff
+       Addr           addrs.ResourceInstance
+       DeposedKey     states.DeposedKey
+       ProviderSchema **ProviderSchema
+       Change         **plans.ResourceInstanceChange
 }
 
 func (n *EvalReadDiff) Eval(ctx EvalContext) (interface{}, error) {
-       diff, lock := ctx.Diff()
+       providerSchema := *n.ProviderSchema
+       changes := ctx.Changes()
+       addr := n.Addr.Absolute(ctx.Path())
 
-       // Acquire the lock so that we can do this safely concurrently
-       lock.Lock()
-       defer lock.Unlock()
+       schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource())
+       if schema == nil {
+               // Should be caught during validation, so we don't bother with a pretty error here
+               return nil, fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Type)
+       }
 
-       // Write the diff
-       modDiff := diff.ModuleByPath(ctx.Path())
-       if modDiff == nil {
+       gen := states.CurrentGen
+       if n.DeposedKey != states.NotDeposed {
+               gen = n.DeposedKey
+       }
+       csrc := changes.GetResourceInstanceChange(addr, gen)
+       if csrc == nil {
+               log.Printf("[TRACE] EvalReadDiff: No planned change recorded for %s", addr)
                return nil, nil
        }
 
-       *n.Diff = modDiff.Resources[n.Name]
+       change, err := csrc.Decode(schema.ImpliedType())
+       if err != nil {
+               return nil, fmt.Errorf("failed to decode planned changes for %s: %s", addr, err)
+       }
+       if n.Change != nil {
+               *n.Change = change
+       }
+
+       log.Printf("[TRACE] EvalReadDiff: Read %s change from plan for %s", change.Action, addr)
 
        return nil, nil
 }
 
-// EvalWriteDiff is an EvalNode implementation that writes the diff to
-// the full diff.
+// EvalWriteDiff is an EvalNode implementation that saves a planned change
+// for an instance object into the set of global planned changes.
 type EvalWriteDiff struct {
-       Name string
-       Diff **InstanceDiff
+       Addr           addrs.ResourceInstance
+       DeposedKey     states.DeposedKey
+       ProviderSchema **ProviderSchema
+       Change         **plans.ResourceInstanceChange
 }
 
 // TODO: test
 func (n *EvalWriteDiff) Eval(ctx EvalContext) (interface{}, error) {
-       diff, lock := ctx.Diff()
-
-       // The diff to write, if its empty it should write nil
-       var diffVal *InstanceDiff
-       if n.Diff != nil {
-               diffVal = *n.Diff
+       changes := ctx.Changes()
+       addr := n.Addr.Absolute(ctx.Path())
+       if n.Change == nil || *n.Change == nil {
+               // Caller sets nil to indicate that we need to remove a change from
+               // the set of changes.
+               gen := states.CurrentGen
+               if n.DeposedKey != states.NotDeposed {
+                       gen = n.DeposedKey
+               }
+               changes.RemoveResourceInstanceChange(addr, gen)
+               return nil, nil
        }
-       if diffVal.Empty() {
-               diffVal = nil
+
+       providerSchema := *n.ProviderSchema
+       change := *n.Change
+
+       if change.Addr.String() != addr.String() || change.DeposedKey != n.DeposedKey {
+               // Should never happen, and indicates a bug in the caller.
+               panic("inconsistent address and/or deposed key in EvalWriteDiff")
        }
 
-       // Acquire the lock so that we can do this safely concurrently
-       lock.Lock()
-       defer lock.Unlock()
+       schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource())
+       if schema == nil {
+               // Should be caught during validation, so we don't bother with a pretty error here
+               return nil, fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Type)
+       }
 
-       // Write the diff
-       modDiff := diff.ModuleByPath(ctx.Path())
-       if modDiff == nil {
-               modDiff = diff.AddModule(ctx.Path())
+       csrc, err := change.Encode(schema.ImpliedType())
+       if err != nil {
+               return nil, fmt.Errorf("failed to encode planned changes for %s: %s", addr, err)
        }
-       if diffVal != nil {
-               modDiff.Resources[n.Name] = diffVal
+
+       changes.AppendResourceInstanceChange(csrc)
+       if n.DeposedKey == states.NotDeposed {
+               log.Printf("[TRACE] EvalWriteDiff: recorded %s change for %s", change.Action, addr)
        } else {
-               delete(modDiff.Resources, n.Name)
+               log.Printf("[TRACE] EvalWriteDiff: recorded %s change for %s deposed object %s", change.Action, addr, n.DeposedKey)
        }
 
        return nil, nil