]>
Commit | Line | Data |
---|---|---|
bae9f6d2 JC |
1 | package terraform |
2 | ||
3 | import ( | |
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. | |
30 | type 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 |
41 | func (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 | 93 | type 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 | |
116 | func (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 |
459 | func (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 | ||
484 | func 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 | ||
545 | func (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. | |
656 | func 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 |
693 | type 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 |
696 | func (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. | |
714 | func 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. | |
741 | type 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 | |
752 | func (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. | |
834 | type 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 |
842 | func (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 | 860 | type EvalReadDiff struct { |
107c1cdb ND |
861 | Addr addrs.ResourceInstance |
862 | DeposedKey states.DeposedKey | |
863 | ProviderSchema **ProviderSchema | |
864 | Change **plans.ResourceInstanceChange | |
bae9f6d2 JC |
865 | } |
866 | ||
867 | func (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 | 903 | type 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 | |
911 | func (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 | } |