]>
Commit | Line | Data |
---|---|---|
bae9f6d2 JC |
1 | package terraform |
2 | ||
3 | import ( | |
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. | |
29 | type 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 |
40 | func (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 | 92 | type 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 | |
115 | func (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 |
474 | func (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 | ||
499 | func 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 | ||
560 | func (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. | |
671 | func 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 |
708 | type 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 |
711 | func (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. | |
729 | func 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. | |
756 | type 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 | |
767 | func (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. | |
850 | type 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 |
858 | func (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 | 876 | type EvalReadDiff struct { |
107c1cdb ND |
877 | Addr addrs.ResourceInstance |
878 | DeposedKey states.DeposedKey | |
879 | ProviderSchema **ProviderSchema | |
880 | Change **plans.ResourceInstanceChange | |
bae9f6d2 JC |
881 | } |
882 | ||
883 | func (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 | 919 | type 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 | |
927 | func (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 | } |