diff --git a/internal/actions/actions.go b/internal/actions/actions.go deleted file mode 100644 index 1a3702547912..000000000000 --- a/internal/actions/actions.go +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright IBM Corp. 2014, 2026 -// SPDX-License-Identifier: BUSL-1.1 - -package actions - -import ( - "sync" - - "github.com/hashicorp/terraform/internal/addrs" - "github.com/zclconf/go-cty/cty" -) - -// Actions keeps track of action declarations accessible to the context. -// It is used to plan and execute actions in the context of a Terraform configuration. -type Actions struct { - // Must hold this lock when accessing all fields after this one. - mu sync.Mutex - - actionInstances addrs.Map[addrs.AbsActionInstance, ActionData] - partialExpandedActions addrs.Map[addrs.PartialExpandedAction, ActionData] -} - -func NewActions() *Actions { - return &Actions{ - actionInstances: addrs.MakeMap[addrs.AbsActionInstance, ActionData](), - partialExpandedActions: addrs.MakeMap[addrs.PartialExpandedAction, ActionData](), - } -} - -type ActionData struct { - ConfigValue cty.Value - ProviderAddr addrs.AbsProviderConfig -} - -func (a *Actions) AddActionInstance(addr addrs.AbsActionInstance, configValue cty.Value, providerAddr addrs.AbsProviderConfig) { - a.mu.Lock() - defer a.mu.Unlock() - - if a.actionInstances.Has(addr) { - panic("action instance already exists: " + addr.String()) - } - - a.actionInstances.Put(addr, ActionData{ - ConfigValue: configValue, - ProviderAddr: providerAddr, - }) -} - -func (a *Actions) GetActionInstance(addr addrs.AbsActionInstance) (*ActionData, bool) { - a.mu.Lock() - defer a.mu.Unlock() - - data, ok := a.actionInstances.GetOk(addr) - - if !ok { - return nil, false - } - - return &data, true -} - -func (a *Actions) GetActionInstanceKeys(addr addrs.AbsAction) []addrs.AbsActionInstance { - a.mu.Lock() - defer a.mu.Unlock() - - result := []addrs.AbsActionInstance{} - for _, data := range a.actionInstances.Elements() { - if data.Key.ContainingAction().Equal(addr) { - result = append(result, data.Key) - } - } - - return result -} - -func (a *Actions) AddPartialExpandedAction(addr addrs.PartialExpandedAction, configValue cty.Value, providerAddr addrs.AbsProviderConfig) { - a.mu.Lock() - defer a.mu.Unlock() - - if a.partialExpandedActions.Has(addr) { - panic("action instance already exists: " + addr.String()) - } - - a.partialExpandedActions.Put(addr, ActionData{ - ConfigValue: configValue, - ProviderAddr: providerAddr, - }) -} - -func (a *Actions) GetPartialExpandedAction(addr addrs.PartialExpandedAction) (*ActionData, bool) { - a.mu.Lock() - defer a.mu.Unlock() - - data, ok := a.partialExpandedActions.GetOk(addr) - - if !ok { - return nil, false - } - - return &data, true -} diff --git a/internal/configs/module.go b/internal/configs/module.go index 5437360713c9..52e63ed320fb 100644 --- a/internal/configs/module.go +++ b/internal/configs/module.go @@ -215,6 +215,15 @@ func (m *Module) ResourceByAddr(addr addrs.Resource) *Resource { } } +// ActionByAddr returns the configuration for the action with the given +// address, or nil if there is no such action. +func (m *Module) ActionByAddr(addr addrs.Action) *Action { + if a, ok := m.Actions[addr.String()]; ok { + return a + } + return nil +} + func (m *Module) appendFile(file *File) hcl.Diagnostics { var diags hcl.Diagnostics diff --git a/internal/instances/expander.go b/internal/instances/expander.go index a0f7ae43a25a..8724a5cd5dac 100644 --- a/internal/instances/expander.go +++ b/internal/instances/expander.go @@ -590,6 +590,20 @@ func (e *Expander) knowsResource(want addrs.AbsResource) bool { return e.exps.knowsResource(want) } +func (e *Expander) knowsActionInstance(want addrs.AbsActionInstance) bool { + e.mu.Lock() + defer e.mu.Unlock() + + return e.exps.knowsActionInstance(want) +} + +func (e *Expander) knowsAction(want addrs.AbsAction) bool { + e.mu.Lock() + defer e.mu.Unlock() + + return e.exps.knowsAction(want) +} + type expanderModule struct { moduleCalls map[addrs.ModuleCall]expansion resources map[addrs.Resource]expansion @@ -1055,7 +1069,7 @@ func (m *expanderModule) onlyActionInstances(actionAddr addrs.Action, parentAddr // GetActionInstanceRepetitionData returns an object describing the values // that should be available for each.key, each.value, and count.index within -// the definition block for the given resource instance. +// the definition block for the given action instance. func (e *Expander) GetActionInstanceRepetitionData(addr addrs.AbsActionInstance) RepetitionData { e.mu.RLock() defer e.mu.RUnlock() @@ -1100,3 +1114,30 @@ func (e *Expander) ActionInstanceKeys(addr addrs.AbsAction) (keyType addrs.Insta } return exp.instanceKeys() } + +func (m *expanderModule) knowsActionInstance(want addrs.AbsActionInstance) bool { + modInst := m.getModuleInstance(want.Module) + if modInst == nil { + return false + } + resourceExp := modInst.actions[want.Action.Action] + if resourceExp == nil { + return false + } + _, knownKeys, _ := resourceExp.instanceKeys() + for _, key := range knownKeys { + if key == want.Action.Key { + return true + } + } + return false +} + +func (m *expanderModule) knowsAction(want addrs.AbsAction) bool { + modInst := m.getModuleInstance(want.Module) + if modInst == nil { + return false + } + _, ret := modInst.actions[want.Action] + return ret +} diff --git a/internal/instances/set.go b/internal/instances/set.go index 119f2a51d117..4d25b1b74778 100644 --- a/internal/instances/set.go +++ b/internal/instances/set.go @@ -52,3 +52,15 @@ func (s Set) HasResource(want addrs.AbsResource) bool { func (s Set) InstancesForModule(modAddr addrs.Module, includeDirectOverrides bool) []addrs.ModuleInstance { return s.exp.expandModule(modAddr, true, includeDirectOverrides) } + +// HasActionInstance returns true if and only if the set contains the actions +// instance with the given address. +func (s Set) HasActionInstance(want addrs.AbsActionInstance) bool { + return s.exp.knowsActionInstance(want) +} + +// HasAction returns true if and only if the set contains the actions with +// the given address, even if that action has no instances. +func (s Set) HasAction(want addrs.AbsAction) bool { + return s.exp.knowsAction(want) +} diff --git a/internal/terraform/context_validate_test.go b/internal/terraform/context_validate_test.go index f4f726e456fa..a1918e35f59b 100644 --- a/internal/terraform/context_validate_test.go +++ b/internal/terraform/context_validate_test.go @@ -2870,6 +2870,14 @@ func TestContext2Validate_deprecatedAttr(t *testing.T) { "main.tf": ` resource "aws_instance" "test" { } + resource "aws_instance" "other" { + lifecycle { + action_trigger { + events = [ before_create ] + actions = [ action.aws_register.example] + } + } + } action "aws_register" "example" { config { host = aws_instance.test.foo @@ -2884,8 +2892,8 @@ func TestContext2Validate_deprecatedAttr(t *testing.T) { Detail: `Deprecated resource attribute "foo" used. Refer to the provider documentation for details.`, Subject: &hcl.Range{ Filename: filepath.Join(c.Module.SourceDir, "main.tf"), - Start: hcl.Pos{Line: 6, Column: 24, Byte: 152}, - End: hcl.Pos{Line: 6, Column: 45, Byte: 173}, + Start: hcl.Pos{Line: 14, Column: 24, Byte: 328}, + End: hcl.Pos{Line: 14, Column: 45, Byte: 349}, }, }) }, diff --git a/internal/terraform/context_walk.go b/internal/terraform/context_walk.go index 886fab9f12a2..18fac7454259 100644 --- a/internal/terraform/context_walk.go +++ b/internal/terraform/context_walk.go @@ -7,7 +7,6 @@ import ( "log" "time" - "github.com/hashicorp/terraform/internal/actions" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/checks" "github.com/hashicorp/terraform/internal/configs" @@ -201,7 +200,6 @@ func (c *Context) graphWalker(graph *Graph, operation walkOperation, opts *graph PlanTimestamp: opts.PlanTimeTimestamp, functionResults: opts.FunctionResults, Forget: opts.Forget, - Actions: actions.NewActions(), Deprecations: deprecation.NewDeprecations(), } } diff --git a/internal/terraform/eval_context.go b/internal/terraform/eval_context.go index 7715fb2ad435..1f2ca2421e7a 100644 --- a/internal/terraform/eval_context.go +++ b/internal/terraform/eval_context.go @@ -9,7 +9,6 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty/cty" - "github.com/hashicorp/terraform/internal/actions" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/checks" "github.com/hashicorp/terraform/internal/configs" @@ -219,11 +218,6 @@ type EvalContext interface { // only allowed in the context of a destroy plan. Forget() bool - // Actions returns the actions object that tracks all of the action - // declarations and their instances that are available in this - // EvalContext. - Actions() *actions.Actions - // Deprecations returns the deprecations object that tracks meta-information // about deprecation, e.g. which module calls suppress deprecation warnings. Deprecations() *deprecation.Deprecations diff --git a/internal/terraform/eval_context_builtin.go b/internal/terraform/eval_context_builtin.go index 30f174680fcf..d7eb3ce4dd74 100644 --- a/internal/terraform/eval_context_builtin.go +++ b/internal/terraform/eval_context_builtin.go @@ -13,7 +13,6 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/function" - "github.com/hashicorp/terraform/internal/actions" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/checks" "github.com/hashicorp/terraform/internal/configs" @@ -93,7 +92,6 @@ type BuiltinEvalContext struct { InstanceExpanderValue *instances.Expander MoveResultsValue refactoring.MoveResults OverrideValues *mocking.Overrides - ActionsValue *actions.Actions DeprecationsValue *deprecation.Deprecations } @@ -663,10 +661,6 @@ func (ctx *BuiltinEvalContext) ClientCapabilities() providers.ClientCapabilities } } -func (ctx *BuiltinEvalContext) Actions() *actions.Actions { - return ctx.ActionsValue -} - func (ctx *BuiltinEvalContext) Deprecations() *deprecation.Deprecations { return ctx.DeprecationsValue } diff --git a/internal/terraform/eval_context_mock.go b/internal/terraform/eval_context_mock.go index e1b54ce43974..0d3542866a65 100644 --- a/internal/terraform/eval_context_mock.go +++ b/internal/terraform/eval_context_mock.go @@ -11,7 +11,6 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/convert" - "github.com/hashicorp/terraform/internal/actions" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/checks" "github.com/hashicorp/terraform/internal/configs" @@ -169,9 +168,6 @@ type MockEvalContext struct { ForgetCalled bool ForgetValues bool - ActionsCalled bool - ActionsState *actions.Actions - DeprecationCalled bool DeprecationState *deprecation.Deprecations } @@ -451,11 +447,6 @@ func (ctx *MockEvalContext) ClientCapabilities() providers.ClientCapabilities { } } -func (c *MockEvalContext) Actions() *actions.Actions { - c.ActionsCalled = true - return c.ActionsState -} - func (c *MockEvalContext) Deprecations() *deprecation.Deprecations { c.DeprecationCalled = true if c.DeprecationState != nil { diff --git a/internal/terraform/graph_walk_context.go b/internal/terraform/graph_walk_context.go index 445398a31db1..04c86f2edbaa 100644 --- a/internal/terraform/graph_walk_context.go +++ b/internal/terraform/graph_walk_context.go @@ -8,7 +8,6 @@ import ( "sync" "time" - "github.com/hashicorp/terraform/internal/actions" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/checks" "github.com/hashicorp/terraform/internal/collections" @@ -57,7 +56,6 @@ type ContextGraphWalker struct { // only allowed in the context of a destroy plan. Forget bool - Actions *actions.Actions Deprecations *deprecation.Deprecations // This is an output. Do not set this, nor read it while a graph walk @@ -146,7 +144,6 @@ func (w *ContextGraphWalker) EvalContext() EvalContext { Evaluator: evaluator, OverrideValues: w.Overrides, forget: w.Forget, - ActionsValue: w.Actions, DeprecationsValue: w.Deprecations, } diff --git a/internal/terraform/node_action_instance.go b/internal/terraform/node_action_instance.go index 26f14055e33b..6d39ce388b2d 100644 --- a/internal/terraform/node_action_instance.go +++ b/internal/terraform/node_action_instance.go @@ -4,8 +4,6 @@ package terraform import ( - "github.com/zclconf/go-cty/cty" - "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/lang/langrefs" @@ -42,46 +40,11 @@ func (n *NodeAbstractActionInstance) Path() addrs.ModuleInstance { } func (n *NodeAbstractActionInstance) Execute(ctx EvalContext, _ walkOperation) tfdiags.Diagnostics { - var diags tfdiags.Diagnostics - deferrals := ctx.Deferrals() if deferrals.DeferralAllowed() && deferrals.ShouldDeferAction(n.Dependencies) { deferrals.ReportActionDeferred(n.Addr, providers.DeferredReasonDeferredPrereq) - return diags - } - - // This should have been caught already - if n.Schema == nil { - panic("NodeActionDeclarationInstance.Execute called without a schema") - } - - allInsts := ctx.InstanceExpander() - keyData := allInsts.GetActionInstanceRepetitionData(n.Addr) - - configVal := cty.NullVal(n.Schema.ConfigSchema.ImpliedType()) - if n.Config.Config != nil { - var configDiags tfdiags.Diagnostics - configVal, _, configDiags = ctx.EvaluateBlock(n.Config.Config, n.Schema.ConfigSchema.DeepCopy(), nil, keyData) - - diags = diags.Append(configDiags) - if configDiags.HasErrors() { - return diags - } - - valDiags := validateResourceForbiddenEphemeralValues(ctx, configVal, n.Schema.ConfigSchema) - diags = diags.Append(valDiags.InConfigBody(n.Config.Config, n.Addr.String())) - - var deprecationDiags tfdiags.Diagnostics - configVal, deprecationDiags = ctx.Deprecations().ValidateAndUnmarkConfig(configVal, n.Schema.ConfigSchema, n.ModulePath()) - diags = diags.Append(deprecationDiags.InConfigBody(n.Config.Config, n.Addr.String())) - - if diags.HasErrors() { - return diags - } } - - ctx.Actions().AddActionInstance(n.Addr, configVal, n.ResolvedProvider) - return diags + return nil } // GraphNodeReferenceable diff --git a/internal/terraform/node_action_invoke.go b/internal/terraform/node_action_invoke.go index 235141ea4d97..28e99d0de3a4 100644 --- a/internal/terraform/node_action_invoke.go +++ b/internal/terraform/node_action_invoke.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" @@ -23,8 +24,8 @@ var ( ) type nodeActionInvokeExpand struct { - Target addrs.Targetable - Config *configs.Action + TargetAction addrs.Targetable + Config *configs.Action resolvedProvider addrs.AbsProviderConfig // set during the graph walk } @@ -52,7 +53,7 @@ func (n *nodeActionInvokeExpand) SetProvider(p addrs.AbsProviderConfig) { } func (n *nodeActionInvokeExpand) ModulePath() addrs.Module { - switch target := n.Target.(type) { + switch target := n.TargetAction.(type) { case addrs.AbsActionInstance: return target.Module.Module() case addrs.AbsAction: @@ -63,7 +64,7 @@ func (n *nodeActionInvokeExpand) ModulePath() addrs.Module { } func (n *nodeActionInvokeExpand) References() []*addrs.Reference { - switch target := n.Target.(type) { + switch target := n.TargetAction.(type) { case addrs.AbsActionInstance: return []*addrs.Reference{ { @@ -84,7 +85,7 @@ func (n *nodeActionInvokeExpand) References() []*addrs.Reference { } } -func (n *nodeActionInvokeExpand) DynamicExpand(context EvalContext) (*Graph, tfdiags.Diagnostics) { +func (n *nodeActionInvokeExpand) DynamicExpand(ctx EvalContext) (*Graph, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics if n.Config == nil { @@ -92,31 +93,24 @@ func (n *nodeActionInvokeExpand) DynamicExpand(context EvalContext) (*Graph, tfd return nil, diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid action target", - fmt.Sprintf("Action %s does not exist within the configuration.", n.Target.String()))) + fmt.Sprintf("Action %s does not exist within the configuration.", n.TargetAction.String()))) } var g Graph - switch addr := n.Target.(type) { + switch addr := n.TargetAction.(type) { case addrs.AbsActionInstance: - if _, ok := context.Actions().GetActionInstance(addr); !ok { - return nil, diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid action", - Detail: fmt.Sprintf("Targeted action does not exist after expansion: %s.", addr), - Subject: n.Config.DeclRange.Ptr(), - }) - } else { - g.Add(&nodeActionInvokeInstance{ - Target: addr, - Config: n.Config, - }) - } - + g.Add(&nodeActionInvokeInstance{ + TargetAction: addr, + Config: n.Config, + resolvedProvider: n.resolvedProvider, + }) case addrs.AbsAction: - for _, target := range context.Actions().GetActionInstanceKeys(addr) { + instances := ctx.InstanceExpander().ExpandAction(addr) + for _, target := range instances { g.Add(&nodeActionInvokeInstance{ - Target: target, - Config: n.Config, + TargetAction: target, + Config: n.Config, + resolvedProvider: n.resolvedProvider, }) } } @@ -130,41 +124,54 @@ var ( ) type nodeActionInvokeInstance struct { - Target addrs.AbsActionInstance - Config *configs.Action + TargetAction addrs.AbsActionInstance + Config *configs.Action + + resolvedProvider addrs.AbsProviderConfig } func (n *nodeActionInvokeInstance) Path() addrs.ModuleInstance { - return n.Target.Module + return n.TargetAction.Module } func (n *nodeActionInvokeInstance) Execute(ctx EvalContext, _ walkOperation) tfdiags.Diagnostics { var diags tfdiags.Diagnostics - actionInstance, ok := ctx.Actions().GetActionInstance(n.Target) - if !ok { - // shouldn't happen, we checked these things exist in the expand node - panic("tried to trigger non-existent action") - } - - ai := plans.ActionInvocationInstance{ - Addr: n.Target, - ActionTrigger: new(plans.InvokeActionTrigger), - ProviderAddr: actionInstance.ProviderAddr, - ConfigValue: ephemeral.RemoveEphemeralValues(actionInstance.ConfigValue), - } - - provider, _, err := getProvider(ctx, actionInstance.ProviderAddr) + provider, schema, err := getProvider(ctx, n.resolvedProvider) if err != nil { return diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Failed to get provider", - Detail: fmt.Sprintf("Failed to get provider while triggering action %s: %s.", n.Target, err), + Detail: fmt.Sprintf("Failed to get provider while triggering action %s: %s.", n.TargetAction, err), Subject: n.Config.DeclRange.Ptr(), }) } + actionSchema := schema.Actions[n.Config.Type] + + // get the action expansion and config for evaluation + allInsts := ctx.InstanceExpander() + keyData := allInsts.GetActionInstanceRepetitionData(n.TargetAction) + + configVal := cty.NullVal(actionSchema.ConfigSchema.ImpliedType()) + if n.Config.Config != nil { + var configDiags tfdiags.Diagnostics + configVal, _, configDiags = ctx.EvaluateBlock(n.Config.Config, actionSchema.ConfigSchema, nil, keyData) + + diags = diags.Append(configDiags) + if configDiags.HasErrors() { + return diags + } + + valDiags := validateResourceForbiddenEphemeralValues(ctx, configVal, actionSchema.ConfigSchema) + diags = diags.Append(valDiags.InConfigBody(n.Config.Config, n.TargetAction.String())) + if valDiags.HasErrors() { + return diags + } + _, deprecationDiags := ctx.Deprecations().ValidateAndUnmarkConfig(configVal, actionSchema.ConfigSchema, n.TargetAction.ConfigAction().Module) + diags = diags.Append(deprecationDiags.InConfigBody(n.Config.Config, n.TargetAction.String())) + } - unmarkedConfig, _ := actionInstance.ConfigValue.UnmarkDeepWithPaths() + unmarkedConfig, _ := configVal.UnmarkDeepWithPaths() if !unmarkedConfig.IsWhollyKnown() { // we're not actually planning or applying changes from the @@ -174,27 +181,34 @@ func (n *nodeActionInvokeInstance) Execute(ctx EvalContext, _ walkOperation) tfd return diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Partially applied configuration", - Detail: fmt.Sprintf("The action %s contains unknown values while planning. This means it is referencing resources that have not yet been created, please run a complete plan/apply cycle to ensure the state matches the configuration before using the -invoke argument.", n.Target.String()), + Detail: fmt.Sprintf("The action %s contains unknown values while planning. This means it is referencing resources that have not yet been created, please run a complete plan/apply cycle to ensure the state matches the configuration before using the -invoke argument.", n.TargetAction.String()), Subject: n.Config.DeclRange.Ptr(), }) } resp := provider.PlanAction(providers.PlanActionRequest{ - ActionType: n.Target.Action.Action.Type, + ActionType: n.TargetAction.Action.Action.Type, ProposedActionData: unmarkedConfig, ClientCapabilities: ctx.ClientCapabilities(), }) - diags = diags.Append(resp.Diagnostics.InConfigBody(n.Config.Config, n.Target.ContainingAction().String())) + diags = diags.Append(resp.Diagnostics.InConfigBody(n.Config.Config, n.TargetAction.ContainingAction().String())) if resp.Deferred != nil { return diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Provider deferred an action", - Detail: fmt.Sprintf("The provider for %s ordered the action deferred. This likely means you are executing the action against a configuration that hasn't been completely applied.", n.Target), + Detail: fmt.Sprintf("The provider for %s ordered the action deferred. This likely means you are executing the action against a configuration that hasn't been completely applied.", n.TargetAction), Subject: n.Config.DeclRange.Ptr(), }) } + ai := plans.ActionInvocationInstance{ + Addr: n.TargetAction, + ActionTrigger: new(plans.InvokeActionTrigger), + ProviderAddr: n.resolvedProvider, + ConfigValue: ephemeral.RemoveEphemeralValues(configVal), + } + ctx.Changes().AppendActionInvocation(&ai) return diags } diff --git a/internal/terraform/node_action_partialexp.go b/internal/terraform/node_action_partialexp.go index 9ce5767af940..4fff79dfcef7 100644 --- a/internal/terraform/node_action_partialexp.go +++ b/internal/terraform/node_action_partialexp.go @@ -9,7 +9,6 @@ import ( "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" ) // NodeActionDeclarationPartialExpanded is a graph node that stands in for @@ -59,22 +58,20 @@ func (n *NodeActionDeclarationPartialExpanded) ActionAddr() addrs.ConfigAction { func (n *NodeActionDeclarationPartialExpanded) Execute(ctx EvalContext, op walkOperation) tfdiags.Diagnostics { var diags tfdiags.Diagnostics ctx.Deferrals().ReportActionExpansionDeferred(n.addr) - configVal := cty.NullVal(n.Schema.ConfigSchema.ImpliedType()) if n.config.Config != nil { var configDiags tfdiags.Diagnostics - configVal, _, configDiags = ctx.EvaluateBlock(n.config.Config, n.Schema.ConfigSchema.DeepCopy(), nil, instances.TotallyUnknownRepetitionData) + configVal, _, configDiags := ctx.EvaluateBlock(n.config.Config, n.Schema.ConfigSchema.DeepCopy(), nil, instances.TotallyUnknownRepetitionData) diags = diags.Append(configDiags) if diags.HasErrors() { return diags } var deprecationDiags tfdiags.Diagnostics - configVal, deprecationDiags = ctx.Deprecations().ValidateAndUnmarkConfig(configVal, n.Schema.ConfigSchema, n.ActionAddr().Module) + _, deprecationDiags = ctx.Deprecations().ValidateAndUnmarkConfig(configVal, n.Schema.ConfigSchema, n.ActionAddr().Module) diags = diags.Append(deprecationDiags.InConfigBody(n.config.Config, n.ActionAddr().String())) if diags.HasErrors() { return diags } } - ctx.Actions().AddPartialExpandedAction(n.addr, configVal, n.resolvedProvider) return nil } diff --git a/internal/terraform/node_action_trigger_apply.go b/internal/terraform/node_action_trigger_apply.go index f6398969077d..81670d75e1b3 100644 --- a/internal/terraform/node_action_trigger_apply.go +++ b/internal/terraform/node_action_trigger_apply.go @@ -47,6 +47,7 @@ func (n *nodeActionTriggerApplyExpand) DynamicExpand(ctx EvalContext) (*Graph, t resolvedProvider: n.resolvedProvider, ActionTriggerRange: n.triggerConfig.invokingSubject.Ptr(), ConditionExpr: n.triggerConfig.conditionExpr, + actionConfig: n.Config, } g.Add(node) invocationMap[ai] = node diff --git a/internal/terraform/node_action_trigger_instance_apply.go b/internal/terraform/node_action_trigger_instance_apply.go index 8c8be8515573..ffa820842855 100644 --- a/internal/terraform/node_action_trigger_instance_apply.go +++ b/internal/terraform/node_action_trigger_instance_apply.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" @@ -23,6 +24,7 @@ type nodeActionTriggerApplyInstance struct { resolvedProvider addrs.AbsProviderConfig ActionTriggerRange *hcl.Range ConditionExpr hcl.Expression + actionConfig *configs.Action } var ( @@ -75,17 +77,8 @@ func (n *nodeActionTriggerApplyInstance) Execute(ctx EvalContext, wo walkOperati }) return diags } - actionData, ok := ctx.Actions().GetActionInstance(ai.Addr) - if !ok { - 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) + + provider, schema, err := getProvider(ctx, n.resolvedProvider) if err != nil { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, @@ -102,16 +95,37 @@ func (n *nodeActionTriggerApplyInstance) Execute(ctx EvalContext, wo walkOperati 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), + Detail: fmt.Sprintf("The action %s was not found in the provider schema for %s", ai.Addr.Action.Action.Type, n.resolvedProvider), Subject: n.ActionTriggerRange, }) return diags } - configValue := actionData.ConfigValue + expander := ctx.InstanceExpander() + keyData := expander.GetActionInstanceRepetitionData(ai.Addr) + configVal := cty.NullVal(actionSchema.ConfigSchema.ImpliedType()) + if n.actionConfig != nil && n.actionConfig.Config != nil { + var configDiags tfdiags.Diagnostics + configVal, _, configDiags = ctx.EvaluateBlock(n.actionConfig.Config, actionSchema.ConfigSchema, nil, keyData) + + diags = diags.Append(configDiags) + if configDiags.HasErrors() { + return diags + } + + valDiags := validateResourceForbiddenEphemeralValues(ctx, configVal, actionSchema.ConfigSchema) + diags = diags.Append(valDiags.InConfigBody(n.actionConfig.Config, ai.Addr.String())) + + if valDiags.HasErrors() { + return diags + } + + _, deprecationDiags := ctx.Deprecations().ValidateAndUnmarkConfig(configVal, actionSchema.ConfigSchema, n.ModulePath()) + diags = diags.Append(deprecationDiags.InConfigBody(n.actionConfig.Config, ai.Addr.String())) + } // Validate that what we planned matches the action data we have. - errs := objchange.AssertObjectCompatible(actionSchema.ConfigSchema, ai.ConfigValue, ephemeral.RemoveEphemeralValues(configValue)) + errs := objchange.AssertObjectCompatible(actionSchema.ConfigSchema, ai.ConfigValue, ephemeral.RemoveEphemeralValues(configVal)) for _, err := range errs { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, @@ -122,7 +136,7 @@ func (n *nodeActionTriggerApplyInstance) Execute(ctx EvalContext, wo walkOperati }) } - if !configValue.IsWhollyKnown() { + if !configVal.IsWhollyKnown() { return diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Action configuration unknown during apply", @@ -146,7 +160,7 @@ func (n *nodeActionTriggerApplyInstance) Execute(ctx EvalContext, wo walkOperati // We don't want to send the marks, but all marks are okay in the context // of an action invocation. We can't reuse our ephemeral free value from // above because we want the ephemeral values to be included. - unmarkedConfigValue, _ := configValue.UnmarkDeep() + unmarkedConfigValue, _ := configVal.UnmarkDeep() resp := provider.InvokeAction(providers.InvokeActionRequest{ ActionType: ai.Addr.Action.Action.Type, PlannedActionData: unmarkedConfigValue, diff --git a/internal/terraform/node_action_trigger_instance_plan.go b/internal/terraform/node_action_trigger_instance_plan.go index acaf78f89d55..b97f04e72b76 100644 --- a/internal/terraform/node_action_trigger_instance_plan.go +++ b/internal/terraform/node_action_trigger_instance_plan.go @@ -79,6 +79,7 @@ func (n *nodeActionTriggerPlanInstance) Execute(ctx EvalContext, operation walkO ai := plans.ActionInvocationInstance{ Addr: n.actionAddress, ActionTrigger: n.lifecycleActionTrigger.ActionTrigger(configs.Unknown), + ProviderAddr: n.resolvedProvider, } change := ctx.Changes().GetResourceInstanceChange(n.lifecycleActionTrigger.resourceAddress, n.lifecycleActionTrigger.resourceAddress.CurrentObject().DeposedKey) @@ -102,8 +103,21 @@ func (n *nodeActionTriggerPlanInstance) Execute(ctx EvalContext, operation walkO panic("Only actions triggered by plan and apply are supported") } - actionInstance, ok := ctx.Actions().GetActionInstance(n.actionAddress) - if !ok { + provider, schema, err := getProvider(ctx, n.resolvedProvider) + 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 + } + actionSchema := schema.Actions[n.actionConfig.Type] + + expander := ctx.InstanceExpander() + if !expander.AllInstances().HasActionInstance(n.actionAddress) { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Reference to non-existent action instance", @@ -112,11 +126,34 @@ func (n *nodeActionTriggerPlanInstance) Execute(ctx EvalContext, operation walkO }) return diags } - ai.ProviderAddr = actionInstance.ProviderAddr + + keyData := expander.GetActionInstanceRepetitionData(n.actionAddress) + configVal := cty.NullVal(actionSchema.ConfigSchema.ImpliedType()) + if n.actionConfig.Config != nil { + var configDiags tfdiags.Diagnostics + configVal, _, configDiags = ctx.EvaluateBlock(n.actionConfig.Config, actionSchema.ConfigSchema, nil, keyData) + + diags = diags.Append(configDiags) + if configDiags.HasErrors() { + return diags + } + + valDiags := validateResourceForbiddenEphemeralValues(ctx, configVal, actionSchema.ConfigSchema) + diags = diags.Append(valDiags.InConfigBody(n.actionConfig.Config, n.actionAddress.String())) + + if valDiags.HasErrors() { + return diags + } + + var deprecationDiags tfdiags.Diagnostics + configVal, deprecationDiags = ctx.Deprecations().ValidateAndUnmarkConfig(configVal, actionSchema.ConfigSchema, n.ModulePath()) + diags = diags.Append(deprecationDiags.InConfigBody(n.actionConfig.Config, ai.Addr.String())) + } + // with resources, the provider would be expected to strip the ephemeral // values out. with actions, we don't get the value back from the // provider so we'll do that ourselves now. - ai.ConfigValue = ephemeral.RemoveEphemeralValues(actionInstance.ConfigValue) + ai.ConfigValue = ephemeral.RemoveEphemeralValues(configVal) triggeredEvents := actionIsTriggeredByEvent(n.lifecycleActionTrigger.events, change.Action) if len(triggeredEvents) == 0 { @@ -141,20 +178,8 @@ func (n *nodeActionTriggerPlanInstance) Execute(ctx EvalContext, operation walkO } } - 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 - } - // We remove the marks for planning, we will record the sensitive values in the plans.ActionInvocationInstance - unmarkedConfig, _ := actionInstance.ConfigValue.UnmarkDeepWithPaths() + unmarkedConfig, _ := configVal.UnmarkDeepWithPaths() cc := ctx.ClientCapabilities() cc.DeferralAllowed = false // for now, deferrals in actions are always disabled diff --git a/internal/terraform/node_action_trigger_partialexp.go b/internal/terraform/node_action_trigger_partialexp.go index 77adb0cfeb67..d52462c57385 100644 --- a/internal/terraform/node_action_trigger_partialexp.go +++ b/internal/terraform/node_action_trigger_partialexp.go @@ -7,9 +7,11 @@ import ( "fmt" "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/lang/ephemeral" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/tfdiags" @@ -84,25 +86,54 @@ func (n *NodeActionTriggerPartialExpanded) Execute(ctx EvalContext, op walkOpera return nil } - actionInstance, ok := ctx.Actions().GetPartialExpandedAction(n.addr) - if !ok { - panic("action is nil") - } - - provider, _, err := getProvider(ctx, actionInstance.ProviderAddr) + provider, schema, err := getProvider(ctx, n.resolvedProvider) if err != nil { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, - Summary: "Failed to get provider", + Summary: fmt.Sprintf("Failed to get provider for %s", n.addr), Detail: fmt.Sprintf("Failed to get provider: %s", err), - Subject: n.lifecycleActionTrigger.invokingSubject, + Subject: &n.config.DeclRange, }) + return diags + } + actionSchema, ok := schema.Actions[n.addr.ConfigAction().Action.Type] + if !ok { + // This should have been caught earlier + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Action %s not found in provider schema", n.addr), + Detail: fmt.Sprintf("The action %s was not found in the provider schema for %s", n.addr.ConfigAction().Action.Type, n.resolvedProvider), + Subject: &n.config.DeclRange, + }) return diags } + expander := ctx.InstanceExpander() + keyData := expander.GetActionInstanceRepetitionData(n.addr.UnknownActionInstance()) + configVal := cty.NullVal(actionSchema.ConfigSchema.ImpliedType()) + if n.config.Config != nil { + var configDiags tfdiags.Diagnostics + configVal, _, configDiags = ctx.EvaluateBlock(n.config.Config, actionSchema.ConfigSchema, nil, keyData) + + diags = diags.Append(configDiags) + if configDiags.HasErrors() { + return diags + } + + valDiags := validateResourceForbiddenEphemeralValues(ctx, configVal, actionSchema.ConfigSchema) + diags = diags.Append(valDiags.InConfigBody(n.config.Config, n.addr.String())) + + if valDiags.HasErrors() { + return diags + } + + _, deprecationDiags := ctx.Deprecations().ValidateAndUnmarkConfig(configVal, actionSchema.ConfigSchema, n.ActionAddr().Module) + diags = diags.Append(deprecationDiags.InConfigBody(n.config.Config, n.addr.String())) + } + // We remove the marks for planning, we will record the sensitive values in the plans.ActionInvocationInstance - unmarkedConfig, _ := actionInstance.ConfigValue.UnmarkDeepWithPaths() + unmarkedConfig, _ := configVal.UnmarkDeepWithPaths() resp := provider.PlanAction(providers.PlanActionRequest{ ActionType: n.addr.ConfigAction().Action.Type, @@ -125,7 +156,7 @@ func (n *NodeActionTriggerPartialExpanded) Execute(ctx EvalContext, op walkOpera ActionTriggerBlockIndex: n.lifecycleActionTrigger.actionTriggerBlockIndex, ActionsListIndex: n.lifecycleActionTrigger.actionListIndex, }, - ConfigValue: actionInstance.ConfigValue, + ConfigValue: ephemeral.RemoveEphemeralValues(configVal), }, providers.DeferredReasonInstanceCountUnknown) } return nil diff --git a/internal/terraform/transform_action_invoke_apply.go b/internal/terraform/transform_action_invoke_apply.go index 6a4ebf725b28..a12ef3a5e88c 100644 --- a/internal/terraform/transform_action_invoke_apply.go +++ b/internal/terraform/transform_action_invoke_apply.go @@ -25,9 +25,14 @@ func (t *ActionInvokeApplyTransformer) Transform(g *Graph) error { // We just want to add all invoke triggered action invocations for _, action := range t.Changes.ActionInvocations { + // get the config for the action! + cfg := t.Config.DescendantForInstance(action.Addr.Module) + actionCfg := cfg.Module.ActionByAddr(action.Addr.Action.Action) + // Add nodes for each action invocation node := &nodeActionTriggerApplyInstance{ ActionInvocation: action, + actionConfig: actionCfg, } g.Add(node) } diff --git a/internal/terraform/transform_action_invoke_plan.go b/internal/terraform/transform_action_invoke_plan.go index fa40db928432..f631c5e2d3c7 100644 --- a/internal/terraform/transform_action_invoke_plan.go +++ b/internal/terraform/transform_action_invoke_plan.go @@ -47,8 +47,8 @@ func (t *ActionInvokePlanTransformer) Transform(g *Graph) error { } g.Add(&nodeActionInvokeExpand{ - Target: target, - Config: config, + TargetAction: target, + Config: config, }) } diff --git a/internal/terraform/transform_targets.go b/internal/terraform/transform_targets.go index afa5cb4b0016..434804d53ad9 100644 --- a/internal/terraform/transform_targets.go +++ b/internal/terraform/transform_targets.go @@ -162,7 +162,7 @@ func (t *TargetsTransformer) nodeIsTarget(v dag.Vertex, targets []addrs.Targetab case GraphNodeConfigResource: vertexAddr = r.ResourceAddr() case *nodeActionInvokeExpand: - vertexAddr = r.Target + vertexAddr = r.TargetAction case *nodeActionTriggerApplyInstance: vertexAddr = r.ActionInvocation.Addr