support count, each, and self for after_ actions

pull/37545/head
Daniel Schmidt 6 months ago
parent 4681288e6e
commit 4d57d3488a

@ -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

@ -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]
}

@ -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

@ -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)

@ -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
}

Loading…
Cancel
Save