diff --git a/internal/terraform/context_apply_action_test.go b/internal/terraform/context_apply_action_test.go index fb943b29d5..2f32e47a58 100644 --- a/internal/terraform/context_apply_action_test.go +++ b/internal/terraform/context_apply_action_test.go @@ -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 { diff --git a/internal/terraform/context_plan_actions_test.go b/internal/terraform/context_plan_actions_test.go index 37f4163b9d..1f5697ed1e 100644 --- a/internal/terraform/context_plan_actions_test.go +++ b/internal/terraform/context_plan_actions_test.go @@ -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" { diff --git a/internal/terraform/node_action_trigger_instance_plan.go b/internal/terraform/node_action_trigger_instance_plan.go new file mode 100644 index 0000000000..4876558e0d --- /dev/null +++ b/internal/terraform/node_action_trigger_instance_plan.go @@ -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 +} diff --git a/internal/terraform/node_action_trigger_plan.go b/internal/terraform/node_action_trigger_plan.go index 24347d8114..004ec93606 100644 --- a/internal/terraform/node_action_trigger_plan.go +++ b/internal/terraform/node_action_trigger_plan.go @@ -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 } diff --git a/internal/terraform/transform_action_plan.go b/internal/terraform/transform_action_plan.go index 70276616a3..4741b4a0aa 100644 --- a/internal/terraform/transform_action_plan.go +++ b/internal/terraform/transform_action_plan.go @@ -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)) + } } } } diff --git a/internal/terraform/transformer_action_diff.go b/internal/terraform/transformer_action_diff.go index 9447790b94..922d775e3f 100644 --- a/internal/terraform/transformer_action_diff.go +++ b/internal/terraform/transformer_action_diff.go @@ -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") }