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