github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/terraform/eval_diff.go (about)

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