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
}
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
}
// 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
}
// 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
}
}
// 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
// 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