improve error message

pull/37467/head
Daniel Schmidt 8 months ago
parent fc1cd88642
commit e6dfb537e9

@ -4,9 +4,12 @@
package terraform
import (
"path/filepath"
"testing"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/hashicorp/terraform/internal/plans"
@ -31,7 +34,7 @@ func TestContext2Apply_actions(t *testing.T) {
expectInvokeActionCalled bool
expectInvokeActionCalls []providers.InvokeActionRequest
expectDiagnostics tfdiags.Diagnostics
expectDiagnostics func(m *configs.Config) tfdiags.Diagnostics
}{
"unreferenced": {
module: map[string]string{
@ -147,7 +150,6 @@ resource "test_object" "a" {
},
"before_create failing": {
toBeImplemented: true, // We need to revisit the diagnostic enhancement
module: map[string]string{
"main.tf": `
action "act_unlinked" "hello" {}
@ -176,17 +178,21 @@ resource "test_object" "a" {
}
},
expectDiagnostics: tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Error,
"Failed to apply actions before test_object.a",
"An error occured while invoking action action.act_unlinked.hello: test case for failing: this simulates a provider failing\n",
),
expectDiagnostics: func(m *configs.Config) tfdiags.Diagnostics {
return tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Error when invoking action",
Detail: "test case for failing: this simulates a provider failing",
Subject: &hcl.Range{
Filename: filepath.Join(m.Module.SourceDir, "main.tf"),
Start: hcl.Pos{Line: 7, Column: 18, Byte: 146},
End: hcl.Pos{Line: 7, Column: 43, Byte: 171},
},
})
},
},
"before_create failing with successfully completed actions": {
toBeImplemented: true, // We need to revisit the diagnostic enhancement
module: map[string]string{
"main.tf": `
action "act_unlinked" "hello" {}
@ -227,21 +233,24 @@ resource "test_object" "a" {
}
},
expectDiagnostics: tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Error,
"Failed to apply actions before test_object.a",
`An error occured while invoking action action.act_unlinked.failure: test case for failing: this simulates a provider failing
The following actions were successfully invoked:
- action.act_unlinked.hello
- action.act_unlinked.world
As the resource did not change, these actions will be re-invoked in the next apply.`,
),
expectDiagnostics: func(m *configs.Config) tfdiags.Diagnostics {
return tfdiags.Diagnostics{}.Append(
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Error when invoking action",
Detail: `test case for failing: this simulates a provider failing`,
Subject: &hcl.Range{
Filename: filepath.Join(m.Module.SourceDir, "main.tf"),
Start: hcl.Pos{Line: 13, Column: 72, Byte: 305},
End: hcl.Pos{Line: 13, Column: 99, Byte: 332},
},
},
)
},
},
"before_create failing when calling invoke": {
toBeImplemented: true, // We need to revisit the diagnostic enhancement
module: map[string]string{
"main.tf": `
action "act_unlinked" "hello" {}
@ -265,115 +274,87 @@ resource "test_object" "a" {
),
}
},
expectDiagnostics: tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Error,
"Failed to apply actions before test_object.a",
"An error occured while invoking action action.act_unlinked.hello: test case for failing: this simulates a provider failing before the action is invoked\n",
),
},
},
"after_create failing": {
toBeImplemented: true, // We need to revisit the diagnostic enhancement
module: map[string]string{
"main.tf": `
action "act_unlinked" "hello" {}
resource "test_object" "a" {
lifecycle {
action_trigger {
events = [after_create]
actions = [action.act_unlinked.hello]
}
}
}
`,
},
expectInvokeActionCalled: true,
events: func(req providers.InvokeActionRequest) []providers.InvokeActionEvent {
return []providers.InvokeActionEvent{
providers.InvokeActionEvent_Completed{
Diagnostics: tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Error,
"test case for failing",
"this simulates a provider failing",
),
expectDiagnostics: func(m *configs.Config) tfdiags.Diagnostics {
return tfdiags.Diagnostics{}.Append(
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Error when invoking action",
Detail: "test case for failing: this simulates a provider failing before the action is invoked",
Subject: &hcl.Range{
Filename: filepath.Join(m.Module.SourceDir, "main.tf"),
Start: hcl.Pos{Line: 7, Column: 18, Byte: 146},
End: hcl.Pos{Line: 7, Column: 43, Byte: 171},
},
},
}
},
expectDiagnostics: tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Error,
"Failed to apply actions after test_object.a",
`An error occured while invoking action action.act_unlinked.hello: test case for failing: this simulates a provider failing
The following actions were not yet invoked:
- action.act_unlinked.hello
These actions will not be triggered in the next apply, please run "terraform invoke" to invoke them.`,
),
)
},
},
"after_create failing with successfully completed actions": {
toBeImplemented: true, // We need to revisit the diagnostic enhancement
"failing an action by action event stops next actions in list": {
module: map[string]string{
"main.tf": `
action "act_unlinked" "hello" {}
action "act_unlinked" "world" {}
action "act_unlinked" "failure" {
config {
attr = "failure"
}
}
action "act_unlinked" "goodbye" {}
resource "test_object" "a" {
lifecycle {
action_trigger {
events = [after_create]
actions = [action.act_unlinked.hello, action.act_unlinked.world, action.act_unlinked.failure]
events = [before_create]
actions = [action.act_unlinked.hello, action.act_unlinked.failure, action.act_unlinked.goodbye]
}
}
}
`,
},
expectInvokeActionCalled: true,
events: func(req providers.InvokeActionRequest) []providers.InvokeActionEvent {
if !req.PlannedActionData.IsNull() && req.PlannedActionData.GetAttr("attr").AsString() == "failure" {
events: func(r providers.InvokeActionRequest) []providers.InvokeActionEvent {
if !r.PlannedActionData.IsNull() && r.PlannedActionData.GetAttr("attr").AsString() == "failure" {
return []providers.InvokeActionEvent{
providers.InvokeActionEvent_Completed{
Diagnostics: tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Error,
"test case for failing",
"this simulates a provider failing",
),
},
Diagnostics: tfdiags.Diagnostics{}.Append(tfdiags.Sourceless(tfdiags.Error, "test case for failing", "this simulates a provider failing")),
},
}
} else {
return []providers.InvokeActionEvent{
providers.InvokeActionEvent_Completed{},
}
}
},
expectDiagnostics: tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Error,
"Failed to apply actions after test_object.a",
`An error occured while invoking action action.act_unlinked.failure: test case for failing: this simulates a provider failing
return []providers.InvokeActionEvent{
providers.InvokeActionEvent_Completed{},
}
The following actions were not yet invoked:
- action.act_unlinked.failure
These actions will not be triggered in the next apply, please run "terraform invoke" to invoke them.`,
),
},
expectDiagnostics: func(m *configs.Config) tfdiags.Diagnostics {
return tfdiags.Diagnostics{}.Append(
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Error when invoking action",
Detail: "test case for failing: this simulates a provider failing",
Subject: &hcl.Range{
Filename: filepath.Join(m.Module.SourceDir, "main.tf"),
Start: hcl.Pos{Line: 13, Column: 45, Byte: 280},
End: hcl.Pos{Line: 13, Column: 72, Byte: 307},
},
},
)
},
// We expect two calls but not the third one, because the second action fails
expectInvokeActionCalls: []providers.InvokeActionRequest{{
ActionType: "act_unlinked",
PlannedActionData: cty.NullVal(cty.Object(map[string]cty.Type{
"attr": cty.String,
})),
}, {
ActionType: "act_unlinked",
PlannedActionData: cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("failure"),
}),
}},
},
"failing an action stops next actions in list": {
toBeImplemented: true, // We need to revisit the diagnostic enhancement
"failing an action during invocation stops next actions in list": {
module: map[string]string{
"main.tf": `
action "act_unlinked" "hello" {}
@ -407,15 +388,19 @@ resource "test_object" "a" {
}
return tfdiags.Diagnostics{}
},
expectDiagnostics: tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Error,
"Failed to apply actions before test_object.a",
`An error occured while invoking action action.act_unlinked.failure: test case for failing: this simulates a provider failing
The following actions were successfully invoked:
- action.act_unlinked.hello
As the resource did not change, these actions will be re-invoked in the next apply.`,
),
expectDiagnostics: func(m *configs.Config) tfdiags.Diagnostics {
return tfdiags.Diagnostics{}.Append(
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Error when invoking action",
Detail: "test case for failing: this simulates a provider failing",
Subject: &hcl.Range{
Filename: filepath.Join(m.Module.SourceDir, "main.tf"),
Start: hcl.Pos{Line: 13, Column: 45, Byte: 280},
End: hcl.Pos{Line: 13, Column: 72, Byte: 307},
},
},
)
},
// We expect two calls but not the third one, because the second action fails
@ -433,7 +418,6 @@ As the resource did not change, these actions will be re-invoked in the next app
},
"failing an action stops next action triggers": {
toBeImplemented: true, // We need to revisit the diagnostic enhancement
module: map[string]string{
"main.tf": `
action "act_unlinked" "hello" {}
@ -475,15 +459,19 @@ resource "test_object" "a" {
}
return tfdiags.Diagnostics{}
},
expectDiagnostics: tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Error,
"Failed to apply actions before test_object.a",
`An error occured while invoking action action.act_unlinked.failure: test case for failing: this simulates a provider failing
The following actions were successfully invoked:
- action.act_unlinked.hello
As the resource did not change, these actions will be re-invoked in the next apply.`,
),
expectDiagnostics: func(m *configs.Config) tfdiags.Diagnostics {
return tfdiags.Diagnostics{}.Append(
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Error when invoking action",
Detail: "test case for failing: this simulates a provider failing",
Subject: &hcl.Range{
Filename: filepath.Join(m.Module.SourceDir, "main.tf"),
Start: hcl.Pos{Line: 17, Column: 18, Byte: 355},
End: hcl.Pos{Line: 17, Column: 45, Byte: 382},
},
},
)
},
// We expect two calls but not the third one, because the second action fails
expectInvokeActionCalls: []providers.InvokeActionRequest{{
@ -962,8 +950,8 @@ resource "test_object" "a" {
tfdiags.AssertNoDiagnostics(t, diags)
_, diags = ctx.Apply(plan, m, nil)
if tc.expectDiagnostics.HasErrors() {
tfdiags.AssertDiagnosticsMatch(t, diags, tc.expectDiagnostics)
if tc.expectDiagnostics != nil {
tfdiags.AssertDiagnosticsMatch(t, diags, tc.expectDiagnostics(m))
} else {
tfdiags.AssertNoDiagnostics(t, diags)
}

@ -692,9 +692,81 @@ resource "test_object" "a" {
expectPlanActionCalled: true,
// We only expect a single diagnostic here, the other should not have been called because the first one failed.
expectPlanDiagnostics: func(m *configs.Config) tfdiags.Diagnostics {
return tfdiags.Diagnostics{
tfdiags.Sourceless(tfdiags.Error, "Planning failed", "Test case simulates an error while planning"),
}
return tfdiags.Diagnostics{}.Append(
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Failed to plan action",
Detail: "Planning failed: Test case simulates an error while planning",
Subject: &hcl.Range{
Filename: filepath.Join(m.Module.SourceDir, "main.tf"),
Start: hcl.Pos{Line: 7, Column: 8, Byte: 149},
End: hcl.Pos{Line: 7, Column: 46, Byte: 177},
},
},
)
},
},
"actions with warnings don't cancel": {
module: map[string]string{
"main.tf": `
action "test_unlinked" "failure" {}
resource "test_object" "a" {
lifecycle {
action_trigger {
events = [before_create]
actions = [action.test_unlinked.failure, action.test_unlinked.failure]
}
action_trigger {
events = [before_create]
actions = [action.test_unlinked.failure]
}
}
}
`,
},
planActionResponse: &providers.PlanActionResponse{
Diagnostics: tfdiags.Diagnostics{
tfdiags.Sourceless(tfdiags.Warning, "Warning during planning", "Test case simulates a warning while planning"),
},
},
expectPlanActionCalled: true,
// We only expect a single diagnostic here, the other should not have been called because the first one failed.
expectPlanDiagnostics: func(m *configs.Config) tfdiags.Diagnostics {
return tfdiags.Diagnostics{}.Append(
&hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Warnings when planning action",
Detail: "Warning during planning: Test case simulates a warning while planning",
Subject: &hcl.Range{
Filename: filepath.Join(m.Module.SourceDir, "main.tf"),
Start: hcl.Pos{Line: 7, Column: 8, Byte: 149},
End: hcl.Pos{Line: 7, Column: 46, Byte: 177},
},
},
&hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Warnings when planning action",
Detail: "Warning during planning: Test case simulates a warning while planning",
Subject: &hcl.Range{
Filename: filepath.Join(m.Module.SourceDir, "main.tf"),
Start: hcl.Pos{Line: 7, Column: 48, Byte: 179},
End: hcl.Pos{Line: 7, Column: 76, Byte: 207},
},
},
&hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Warnings when planning action",
Detail: "Warning during planning: Test case simulates a warning while planning",
Subject: &hcl.Range{
Filename: filepath.Join(m.Module.SourceDir, "main.tf"),
Start: hcl.Pos{Line: 11, Column: 8, Byte: 284},
End: hcl.Pos{Line: 11, Column: 46, Byte: 312},
},
},
)
},
},

@ -6,6 +6,7 @@ package terraform
import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/plans/objchange"
@ -14,8 +15,9 @@ import (
)
type nodeActionTriggerApply struct {
ActionInvocation *plans.ActionInvocationInstanceSrc
resolvedProvider addrs.AbsProviderConfig
ActionInvocation *plans.ActionInvocationInstanceSrc
resolvedProvider addrs.AbsProviderConfig
ActionTriggerRange *hcl.Range
}
var (
@ -34,40 +36,44 @@ func (n *nodeActionTriggerApply) Execute(ctx EvalContext, wo walkOperation) tfdi
// TODO: Handle verifying the condition here, if we have any.
ai := ctx.Changes().GetActionInvocation(actionInvocation.Addr, actionInvocation.ActionTrigger)
if ai == nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Action invocation not found in plan",
"Could not find action invocation for address "+actionInvocation.Addr.String(),
))
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Action invocation not found in plan",
Detail: "Could not find action invocation for address " + actionInvocation.Addr.String(),
Subject: n.ActionTriggerRange,
})
return diags
}
actionData, ok := ctx.Actions().GetActionInstance(ai.Addr)
if !ok {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Action instance not found",
"Could not find action instance for address "+ai.Addr.String(),
))
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Action instance not found",
Detail: "Could not find action instance for address " + ai.Addr.String(),
Subject: n.ActionTriggerRange,
})
return diags
}
provider, schema, err := getProvider(ctx, actionData.ProviderAddr)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
fmt.Sprintf("Failed to get provider for %s", ai.Addr),
fmt.Sprintf("Failed to get provider: %s", err),
))
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Failed to get provider for %s", ai.Addr),
Detail: fmt.Sprintf("Failed to get provider: %s", err),
Subject: n.ActionTriggerRange,
})
return diags
}
actionSchema, ok := schema.Actions[ai.Addr.Action.Action.Type]
if !ok {
// This should have been caught earlier
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
fmt.Sprintf("Action %s not found in provider schema", ai.Addr),
fmt.Sprintf("The action %s was not found in the provider schema for %s", ai.Addr.Action.Action.Type, actionData.ProviderAddr),
))
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Action %s not found in provider schema", ai.Addr),
Detail: fmt.Sprintf("The action %s was not found in the provider schema for %s", ai.Addr.Action.Action.Type, actionData.ProviderAddr),
Subject: n.ActionTriggerRange,
})
return diags
}
@ -77,14 +83,13 @@ func (n *nodeActionTriggerApply) Execute(ctx EvalContext, wo walkOperation) tfdi
// Validate that what we planned matches the action data we have.
errs := objchange.AssertObjectCompatible(actionSchema.ConfigSchema, ai.ConfigValue, unmarkedConfigValue)
for _, err := range errs {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Provider produced inconsistent final plan",
fmt.Sprintf(
"When expanding the plan for %s to include new values learned so far during apply, provider %q produced an invalid new value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.",
ai.Addr, actionData.ProviderAddr.Provider.String(), tfdiags.FormatError(err),
),
))
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Provider produced inconsistent final plan",
Detail: fmt.Sprintf("When expanding the plan for %s to include new values learned so far during apply, provider %q produced an invalid new value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.",
ai.Addr, actionData.ProviderAddr.Provider.String(), tfdiags.FormatError(err)),
Subject: n.ActionTriggerRange,
})
}
hookIdentity := HookActionIdentity{
@ -101,8 +106,12 @@ func (n *nodeActionTriggerApply) Execute(ctx EvalContext, wo walkOperation) tfdi
ClientCapabilities: ctx.ClientCapabilities(),
})
diags = diags.Append(resp.Diagnostics)
if resp.Diagnostics.HasErrors() {
respDiags := n.AddSubjectToDiagnostics(resp.Diagnostics)
diags = diags.Append(respDiags)
if respDiags.HasErrors() {
ctx.Hook(func(h Hook) (HookAction, error) {
return h.CompleteAction(hookIdentity, respDiags.Err())
})
return diags
}
@ -113,7 +122,8 @@ func (n *nodeActionTriggerApply) Execute(ctx EvalContext, wo walkOperation) tfdi
return h.ProgressAction(hookIdentity, ev.Message)
})
case providers.InvokeActionEvent_Completed:
diags = diags.Append(ev.Diagnostics)
// Enhance the diagnostics
diags = diags.Append(n.AddSubjectToDiagnostics(ev.Diagnostics))
ctx.Hook(func(h Hook) (HookAction, error) {
return h.CompleteAction(hookIdentity, ev.Diagnostics.Err())
})
@ -155,3 +165,25 @@ func (n *nodeActionTriggerApply) References() []*addrs.Reference {
func (n *nodeActionTriggerApply) ModulePath() addrs.Module {
return n.ActionInvocation.Addr.Module.Module()
}
func (n *nodeActionTriggerApply) AddSubjectToDiagnostics(input tfdiags.Diagnostics) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
if len(input) > 0 {
severity := hcl.DiagWarning
message := "Warning when invoking action"
err := input.Warnings().ErrWithWarnings()
if input.HasErrors() {
severity = hcl.DiagError
message = "Error when invoking action"
err = input.ErrWithWarnings()
}
diags = diags.Append(&hcl.Diagnostic{
Severity: severity,
Summary: message,
Detail: err.Error(),
Subject: n.ActionTriggerRange,
})
}
return diags
}

@ -100,12 +100,28 @@ func (n *nodeActionTriggerPlanInstance) Execute(ctx EvalContext, operation walkO
ClientCapabilities: ctx.ClientCapabilities(),
})
// TODO: Deal with deferred responses
diags = diags.Append(resp.Diagnostics)
if diags.HasErrors() {
if len(resp.Diagnostics) > 0 {
severity := hcl.DiagWarning
message := "Warnings when planning action"
err := resp.Diagnostics.Warnings().ErrWithWarnings()
if resp.Diagnostics.HasErrors() {
severity = hcl.DiagError
message = "Failed to plan action"
err = resp.Diagnostics.ErrWithWarnings()
}
diags = diags.Append(&hcl.Diagnostic{
Severity: severity,
Summary: message,
Detail: err.Error(),
Subject: n.lifecycleActionTrigger.invokingSubject,
})
}
if resp.Diagnostics.HasErrors() {
return diags
}
// TODO: Deal with deferred responses
ctx.Changes().AppendActionInvocation(&plans.ActionInvocationInstance{
Addr: n.actionAddress,
ProviderAddr: actionInstance.ProviderAddr,

@ -4,6 +4,8 @@
package terraform
import (
"fmt"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/dag"
@ -35,6 +37,28 @@ func (t *ActionDiffTransformer) Transform(g *Graph) error {
ActionInvocation: action,
}
// If the action invocations is triggered within the lifecycle of a resource
// we want to add information about the source location to the apply node
if at, ok := action.ActionTrigger.(plans.LifecycleActionTrigger); ok {
moduleInstance := t.Config.DescendantForInstance(at.TriggeringResourceAddr.Module)
if moduleInstance == nil {
panic(fmt.Sprintf("Could not find module instance for resource %s in config", at.TriggeringResourceAddr.String()))
}
resourceInstance := moduleInstance.Module.ResourceByAddr(at.TriggeringResourceAddr.Resource.Resource)
if resourceInstance == nil {
panic(fmt.Sprintf("Could not find resource instance for resource %s in config", at.TriggeringResourceAddr.String()))
}
triggerBlock := resourceInstance.Managed.ActionTriggers[at.ActionTriggerBlockIndex]
if triggerBlock == nil {
panic(fmt.Sprintf("Could not find action trigger block %d for resource %s in config", at.ActionTriggerBlockIndex, at.TriggeringResourceAddr.String()))
}
act := triggerBlock.Actions[at.ActionsListIndex]
node.ActionTriggerRange = &act.Range
}
g.Add(node)
invocationMap[action] = node

Loading…
Cancel
Save