]>
Commit | Line | Data |
---|---|---|
bae9f6d2 JC |
1 | package terraform |
2 | ||
3 | import ( | |
4 | "fmt" | |
5 | "log" | |
107c1cdb | 6 | "strings" |
bae9f6d2 JC |
7 | |
8 | "github.com/hashicorp/go-multierror" | |
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/provisioners" | |
18 | "github.com/hashicorp/terraform/states" | |
19 | "github.com/hashicorp/terraform/tfdiags" | |
bae9f6d2 JC |
20 | ) |
21 | ||
22 | // EvalApply is an EvalNode implementation that writes the diff to | |
23 | // the full diff. | |
24 | type EvalApply struct { | |
107c1cdb ND |
25 | Addr addrs.ResourceInstance |
26 | Config *configs.Resource | |
27 | Dependencies []addrs.Referenceable | |
28 | State **states.ResourceInstanceObject | |
29 | Change **plans.ResourceInstanceChange | |
30 | ProviderAddr addrs.AbsProviderConfig | |
31 | Provider *providers.Interface | |
32 | ProviderSchema **ProviderSchema | |
33 | Output **states.ResourceInstanceObject | |
34 | CreateNew *bool | |
35 | Error *error | |
bae9f6d2 JC |
36 | } |
37 | ||
38 | // TODO: test | |
39 | func (n *EvalApply) Eval(ctx EvalContext) (interface{}, error) { | |
107c1cdb ND |
40 | var diags tfdiags.Diagnostics |
41 | ||
42 | change := *n.Change | |
bae9f6d2 JC |
43 | provider := *n.Provider |
44 | state := *n.State | |
107c1cdb | 45 | absAddr := n.Addr.Absolute(ctx.Path()) |
bae9f6d2 | 46 | |
107c1cdb ND |
47 | if state == nil { |
48 | state = &states.ResourceInstanceObject{} | |
49 | } | |
50 | ||
51 | schema, _ := (*n.ProviderSchema).SchemaForResourceType(n.Addr.Resource.Mode, n.Addr.Resource.Type) | |
52 | if schema == nil { | |
53 | // Should be caught during validation, so we don't bother with a pretty error here | |
54 | return nil, fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Type) | |
55 | } | |
56 | ||
57 | if n.CreateNew != nil { | |
58 | *n.CreateNew = (change.Action == plans.Create || change.Action.IsReplace()) | |
bae9f6d2 JC |
59 | } |
60 | ||
107c1cdb ND |
61 | configVal := cty.NullVal(cty.DynamicPseudoType) |
62 | if n.Config != nil { | |
63 | var configDiags tfdiags.Diagnostics | |
64 | keyData := EvalDataForInstanceKey(n.Addr.Key) | |
65 | configVal, _, configDiags = ctx.EvaluateBlock(n.Config.Config, schema, nil, keyData) | |
66 | diags = diags.Append(configDiags) | |
67 | if configDiags.HasErrors() { | |
68 | return nil, diags.Err() | |
bae9f6d2 JC |
69 | } |
70 | } | |
71 | ||
107c1cdb ND |
72 | log.Printf("[DEBUG] %s: applying the planned %s change", n.Addr.Absolute(ctx.Path()), change.Action) |
73 | resp := provider.ApplyResourceChange(providers.ApplyResourceChangeRequest{ | |
74 | TypeName: n.Addr.Resource.Type, | |
75 | PriorState: change.Before, | |
76 | Config: configVal, | |
77 | PlannedState: change.After, | |
78 | PlannedPrivate: change.Private, | |
79 | }) | |
80 | applyDiags := resp.Diagnostics | |
81 | if n.Config != nil { | |
82 | applyDiags = applyDiags.InConfigBody(n.Config.Config) | |
bae9f6d2 | 83 | } |
107c1cdb ND |
84 | diags = diags.Append(applyDiags) |
85 | ||
86 | // Even if there are errors in the returned diagnostics, the provider may | |
87 | // have returned a _partial_ state for an object that already exists but | |
88 | // failed to fully configure, and so the remaining code must always run | |
89 | // to completion but must be defensive against the new value being | |
90 | // incomplete. | |
91 | newVal := resp.NewState | |
92 | ||
93 | if newVal == cty.NilVal { | |
94 | // Providers are supposed to return a partial new value even when errors | |
95 | // occur, but sometimes they don't and so in that case we'll patch that up | |
96 | // by just using the prior state, so we'll at least keep track of the | |
97 | // object for the user to retry. | |
98 | newVal = change.Before | |
99 | ||
100 | // As a special case, we'll set the new value to null if it looks like | |
101 | // we were trying to execute a delete, because the provider in this case | |
102 | // probably left the newVal unset intending it to be interpreted as "null". | |
103 | if change.After.IsNull() { | |
104 | newVal = cty.NullVal(schema.ImpliedType()) | |
105 | } | |
bae9f6d2 | 106 | |
107c1cdb ND |
107 | // Ideally we'd produce an error or warning here if newVal is nil and |
108 | // there are no errors in diags, because that indicates a buggy | |
109 | // provider not properly reporting its result, but unfortunately many | |
110 | // of our historical test mocks behave in this way and so producing | |
111 | // a diagnostic here fails hundreds of tests. Instead, we must just | |
112 | // silently retain the old value for now. Returning a nil value with | |
113 | // no errors is still always considered a bug in the provider though, | |
114 | // and should be fixed for any "real" providers that do it. | |
bae9f6d2 JC |
115 | } |
116 | ||
107c1cdb ND |
117 | var conformDiags tfdiags.Diagnostics |
118 | for _, err := range newVal.Type().TestConformance(schema.ImpliedType()) { | |
119 | conformDiags = conformDiags.Append(tfdiags.Sourceless( | |
120 | tfdiags.Error, | |
121 | "Provider produced invalid object", | |
122 | fmt.Sprintf( | |
123 | "Provider %q produced an invalid value after apply for %s. The result cannot not be saved in the Terraform state.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", | |
124 | n.ProviderAddr.ProviderConfig.Type, tfdiags.FormatErrorPrefixed(err, absAddr.String()), | |
125 | ), | |
126 | )) | |
127 | } | |
128 | diags = diags.Append(conformDiags) | |
129 | if conformDiags.HasErrors() { | |
130 | // Bail early in this particular case, because an object that doesn't | |
131 | // conform to the schema can't be saved in the state anyway -- the | |
132 | // serializer will reject it. | |
133 | return nil, diags.Err() | |
134 | } | |
135 | ||
136 | // After this point we have a type-conforming result object and so we | |
137 | // must always run to completion to ensure it can be saved. If n.Error | |
138 | // is set then we must not return a non-nil error, in order to allow | |
139 | // evaluation to continue to a later point where our state object will | |
140 | // be saved. | |
141 | ||
142 | // By this point there must not be any unknown values remaining in our | |
143 | // object, because we've applied the change and we can't save unknowns | |
144 | // in our persistent state. If any are present then we will indicate an | |
145 | // error (which is always a bug in the provider) but we will also replace | |
146 | // them with nulls so that we can successfully save the portions of the | |
147 | // returned value that are known. | |
148 | if !newVal.IsWhollyKnown() { | |
149 | // To generate better error messages, we'll go for a walk through the | |
150 | // value and make a separate diagnostic for each unknown value we | |
151 | // find. | |
152 | cty.Walk(newVal, func(path cty.Path, val cty.Value) (bool, error) { | |
153 | if !val.IsKnown() { | |
154 | pathStr := tfdiags.FormatCtyPath(path) | |
155 | diags = diags.Append(tfdiags.Sourceless( | |
156 | tfdiags.Error, | |
157 | "Provider returned invalid result object after apply", | |
158 | fmt.Sprintf( | |
159 | "After the apply operation, the provider still indicated an unknown value for %s%s. All values must be known after apply, so this is always a bug in the provider and should be reported in the provider's own repository. Terraform will still save the other known object values in the state.", | |
160 | n.Addr.Absolute(ctx.Path()), pathStr, | |
161 | ), | |
162 | )) | |
163 | } | |
164 | return true, nil | |
165 | }) | |
166 | ||
167 | // NOTE: This operation can potentially be lossy if there are multiple | |
168 | // elements in a set that differ only by unknown values: after | |
169 | // replacing with null these will be merged together into a single set | |
170 | // element. Since we can only get here in the presence of a provider | |
171 | // bug, we accept this because storing a result here is always a | |
172 | // best-effort sort of thing. | |
173 | newVal = cty.UnknownAsNull(newVal) | |
174 | } | |
175 | ||
176 | if change.Action != plans.Delete && !diags.HasErrors() { | |
177 | // Only values that were marked as unknown in the planned value are allowed | |
178 | // to change during the apply operation. (We do this after the unknown-ness | |
179 | // check above so that we also catch anything that became unknown after | |
180 | // being known during plan.) | |
181 | // | |
182 | // If we are returning other errors anyway then we'll give this | |
183 | // a pass since the other errors are usually the explanation for | |
184 | // this one and so it's more helpful to let the user focus on the | |
185 | // root cause rather than distract with this extra problem. | |
186 | if errs := objchange.AssertObjectCompatible(schema, change.After, newVal); len(errs) > 0 { | |
187 | if resp.LegacyTypeSystem { | |
188 | // The shimming of the old type system in the legacy SDK is not precise | |
189 | // enough to pass this consistency check, so we'll give it a pass here, | |
190 | // but we will generate a warning about it so that we are more likely | |
191 | // to notice in the logs if an inconsistency beyond the type system | |
192 | // leads to a downstream provider failure. | |
193 | var buf strings.Builder | |
194 | fmt.Fprintf(&buf, "[WARN] Provider %q produced an unexpected new value 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) | |
195 | for _, err := range errs { | |
196 | fmt.Fprintf(&buf, "\n - %s", tfdiags.FormatError(err)) | |
197 | } | |
198 | log.Print(buf.String()) | |
199 | ||
200 | // The sort of inconsistency we won't catch here is if a known value | |
201 | // in the plan is changed during apply. That can cause downstream | |
202 | // problems because a dependent resource would make its own plan based | |
203 | // on the planned value, and thus get a different result during the | |
204 | // apply phase. This will usually lead to a "Provider produced invalid plan" | |
205 | // error that incorrectly blames the downstream resource for the change. | |
206 | ||
207 | } else { | |
208 | for _, err := range errs { | |
209 | diags = diags.Append(tfdiags.Sourceless( | |
210 | tfdiags.Error, | |
211 | "Provider produced inconsistent result after apply", | |
212 | fmt.Sprintf( | |
213 | "When applying changes to %s, provider %q produced an unexpected new value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", | |
214 | absAddr, n.ProviderAddr.ProviderConfig.Type, tfdiags.FormatError(err), | |
215 | ), | |
216 | )) | |
217 | } | |
218 | } | |
219 | } | |
220 | } | |
221 | ||
222 | // If a provider returns a null or non-null object at the wrong time then | |
223 | // we still want to save that but it often causes some confusing behaviors | |
224 | // where it seems like Terraform is failing to take any action at all, | |
225 | // so we'll generate some errors to draw attention to it. | |
226 | if !diags.HasErrors() { | |
227 | if change.Action == plans.Delete && !newVal.IsNull() { | |
228 | diags = diags.Append(tfdiags.Sourceless( | |
229 | tfdiags.Error, | |
230 | "Provider returned invalid result object after apply", | |
231 | fmt.Sprintf( | |
232 | "After applying a %s plan, the provider returned a non-null object for %s. Destroying should always produce a null value, so this is always a bug in the provider and should be reported in the provider's own repository. Terraform will still save this errant object in the state for debugging and recovery.", | |
233 | change.Action, n.Addr.Absolute(ctx.Path()), | |
234 | ), | |
235 | )) | |
236 | } | |
237 | if change.Action != plans.Delete && newVal.IsNull() { | |
238 | diags = diags.Append(tfdiags.Sourceless( | |
239 | tfdiags.Error, | |
240 | "Provider returned invalid result object after apply", | |
241 | fmt.Sprintf( | |
242 | "After applying a %s plan, the provider returned a null object for %s. Only destroying should always produce a null value, so this is always a bug in the provider and should be reported in the provider's own repository.", | |
243 | change.Action, n.Addr.Absolute(ctx.Path()), | |
244 | ), | |
245 | )) | |
246 | } | |
bae9f6d2 | 247 | } |
bae9f6d2 | 248 | |
107c1cdb ND |
249 | // Sometimes providers return a null value when an operation fails for some |
250 | // reason, but we'd rather keep the prior state so that the error can be | |
251 | // corrected on a subsequent run. We must only do this for null new value | |
252 | // though, or else we may discard partial updates the provider was able to | |
253 | // complete. | |
254 | if diags.HasErrors() && newVal.IsNull() { | |
255 | // Otherwise, we'll continue but using the prior state as the new value, | |
256 | // making this effectively a no-op. If the item really _has_ been | |
257 | // deleted then our next refresh will detect that and fix it up. | |
258 | // If change.Action is Create then change.Before will also be null, | |
259 | // which is fine. | |
260 | newVal = change.Before | |
bae9f6d2 JC |
261 | } |
262 | ||
107c1cdb ND |
263 | var newState *states.ResourceInstanceObject |
264 | if !newVal.IsNull() { // null value indicates that the object is deleted, so we won't set a new state in that case | |
265 | newState = &states.ResourceInstanceObject{ | |
266 | Status: states.ObjectReady, | |
267 | Value: newVal, | |
268 | Private: resp.Private, | |
269 | Dependencies: n.Dependencies, // Should be populated by the caller from the StateDependencies method on the resource instance node | |
bae9f6d2 JC |
270 | } |
271 | } | |
272 | ||
273 | // Write the final state | |
274 | if n.Output != nil { | |
107c1cdb | 275 | *n.Output = newState |
bae9f6d2 JC |
276 | } |
277 | ||
107c1cdb ND |
278 | if diags.HasErrors() { |
279 | // If the caller provided an error pointer then they are expected to | |
280 | // handle the error some other way and we treat our own result as | |
281 | // success. | |
bae9f6d2 | 282 | if n.Error != nil { |
107c1cdb ND |
283 | err := diags.Err() |
284 | *n.Error = err | |
285 | log.Printf("[DEBUG] %s: apply errored, but we're indicating that via the Error pointer rather than returning it: %s", n.Addr.Absolute(ctx.Path()), err) | |
286 | return nil, nil | |
bae9f6d2 JC |
287 | } |
288 | } | |
289 | ||
107c1cdb | 290 | return nil, diags.ErrWithWarnings() |
bae9f6d2 JC |
291 | } |
292 | ||
293 | // EvalApplyPre is an EvalNode implementation that does the pre-Apply work | |
294 | type EvalApplyPre struct { | |
107c1cdb ND |
295 | Addr addrs.ResourceInstance |
296 | Gen states.Generation | |
297 | State **states.ResourceInstanceObject | |
298 | Change **plans.ResourceInstanceChange | |
bae9f6d2 JC |
299 | } |
300 | ||
301 | // TODO: test | |
302 | func (n *EvalApplyPre) Eval(ctx EvalContext) (interface{}, error) { | |
107c1cdb ND |
303 | change := *n.Change |
304 | absAddr := n.Addr.Absolute(ctx.Path()) | |
bae9f6d2 | 305 | |
107c1cdb ND |
306 | if change == nil { |
307 | panic(fmt.Sprintf("EvalApplyPre for %s called with nil Change", absAddr)) | |
bae9f6d2 | 308 | } |
bae9f6d2 | 309 | |
107c1cdb ND |
310 | if resourceHasUserVisibleApply(n.Addr) { |
311 | priorState := change.Before | |
312 | plannedNewState := change.After | |
313 | ||
bae9f6d2 | 314 | err := ctx.Hook(func(h Hook) (HookAction, error) { |
107c1cdb | 315 | return h.PreApply(absAddr, n.Gen, change.Action, priorState, plannedNewState) |
bae9f6d2 JC |
316 | }) |
317 | if err != nil { | |
318 | return nil, err | |
319 | } | |
320 | } | |
321 | ||
322 | return nil, nil | |
323 | } | |
324 | ||
325 | // EvalApplyPost is an EvalNode implementation that does the post-Apply work | |
326 | type EvalApplyPost struct { | |
107c1cdb ND |
327 | Addr addrs.ResourceInstance |
328 | Gen states.Generation | |
329 | State **states.ResourceInstanceObject | |
bae9f6d2 JC |
330 | Error *error |
331 | } | |
332 | ||
333 | // TODO: test | |
334 | func (n *EvalApplyPost) Eval(ctx EvalContext) (interface{}, error) { | |
335 | state := *n.State | |
336 | ||
107c1cdb ND |
337 | if resourceHasUserVisibleApply(n.Addr) { |
338 | absAddr := n.Addr.Absolute(ctx.Path()) | |
339 | var newState cty.Value | |
340 | if state != nil { | |
341 | newState = state.Value | |
342 | } else { | |
343 | newState = cty.NullVal(cty.DynamicPseudoType) | |
344 | } | |
345 | var err error | |
346 | if n.Error != nil { | |
347 | err = *n.Error | |
348 | } | |
349 | ||
350 | hookErr := ctx.Hook(func(h Hook) (HookAction, error) { | |
351 | return h.PostApply(absAddr, n.Gen, newState, err) | |
bae9f6d2 | 352 | }) |
107c1cdb ND |
353 | if hookErr != nil { |
354 | return nil, hookErr | |
bae9f6d2 JC |
355 | } |
356 | } | |
357 | ||
358 | return nil, *n.Error | |
359 | } | |
360 | ||
107c1cdb ND |
361 | // EvalMaybeTainted is an EvalNode that takes the planned change, new value, |
362 | // and possible error from an apply operation and produces a new instance | |
363 | // object marked as tainted if it appears that a create operation has failed. | |
364 | // | |
365 | // This EvalNode never returns an error, to ensure that a subsequent EvalNode | |
366 | // can still record the possibly-tainted object in the state. | |
367 | type EvalMaybeTainted struct { | |
368 | Addr addrs.ResourceInstance | |
369 | Gen states.Generation | |
370 | Change **plans.ResourceInstanceChange | |
371 | State **states.ResourceInstanceObject | |
372 | Error *error | |
373 | ||
374 | // If StateOutput is not nil, its referent will be assigned either the same | |
375 | // pointer as State or a new object with its status set as Tainted, | |
376 | // depending on whether an error is given and if this was a create action. | |
377 | StateOutput **states.ResourceInstanceObject | |
378 | } | |
379 | ||
380 | // TODO: test | |
381 | func (n *EvalMaybeTainted) Eval(ctx EvalContext) (interface{}, error) { | |
382 | state := *n.State | |
383 | change := *n.Change | |
384 | err := *n.Error | |
385 | ||
386 | if state != nil && state.Status == states.ObjectTainted { | |
387 | log.Printf("[TRACE] EvalMaybeTainted: %s was already tainted, so nothing to do", n.Addr.Absolute(ctx.Path())) | |
388 | return nil, nil | |
389 | } | |
390 | ||
391 | if n.StateOutput != nil { | |
392 | if err != nil && change.Action == plans.Create { | |
393 | // If there are errors during a _create_ then the object is | |
394 | // in an undefined state, and so we'll mark it as tainted so | |
395 | // we can try again on the next run. | |
396 | // | |
397 | // We don't do this for other change actions because errors | |
398 | // during updates will often not change the remote object at all. | |
399 | // If there _were_ changes prior to the error, it's the provider's | |
400 | // responsibility to record the effect of those changes in the | |
401 | // object value it returned. | |
402 | log.Printf("[TRACE] EvalMaybeTainted: %s encountered an error during creation, so it is now marked as tainted", n.Addr.Absolute(ctx.Path())) | |
403 | *n.StateOutput = state.AsTainted() | |
404 | } else { | |
405 | *n.StateOutput = state | |
406 | } | |
407 | } | |
408 | ||
409 | return nil, nil | |
410 | } | |
411 | ||
15c0b25d AP |
412 | // resourceHasUserVisibleApply returns true if the given resource is one where |
413 | // apply actions should be exposed to the user. | |
414 | // | |
415 | // Certain resources do apply actions only as an implementation detail, so | |
416 | // these should not be advertised to code outside of this package. | |
107c1cdb | 417 | func resourceHasUserVisibleApply(addr addrs.ResourceInstance) bool { |
15c0b25d AP |
418 | // Only managed resources have user-visible apply actions. |
419 | // In particular, this excludes data resources since we "apply" these | |
420 | // only as an implementation detail of removing them from state when | |
421 | // they are destroyed. (When reading, they don't get here at all because | |
422 | // we present them as "Refresh" actions.) | |
107c1cdb | 423 | return addr.ContainingResource().Mode == addrs.ManagedResourceMode |
15c0b25d AP |
424 | } |
425 | ||
bae9f6d2 JC |
426 | // EvalApplyProvisioners is an EvalNode implementation that executes |
427 | // the provisioners for a resource. | |
428 | // | |
429 | // TODO(mitchellh): This should probably be split up into a more fine-grained | |
430 | // ApplyProvisioner (single) that is looped over. | |
431 | type EvalApplyProvisioners struct { | |
107c1cdb ND |
432 | Addr addrs.ResourceInstance |
433 | State **states.ResourceInstanceObject | |
434 | ResourceConfig *configs.Resource | |
bae9f6d2 JC |
435 | CreateNew *bool |
436 | Error *error | |
437 | ||
438 | // When is the type of provisioner to run at this point | |
107c1cdb | 439 | When configs.ProvisionerWhen |
bae9f6d2 JC |
440 | } |
441 | ||
442 | // TODO: test | |
443 | func (n *EvalApplyProvisioners) Eval(ctx EvalContext) (interface{}, error) { | |
107c1cdb | 444 | absAddr := n.Addr.Absolute(ctx.Path()) |
bae9f6d2 | 445 | state := *n.State |
107c1cdb ND |
446 | if state == nil { |
447 | log.Printf("[TRACE] EvalApplyProvisioners: %s has no state, so skipping provisioners", n.Addr) | |
448 | return nil, nil | |
449 | } | |
450 | if n.When == configs.ProvisionerWhenCreate && n.CreateNew != nil && !*n.CreateNew { | |
bae9f6d2 | 451 | // If we're not creating a new resource, then don't run provisioners |
107c1cdb ND |
452 | log.Printf("[TRACE] EvalApplyProvisioners: %s is not freshly-created, so no provisioning is required", n.Addr) |
453 | return nil, nil | |
454 | } | |
455 | if state.Status == states.ObjectTainted { | |
456 | // No point in provisioning an object that is already tainted, since | |
457 | // it's going to get recreated on the next apply anyway. | |
458 | log.Printf("[TRACE] EvalApplyProvisioners: %s is tainted, so skipping provisioning", n.Addr) | |
bae9f6d2 JC |
459 | return nil, nil |
460 | } | |
461 | ||
462 | provs := n.filterProvisioners() | |
463 | if len(provs) == 0 { | |
464 | // We have no provisioners, so don't do anything | |
465 | return nil, nil | |
466 | } | |
467 | ||
bae9f6d2 | 468 | if n.Error != nil && *n.Error != nil { |
bae9f6d2 JC |
469 | // We're already tainted, so just return out |
470 | return nil, nil | |
471 | } | |
472 | ||
473 | { | |
474 | // Call pre hook | |
475 | err := ctx.Hook(func(h Hook) (HookAction, error) { | |
107c1cdb | 476 | return h.PreProvisionInstance(absAddr, state.Value) |
bae9f6d2 JC |
477 | }) |
478 | if err != nil { | |
479 | return nil, err | |
480 | } | |
481 | } | |
482 | ||
483 | // If there are no errors, then we append it to our output error | |
484 | // if we have one, otherwise we just output it. | |
485 | err := n.apply(ctx, provs) | |
486 | if err != nil { | |
15c0b25d | 487 | *n.Error = multierror.Append(*n.Error, err) |
107c1cdb ND |
488 | if n.Error == nil { |
489 | return nil, err | |
490 | } else { | |
491 | log.Printf("[TRACE] EvalApplyProvisioners: %s provisioning failed, but we will continue anyway at the caller's request", absAddr) | |
492 | return nil, nil | |
493 | } | |
bae9f6d2 JC |
494 | } |
495 | ||
496 | { | |
497 | // Call post hook | |
498 | err := ctx.Hook(func(h Hook) (HookAction, error) { | |
107c1cdb | 499 | return h.PostProvisionInstance(absAddr, state.Value) |
bae9f6d2 JC |
500 | }) |
501 | if err != nil { | |
502 | return nil, err | |
503 | } | |
504 | } | |
505 | ||
506 | return nil, nil | |
507 | } | |
508 | ||
509 | // filterProvisioners filters the provisioners on the resource to only | |
510 | // the provisioners specified by the "when" option. | |
107c1cdb | 511 | func (n *EvalApplyProvisioners) filterProvisioners() []*configs.Provisioner { |
bae9f6d2 | 512 | // Fast path the zero case |
107c1cdb | 513 | if n.ResourceConfig == nil || n.ResourceConfig.Managed == nil { |
bae9f6d2 JC |
514 | return nil |
515 | } | |
516 | ||
107c1cdb | 517 | if len(n.ResourceConfig.Managed.Provisioners) == 0 { |
bae9f6d2 JC |
518 | return nil |
519 | } | |
520 | ||
107c1cdb ND |
521 | result := make([]*configs.Provisioner, 0, len(n.ResourceConfig.Managed.Provisioners)) |
522 | for _, p := range n.ResourceConfig.Managed.Provisioners { | |
bae9f6d2 JC |
523 | if p.When == n.When { |
524 | result = append(result, p) | |
525 | } | |
526 | } | |
527 | ||
528 | return result | |
529 | } | |
530 | ||
107c1cdb ND |
531 | func (n *EvalApplyProvisioners) apply(ctx EvalContext, provs []*configs.Provisioner) error { |
532 | var diags tfdiags.Diagnostics | |
533 | instanceAddr := n.Addr | |
534 | absAddr := instanceAddr.Absolute(ctx.Path()) | |
535 | ||
536 | // If there's a connection block defined directly inside the resource block | |
537 | // then it'll serve as a base connection configuration for all of the | |
538 | // provisioners. | |
539 | var baseConn hcl.Body | |
540 | if n.ResourceConfig.Managed != nil && n.ResourceConfig.Managed.Connection != nil { | |
541 | baseConn = n.ResourceConfig.Managed.Connection.Config | |
542 | } | |
bae9f6d2 JC |
543 | |
544 | for _, prov := range provs { | |
107c1cdb ND |
545 | log.Printf("[TRACE] EvalApplyProvisioners: provisioning %s with %q", absAddr, prov.Type) |
546 | ||
bae9f6d2 JC |
547 | // Get the provisioner |
548 | provisioner := ctx.Provisioner(prov.Type) | |
107c1cdb | 549 | schema := ctx.ProvisionerSchema(prov.Type) |
bae9f6d2 | 550 | |
107c1cdb | 551 | keyData := EvalDataForInstanceKey(instanceAddr.Key) |
bae9f6d2 | 552 | |
107c1cdb ND |
553 | // Evaluate the main provisioner configuration. |
554 | config, _, configDiags := ctx.EvaluateBlock(prov.Config, schema, instanceAddr, keyData) | |
555 | diags = diags.Append(configDiags) | |
556 | ||
557 | // If the provisioner block contains a connection block of its own then | |
558 | // it can override the base connection configuration, if any. | |
559 | var localConn hcl.Body | |
560 | if prov.Connection != nil { | |
561 | localConn = prov.Connection.Config | |
bae9f6d2 JC |
562 | } |
563 | ||
107c1cdb ND |
564 | var connBody hcl.Body |
565 | switch { | |
566 | case baseConn != nil && localConn != nil: | |
567 | // Our standard merging logic applies here, similar to what we do | |
568 | // with _override.tf configuration files: arguments from the | |
569 | // base connection block will be masked by any arguments of the | |
570 | // same name in the local connection block. | |
571 | connBody = configs.MergeBodies(baseConn, localConn) | |
572 | case baseConn != nil: | |
573 | connBody = baseConn | |
574 | case localConn != nil: | |
575 | connBody = localConn | |
bae9f6d2 | 576 | } |
107c1cdb ND |
577 | |
578 | // start with an empty connInfo | |
579 | connInfo := cty.NullVal(connectionBlockSupersetSchema.ImpliedType()) | |
580 | ||
581 | if connBody != nil { | |
582 | var connInfoDiags tfdiags.Diagnostics | |
583 | connInfo, _, connInfoDiags = ctx.EvaluateBlock(connBody, connectionBlockSupersetSchema, instanceAddr, keyData) | |
584 | diags = diags.Append(connInfoDiags) | |
585 | if diags.HasErrors() { | |
586 | // "on failure continue" setting only applies to failures of the | |
587 | // provisioner itself, not to invalid configuration. | |
588 | return diags.Err() | |
bae9f6d2 JC |
589 | } |
590 | } | |
bae9f6d2 JC |
591 | |
592 | { | |
593 | // Call pre hook | |
594 | err := ctx.Hook(func(h Hook) (HookAction, error) { | |
107c1cdb | 595 | return h.PreProvisionInstanceStep(absAddr, prov.Type) |
bae9f6d2 JC |
596 | }) |
597 | if err != nil { | |
598 | return err | |
599 | } | |
600 | } | |
601 | ||
602 | // The output function | |
603 | outputFn := func(msg string) { | |
604 | ctx.Hook(func(h Hook) (HookAction, error) { | |
107c1cdb | 605 | h.ProvisionOutput(absAddr, prov.Type, msg) |
bae9f6d2 JC |
606 | return HookActionContinue, nil |
607 | }) | |
608 | } | |
609 | ||
bae9f6d2 | 610 | output := CallbackUIOutput{OutputFn: outputFn} |
107c1cdb ND |
611 | resp := provisioner.ProvisionResource(provisioners.ProvisionResourceRequest{ |
612 | Config: config, | |
613 | Connection: connInfo, | |
614 | UIOutput: &output, | |
615 | }) | |
616 | applyDiags := resp.Diagnostics.InConfigBody(prov.Config) | |
bae9f6d2 JC |
617 | |
618 | // Call post hook | |
619 | hookErr := ctx.Hook(func(h Hook) (HookAction, error) { | |
107c1cdb | 620 | return h.PostProvisionInstanceStep(absAddr, prov.Type, applyDiags.Err()) |
bae9f6d2 JC |
621 | }) |
622 | ||
107c1cdb ND |
623 | switch prov.OnFailure { |
624 | case configs.ProvisionerOnFailureContinue: | |
625 | if applyDiags.HasErrors() { | |
626 | log.Printf("[WARN] Errors while provisioning %s with %q, but continuing as requested in configuration", n.Addr, prov.Type) | |
627 | } else { | |
628 | // Maybe there are warnings that we still want to see | |
629 | diags = diags.Append(applyDiags) | |
630 | } | |
631 | default: | |
632 | diags = diags.Append(applyDiags) | |
633 | if applyDiags.HasErrors() { | |
634 | log.Printf("[WARN] Errors while provisioning %s with %q, so aborting", n.Addr, prov.Type) | |
635 | return diags.Err() | |
bae9f6d2 JC |
636 | } |
637 | } | |
638 | ||
639 | // Deal with the hook | |
640 | if hookErr != nil { | |
641 | return hookErr | |
642 | } | |
643 | } | |
644 | ||
107c1cdb | 645 | return diags.ErrWithWarnings() |
bae9f6d2 | 646 | } |