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