diff --git a/internal/lang/eval.go b/internal/lang/eval.go index 706a552a6b..42b092ad02 100644 --- a/internal/lang/eval.go +++ b/internal/lang/eval.go @@ -312,7 +312,7 @@ func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceabl // this codepath doesn't really "know about". If the "self" // object starts being supported in more contexts later then // we'll need to adjust this message. - Detail: `The "self" object is not available in this context. This object can be used only in resource provisioner, connection, and postcondition blocks.`, + Detail: `The "self" object is not available in this context. This object can be used only in resource provisioner, connection, postcondition blocks, and in the action_trigger condition attributes of after_create & after_update actions.`, Subject: ref.SourceRange.ToHCL().Ptr(), }) continue diff --git a/internal/terraform/context_plan_actions_test.go b/internal/terraform/context_plan_actions_test.go index e59835a55a..bf25ab8a75 100644 --- a/internal/terraform/context_plan_actions_test.go +++ b/internal/terraform/context_plan_actions_test.go @@ -2467,6 +2467,50 @@ resource "test_object" "a" { } action_trigger { events = [after_update] +<<<<<<< HEAD +======= + condition = self.name == "bar" + actions = [action.test_unlinked.world] + } + } +} +`, + }, + expectPlanActionCalled: false, + + expectPlanDiagnostics: func(m *configs.Config) tfdiags.Diagnostics { + // We only expect one diagnostic, as the other condition is valid + return tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Self reference not allowed", + Detail: `The condition expression cannot reference "self" if the action is run before the resource is applied.`, + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 11, Column: 19, Byte: 199}, + End: hcl.Pos{Line: 11, Column: 37, Byte: 217}, + }, + }) + }, + }, + + "using self in after_* condition": { + module: map[string]string{ + "main.tf": ` + +action "test_unlinked" "hello" {} +action "test_unlinked" "world" {} + +resource "test_object" "a" { + name = "foo" + lifecycle { + action_trigger { + events = [after_create] + condition = self.name == "foo" + actions = [action.test_unlinked.hello] + } + action_trigger { + events = [after_update] +>>>>>>> 92bf31941e (support count, each, and self for after_ actions) condition = self.name == "bar" actions = [action.test_unlinked.world] } diff --git a/internal/terraform/evaluate_valid.go b/internal/terraform/evaluate_valid.go index 16315abd5d..9b81f23bdf 100644 --- a/internal/terraform/evaluate_valid.go +++ b/internal/terraform/evaluate_valid.go @@ -64,7 +64,7 @@ func (e *Evaluator) staticValidateReference(ref *addrs.Reference, modCfg *config // this codepath doesn't really "know about". If the "self" // object starts being supported in more contexts later then // we'll need to adjust this message. - Detail: `The "self" object is not available in this context. This object can be used only in resource provisioner, connection, and postcondition blocks.`, + Detail: `The "self" object is not available in this context. This object can be used only in resource provisioner, connection, postcondition blocks, and in the action_trigger condition attributes of after_create & after_update actions.`, Subject: ref.SourceRange.ToHCL().Ptr(), }) return diags diff --git a/internal/terraform/node_action_trigger_apply.go b/internal/terraform/node_action_trigger_apply.go index 9eab1ba990..99d18b3224 100644 --- a/internal/terraform/node_action_trigger_apply.go +++ b/internal/terraform/node_action_trigger_apply.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/lang/ephemeral" "github.com/hashicorp/terraform/internal/lang/langrefs" "github.com/hashicorp/terraform/internal/plans" @@ -38,23 +39,28 @@ func (n *nodeActionTriggerApply) Execute(ctx EvalContext, wo walkOperation) tfdi actionInvocation := n.ActionInvocation if n.ConditionExpr != nil { - condition, conditionDiags := evaluateCondition(ctx, n.ConditionExpr) + // We know this must be a lifecycle action, otherwise we would have no condition + at := actionInvocation.ActionTrigger.(plans.LifecycleActionTrigger) + condition, conditionDiags := evaluateCondition(ctx, conditionContext{ + // For applying the triggering event is sufficient, if the condition could not have + // been evaluated due to in invalid mix of events we would have caught it durin planning. + events: []configs.ActionTriggerEvent{at.ActionTriggerEvent}, + conditionExpr: n.ConditionExpr, + resourceAddress: at.TriggeringResourceAddr, + }) diags = diags.Append(conditionDiags) if diags.HasErrors() { return diags } - if !condition.IsWhollyKnown() { + + if !condition { return diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, - Summary: "Condition expression is not known", - Detail: "During apply the condition expression must be known, and must evaluate to a boolean value", + Summary: "Condition changed evaluation during apply", + Detail: "The condition evaluated to false during apply, but was true during planning. This may lead to unexpected behavior.", Subject: n.ConditionExpr.Range().Ptr(), }) } - // If the condition evaluates to false, skip the action - if condition.False() { - return diags - } } ai := ctx.Changes().GetActionInvocation(actionInvocation.Addr, actionInvocation.ActionTrigger) diff --git a/internal/terraform/node_action_trigger_instance_plan.go b/internal/terraform/node_action_trigger_instance_plan.go index 1f9f920e65..7826a6401b 100644 --- a/internal/terraform/node_action_trigger_instance_plan.go +++ b/internal/terraform/node_action_trigger_instance_plan.go @@ -10,7 +10,9 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" + "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/plans" "github.com/hashicorp/terraform/internal/plans/deferring" "github.com/hashicorp/terraform/internal/providers" @@ -116,14 +118,18 @@ func (n *nodeActionTriggerPlanInstance) Execute(ctx EvalContext, operation walkO // Evaluate the condition expression if it exists (otherwise it's true) if n.lifecycleActionTrigger != nil && n.lifecycleActionTrigger.conditionExpr != nil { - condition, conditionDiags := evaluateCondition(ctx, n.lifecycleActionTrigger.conditionExpr) + condition, conditionDiags := evaluateCondition(ctx, conditionContext{ + events: n.lifecycleActionTrigger.events, + conditionExpr: n.lifecycleActionTrigger.conditionExpr, + resourceAddress: n.lifecycleActionTrigger.resourceAddress, + }) diags = diags.Append(conditionDiags) if conditionDiags.HasErrors() { return conditionDiags } // The condition is false so we skip the action - if condition.False() { + if !condition { return diags } } @@ -201,27 +207,92 @@ func (n *nodeActionTriggerPlanInstance) Path() addrs.ModuleInstance { return n.actionAddress.Module } -func evaluateCondition(ctx EvalContext, conditionExpr hcl.Expression) (cty.Value, tfdiags.Diagnostics) { - // TODO: Support self in conditions - val, diags := ctx.EvaluateExpr(conditionExpr, cty.Bool, nil) +type conditionContext struct { + events []configs.ActionTriggerEvent + conditionExpr hcl.Expression + resourceAddress addrs.AbsResourceInstance +} + +func evaluateCondition(ctx EvalContext, at conditionContext) (bool, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + rd := instances.RepetitionData{} + var self addrs.Referenceable = nil + if containsBeforeEvent(at.events) { + // If events contains a before event we want to error if count, each, or self is used + refs, refDiags := langrefs.ReferencesInExpr(addrs.ParseRef, at.conditionExpr) + 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" if the action is run before the resource is applied.`, + Subject: at.conditionExpr.Range().Ptr(), + }) + } + + 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: at.conditionExpr.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: at.conditionExpr.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(at.resourceAddress) + self = at.resourceAddress.Resource + } + + scope := ctx.EvaluationScope(self, nil, rd) + val, conditionEvalDiags := scope.EvalExpr(at.conditionExpr, cty.Bool) + diags = diags.Append(conditionEvalDiags) if diags.HasErrors() { - return cty.False, diags + return false, diags } - // TODO: Support unknown condition values if !val.IsWhollyKnown() { - panic("condition is not wholly known") - } - // If the condition is neither true nor false, it's an error - if !(val.True() || val.False()) { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, - Summary: "Invalid condition", - Detail: "The condition must be either true or false", - Subject: conditionExpr.Range().Ptr(), + Summary: "Condition must be known", + Detail: "The condition expression resulted in an unknown value, but it must be a known boolean value.", + Subject: at.conditionExpr.Range().Ptr(), }) - return cty.False, diags + return false, diags } - return val, nil + return val.True(), nil +} + +func containsBeforeEvent(events []configs.ActionTriggerEvent) bool { + for _, event := range events { + switch event { + case configs.BeforeCreate, configs.BeforeUpdate: + return true + default: + continue + } + } + return false }