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