// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: BUSL-1.1 package configs import ( "fmt" "slices" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/lang/langrefs" "github.com/hashicorp/terraform/internal/tfdiags" ) func invalidActionDiag(subj *hcl.Range) *hcl.Diagnostic { return &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: `Invalid action argument inside action_triggers`, Detail: `action_triggers.actions must only refer to actions in the current module.`, Subject: subj, } } // Action represents an "action" block inside a configuration type Action struct { Name string Type string Config hcl.Body Count hcl.Expression ForEach hcl.Expression ProviderConfigRef *ProviderConfigRef Provider addrs.Provider DeclRange hcl.Range TypeRange hcl.Range } // ActionTrigger represents a configured "action_trigger" inside the lifecycle // block of a managed resource. type ActionTrigger struct { Condition hcl.Expression Events []ActionTriggerEvent Actions []ActionRef // References to actions DeclRange hcl.Range } // ActionTriggerEvent is an enum for valid values for events for action // triggers. type ActionTriggerEvent int //go:generate go tool golang.org/x/tools/cmd/stringer -type ActionTriggerEvent const ( Unknown ActionTriggerEvent = iota BeforeCreate AfterCreate BeforeUpdate AfterUpdate BeforeDestroy AfterDestroy Invoke ) // ActionRef represents a reference to a configured Action type ActionRef struct { Expr hcl.Expression Range hcl.Range } func decodeActionTriggerBlock(block *hcl.Block) (*ActionTrigger, hcl.Diagnostics) { var diags hcl.Diagnostics a := &ActionTrigger{ Events: []ActionTriggerEvent{}, Actions: []ActionRef{}, Condition: nil, } content, bodyDiags := block.Body.Content(actionTriggerSchema) diags = append(diags, bodyDiags...) if attr, exists := content.Attributes["condition"]; exists { a.Condition = attr.Expr } if attr, exists := content.Attributes["events"]; exists { exprs, ediags := hcl.ExprList(attr.Expr) diags = append(diags, ediags...) events := []ActionTriggerEvent{} for _, expr := range exprs { var event ActionTriggerEvent switch hcl.ExprAsKeyword(expr) { case "before_create": event = BeforeCreate case "after_create": event = AfterCreate case "before_update": event = BeforeUpdate case "after_update": event = AfterUpdate default: diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Invalid \"event\" value %s", hcl.ExprAsKeyword(expr)), Detail: "The \"event\" argument supports the following values: before_create, after_create, before_update, after_update.", Subject: expr.Range().Ptr(), }) continue } // Check for duplicate events if slices.Contains(events, event) { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Duplicate %q event", hcl.ExprAsKeyword(expr)), Detail: "The event is already defined in this action_trigger block.", Subject: expr.Range().Ptr(), }) continue } events = append(events, event) } a.Events = events } if attr, exists := content.Attributes["actions"]; exists { actionRefs, ediags := decodeActionTriggerRef(attr.Expr) diags = append(diags, ediags...) a.Actions = actionRefs } if len(a.Actions) == 0 { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "No actions specified", Detail: "At least one action must be specified for an action_trigger.", Subject: block.DefRange.Ptr(), }) } if len(a.Events) == 0 { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "No events specified", Detail: "At least one event must be specified for an action_trigger.", Subject: block.DefRange.Ptr(), }) } return a, diags } func decodeActionBlock(block *hcl.Block) (*Action, hcl.Diagnostics) { var diags hcl.Diagnostics a := &Action{ Type: block.Labels[0], Name: block.Labels[1], DeclRange: block.DefRange, TypeRange: block.LabelRanges[0], } if !hclsyntax.ValidIdentifier(a.Type) { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid action type name", Detail: badIdentifierDetail, Subject: &block.LabelRanges[0], }) } if !hclsyntax.ValidIdentifier(a.Name) { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid action name", Detail: badIdentifierDetail, Subject: &block.LabelRanges[1], }) } content, moreDiags := block.Body.Content(actionBlockSchema) diags = append(diags, moreDiags...) if attr, exists := content.Attributes["count"]; exists { a.Count = attr.Expr } if attr, exists := content.Attributes["for_each"]; exists { a.ForEach = attr.Expr // Cannot have count and for_each on the same action block if a.Count != nil { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: `Invalid combination of "count" and "for_each"`, Detail: `The "count" and "for_each" meta-arguments are mutually-exclusive, only one should be used.`, Subject: &attr.NameRange, }) } } for _, block := range content.Blocks { switch block.Type { case "config": if a.Config != nil { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Duplicate config block", Detail: "An action must contain only one nested \"config\" block.", Subject: block.DefRange.Ptr(), }) return nil, diags } a.Config = block.Body default: // Should not get here because the above should cover all // block types declared in the schema. panic(fmt.Sprintf("unhandled block type %q", block.Type)) } } if attr, exists := content.Attributes["provider"]; exists { var providerDiags hcl.Diagnostics a.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr.Expr, "provider") diags = append(diags, providerDiags...) } return a, diags } // actionBlockSchema is the schema for an action type within terraform. var actionBlockSchema = &hcl.BodySchema{ Attributes: commonActionAttributes, Blocks: []hcl.BlockHeaderSchema{ {Type: "config"}, }, } var commonActionAttributes = []hcl.AttributeSchema{ { Name: "count", }, { Name: "for_each", }, { Name: "provider", }, } var actionTriggerSchema = &hcl.BodySchema{ Attributes: []hcl.AttributeSchema{ { Name: "events", Required: true, }, { Name: "condition", Required: false, }, { Name: "actions", Required: true, }, }, } func (a *Action) moduleUniqueKey() string { return a.Addr().String() } // Addr returns a resource address for the receiver that is relative to the // resource's containing module. func (a *Action) Addr() addrs.Action { return addrs.Action{ Type: a.Type, Name: a.Name, } } // ProviderConfigAddr returns the address for the provider configuration that // should be used for this action. This function returns a default provider // config addr if an explicit "provider" argument was not provided. func (a *Action) ProviderConfigAddr() addrs.LocalProviderConfig { if a.ProviderConfigRef == nil { // If no specific "provider" argument is given, we want to look up the // provider config where the local name matches the implied provider // from the resource type. This may be different from the resource's // provider type. return addrs.LocalProviderConfig{ LocalName: a.Addr().ImpliedProvider(), } } return addrs.LocalProviderConfig{ LocalName: a.ProviderConfigRef.Name, Alias: a.ProviderConfigRef.Alias, } } // decodeActionTriggerRef decodes and does basic validation of the Actions // expression list inside a resource's ActionTrigger block, ensuring each only // reference a single action. This function was largely copied from // decodeReplaceTriggeredBy, but is much more permissive in what References are // allowed. func decodeActionTriggerRef(expr hcl.Expression) ([]ActionRef, hcl.Diagnostics) { exprs, diags := hcl.ExprList(expr) if diags.HasErrors() { return nil, diags } actionRefs := make([]ActionRef, len(exprs)) for i, expr := range exprs { // Since we are manually parsing the action_trigger.Actions argument, we // need to specially handle json configs, in which case the values will // be json strings rather than hcl. To simplify parsing however we will // decode the individual list elements, rather than the entire // expression. var jsDiags hcl.Diagnostics expr, jsDiags = unwrapJSONRefExpr(expr) diags = diags.Extend(jsDiags) if diags.HasErrors() { continue } actionRefs[i] = ActionRef{ Expr: expr, Range: expr.Range(), } refs, refDiags := langrefs.ReferencesInExpr(addrs.ParseRef, expr) for _, diag := range refDiags { severity := hcl.DiagError if diag.Severity() == tfdiags.Warning { severity = hcl.DiagWarning } diags = append(diags, &hcl.Diagnostic{ Severity: severity, Summary: diag.Description().Summary, Detail: diag.Description().Detail, Subject: expr.Range().Ptr(), }) } if refDiags.HasErrors() { continue } actionCount := 0 for _, ref := range refs { switch ref.Subject.(type) { case addrs.Action, addrs.ActionInstance: actionCount++ case addrs.ModuleCall, addrs.ModuleCallInstance, addrs.ModuleCallInstanceOutput: diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid reference to action outside this module", Detail: "Actions can only be referenced in the module they are declared in.", Subject: expr.Range().Ptr(), }) continue case addrs.Resource, addrs.ResourceInstance: // definitely not an action diags = append(diags, invalidActionDiag(expr.Range().Ptr())) continue default: // we've checked what we can } } switch { case actionCount == 0: diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "No actions specified", Detail: "At least one action must be specified for an action_trigger.", Subject: expr.Range().Ptr(), }) case actionCount > 1: diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid action expression", Detail: "Multiple action references in actions expression.", Subject: expr.Range().Ptr(), }) } } return actionRefs, diags }