aboutsummaryrefslogtreecommitdiffhomepage
path: root/vendor/github.com/hashicorp/terraform/terraform/eval_diff.go
diff options
context:
space:
mode:
authorNathan Dench <ndenc2@gmail.com>2019-05-24 15:16:44 +1000
committerNathan Dench <ndenc2@gmail.com>2019-05-24 15:16:44 +1000
commit107c1cdb09c575aa2f61d97f48d8587eb6bada4c (patch)
treeca7d008643efc555c388baeaf1d986e0b6b3e28c /vendor/github.com/hashicorp/terraform/terraform/eval_diff.go
parent844b5a68d8af4791755b8f0ad293cc99f5959183 (diff)
downloadterraform-provider-statuscake-107c1cdb09c575aa2f61d97f48d8587eb6bada4c.tar.gz
terraform-provider-statuscake-107c1cdb09c575aa2f61d97f48d8587eb6bada4c.tar.zst
terraform-provider-statuscake-107c1cdb09c575aa2f61d97f48d8587eb6bada4c.zip
Upgrade to 0.12
Diffstat (limited to 'vendor/github.com/hashicorp/terraform/terraform/eval_diff.go')
-rw-r--r--vendor/github.com/hashicorp/terraform/terraform/eval_diff.go906
1 files changed, 678 insertions, 228 deletions
diff --git a/vendor/github.com/hashicorp/terraform/terraform/eval_diff.go b/vendor/github.com/hashicorp/terraform/terraform/eval_diff.go
index 26205ce..b7acfb0 100644
--- a/vendor/github.com/hashicorp/terraform/terraform/eval_diff.go
+++ b/vendor/github.com/hashicorp/terraform/terraform/eval_diff.go
@@ -1,92 +1,114 @@
1package terraform 1package terraform
2 2
3import ( 3import (
4 "bytes"
4 "fmt" 5 "fmt"
5 "log" 6 "log"
7 "reflect"
6 "strings" 8 "strings"
7 9
8 "github.com/hashicorp/terraform/config" 10 "github.com/hashicorp/hcl2/hcl"
9 "github.com/hashicorp/terraform/version" 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"
10) 20)
11 21
12// EvalCompareDiff is an EvalNode implementation that compares two diffs 22// EvalCheckPlannedChange is an EvalNode implementation that produces errors
13// and errors if the diffs are not equal. 23// if the _actual_ expected value is not compatible with what was recorded
14type EvalCompareDiff struct { 24// in the plan.
15 Info *InstanceInfo 25//
16 One, Two **InstanceDiff 26// Errors here are most often indicative of a bug in the provider, so our
27// error messages will report with that in mind. It's also possible that
28// there's a bug in Terraform's Core's own "proposed new value" code in
29// EvalDiff.
30type EvalCheckPlannedChange struct {
31 Addr addrs.ResourceInstance
32 ProviderAddr addrs.AbsProviderConfig
33 ProviderSchema **ProviderSchema
34
35 // We take ResourceInstanceChange objects here just because that's what's
36 // convenient to pass in from the evaltree implementation, but we really
37 // only look at the "After" value of each change.
38 Planned, Actual **plans.ResourceInstanceChange
17} 39}
18 40
19// TODO: test 41func (n *EvalCheckPlannedChange) Eval(ctx EvalContext) (interface{}, error) {
20func (n *EvalCompareDiff) Eval(ctx EvalContext) (interface{}, error) { 42 providerSchema := *n.ProviderSchema
21 one, two := *n.One, *n.Two 43 plannedChange := *n.Planned
22 44 actualChange := *n.Actual
23 // If either are nil, let them be empty 45
24 if one == nil { 46 schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource())
25 one = new(InstanceDiff) 47 if schema == nil {
26 one.init() 48 // Should be caught during validation, so we don't bother with a pretty error here
27 } 49 return nil, fmt.Errorf("provider does not support %q", n.Addr.Resource.Type)
28 if two == nil { 50 }
29 two = new(InstanceDiff) 51
30 two.init() 52 var diags tfdiags.Diagnostics
31 } 53 absAddr := n.Addr.Absolute(ctx.Path())
32 oneId, _ := one.GetAttribute("id") 54
33 twoId, _ := two.GetAttribute("id") 55 log.Printf("[TRACE] EvalCheckPlannedChange: Verifying that actual change (action %s) matches planned change (action %s)", actualChange.Action, plannedChange.Action)
34 one.DelAttribute("id") 56
35 two.DelAttribute("id") 57 if plannedChange.Action != actualChange.Action {
36 defer func() { 58 switch {
37 if oneId != nil { 59 case plannedChange.Action == plans.Update && actualChange.Action == plans.NoOp:
38 one.SetAttribute("id", oneId) 60 // It's okay for an update to become a NoOp once we've filled in
39 } 61 // all of the unknown values, since the final values might actually
40 if twoId != nil { 62 // match what was there before after all.
41 two.SetAttribute("id", twoId) 63 log.Printf("[DEBUG] After incorporating new values learned so far during apply, %s change has become NoOp", absAddr)
42 } 64 default:
43 }() 65 diags = diags.Append(tfdiags.Sourceless(
44 66 tfdiags.Error,
45 if same, reason := one.Same(two); !same { 67 "Provider produced inconsistent final plan",
46 log.Printf("[ERROR] %s: diffs didn't match", n.Info.Id) 68 fmt.Sprintf(
47 log.Printf("[ERROR] %s: reason: %s", n.Info.Id, reason) 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.",
48 log.Printf("[ERROR] %s: diff one: %#v", n.Info.Id, one) 70 absAddr, n.ProviderAddr.ProviderConfig.Type,
49 log.Printf("[ERROR] %s: diff two: %#v", n.Info.Id, two) 71 plannedChange.Action, actualChange.Action,
50 return nil, fmt.Errorf( 72 ),
51 "%s: diffs didn't match during apply. This is a bug with "+ 73 ))
52 "Terraform and should be reported as a GitHub Issue.\n"+ 74 }
53 "\n"+
54 "Please include the following information in your report:\n"+
55 "\n"+
56 " Terraform Version: %s\n"+
57 " Resource ID: %s\n"+
58 " Mismatch reason: %s\n"+
59 " Diff One (usually from plan): %#v\n"+
60 " Diff Two (usually from apply): %#v\n"+
61 "\n"+
62 "Also include as much context as you can about your config, state, "+
63 "and the steps you performed to trigger this error.\n",
64 n.Info.Id, version.Version, n.Info.Id, reason, one, two)
65 } 75 }
66 76
67 return nil, nil 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()
68} 89}
69 90
70// EvalDiff is an EvalNode implementation that does a refresh for 91// EvalDiff is an EvalNode implementation that detects changes for a given
71// a resource. 92// resource instance.
72type EvalDiff struct { 93type EvalDiff struct {
73 Name string 94 Addr addrs.ResourceInstance
74 Info *InstanceInfo 95 Config *configs.Resource
75 Config **ResourceConfig 96 Provider *providers.Interface
76 Provider *ResourceProvider 97 ProviderAddr addrs.AbsProviderConfig
77 Diff **InstanceDiff 98 ProviderSchema **ProviderSchema
78 State **InstanceState 99 State **states.ResourceInstanceObject
79 OutputDiff **InstanceDiff 100 PreviousDiff **plans.ResourceInstanceChange
80 OutputState **InstanceState 101
81 102 // CreateBeforeDestroy is set if either the resource's own config sets
82 // Resource is needed to fetch the ignore_changes list so we can 103 // create_before_destroy explicitly or if dependencies have forced the
83 // filter user-requested ignored attributes from the diff. 104 // resource to be handled as create_before_destroy in order to avoid
84 Resource *config.Resource 105 // a dependency cycle.
85 106 CreateBeforeDestroy bool
86 // Stub is used to flag the generated InstanceDiff as a stub. This is used to 107
87 // ensure that the node exists to perform interpolations and generate 108 OutputChange **plans.ResourceInstanceChange
88 // computed paths off of, but not as an actual diff where resouces should be 109 OutputValue *cty.Value
89 // counted, and not as a diff that should be acted on. 110 OutputState **states.ResourceInstanceObject
111
90 Stub bool 112 Stub bool
91} 113}
92 114
@@ -95,81 +117,303 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
95 state := *n.State 117 state := *n.State
96 config := *n.Config 118 config := *n.Config
97 provider := *n.Provider 119 provider := *n.Provider
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)
98 166
99 // Call pre-diff hook 167 // Call pre-diff hook
100 if !n.Stub { 168 if !n.Stub {
101 err := ctx.Hook(func(h Hook) (HookAction, error) { 169 err := ctx.Hook(func(h Hook) (HookAction, error) {
102 return h.PreDiff(n.Info, state) 170 return h.PreDiff(absAddr, states.CurrentGen, priorVal, proposedNewVal)
103 }) 171 })
104 if err != nil { 172 if err != nil {
105 return nil, err 173 return nil, err
106 } 174 }
107 } 175 }
108 176
109 // The state for the diff must never be nil 177 // The provider gets an opportunity to customize the proposed new value,
110 diffState := state 178 // which in turn produces the _planned_ new value.
111 if diffState == nil { 179 resp := provider.PlanResourceChange(providers.PlanResourceChangeRequest{
112 diffState = new(InstanceState) 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 }
113 } 245 }
114 diffState.init()
115 246
116 // Diff! 247 {
117 diff, err := provider.Diff(n.Info, diffState, config) 248 var moreDiags tfdiags.Diagnostics
118 if err != nil { 249 plannedNewVal, moreDiags = n.processIgnoreChanges(priorVal, plannedNewVal)
119 return nil, err 250 diags = diags.Append(moreDiags)
120 } 251 if moreDiags.HasErrors() {
121 if diff == nil { 252 return nil, diags.Err()
122 diff = new(InstanceDiff) 253 }
123 } 254 }
124 255
125 // Set DestroyDeposed if we have deposed instances 256 // The provider produces a list of paths to attributes whose changes mean
126 _, err = readInstanceFromState(ctx, n.Name, nil, func(rs *ResourceState) (*InstanceState, error) { 257 // that we must replace rather than update an existing remote object.
127 if len(rs.Deposed) > 0 { 258 // However, we only need to do that if the identified attributes _have_
128 diff.DestroyDeposed = true 259 // actually changed -- particularly after we may have undone some of the
129 } 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 }
130 270
131 return nil, nil 271 priorChangedVal, priorPathDiags := hcl.ApplyPath(priorVal, path, nil)
132 }) 272 plannedChangedVal, plannedPathDiags := hcl.ApplyPath(plannedNewVal, path, nil)
133 if err != nil { 273 if plannedPathDiags.HasErrors() && priorPathDiags.HasErrors() {
134 return nil, err 274 // This means the path was invalid in both the prior and new
135 } 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 }
136 286
137 // Preserve the DestroyTainted flag 287 // Make sure we have valid Values for both values.
138 if n.Diff != nil { 288 // Note: if the opposing value was of the type
139 diff.SetTainted((*n.Diff).GetDestroyTainted()) 289 // cty.DynamicPseudoType, the type assigned here may not exactly
140 } 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 }
141 302
142 // Require a destroy if there is an ID and it requires new. 303 eqV := plannedChangedVal.Equals(priorChangedVal)
143 if diff.RequiresNew() && state != nil && state.ID != "" { 304 if !eqV.IsKnown() || eqV.False() {
144 diff.SetDestroy(true) 305 reqRep.Add(path)
306 }
307 }
308 if diags.HasErrors() {
309 return nil, diags.Err()
310 }
145 } 311 }
146 312
147 // If we're creating a new resource, compute its ID 313 eqV := plannedNewVal.Equals(priorVal)
148 if diff.RequiresNew() || state == nil || state.ID == "" { 314 eq := eqV.IsKnown() && eqV.True()
149 var oldID string 315
150 if state != nil { 316 var action plans.Action
151 oldID = state.Attributes["id"] 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
152 } 329 }
153 330 default:
154 // Add diff to compute new ID 331 action = plans.Update
155 diff.init() 332 // "Delete" is never chosen here, because deletion plans are always
156 diff.SetAttribute("id", &ResourceAttrDiff{ 333 // created more directly elsewhere, such as in "orphan" handling.
157 Old: oldID, 334 }
158 NewComputed: true, 335
159 RequiresNew: true, 336 if action.IsReplace() {
160 Type: DiffAttrOutput, 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,
161 }) 359 })
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 }
162 } 384 }
163 385
164 // filter out ignored resources 386 // If our prior value was tainted then we actually want this to appear
165 if err := n.processIgnoreChanges(diff); err != nil { 387 // as a replace change, even though so far we've been treating it as a
166 return nil, err 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 }
167 } 411 }
168 412
169 // Call post-refresh hook 413 // Call post-refresh hook
170 if !n.Stub { 414 if !n.Stub {
171 err = ctx.Hook(func(h Hook) (HookAction, error) { 415 err := ctx.Hook(func(h Hook) (HookAction, error) {
172 return h.PostDiff(n.Info, diff) 416 return h.PostDiff(absAddr, states.CurrentGen, action, priorVal, plannedNewVal)
173 }) 417 })
174 if err != nil { 418 if err != nil {
175 return nil, err 419 return nil, err
@@ -177,30 +421,135 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
177 } 421 }
178 422
179 // Update our output if we care 423 // Update our output if we care
180 if n.OutputDiff != nil { 424 if n.OutputChange != nil {
181 *n.OutputDiff = diff 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
182 } 440 }
183 441
184 // Update the state if we care 442 // Update the state if we care
185 if n.OutputState != nil { 443 if n.OutputState != nil {
186 *n.OutputState = state 444 *n.OutputState = &states.ResourceInstanceObject{
187 445 // We use the special "planned" status here to note that this
188 // Merge our state so that the state is updated with our plan 446 // object's value is not yet complete. Objects with this status
189 if !diff.Empty() && n.OutputState != nil { 447 // cannot be used during expression evaluation, so the caller
190 *n.OutputState = state.MergeDiff(diff) 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,
191 } 453 }
192 } 454 }
193 455
194 return nil, nil 456 return nil, nil
195} 457}
196 458
197func (n *EvalDiff) processIgnoreChanges(diff *InstanceDiff) error { 459func (n *EvalDiff) processIgnoreChanges(prior, proposed cty.Value) (cty.Value, tfdiags.Diagnostics) {
198 if diff == nil || n.Resource == nil || n.Resource.Id() == "" { 460 // ignore_changes only applies when an object already exists, since we
461 // can't ignore changes to a thing we've not created yet.
462 if prior.IsNull() {
463 return proposed, nil
464 }
465
466 ignoreChanges := n.Config.Managed.IgnoreChanges
467 ignoreAll := n.Config.Managed.IgnoreAllChanges
468
469 if len(ignoreChanges) == 0 && !ignoreAll {
470 return proposed, nil
471 }
472 if ignoreAll {
473 return prior, nil
474 }
475 if prior.IsNull() || proposed.IsNull() {
476 // Ignore changes doesn't apply when we're creating for the first time.
477 // Proposed should never be null here, but if it is then we'll just let it be.
478 return proposed, nil
479 }
480
481 return processIgnoreChangesIndividual(prior, proposed, ignoreChanges)
482}
483
484func processIgnoreChangesIndividual(prior, proposed cty.Value, ignoreChanges []hcl.Traversal) (cty.Value, tfdiags.Diagnostics) {
485 // When we walk below we will be using cty.Path values for comparison, so
486 // we'll convert our traversals here so we can compare more easily.
487 ignoreChangesPath := make([]cty.Path, len(ignoreChanges))
488 for i, traversal := range ignoreChanges {
489 path := make(cty.Path, len(traversal))
490 for si, step := range traversal {
491 switch ts := step.(type) {
492 case hcl.TraverseRoot:
493 path[si] = cty.GetAttrStep{
494 Name: ts.Name,
495 }
496 case hcl.TraverseAttr:
497 path[si] = cty.GetAttrStep{
498 Name: ts.Name,
499 }
500 case hcl.TraverseIndex:
501 path[si] = cty.IndexStep{
502 Key: ts.Key,
503 }
504 default:
505 panic(fmt.Sprintf("unsupported traversal step %#v", step))
506 }
507 }
508 ignoreChangesPath[i] = path
509 }
510
511 var diags tfdiags.Diagnostics
512 ret, _ := cty.Transform(proposed, func(path cty.Path, v cty.Value) (cty.Value, error) {
513 // First we must see if this is a path that's being ignored at all.
514 // We're looking for an exact match here because this walk will visit
515 // leaf values first and then their containers, and we want to do
516 // the "ignore" transform once we reach the point indicated, throwing
517 // away any deeper values we already produced at that point.
518 var ignoreTraversal hcl.Traversal
519 for i, candidate := range ignoreChangesPath {
520 if reflect.DeepEqual(path, candidate) {
521 ignoreTraversal = ignoreChanges[i]
522 }
523 }
524 if ignoreTraversal == nil {
525 return v, nil
526 }
527
528 // If we're able to follow the same path through the prior value,
529 // we'll take the value there instead, effectively undoing the
530 // change that was planned.
531 priorV, diags := hcl.ApplyPath(prior, path, nil)
532 if diags.HasErrors() {
533 // We just ignore the errors and move on here, since we assume it's
534 // just because the prior value was a slightly-different shape.
535 // It could potentially also be that the traversal doesn't match
536 // the schema, but we should've caught that during the validate
537 // walk if so.
538 return v, nil
539 }
540 return priorV, nil
541 })
542 return ret, diags
543}
544
545func (n *EvalDiff) processIgnoreChangesOld(diff *InstanceDiff) error {
546 if diff == nil || n.Config == nil || n.Config.Managed == nil {
199 return nil 547 return nil
200 } 548 }
201 ignoreChanges := n.Resource.Lifecycle.IgnoreChanges 549 ignoreChanges := n.Config.Managed.IgnoreChanges
550 ignoreAll := n.Config.Managed.IgnoreAllChanges
202 551
203 if len(ignoreChanges) == 0 { 552 if len(ignoreChanges) == 0 && !ignoreAll {
204 return nil 553 return nil
205 } 554 }
206 555
@@ -220,9 +569,14 @@ func (n *EvalDiff) processIgnoreChanges(diff *InstanceDiff) error {
220 569
221 // get the complete set of keys we want to ignore 570 // get the complete set of keys we want to ignore
222 ignorableAttrKeys := make(map[string]bool) 571 ignorableAttrKeys := make(map[string]bool)
223 for _, ignoredKey := range ignoreChanges { 572 for k := range attrs {
224 for k := range attrs { 573 if ignoreAll {
225 if ignoredKey == "*" || strings.HasPrefix(k, ignoredKey) { 574 ignorableAttrKeys[k] = true
575 continue
576 }
577 for _, ignoredTraversal := range ignoreChanges {
578 ignoredKey := legacyFlatmapKeyForTraversal(ignoredTraversal)
579 if k == ignoredKey || strings.HasPrefix(k, ignoredKey+".") {
226 ignorableAttrKeys[k] = true 580 ignorableAttrKeys[k] = true
227 } 581 }
228 } 582 }
@@ -285,14 +639,56 @@ func (n *EvalDiff) processIgnoreChanges(diff *InstanceDiff) error {
285 639
286 // If we didn't hit any of our early exit conditions, we can filter the diff. 640 // If we didn't hit any of our early exit conditions, we can filter the diff.
287 for k := range ignorableAttrKeys { 641 for k := range ignorableAttrKeys {
288 log.Printf("[DEBUG] [EvalIgnoreChanges] %s - Ignoring diff attribute: %s", 642 log.Printf("[DEBUG] [EvalIgnoreChanges] %s: Ignoring diff attribute: %s", n.Addr.String(), k)
289 n.Resource.Id(), k)
290 diff.DelAttribute(k) 643 diff.DelAttribute(k)
291 } 644 }
292 645
293 return nil 646 return nil
294} 647}
295 648
649// legacyFlagmapKeyForTraversal constructs a key string compatible with what
650// the flatmap package would generate for an attribute addressable by the given
651// traversal.
652//
653// This is used only to shim references to attributes within the diff and
654// state structures, which have not (at the time of writing) yet been updated
655// to use the newer HCL-based representations.
656func legacyFlatmapKeyForTraversal(traversal hcl.Traversal) string {
657 var buf bytes.Buffer
658 first := true
659 for _, step := range traversal {
660 if !first {
661 buf.WriteByte('.')
662 }
663 switch ts := step.(type) {
664 case hcl.TraverseRoot:
665 buf.WriteString(ts.Name)
666 case hcl.TraverseAttr:
667 buf.WriteString(ts.Name)
668 case hcl.TraverseIndex:
669 val := ts.Key
670 switch val.Type() {
671 case cty.Number:
672 bf := val.AsBigFloat()
673 buf.WriteString(bf.String())
674 case cty.String:
675 s := val.AsString()
676 buf.WriteString(s)
677 default:
678 // should never happen, since no other types appear in
679 // traversals in practice.
680 buf.WriteByte('?')
681 }
682 default:
683 // should never happen, since we've covered all of the types
684 // that show up in parsed traversals in practice.
685 buf.WriteByte('?')
686 }
687 first = false
688 }
689 return buf.String()
690}
691
296// a group of key-*ResourceAttrDiff pairs from the same flatmapped container 692// a group of key-*ResourceAttrDiff pairs from the same flatmapped container
297type flatAttrDiff map[string]*ResourceAttrDiff 693type flatAttrDiff map[string]*ResourceAttrDiff
298 694
@@ -343,159 +739,213 @@ func groupContainers(d *InstanceDiff) map[string]flatAttrDiff {
343// EvalDiffDestroy is an EvalNode implementation that returns a plain 739// EvalDiffDestroy is an EvalNode implementation that returns a plain
344// destroy diff. 740// destroy diff.
345type EvalDiffDestroy struct { 741type EvalDiffDestroy struct {
346 Info *InstanceInfo 742 Addr addrs.ResourceInstance
347 State **InstanceState 743 DeposedKey states.DeposedKey
348 Output **InstanceDiff 744 State **states.ResourceInstanceObject
745 ProviderAddr addrs.AbsProviderConfig
746
747 Output **plans.ResourceInstanceChange
748 OutputState **states.ResourceInstanceObject
349} 749}
350 750
351// TODO: test 751// TODO: test
352func (n *EvalDiffDestroy) Eval(ctx EvalContext) (interface{}, error) { 752func (n *EvalDiffDestroy) Eval(ctx EvalContext) (interface{}, error) {
753 absAddr := n.Addr.Absolute(ctx.Path())
353 state := *n.State 754 state := *n.State
354 755
355 // If there is no state or we don't have an ID, we're already destroyed 756 if n.ProviderAddr.ProviderConfig.Type == "" {
356 if state == nil || state.ID == "" { 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() {
357 return nil, nil 767 return nil, nil
358 } 768 }
359 769
360 // Call pre-diff hook 770 // Call pre-diff hook
361 err := ctx.Hook(func(h Hook) (HookAction, error) { 771 err := ctx.Hook(func(h Hook) (HookAction, error) {
362 return h.PreDiff(n.Info, state) 772 return h.PreDiff(
773 absAddr, n.DeposedKey.Generation(),
774 state.Value,
775 cty.NullVal(cty.DynamicPseudoType),
776 )
363 }) 777 })
364 if err != nil { 778 if err != nil {
365 return nil, err 779 return nil, err
366 } 780 }
367 781
368 // The diff 782 // Change is always the same for a destroy. We don't need the provider's
369 diff := &InstanceDiff{Destroy: true} 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 }
370 795
371 // Call post-diff hook 796 // Call post-diff hook
372 err = ctx.Hook(func(h Hook) (HookAction, error) { 797 err = ctx.Hook(func(h Hook) (HookAction, error) {
373 return h.PostDiff(n.Info, diff) 798 return h.PostDiff(
799 absAddr,
800 n.DeposedKey.Generation(),
801 change.Action,
802 change.Before,
803 change.After,
804 )
374 }) 805 })
375 if err != nil { 806 if err != nil {
376 return nil, err 807 return nil, err
377 } 808 }
378 809
379 // Update our output 810 // Update our output
380 *n.Output = diff 811 *n.Output = change
381
382 return nil, nil
383}
384
385// EvalDiffDestroyModule is an EvalNode implementation that writes the diff to
386// the full diff.
387type EvalDiffDestroyModule struct {
388 Path []string
389}
390
391// TODO: test
392func (n *EvalDiffDestroyModule) Eval(ctx EvalContext) (interface{}, error) {
393 diff, lock := ctx.Diff()
394
395 // Acquire the lock so that we can do this safely concurrently
396 lock.Lock()
397 defer lock.Unlock()
398 812
399 // Write the diff 813 if n.OutputState != nil {
400 modDiff := diff.ModuleByPath(n.Path) 814 // Record our proposed new state, which is nil because we're destroying.
401 if modDiff == nil { 815 *n.OutputState = nil
402 modDiff = diff.AddModule(n.Path)
403 } 816 }
404 modDiff.Destroy = true
405 817
406 return nil, nil 818 return nil, nil
407} 819}
408 820
409// EvalFilterDiff is an EvalNode implementation that filters the diff 821// EvalReduceDiff is an EvalNode implementation that takes a planned resource
410// according to some filter. 822// instance change as might be produced by EvalDiff or EvalDiffDestroy and
411type EvalFilterDiff struct { 823// "simplifies" it to a single atomic action to be performed by a specific
412 // Input and output 824// graph node.
413 Diff **InstanceDiff 825//
414 Output **InstanceDiff 826// Callers must specify whether they are a destroy node or a regular apply
415 827// node. If the result is NoOp then the given change requires no action for
416 // Destroy, if true, will only include a destroy diff if it is set. 828// the specific graph node calling this and so evaluation of the that graph
417 Destroy bool 829// node should exit early and take no action.
830//
831// The object written to OutChange may either be identical to InChange or
832// a new change object derived from InChange. Because of the former case, the
833// caller must not mutate the object returned in OutChange.
834type EvalReduceDiff struct {
835 Addr addrs.ResourceInstance
836 InChange **plans.ResourceInstanceChange
837 Destroy bool
838 OutChange **plans.ResourceInstanceChange
418} 839}
419 840
420func (n *EvalFilterDiff) Eval(ctx EvalContext) (interface{}, error) { 841// TODO: test
421 if *n.Diff == nil { 842func (n *EvalReduceDiff) Eval(ctx EvalContext) (interface{}, error) {
422 return nil, nil 843 in := *n.InChange
423 } 844 out := in.Simplify(n.Destroy)
424 845 if n.OutChange != nil {
425 input := *n.Diff 846 *n.OutChange = out
426 result := new(InstanceDiff) 847 }
427 848 if out.Action != in.Action {
428 if n.Destroy { 849 if n.Destroy {
429 if input.GetDestroy() || input.RequiresNew() { 850 log.Printf("[TRACE] EvalReduceDiff: %s change simplified from %s to %s for destroy node", n.Addr, in.Action, out.Action)
430 result.SetDestroy(true) 851 } else {
852 log.Printf("[TRACE] EvalReduceDiff: %s change simplified from %s to %s for apply node", n.Addr, in.Action, out.Action)
431 } 853 }
432 } 854 }
433
434 if n.Output != nil {
435 *n.Output = result
436 }
437
438 return nil, nil 855 return nil, nil
439} 856}
440 857
441// EvalReadDiff is an EvalNode implementation that writes the diff to 858// EvalReadDiff is an EvalNode implementation that retrieves the planned
442// the full diff. 859// change for a particular resource instance object.
443type EvalReadDiff struct { 860type EvalReadDiff struct {
444 Name string 861 Addr addrs.ResourceInstance
445 Diff **InstanceDiff 862 DeposedKey states.DeposedKey
863 ProviderSchema **ProviderSchema
864 Change **plans.ResourceInstanceChange
446} 865}
447 866
448func (n *EvalReadDiff) Eval(ctx EvalContext) (interface{}, error) { 867func (n *EvalReadDiff) Eval(ctx EvalContext) (interface{}, error) {
449 diff, lock := ctx.Diff() 868 providerSchema := *n.ProviderSchema
869 changes := ctx.Changes()
870 addr := n.Addr.Absolute(ctx.Path())
450 871
451 // Acquire the lock so that we can do this safely concurrently 872 schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource())
452 lock.Lock() 873 if schema == nil {
453 defer lock.Unlock() 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 }
454 877
455 // Write the diff 878 gen := states.CurrentGen
456 modDiff := diff.ModuleByPath(ctx.Path()) 879 if n.DeposedKey != states.NotDeposed {
457 if modDiff == nil { 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)
458 return nil, nil 885 return nil, nil
459 } 886 }
460 887
461 *n.Diff = modDiff.Resources[n.Name] 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)
462 897
463 return nil, nil 898 return nil, nil
464} 899}
465 900
466// EvalWriteDiff is an EvalNode implementation that writes the diff to 901// EvalWriteDiff is an EvalNode implementation that saves a planned change
467// the full diff. 902// for an instance object into the set of global planned changes.
468type EvalWriteDiff struct { 903type EvalWriteDiff struct {
469 Name string 904 Addr addrs.ResourceInstance
470 Diff **InstanceDiff 905 DeposedKey states.DeposedKey
906 ProviderSchema **ProviderSchema
907 Change **plans.ResourceInstanceChange
471} 908}
472 909
473// TODO: test 910// TODO: test
474func (n *EvalWriteDiff) Eval(ctx EvalContext) (interface{}, error) { 911func (n *EvalWriteDiff) Eval(ctx EvalContext) (interface{}, error) {
475 diff, lock := ctx.Diff() 912 changes := ctx.Changes()
476 913 addr := n.Addr.Absolute(ctx.Path())
477 // The diff to write, if its empty it should write nil 914 if n.Change == nil || *n.Change == nil {
478 var diffVal *InstanceDiff 915 // Caller sets nil to indicate that we need to remove a change from
479 if n.Diff != nil { 916 // the set of changes.
480 diffVal = *n.Diff 917 gen := states.CurrentGen
918 if n.DeposedKey != states.NotDeposed {
919 gen = n.DeposedKey
920 }
921 changes.RemoveResourceInstanceChange(addr, gen)
922 return nil, nil
481 } 923 }
482 if diffVal.Empty() { 924
483 diffVal = nil 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")
484 } 931 }
485 932
486 // Acquire the lock so that we can do this safely concurrently 933 schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource())
487 lock.Lock() 934 if schema == nil {
488 defer lock.Unlock() 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 }
489 938
490 // Write the diff 939 csrc, err := change.Encode(schema.ImpliedType())
491 modDiff := diff.ModuleByPath(ctx.Path()) 940 if err != nil {
492 if modDiff == nil { 941 return nil, fmt.Errorf("failed to encode planned changes for %s: %s", addr, err)
493 modDiff = diff.AddModule(ctx.Path())
494 } 942 }
495 if diffVal != nil { 943
496 modDiff.Resources[n.Name] = diffVal 944 changes.AppendResourceInstanceChange(csrc)
945 if n.DeposedKey == states.NotDeposed {
946 log.Printf("[TRACE] EvalWriteDiff: recorded %s change for %s", change.Action, addr)
497 } else { 947 } else {
498 delete(modDiff.Resources, n.Name) 948 log.Printf("[TRACE] EvalWriteDiff: recorded %s change for %s deposed object %s", change.Action, addr, n.DeposedKey)
499 } 949 }
500 950
501 return nil, nil 951 return nil, nil