mirror of
https://github.com/hashicorp/terraform.git
synced 2026-06-09 00:42:48 -04:00
395 lines
10 KiB
Go
395 lines
10 KiB
Go
// 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
|
|
}
|