diff --git a/internal/providers/provider.go b/internal/providers/provider.go index 011294ebfc..cb89ade332 100644 --- a/internal/providers/provider.go +++ b/internal/providers/provider.go @@ -197,7 +197,7 @@ type GetProviderSchemaResponse struct { // StateStores maps the state store type name to that type's schema. StateStores map[string]Schema - // Actions maps the name of the action to its schema. + // Actions maps the type name of the action to its schema. Actions map[string]ActionSchema // Diagnostics contains any warnings or errors from the method call. diff --git a/internal/terraform/graph_builder_apply.go b/internal/terraform/graph_builder_apply.go index fba4f8a16f..39713792bb 100644 --- a/internal/terraform/graph_builder_apply.go +++ b/internal/terraform/graph_builder_apply.go @@ -162,20 +162,6 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer { Config: b.Config, }, - &ActionTriggerConfigTransformer{ - Config: b.Config, - Operation: b.Operation, - ActionTargets: b.ActionTargets, - - ConcreteActionTriggerNodeFunc: func(node *nodeAbstractActionTrigger, timing RelativeActionTiming) dag.Vertex { - return &nodeActionTriggerApplyExpand{ - nodeAbstractActionTrigger: node, - - relativeTiming: timing, - } - }, - }, - &ActionInvokeApplyTransformer{ Config: b.Config, Operation: b.Operation, diff --git a/internal/terraform/graph_builder_plan.go b/internal/terraform/graph_builder_plan.go index 19d2cc50eb..22d6678db4 100644 --- a/internal/terraform/graph_builder_plan.go +++ b/internal/terraform/graph_builder_plan.go @@ -172,19 +172,6 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer { generateConfigPathForImportTargets: b.GenerateConfigPath, }, - &ActionTriggerConfigTransformer{ - Config: b.Config, - Operation: b.Operation, - ActionTargets: b.ActionTargets, - queryPlanMode: b.queryPlan, - - ConcreteActionTriggerNodeFunc: func(node *nodeAbstractActionTrigger, _ RelativeActionTiming) dag.Vertex { - return &nodeActionTriggerPlanExpand{ - nodeAbstractActionTrigger: node, - } - }, - }, - &ActionInvokePlanTransformer{ Config: b.Config, Operation: b.Operation, diff --git a/internal/terraform/node_resource_abstract.go b/internal/terraform/node_resource_abstract.go index dc5f405e84..b6bf510538 100644 --- a/internal/terraform/node_resource_abstract.go +++ b/internal/terraform/node_resource_abstract.go @@ -461,10 +461,11 @@ func (n *NodeAbstractResource) ActionsProvidedBy() addrs.Map[addrs.ConfigAction, for _, at := range n.Config.Managed.ActionTriggers { for _, aRef := range at.Actions { - actionCfg, ok := n.ActionConfigs.GetOk(aRef.ConfigAction.Action.InModule(n.ModulePath())) + actionCfg, ok := n.ActionConfigs.GetOk(aRef.ConfigAction) if !ok { // this really shouldn't happen, but is it worth a panic? - continue + // maybe. + panic("action config not found!") } if actionCfg != nil { diff --git a/internal/terraform/node_resource_plan_instance.go b/internal/terraform/node_resource_plan_instance.go index c9810c864d..08cd2d60a1 100644 --- a/internal/terraform/node_resource_plan_instance.go +++ b/internal/terraform/node_resource_plan_instance.go @@ -18,7 +18,6 @@ import ( "github.com/hashicorp/terraform/internal/genconfig" "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/lang/ephemeral" - "github.com/hashicorp/terraform/internal/lang/langrefs" "github.com/hashicorp/terraform/internal/moduletest/mocking" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/plans/deferring" @@ -1048,9 +1047,6 @@ func (n *NodePlannableResourceInstance) planActions(ctx EvalContext, change *pla // This includes create triggers for actions such as "createBeforeDelete" (but no delete yet) before, _ := n.triggersForEvent(change.Action) var diags tfdiags.Diagnostics - // we don't need to worry about planning actions in configured order, as - // long as they are shown to the user and applied in order (we don't get new - // planned values from the provider for actions). for _, trigger := range before { if trigger.Condition != nil { condition, condDiags := n.evaluateActionCondition(ctx, trigger) @@ -1062,35 +1058,12 @@ func (n *NodePlannableResourceInstance) planActions(ctx EvalContext, change *pla if !condition { continue } + n.planTriggeredActions(ctx, trigger, change.Action) } else { - n.planTriggeredActions(trigger.Actions) + n.planTriggeredActions(ctx, trigger, change.Action) } } - // 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", - // Detail: "Action instance was not found in the current context.", - // Subject: n.lifecycleActionTrigger.invokingSubject, - // }) - // return diags - // } - return nil } @@ -1218,62 +1191,13 @@ func actionIsTriggeredByEvent(events []configs.ActionTriggerEvent, action plans. return triggeredEvents } -// FIXME: replace this with a more reusable version of evaluateActionCondition from node_action_trigger_instance_plan +// @mildwonkey FIXME: replace this with a more reusable version of evaluateActionCondition from node_action_trigger_instance_plan? or something? this is a copy. func (n *NodePlannableResourceInstance) evaluateActionCondition(ctx EvalContext, trigger *configs.ActionTrigger) (bool, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - rd := instances.RepetitionData{} - refs, refDiags := langrefs.ReferencesInExpr(addrs.ParseRef, trigger.Condition) - diags = diags.Append(refDiags) - if diags.HasErrors() { - return false, diags - } - - for _, ref := range refs { - if ref.Subject == addrs.Self { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Self reference not allowed", - Detail: `The condition expression cannot reference "self".`, - Subject: trigger.Condition.Range().Ptr(), - }) - } - } - - if diags.HasErrors() { - return false, diags - } - - if containsBeforeEvent(trigger.Events) { - // If events contains a before event we want to error if count or each is used - for _, ref := range refs { - if _, ok := ref.Subject.(addrs.CountAttr); ok { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Count reference not allowed", - Detail: `The condition expression cannot reference "count" if the action is run before the resource is applied.`, - Subject: trigger.Condition.Range().Ptr(), - }) - } - - if _, ok := ref.Subject.(addrs.ForEachAttr); ok { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Each reference not allowed", - Detail: `The condition expression cannot reference "each" if the action is run before the resource is applied.`, - Subject: trigger.Condition.Range().Ptr(), - }) - } - - if diags.HasErrors() { - return false, diags - } - } - } else { - // If there are only after events we allow self, count, and each - expander := ctx.InstanceExpander() - rd = expander.GetResourceInstanceRepetitionData(n.Addr) - } + // the condition may use count or for_each (but only with after_* actions) + expander := ctx.InstanceExpander() + rd := expander.GetResourceInstanceRepetitionData(n.Addr) scope := ctx.EvaluationScope(nil, nil, rd) val, conditionEvalDiags := scope.EvalExpr(trigger.Condition, cty.Bool) @@ -1296,12 +1220,159 @@ func (n *NodePlannableResourceInstance) evaluateActionCondition(ctx EvalContext, } // planTriggeredActions plans and records planned actions for every action listed. The caller is responsible for checking that the action condition is true. -func (n *NodePlannableResourceInstance) planTriggeredActions(actionRefs []configs.ActionRef) tfdiags.Diagnostics { +func (n *NodePlannableResourceInstance) planTriggeredActions(ctx EvalContext, trigger *configs.ActionTrigger, event plans.Action) tfdiags.Diagnostics { var diags tfdiags.Diagnostics - for _, actionRef := range actionRefs { - // get the action config from the action ref - fmt.Println(actionRef) + for _, actionRef := range trigger.Actions { + actionCfg, ok := n.ActionConfigs.GetOk(actionRef.ConfigAction) + if !ok { + // this should not be possible + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Missing action config in resource", + fmt.Sprintf("The Action configuration for %s was not attached to the resource. This is a bug with terraform and should be reported.", actionRef.ConfigAction), + )) + return diags + } + + prov, ok := n.resolvedActionProviders.GetOk(actionRef.ConfigAction) + if !ok { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Provider not found", + Detail: fmt.Sprintf("The provider for action %s was not found in the current context.", actionRef.ConfigAction), + Subject: &actionRef.Range, + }) + return diags + } + + provider, providerSchema, err := getProvider(ctx, prov) + diags = diags.Append(err) + if diags.HasErrors() { + return diags + } + + actionSchema := providerSchema.Actions[actionCfg.Type] + + // The configAction was derived from the ActionRef hcl.Expression referenced inside the resource's lifecycle block, and has not yet been + // expanded or fully evaluated, so we will do that now. + // Grab the instance key, necessary if the action uses [count.index] or [each.key] + repData := instances.RepetitionData{} + switch k := n.Addr.Resource.Key.(type) { + case addrs.IntKey: + repData.CountIndex = k.Value() + case addrs.StringKey: + repData.EachKey = k.Value() + repData.EachValue = cty.DynamicVal + } + + ref, evalActionDiags := evaluateActionExpression(actionRef.Expr, repData) + diags = append(diags, evalActionDiags...) + if diags.HasErrors() { + continue + } + + // The reference is either an action or action instance + var actionAddr addrs.AbsActionInstance + switch sub := ref.Subject.(type) { + case addrs.Action: + actionAddr = sub.Absolute(n.Addr.Module).Instance(addrs.NoKey) + case addrs.ActionInstance: + actionAddr = sub.Absolute(n.Addr.Module) + } + + if !ctx.InstanceExpander().AllInstances().HasActionInstance(actionAddr) { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to non-existent action instance", + Detail: "Action instance was not found in the current context.", + Subject: &actionRef.Range, + }) + } + + // now evaluate embedded action config using the *action* instance repetition data + keyData := ctx.InstanceExpander().GetActionInstanceRepetitionData(actionAddr) + configVal := cty.NullVal(actionSchema.ConfigSchema.ImpliedType()) + if actionCfg.Config != nil { + var configDiags tfdiags.Diagnostics + configVal, _, configDiags = ctx.EvaluateBlock(actionCfg.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(actionCfg.Config, actionRef.ConfigAction.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(actionCfg.Config, actionAddr.String())) + } + + // We remove the marks for planning + unmarkedConfig, _ := configVal.UnmarkDeepWithPaths() + + cc := ctx.ClientCapabilities() + cc.DeferralAllowed = false // for now, deferrals in actions are always disabled + resp := provider.PlanAction(providers.PlanActionRequest{ + ActionType: actionCfg.Type, + ProposedActionData: unmarkedConfig, + ClientCapabilities: cc, + }) + diags = append(diags, resp.Diagnostics...) + if diags.HasErrors() { + return diags + } + + // i dunno what this is all about + // 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 = append(diags, tfdiags.Sourceless( + // tfdiags.Severity(severity), + // )) + + // diags = diags.Append(&hcl.Diagnostic{ + // Severity: severity, + // Summary: message, + // Detail: err.Error(), + // //Subject: n.lifecycleActionTrigger.invokingSubject, ??? This is really a sourceless issue + // }) + // } + if resp.Deferred != nil { + // we always set allow_deferrals to be false for actions, so this + // should not happen + diags = diags.Append(deferring.UnexpectedProviderDeferralDiagnostic(actionAddr)) + } + if resp.Diagnostics.HasErrors() { + return diags + } + + ai := &plans.ActionInvocationInstance{ + Addr: actionAddr, + ActionTrigger: &plans.ResourceActionTrigger{ + TriggeringResourceAddr: n.Addr, + //ActionTriggerEvent: event, + // ugh + // i have none of this + }, + ProviderAddr: prov, + // we don't get a value back from the provider, so it's up to us to remove the ephemeral values before storing. + ConfigValue: ephemeral.RemoveEphemeralValues(configVal), + } + ctx.Changes().AppendActionInvocation(ai) } return diags