1package terraform
2
3import (
4	"fmt"
5	"log"
6	"strings"
7
8	"github.com/hashicorp/hcl2/hcl"
9	"github.com/zclconf/go-cty/cty"
10
11	"github.com/hashicorp/terraform-plugin-sdk/internal/addrs"
12	"github.com/hashicorp/terraform-plugin-sdk/internal/configs"
13	"github.com/hashicorp/terraform-plugin-sdk/internal/plans"
14	"github.com/hashicorp/terraform-plugin-sdk/internal/plans/objchange"
15	"github.com/hashicorp/terraform-plugin-sdk/internal/providers"
16	"github.com/hashicorp/terraform-plugin-sdk/internal/states"
17	"github.com/hashicorp/terraform-plugin-sdk/internal/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.
28type 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
39func (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.
91type 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
114func (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 == "" {
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
483func (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
508func 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// EvalDiffDestroy is an EvalNode implementation that returns a plain
570// destroy diff.
571type EvalDiffDestroy struct {
572	Addr         addrs.ResourceInstance
573	DeposedKey   states.DeposedKey
574	State        **states.ResourceInstanceObject
575	ProviderAddr addrs.AbsProviderConfig
576
577	Output      **plans.ResourceInstanceChange
578	OutputState **states.ResourceInstanceObject
579}
580
581// TODO: test
582func (n *EvalDiffDestroy) Eval(ctx EvalContext) (interface{}, error) {
583	absAddr := n.Addr.Absolute(ctx.Path())
584	state := *n.State
585
586	if n.ProviderAddr.ProviderConfig.Type == "" {
587		if n.DeposedKey == "" {
588			panic(fmt.Sprintf("EvalDiffDestroy for %s does not have ProviderAddr set", absAddr))
589		} else {
590			panic(fmt.Sprintf("EvalDiffDestroy for %s (deposed %s) does not have ProviderAddr set", absAddr, n.DeposedKey))
591		}
592	}
593
594	// If there is no state or our attributes object is null then we're already
595	// destroyed.
596	if state == nil || state.Value.IsNull() {
597		return nil, nil
598	}
599
600	// Call pre-diff hook
601	err := ctx.Hook(func(h Hook) (HookAction, error) {
602		return h.PreDiff(
603			absAddr, n.DeposedKey.Generation(),
604			state.Value,
605			cty.NullVal(cty.DynamicPseudoType),
606		)
607	})
608	if err != nil {
609		return nil, err
610	}
611
612	// Change is always the same for a destroy. We don't need the provider's
613	// help for this one.
614	// TODO: Should we give the provider an opportunity to veto this?
615	change := &plans.ResourceInstanceChange{
616		Addr:       absAddr,
617		DeposedKey: n.DeposedKey,
618		Change: plans.Change{
619			Action: plans.Delete,
620			Before: state.Value,
621			After:  cty.NullVal(cty.DynamicPseudoType),
622		},
623		Private:      state.Private,
624		ProviderAddr: n.ProviderAddr,
625	}
626
627	// Call post-diff hook
628	err = ctx.Hook(func(h Hook) (HookAction, error) {
629		return h.PostDiff(
630			absAddr,
631			n.DeposedKey.Generation(),
632			change.Action,
633			change.Before,
634			change.After,
635		)
636	})
637	if err != nil {
638		return nil, err
639	}
640
641	// Update our output
642	*n.Output = change
643
644	if n.OutputState != nil {
645		// Record our proposed new state, which is nil because we're destroying.
646		*n.OutputState = nil
647	}
648
649	return nil, nil
650}
651
652// EvalReduceDiff is an EvalNode implementation that takes a planned resource
653// instance change as might be produced by EvalDiff or EvalDiffDestroy and
654// "simplifies" it to a single atomic action to be performed by a specific
655// graph node.
656//
657// Callers must specify whether they are a destroy node or a regular apply
658// node.  If the result is NoOp then the given change requires no action for
659// the specific graph node calling this and so evaluation of the that graph
660// node should exit early and take no action.
661//
662// The object written to OutChange may either be identical to InChange or
663// a new change object derived from InChange. Because of the former case, the
664// caller must not mutate the object returned in OutChange.
665type EvalReduceDiff struct {
666	Addr      addrs.ResourceInstance
667	InChange  **plans.ResourceInstanceChange
668	Destroy   bool
669	OutChange **plans.ResourceInstanceChange
670}
671
672// TODO: test
673func (n *EvalReduceDiff) Eval(ctx EvalContext) (interface{}, error) {
674	in := *n.InChange
675	out := in.Simplify(n.Destroy)
676	if n.OutChange != nil {
677		*n.OutChange = out
678	}
679	if out.Action != in.Action {
680		if n.Destroy {
681			log.Printf("[TRACE] EvalReduceDiff: %s change simplified from %s to %s for destroy node", n.Addr, in.Action, out.Action)
682		} else {
683			log.Printf("[TRACE] EvalReduceDiff: %s change simplified from %s to %s for apply node", n.Addr, in.Action, out.Action)
684		}
685	}
686	return nil, nil
687}
688
689// EvalReadDiff is an EvalNode implementation that retrieves the planned
690// change for a particular resource instance object.
691type EvalReadDiff struct {
692	Addr           addrs.ResourceInstance
693	DeposedKey     states.DeposedKey
694	ProviderSchema **ProviderSchema
695	Change         **plans.ResourceInstanceChange
696}
697
698func (n *EvalReadDiff) Eval(ctx EvalContext) (interface{}, error) {
699	providerSchema := *n.ProviderSchema
700	changes := ctx.Changes()
701	addr := n.Addr.Absolute(ctx.Path())
702
703	schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource())
704	if schema == nil {
705		// Should be caught during validation, so we don't bother with a pretty error here
706		return nil, fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Type)
707	}
708
709	gen := states.CurrentGen
710	if n.DeposedKey != states.NotDeposed {
711		gen = n.DeposedKey
712	}
713	csrc := changes.GetResourceInstanceChange(addr, gen)
714	if csrc == nil {
715		log.Printf("[TRACE] EvalReadDiff: No planned change recorded for %s", addr)
716		return nil, nil
717	}
718
719	change, err := csrc.Decode(schema.ImpliedType())
720	if err != nil {
721		return nil, fmt.Errorf("failed to decode planned changes for %s: %s", addr, err)
722	}
723	if n.Change != nil {
724		*n.Change = change
725	}
726
727	log.Printf("[TRACE] EvalReadDiff: Read %s change from plan for %s", change.Action, addr)
728
729	return nil, nil
730}
731
732// EvalWriteDiff is an EvalNode implementation that saves a planned change
733// for an instance object into the set of global planned changes.
734type EvalWriteDiff struct {
735	Addr           addrs.ResourceInstance
736	DeposedKey     states.DeposedKey
737	ProviderSchema **ProviderSchema
738	Change         **plans.ResourceInstanceChange
739}
740
741// TODO: test
742func (n *EvalWriteDiff) Eval(ctx EvalContext) (interface{}, error) {
743	changes := ctx.Changes()
744	addr := n.Addr.Absolute(ctx.Path())
745	if n.Change == nil || *n.Change == nil {
746		// Caller sets nil to indicate that we need to remove a change from
747		// the set of changes.
748		gen := states.CurrentGen
749		if n.DeposedKey != states.NotDeposed {
750			gen = n.DeposedKey
751		}
752		changes.RemoveResourceInstanceChange(addr, gen)
753		return nil, nil
754	}
755
756	providerSchema := *n.ProviderSchema
757	change := *n.Change
758
759	if change.Addr.String() != addr.String() || change.DeposedKey != n.DeposedKey {
760		// Should never happen, and indicates a bug in the caller.
761		panic("inconsistent address and/or deposed key in EvalWriteDiff")
762	}
763
764	schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource())
765	if schema == nil {
766		// Should be caught during validation, so we don't bother with a pretty error here
767		return nil, fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Type)
768	}
769
770	csrc, err := change.Encode(schema.ImpliedType())
771	if err != nil {
772		return nil, fmt.Errorf("failed to encode planned changes for %s: %s", addr, err)
773	}
774
775	changes.AppendResourceInstanceChange(csrc)
776	if n.DeposedKey == states.NotDeposed {
777		log.Printf("[TRACE] EvalWriteDiff: recorded %s change for %s", change.Action, addr)
778	} else {
779		log.Printf("[TRACE] EvalWriteDiff: recorded %s change for %s deposed object %s", change.Action, addr, n.DeposedKey)
780	}
781
782	return nil, nil
783}
784