]> git.immae.eu Git - github/fretlink/terraform-provider-statuscake.git/blame - vendor/github.com/hashicorp/terraform/terraform/eval_diff.go
Upgrade to 0.12
[github/fretlink/terraform-provider-statuscake.git] / vendor / github.com / hashicorp / terraform / terraform / eval_diff.go
CommitLineData
bae9f6d2
JC
1package terraform
2
3import (
107c1cdb 4 "bytes"
bae9f6d2
JC
5 "fmt"
6 "log"
107c1cdb 7 "reflect"
bae9f6d2
JC
8 "strings"
9
107c1cdb
ND
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"
bae9f6d2
JC
20)
21
107c1cdb
ND
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
bae9f6d2
JC
39}
40
107c1cdb
ND
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 }
bae9f6d2
JC
75 }
76
107c1cdb
ND
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()
bae9f6d2
JC
89}
90
107c1cdb
ND
91// EvalDiff is an EvalNode implementation that detects changes for a given
92// resource instance.
bae9f6d2 93type EvalDiff struct {
107c1cdb
ND
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
c680a8e1 112 Stub bool
bae9f6d2
JC
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
107c1cdb
ND
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)
bae9f6d2
JC
166
167 // Call pre-diff hook
c680a8e1
RS
168 if !n.Stub {
169 err := ctx.Hook(func(h Hook) (HookAction, error) {
107c1cdb 170 return h.PreDiff(absAddr, states.CurrentGen, priorVal, proposedNewVal)
c680a8e1
RS
171 })
172 if err != nil {
173 return nil, err
174 }
bae9f6d2
JC
175 }
176
107c1cdb
ND
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 }
bae9f6d2 245 }
bae9f6d2 246
107c1cdb
ND
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 }
bae9f6d2
JC
254 }
255
107c1cdb
ND
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 }
bae9f6d2 270
107c1cdb
ND
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 }
bae9f6d2 286
107c1cdb
ND
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 }
bae9f6d2 302
107c1cdb
ND
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 }
bae9f6d2
JC
311 }
312
107c1cdb
ND
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
bae9f6d2 329 }
107c1cdb
ND
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,
bae9f6d2 359 })
107c1cdb
ND
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 }
bae9f6d2
JC
384 }
385
107c1cdb
ND
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 }
bae9f6d2
JC
411 }
412
413 // Call post-refresh hook
c680a8e1 414 if !n.Stub {
107c1cdb
ND
415 err := ctx.Hook(func(h Hook) (HookAction, error) {
416 return h.PostDiff(absAddr, states.CurrentGen, action, priorVal, plannedNewVal)
c680a8e1
RS
417 })
418 if err != nil {
419 return nil, err
420 }
bae9f6d2
JC
421 }
422
c680a8e1 423 // Update our output if we care
107c1cdb
ND
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
c680a8e1 440 }
bae9f6d2
JC
441
442 // Update the state if we care
443 if n.OutputState != nil {
107c1cdb
ND
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,
bae9f6d2
JC
453 }
454 }
455
456 return nil, nil
457}
458
107c1cdb
ND
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 {
bae9f6d2
JC
547 return nil
548 }
107c1cdb
ND
549 ignoreChanges := n.Config.Managed.IgnoreChanges
550 ignoreAll := n.Config.Managed.IgnoreAllChanges
bae9f6d2 551
107c1cdb 552 if len(ignoreChanges) == 0 && !ignoreAll {
bae9f6d2
JC
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)
107c1cdb
ND
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+".") {
bae9f6d2
JC
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 {
15c0b25d 613 if v.keepDiff(ignorableAttrKeys) {
bae9f6d2 614 // At least one key has changes, so list all the sibling keys
15c0b25d 615 // to keep in the diff
bae9f6d2
JC
616 for k := range v {
617 keep[k] = true
15c0b25d
AP
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)
bae9f6d2
JC
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 {
107c1cdb 642 log.Printf("[DEBUG] [EvalIgnoreChanges] %s: Ignoring diff attribute: %s", n.Addr.String(), k)
bae9f6d2
JC
643 diff.DelAttribute(k)
644 }
645
646 return nil
647}
648
107c1cdb
ND
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
bae9f6d2
JC
692// a group of key-*ResourceAttrDiff pairs from the same flatmapped container
693type flatAttrDiff map[string]*ResourceAttrDiff
694
15c0b25d
AP
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 {
bae9f6d2
JC
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 {
107c1cdb
ND
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
bae9f6d2
JC
749}
750
751// TODO: test
752func (n *EvalDiffDestroy) Eval(ctx EvalContext) (interface{}, error) {
107c1cdb 753 absAddr := n.Addr.Absolute(ctx.Path())
bae9f6d2
JC
754 state := *n.State
755
107c1cdb
ND
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() {
bae9f6d2
JC
767 return nil, nil
768 }
769
770 // Call pre-diff hook
771 err := ctx.Hook(func(h Hook) (HookAction, error) {
107c1cdb
ND
772 return h.PreDiff(
773 absAddr, n.DeposedKey.Generation(),
774 state.Value,
775 cty.NullVal(cty.DynamicPseudoType),
776 )
bae9f6d2
JC
777 })
778 if err != nil {
779 return nil, err
780 }
781
107c1cdb
ND
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 }
bae9f6d2
JC
795
796 // Call post-diff hook
797 err = ctx.Hook(func(h Hook) (HookAction, error) {
107c1cdb
ND
798 return h.PostDiff(
799 absAddr,
800 n.DeposedKey.Generation(),
801 change.Action,
802 change.Before,
803 change.After,
804 )
bae9f6d2
JC
805 })
806 if err != nil {
807 return nil, err
808 }
809
810 // Update our output
107c1cdb 811 *n.Output = change
bae9f6d2 812
107c1cdb
ND
813 if n.OutputState != nil {
814 // Record our proposed new state, which is nil because we're destroying.
815 *n.OutputState = nil
bae9f6d2 816 }
bae9f6d2
JC
817
818 return nil, nil
819}
820
107c1cdb
ND
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
bae9f6d2
JC
839}
840
107c1cdb
ND
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)
bae9f6d2
JC
853 }
854 }
bae9f6d2
JC
855 return nil, nil
856}
857
107c1cdb
ND
858// EvalReadDiff is an EvalNode implementation that retrieves the planned
859// change for a particular resource instance object.
bae9f6d2 860type EvalReadDiff struct {
107c1cdb
ND
861 Addr addrs.ResourceInstance
862 DeposedKey states.DeposedKey
863 ProviderSchema **ProviderSchema
864 Change **plans.ResourceInstanceChange
bae9f6d2
JC
865}
866
867func (n *EvalReadDiff) Eval(ctx EvalContext) (interface{}, error) {
107c1cdb
ND
868 providerSchema := *n.ProviderSchema
869 changes := ctx.Changes()
870 addr := n.Addr.Absolute(ctx.Path())
bae9f6d2 871
107c1cdb
ND
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 }
bae9f6d2 877
107c1cdb
ND
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)
bae9f6d2
JC
885 return nil, nil
886 }
887
107c1cdb
ND
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)
bae9f6d2
JC
897
898 return nil, nil
899}
900
107c1cdb
ND
901// EvalWriteDiff is an EvalNode implementation that saves a planned change
902// for an instance object into the set of global planned changes.
bae9f6d2 903type EvalWriteDiff struct {
107c1cdb
ND
904 Addr addrs.ResourceInstance
905 DeposedKey states.DeposedKey
906 ProviderSchema **ProviderSchema
907 Change **plans.ResourceInstanceChange
bae9f6d2
JC
908}
909
910// TODO: test
911func (n *EvalWriteDiff) Eval(ctx EvalContext) (interface{}, error) {
107c1cdb
ND
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
bae9f6d2 923 }
107c1cdb
ND
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")
bae9f6d2
JC
931 }
932
107c1cdb
ND
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 }
bae9f6d2 938
107c1cdb
ND
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)
bae9f6d2 942 }
107c1cdb
ND
943
944 changes.AppendResourceInstanceChange(csrc)
945 if n.DeposedKey == states.NotDeposed {
946 log.Printf("[TRACE] EvalWriteDiff: recorded %s change for %s", change.Action, addr)
bae9f6d2 947 } else {
107c1cdb 948 log.Printf("[TRACE] EvalWriteDiff: recorded %s change for %s deposed object %s", change.Action, addr, n.DeposedKey)
bae9f6d2
JC
949 }
950
951 return nil, nil
952}