@ -6,9 +6,12 @@ package terraform
import (
"fmt"
"sort"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/actions"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/dag"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/plans/objchange"
@ -39,10 +42,16 @@ func (n *nodeActionApply) DotNode(string, *dag.DotOpts) *dag.DotNode {
}
func ( n * nodeActionApply ) Execute ( ctx EvalContext , _ walkOperation ) ( diags tfdiags . Diagnostics ) {
return invokeActions ( ctx , n . ActionInvocation s)
return invokeActions WithEnhancedDiagnostics ( ctx , n . ActionInvocation s, & n . TriggeringResourceaddr s)
}
func invokeActions ( ctx EvalContext , actionInvocations [ ] * plans . ActionInvocationInstanceSrc ) tfdiags . Diagnostics {
func invokeActionsWithEnhancedDiagnostics ( ctx EvalContext , actionInvocations [ ] * plans . ActionInvocationInstanceSrc , triggeringResourceAddrs * addrs . AbsResourceInstance ) tfdiags . Diagnostics {
finishedActionInvocations , diags := invokeActions ( ctx , actionInvocations )
return betterDiags ( finishedActionInvocations , actionInvocations , diags , triggeringResourceAddrs )
}
func invokeActions ( ctx EvalContext , actionInvocations [ ] * plans . ActionInvocationInstanceSrc ) ( [ ] * plans . ActionInvocationInstance , tfdiags . Diagnostics ) {
var finishedActionInvocations [ ] * plans . ActionInvocationInstance
var diags tfdiags . Diagnostics
// First we order the action invocations by their trigger block index and events list index.
// This way we have the correct order of execution.
@ -56,7 +65,7 @@ func invokeActions(ctx EvalContext, actionInvocations []*plans.ActionInvocationI
fmt . Sprintf ( "Failed to find action invocation instance %s in changes." , ai . Addr ) ,
fmt . Sprintf ( "The action invocation instance %s was not found in the changes for %s." , ai . Addr , ai . TriggeringResourceAddr . String ( ) ) ,
) )
return diags
return finishedActionInvocations, diags
}
orderedActionInvocations = append ( orderedActionInvocations , ai )
@ -78,7 +87,7 @@ func invokeActions(ctx EvalContext, actionInvocations []*plans.ActionInvocationI
"Action instance not found" ,
"Could not find action instance for address " + invocation . Addr . String ( ) ,
) )
return diags
return finishedActionInvocations, diags
}
orderedActionData [ i ] = ai
@ -99,7 +108,7 @@ func invokeActions(ctx EvalContext, actionInvocations []*plans.ActionInvocationI
fmt . Sprintf ( "Failed to get provider for %s" , ai . Addr ) ,
fmt . Sprintf ( "Failed to get provider: %s" , err ) ,
) )
return diags
return finishedActionInvocations, diags
}
actionSchema , ok := schema . Actions [ ai . Addr . Action . Action . Type ]
@ -110,7 +119,7 @@ func invokeActions(ctx EvalContext, actionInvocations []*plans.ActionInvocationI
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 ) ,
) )
return diags
return finishedActionInvocations, diags
}
// We don't want to send the marks, but all marks are okay in the context of an action invocation.
@ -147,7 +156,7 @@ func invokeActions(ctx EvalContext, actionInvocations []*plans.ActionInvocationI
diags = diags . Append ( resp . Diagnostics )
if resp . Diagnostics . HasErrors ( ) {
return diags
return finishedActionInvocations, diags
}
for event := range resp . Events {
@ -162,11 +171,9 @@ func invokeActions(ctx EvalContext, actionInvocations []*plans.ActionInvocationI
return h . CompleteAction ( hookIdentity , ev . Diagnostics . Err ( ) )
} )
if ev . Diagnostics . HasErrors ( ) {
// TODO: We would want to add some warning / error telling the user how to recover
// from this, or maybe attach this info to the diagnostics sent by the provider.
// For now we just return the diagnostics.
return diags
return finishedActionInvocations , diags
} else {
finishedActionInvocations = append ( finishedActionInvocations , ai )
}
default :
panic ( fmt . Sprintf ( "unexpected action event type %T" , ev ) )
@ -174,7 +181,7 @@ func invokeActions(ctx EvalContext, actionInvocations []*plans.ActionInvocationI
}
}
return diags
return finishedActionInvocations, diags
}
func ( n * nodeActionApply ) ModulePath ( ) addrs . Module {
@ -201,3 +208,99 @@ func (n *nodeActionApply) Actions() []addrs.ConfigAction {
}
return ret
}
func betterDiags ( finishedActionInvocations [ ] * plans . ActionInvocationInstance , allActionInvocations [ ] * plans . ActionInvocationInstanceSrc , diags tfdiags . Diagnostics , triggeringResourceAddrs * addrs . AbsResourceInstance ) tfdiags . Diagnostics {
// If everything went well, we can return the diagnostics as is.
if ! diags . HasErrors ( ) {
return diags
}
if triggeringResourceAddrs == nil {
panic ( "We currently don't support actions without triggering resources in this code path, this is a bug." )
}
// Something went wrong, the user might need to take action so that
// - the actions that failed or that were not executed can be retried
// - the user can undo side-effects of actions that were executed successfully before the
// failure and will be re-run in the next apply.
// We know that the last not run action invocation is the one that triggered the failure, so we can use that to inform the user.
// TODO: We should have a source range on the action invocation to use in the subject of the diagnostic.
failingActionInvocation := allActionInvocations [ len ( finishedActionInvocations ) ]
if areBeforeActionInvocations ( allActionInvocations ) {
// Before actions need to let the user know that they will be re-run in the next apply
alreadyRunActionText := ""
if len ( finishedActionInvocations ) > 0 {
alreadyRunActions := [ ] string { }
for _ , ai := range finishedActionInvocations {
alreadyRunActions = append ( alreadyRunActions , fmt . Sprintf ( "- %s" , ai . Addr ) )
}
alreadyRunActionText = fmt . Sprintf ( ` The following actions were successfully invoked :
% s
As the resource did not change , these actions will be re - invoked in the next apply . ` ,
strings . Join ( alreadyRunActions , "\n" ) )
}
return tfdiags . Diagnostics { } . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : fmt . Sprintf ( "Failed to apply actions before %s" , triggeringResourceAddrs ) ,
Detail : fmt . Sprintf (
` An error occured while invoking action % s : % s
% s ` ,
failingActionInvocation . Addr . String ( ) ,
diags . ErrWithWarnings ( ) ,
alreadyRunActionText ,
) ,
// TODO: Add subject here (we need to record the source range in the action invocation)
} )
} else {
missingActionInvocations := allActionInvocations [ len ( finishedActionInvocations ) : ]
missingActions := [ ] string { }
for _ , ai := range missingActionInvocations {
missingActions = append ( missingActions , fmt . Sprintf ( "- %s" , ai . Addr ) )
}
return tfdiags . Diagnostics { } . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : fmt . Sprintf ( "Failed to apply actions after %s" , triggeringResourceAddrs ) ,
Detail : fmt . Sprintf (
` An error occured while invoking action % s : % s
The following actions were not yet invoked :
% s
These actions will not be triggered in the next apply , please run "terraform invoke" to invoke them . ` ,
failingActionInvocation . Addr . String ( ) ,
diags . ErrWithWarnings ( ) ,
strings . Join ( missingActions , "\n" ) ,
) ,
// TODO: Add subject here (we need to record the source range in the action invocation)
} )
}
}
// areBeforeActionInvocations checks if all action invocations are for before actions.
// It panics if the action invocations are empty or if they have different trigger events.
func areBeforeActionInvocations ( actionInvocations [ ] * plans . ActionInvocationInstanceSrc ) bool {
if len ( actionInvocations ) == 0 {
panic ( "areBeforeActionInvocations called with empty actionInvocations" )
}
firstEvent := actionInvocations [ 0 ] . TriggerEvent
for _ , ai := range actionInvocations {
if ai . TriggerEvent != firstEvent {
panic ( fmt . Sprintf ( "areBeforeActionInvocations called with action invocations with different trigger events: %s != %s" , firstEvent , ai . TriggerEvent ) )
}
}
switch firstEvent {
case configs . BeforeCreate , configs . BeforeUpdate , configs . BeforeDestroy :
return true
case configs . AfterCreate , configs . AfterUpdate , configs . AfterDestroy :
return false
default :
panic ( fmt . Sprintf ( "areBeforeActionInvocations called with unknown trigger event: %s" , firstEvent ) )
}
}