actions: expand plan triggers to deal with modules

actions/invoke-command-prototype
Daniel Schmidt 6 months ago
parent 470ca6e5d0
commit 7fe8d87ed6

@ -677,6 +677,8 @@ resource "test_object" "a" {
}),
}},
},
// TODO: Test cases for applying an action within a module (instance)
} {
t.Run(name, func(t *testing.T) {
if tc.toBeImplemented {

@ -880,7 +880,6 @@ action "test_linked" "hello" {}
},
"triggered within module": {
toBeImplemented: true, // TODO: Look into this
module: map[string]string{
"main.tf": `
module "mod" {
@ -937,7 +936,6 @@ resource "other_object" "a" {
},
"triggered within module instance": {
toBeImplemented: true, // TODO: Look into this
module: map[string]string{
"main.tf": `
module "mod" {
@ -1020,7 +1018,6 @@ resource "other_object" "a" {
},
"provider is within module": {
toBeImplemented: true, // TODO: Look into this
module: map[string]string{
"main.tf": `
module "mod" {

@ -0,0 +1,133 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package terraform
import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/tfdiags"
)
type nodeActionTriggerPlanInstance struct {
actionAddress addrs.AbsActionInstance
resolvedProvider addrs.AbsProviderConfig
actionConfig *configs.Action
lifecycleActionTrigger *lifecycleActionTriggerInstance
}
type lifecycleActionTriggerInstance struct {
resourceAddress addrs.AbsResourceInstance
events []configs.ActionTriggerEvent
//condition hcl.Expression
actionTriggerBlockIndex int
actionListIndex int
invokingSubject *hcl.Range
}
func (at *lifecycleActionTriggerInstance) Name() string {
return fmt.Sprintf("%s.lifecycle.action_trigger[%d].actions[%d]", at.resourceAddress.String(), at.actionTriggerBlockIndex, at.actionListIndex)
}
var (
_ GraphNodeModuleInstance = (*nodeActionTriggerPlanInstance)(nil)
_ GraphNodeExecutable = (*nodeActionTriggerPlanInstance)(nil)
)
func (n *nodeActionTriggerPlanInstance) Name() string {
triggeredBy := "triggered by "
if n.lifecycleActionTrigger != nil {
triggeredBy += n.lifecycleActionTrigger.resourceAddress.String()
} else {
triggeredBy += "unknown"
}
return fmt.Sprintf("%s %s", n.actionAddress.String(), triggeredBy)
}
func (n *nodeActionTriggerPlanInstance) Execute(ctx EvalContext, operation walkOperation) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
if n.lifecycleActionTrigger == nil {
panic("Only actions triggered by plan and apply are supported")
}
change := ctx.Changes().GetResourceInstanceChange(n.lifecycleActionTrigger.resourceAddress, n.lifecycleActionTrigger.resourceAddress.CurrentObject().DeposedKey)
if change == nil {
panic("change cannot be nil")
}
triggeringEvent, isTriggered := actionIsTriggeredByEvent(n.lifecycleActionTrigger.events, change.Action)
if !isTriggered {
return diags
}
if triggeringEvent == nil {
panic("triggeringEvent cannot be nil")
}
actionInstance, ok := ctx.Actions().GetActionInstance(n.actionAddress)
if !ok {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Reference to non-existant action instance",
Detail: "Action instance was not found in the current context.",
Subject: n.lifecycleActionTrigger.invokingSubject,
})
return diags
}
provider, _, err := getProvider(ctx, actionInstance.ProviderAddr)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Failed to get provider",
Detail: fmt.Sprintf("Failed to get provider: %s", err),
Subject: n.lifecycleActionTrigger.invokingSubject,
})
return diags
}
resp := provider.PlanAction(providers.PlanActionRequest{
ActionType: n.actionAddress.Action.Action.Type,
ProposedActionData: actionInstance.ConfigValue,
ClientCapabilities: ctx.ClientCapabilities(),
})
// TODO: Deal with deferred responses
diags = diags.Append(resp.Diagnostics)
if diags.HasErrors() {
return diags
}
ctx.Changes().AppendActionInvocation(&plans.ActionInvocationInstance{
Addr: n.actionAddress,
ProviderAddr: actionInstance.ProviderAddr,
ActionTrigger: plans.LifecycleActionTrigger{
TriggeringResourceAddr: n.lifecycleActionTrigger.resourceAddress,
ActionTriggerEvent: *triggeringEvent,
ActionTriggerBlockIndex: n.lifecycleActionTrigger.actionTriggerBlockIndex,
ActionsListIndex: n.lifecycleActionTrigger.actionListIndex,
},
ConfigValue: actionInstance.ConfigValue,
})
return diags
}
func (n *nodeActionTriggerPlanInstance) ModulePath() addrs.Module {
return n.Path().Module()
}
func (n *nodeActionTriggerPlanInstance) Path() addrs.ModuleInstance {
// Actions can only be triggered by the CLI in which case they belong to the module they are in
// or by resources during plan/apply in which case both the resource and action must belong
// to the same module. So we can simply return the module path of the action.
return n.actionAddress.Module
}

@ -9,15 +9,14 @@ import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/tfdiags"
)
type nodeActionTriggerPlan struct {
actionAddress addrs.AbsActionInstance
resolvedProvider addrs.AbsProviderConfig
actionConfig *configs.Action
type nodeActionTriggerPlanExpand struct {
actionAddress addrs.ConfigAction
actionInstanceKey addrs.InstanceKey // TODO: This should probably be a new address? Look at resources
resolvedProvider addrs.AbsProviderConfig
actionConfig *configs.Action
lifecycleActionTrigger *lifecycleActionTrigger
}
@ -36,12 +35,11 @@ func (at *lifecycleActionTrigger) Name() string {
}
var (
_ GraphNodeExecutable = (*nodeActionTriggerPlan)(nil)
_ GraphNodeReferencer = (*nodeActionTriggerPlan)(nil)
_ GraphNodeProviderConsumer = (*nodeActionTriggerPlan)(nil)
_ GraphNodeDynamicExpandable = (*nodeActionTriggerPlanExpand)(nil)
_ GraphNodeReferencer = (*nodeActionTriggerPlanExpand)(nil)
)
func (n *nodeActionTriggerPlan) Name() string {
func (n *nodeActionTriggerPlanExpand) Name() string {
triggeredBy := "triggered by "
if n.lifecycleActionTrigger != nil {
triggeredBy += n.lifecycleActionTrigger.resourceAddress.String()
@ -52,87 +50,49 @@ func (n *nodeActionTriggerPlan) Name() string {
return fmt.Sprintf("%s %s", n.actionAddress.String(), triggeredBy)
}
func (n *nodeActionTriggerPlan) Execute(ctx EvalContext, operation walkOperation) tfdiags.Diagnostics {
func (n *nodeActionTriggerPlanExpand) DynamicExpand(ctx EvalContext) (*Graph, tfdiags.Diagnostics) {
var g Graph
var diags tfdiags.Diagnostics
if n.lifecycleActionTrigger == nil {
panic("Only actions triggered by plan and apply are supported")
}
_, keys, _ := ctx.InstanceExpander().ResourceInstanceKeys(n.lifecycleActionTrigger.resourceAddress.Absolute(addrs.RootModuleInstance))
for _, key := range keys {
change := ctx.Changes().
GetResourceInstanceChange(
n.lifecycleActionTrigger.resourceAddress.Absolute(
addrs.RootModuleInstance).
Instance(key),
addrs.NotDeposed)
if change == nil {
panic("change cannot be nil")
expander := ctx.InstanceExpander()
// First we expand the module
moduleInstances := expander.ExpandModule(n.lifecycleActionTrigger.resourceAddress.Module, false)
for _, module := range moduleInstances {
_, keys, _ := expander.ResourceInstanceKeys(n.lifecycleActionTrigger.resourceAddress.Absolute(module))
for _, key := range keys {
absResourceInstanceAddr := n.lifecycleActionTrigger.resourceAddress.Absolute(module).Instance(key)
absActionAddr := n.actionAddress.Absolute(module).Instance(n.actionInstanceKey)
node := nodeActionTriggerPlanInstance{
actionAddress: absActionAddr,
resolvedProvider: n.resolvedProvider,
actionConfig: n.actionConfig,
lifecycleActionTrigger: &lifecycleActionTriggerInstance{
resourceAddress: absResourceInstanceAddr,
events: n.lifecycleActionTrigger.events,
actionTriggerBlockIndex: n.lifecycleActionTrigger.actionTriggerBlockIndex,
actionListIndex: n.lifecycleActionTrigger.actionListIndex,
invokingSubject: n.lifecycleActionTrigger.invokingSubject,
},
}
g.Add(&node)
}
triggeringEvent, isTriggered := actionIsTriggeredByEvent(n.lifecycleActionTrigger.events, change.Action)
if !isTriggered {
return nil
}
actionInstance, ok := ctx.Actions().GetActionInstance(n.actionAddress)
if !ok {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Reference to non-existant action instance",
Detail: "Action instance was not found in the current context.",
Subject: n.lifecycleActionTrigger.invokingSubject,
})
return diags
}
provider, _, err := getProvider(ctx, actionInstance.ProviderAddr)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Failed to get provider",
Detail: fmt.Sprintf("Failed to get provider: %s", err),
Subject: n.lifecycleActionTrigger.invokingSubject,
})
return diags
}
resp := provider.PlanAction(providers.PlanActionRequest{
ActionType: n.actionAddress.Action.Action.Type,
ProposedActionData: actionInstance.ConfigValue,
ClientCapabilities: ctx.ClientCapabilities(),
})
// TODO: Deal with deferred responses
diags = diags.Append(resp.Diagnostics)
if diags.HasErrors() {
return diags
}
ctx.Changes().AppendActionInvocation(&plans.ActionInvocationInstance{
Addr: n.actionAddress,
ProviderAddr: actionInstance.ProviderAddr,
ActionTrigger: plans.LifecycleActionTrigger{
TriggeringResourceAddr: n.lifecycleActionTrigger.resourceAddress.Absolute(addrs.RootModuleInstance).Instance(key),
ActionTriggerEvent: *triggeringEvent,
ActionTriggerBlockIndex: n.lifecycleActionTrigger.actionTriggerBlockIndex,
ActionsListIndex: n.lifecycleActionTrigger.actionListIndex,
},
ConfigValue: actionInstance.ConfigValue,
})
}
return diags
addRootNodeToGraph(&g)
return &g, diags
}
func (n *nodeActionTriggerPlan) ModulePath() addrs.Module {
return addrs.RootModule
func (n *nodeActionTriggerPlanExpand) ModulePath() addrs.Module {
return n.actionAddress.Module
}
func (n *nodeActionTriggerPlan) References() []*addrs.Reference {
func (n *nodeActionTriggerPlanExpand) References() []*addrs.Reference {
var refs []*addrs.Reference
refs = append(refs, &addrs.Reference{
Subject: n.actionAddress.Action,
@ -147,7 +107,7 @@ func (n *nodeActionTriggerPlan) References() []*addrs.Reference {
return refs
}
func (n *nodeActionTriggerPlan) ProvidedBy() (addr addrs.ProviderConfig, exact bool) {
func (n *nodeActionTriggerPlanExpand) ProvidedBy() (addr addrs.ProviderConfig, exact bool) {
if n.resolvedProvider.Provider.Type != "" {
return n.resolvedProvider, true
}
@ -160,10 +120,10 @@ func (n *nodeActionTriggerPlan) ProvidedBy() (addr addrs.ProviderConfig, exact b
}, false
}
func (n *nodeActionTriggerPlan) Provider() (provider addrs.Provider) {
func (n *nodeActionTriggerPlanExpand) Provider() (provider addrs.Provider) {
return n.actionConfig.Provider
}
func (n *nodeActionTriggerPlan) SetProvider(config addrs.AbsProviderConfig) {
func (n *nodeActionTriggerPlanExpand) SetProvider(config addrs.AbsProviderConfig) {
n.resolvedProvider = config
}

@ -8,6 +8,7 @@ import (
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/dag"
)
type ActionPlanTransformer struct {
@ -44,6 +45,23 @@ func (t *ActionPlanTransformer) transformSingle(g *Graph, config *configs.Config
actionConfigs.Put(a.Addr().InModule(config.Path), a)
}
resourceNodes := addrs.MakeMap[addrs.ConfigResource, []GraphNodeConfigResource]()
for _, node := range g.Vertices() {
rn, ok := node.(GraphNodeConfigResource)
if !ok {
continue
}
// We ignore any instances that _also_ implement
// GraphNodeResourceInstance, since in the unlikely event that they
// do exist we'd probably end up creating cycles by connecting them.
if _, ok := node.(GraphNodeResourceInstance); ok {
continue
}
rAddr := rn.ResourceAddr()
resourceNodes.Put(rAddr, append(resourceNodes.Get(rAddr), rn))
}
for _, r := range config.Module.ManagedResources {
for i, at := range r.Managed.ActionTriggers {
for j, action := range at.Actions {
@ -51,30 +69,39 @@ func (t *ActionPlanTransformer) transformSingle(g *Graph, config *configs.Config
if parseRefDiags != nil {
return parseRefDiags.Err()
}
var instance addrs.AbsActionInstance
var instance addrs.ConfigAction
actionInstanceKey := addrs.NoKey
switch ai := ref.Subject.(type) {
case addrs.Action:
instance = ai.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
instance = ai.InModule(config.Path)
case addrs.ActionInstance:
instance = ai.Absolute(addrs.RootModuleInstance)
instance = ai.Action.InModule(config.Path)
actionInstanceKey = ai.Key
default:
// This should have been caught during validation
panic(fmt.Sprintf("unexpected action address %T", ai))
}
actionConfig, ok := actionConfigs.GetOk(instance.ConfigAction())
actionConfig, ok := actionConfigs.GetOk(instance)
if !ok {
// This should have been caught during validation
panic(fmt.Sprintf("actionConfig not found for %s", instance))
panic(fmt.Sprintf("action config not found for %s", instance))
}
nat := &nodeActionTriggerPlan{
actionAddress: instance,
actionConfig: actionConfig,
resourceAddr := r.Addr().InModule(config.Path)
resourceNode, ok := resourceNodes.GetOk(resourceAddr)
if !ok {
panic(fmt.Sprintf("Could not find node for %s", resourceAddr))
}
nat := &nodeActionTriggerPlanExpand{
actionAddress: instance,
actionInstanceKey: actionInstanceKey,
actionConfig: actionConfig,
lifecycleActionTrigger: &lifecycleActionTrigger{
events: at.Events,
resourceAddress: r.Addr().InModule(config.Path),
resourceAddress: resourceAddr,
actionTriggerBlockIndex: i,
actionListIndex: j,
invokingSubject: action.Traversal.SourceRange().Ptr(),
@ -82,6 +109,11 @@ func (t *ActionPlanTransformer) transformSingle(g *Graph, config *configs.Config
}
g.Add(nat)
// We always want to plan after the resource is done planning
for _, node := range resourceNode {
g.Connect(dag.BasicEdge(nat, node))
}
}
}
}

@ -18,14 +18,14 @@ type ActionDiffTransformer struct {
}
func (t *ActionDiffTransformer) Transform(g *Graph) error {
applyNodes := addrs.MakeMap[addrs.AbsResource, *nodeExpandApplyableResource]()
applyNodes := addrs.MakeMap[addrs.ConfigResource, *nodeExpandApplyableResource]()
for _, vs := range g.Vertices() {
applyableResource, ok := vs.(*nodeExpandApplyableResource)
if !ok {
continue
}
applyNodes.Put(applyableResource.Addr.Absolute(addrs.RootModuleInstance), applyableResource)
applyNodes.Put(applyableResource.Addr, applyableResource)
}
for _, action := range t.Changes.ActionInvocations {
@ -40,7 +40,7 @@ func (t *ActionDiffTransformer) Transform(g *Graph) error {
// Add edges
if lat, ok := action.ActionTrigger.(plans.LifecycleActionTrigger); ok {
// Add edges for lifecycle action triggers
resourceNode, ok := applyNodes.GetOk(lat.TriggeringResourceAddr.ContainingResource())
resourceNode, ok := applyNodes.GetOk(lat.TriggeringResourceAddr.ConfigResource())
if !ok {
panic("Could not find resource node for lifecycle action trigger")
}

Loading…
Cancel
Save