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