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