terraform/internal/addrs/action.go
2026-02-17 13:56:34 +00:00

584 lines
16 KiB
Go

// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package addrs
import (
"fmt"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/terraform/internal/tfdiags"
)
// Action is an address for an action block within configuration, which
// contains potentially-multiple action instances if that configuration
// block uses "count" or "for_each".
type Action struct {
referenceable
Type string
Name string
}
func (a Action) String() string {
return fmt.Sprintf("action.%s.%s", a.Type, a.Name)
}
func (a Action) Equal(o Action) bool {
return a.Name == o.Name && a.Type == o.Type
}
func (a Action) Less(o Action) bool {
switch {
case a.Type != o.Type:
return a.Type < o.Type
case a.Name != o.Name:
return a.Name < o.Name
default:
return false
}
}
func (a Action) UniqueKey() UniqueKey {
return a // An Action is its own UniqueKey
}
func (a Action) uniqueKeySigil() {}
// Instance produces the address for a specific instance of the receiver
// that is identified by the given key.
func (a Action) Instance(key InstanceKey) ActionInstance {
return ActionInstance{
Action: a,
Key: key,
}
}
// Absolute returns an AbsAction from the receiver and the given module
// instance address.
func (a Action) Absolute(module ModuleInstance) AbsAction {
return AbsAction{
Module: module,
Action: a,
}
}
// InModule returns a ConfigAction from the receiver and the given module
// address.
func (a Action) InModule(module Module) ConfigAction {
return ConfigAction{
Module: module,
Action: a,
}
}
// ImpliedProvider returns the implied provider type name, for e.g. the "aws" in
// "aws_instance"
func (a Action) ImpliedProvider() string {
typeName := a.Type
if under := strings.Index(typeName, "_"); under != -1 {
typeName = typeName[:under]
}
return typeName
}
// ActionInstance is an address for a specific instance of an action.
// When an action is defined in configuration with "count" or "for_each" it
// produces zero or more instances, which can be addressed using this type.
type ActionInstance struct {
referenceable
Action Action
Key InstanceKey
}
func (a ActionInstance) ContainingAction() Action {
return a.Action
}
func (a ActionInstance) String() string {
if a.Key == NoKey {
return a.Action.String()
}
return a.Action.String() + a.Key.String()
}
func (a ActionInstance) Equal(o ActionInstance) bool {
return a.Key == o.Key && a.Action.Equal(o.Action)
}
func (a ActionInstance) Less(o ActionInstance) bool {
if !a.Action.Equal(o.Action) {
return a.Action.Less(o.Action)
}
if a.Key != o.Key {
return InstanceKeyLess(a.Key, o.Key)
}
return false
}
func (a ActionInstance) UniqueKey() UniqueKey {
return a // An ActionInstance is its own UniqueKey
}
func (a ActionInstance) uniqueKeySigil() {}
// Absolute returns an AbsActionInstance from the receiver and the given module
// instance address.
func (a ActionInstance) Absolute(module ModuleInstance) AbsActionInstance {
return AbsActionInstance{
Module: module,
Action: a,
}
}
// AbsAction is an absolute address for an action under a given module path.
type AbsAction struct {
targetable
Module ModuleInstance
Action Action
}
// Action returns the address of a particular action within the receiver.
func (m ModuleInstance) Action(typeName string, name string) AbsAction {
return AbsAction{
Module: m,
Action: Action{
Type: typeName,
Name: name,
},
}
}
// Instance produces the address for a specific instance of the receiver that is
// identified by the given key.
func (a AbsAction) Instance(key InstanceKey) AbsActionInstance {
return AbsActionInstance{
Module: a.Module,
Action: a.Action.Instance(key),
}
}
// Config returns the unexpanded ConfigAction for this AbsAction.
func (a AbsAction) Config() ConfigAction {
return ConfigAction{
Module: a.Module.Module(),
Action: a.Action,
}
}
// TargetContains implements Targetable
func (a AbsAction) TargetContains(other Targetable) bool {
switch to := other.(type) {
case AbsAction:
return a.Equal(to)
case AbsActionInstance:
return a.Equal(to.ContainingAction())
default:
return false
}
}
// AddrType implements Targetable
func (a AbsAction) AddrType() TargetableAddrType {
return ActionAddrType
}
func (a AbsAction) String() string {
if len(a.Module) == 0 {
return a.Action.String()
}
return fmt.Sprintf("%s.%s", a.Module.String(), a.Action.String())
}
func (a AbsAction) Equal(o AbsAction) bool {
return a.Module.Equal(o.Module) && a.Action.Equal(o.Action)
}
func (a AbsAction) Less(o AbsAction) bool {
if !a.Module.Equal(o.Module) {
return a.Module.Less(o.Module)
}
if !a.Action.Equal(o.Action) {
return a.Action.Less(o.Action)
}
return false
}
type absActionKey string
func (a absActionKey) uniqueKeySigil() {}
func (a AbsAction) UniqueKey() UniqueKey {
return absActionKey(a.String())
}
// AbsActionInstance is an absolute address for an action instance under a
// given module path.
type AbsActionInstance struct {
targetable
Module ModuleInstance
Action ActionInstance
}
// ActionInstance returns the address of a particular action instance within the receiver.
func (m ModuleInstance) ActionInstance(typeName string, name string, key InstanceKey) AbsActionInstance {
return AbsActionInstance{
Module: m,
Action: ActionInstance{
Action: Action{
Type: typeName,
Name: name,
},
Key: key,
},
}
}
// ContainingAction returns the address of the action that contains the
// receiving action instance. In other words, it discards the key portion of the
// address to produce an AbsAction value.
func (a AbsActionInstance) ContainingAction() AbsAction {
return AbsAction{
Module: a.Module,
Action: a.Action.ContainingAction(),
}
}
// ConfigAction returns the address of the configuration block that declared
// this instance.
func (a AbsActionInstance) ConfigAction() ConfigAction {
return ConfigAction{
Module: a.Module.Module(),
Action: a.Action.Action,
}
}
func (a AbsActionInstance) String() string {
if len(a.Module) == 0 {
return a.Action.String()
}
return fmt.Sprintf("%s.%s", a.Module.String(), a.Action.String())
}
// TargetContains implements Targetable
func (a AbsActionInstance) TargetContains(other Targetable) bool {
switch to := other.(type) {
case AbsAction:
return to.Equal(a.ContainingAction()) && a.Action.Key == NoKey
case AbsActionInstance:
return to.Equal(a)
default:
return false
}
}
// AddrType implements Targetable
func (a AbsActionInstance) AddrType() TargetableAddrType {
return ActionInstanceAddrType
}
func (a AbsActionInstance) Equal(o AbsActionInstance) bool {
return a.Module.Equal(o.Module) && a.Action.Equal(o.Action)
}
// Less returns true if the receiver should sort before the given other value
// in a sorted list of addresses.
func (a AbsActionInstance) Less(o AbsActionInstance) bool {
if !a.Module.Equal(o.Module) {
return a.Module.Less(o.Module)
}
if !a.Action.Equal(o.Action) {
return a.Action.Less(o.Action)
}
return false
}
type absActionInstanceKey string
func (a AbsActionInstance) UniqueKey() UniqueKey {
return absActionInstanceKey(a.String())
}
func (a absActionInstanceKey) uniqueKeySigil() {}
// ConfigAction is the address for an action within the configuration.
type ConfigAction struct {
Module Module
Action Action
}
// Action returns the address of a particular action within the module.
func (m Module) Action(typeName string, name string) ConfigAction {
return ConfigAction{
Module: m,
Action: Action{
Type: typeName,
Name: name,
},
}
}
// Absolute produces the address for the receiver within a specific module instance.
func (a ConfigAction) Absolute(module ModuleInstance) AbsAction {
return AbsAction{
Module: module,
Action: a.Action,
}
}
func (a ConfigAction) String() string {
if len(a.Module) == 0 {
return a.Action.String()
}
return fmt.Sprintf("%s.%s", a.Module.String(), a.Action.String())
}
func (a ConfigAction) Equal(o ConfigAction) bool {
return a.Module.Equal(o.Module) && a.Action.Equal(o.Action)
}
func (a ConfigAction) UniqueKey() UniqueKey {
return configActionKey(a.String())
}
type configActionKey string
func (k configActionKey) uniqueKeySigil() {}
// ParseAbsActionInstanceStr is a helper wrapper around
// ParseAbsActionInstance that takes a string and parses it with the HCL
// native syntax traversal parser before interpreting it.
//
// Error diagnostics are returned if either the parsing fails or the analysis
// of the traversal fails. There is no way for the caller to distinguish the
// two kinds of diagnostics programmatically. If error diagnostics are returned
// the returned address may be incomplete.
//
// Since this function has no context about the source of the given string,
// any returned diagnostics will not have meaningful source location
// information.
func ParseAbsActionInstanceStr(str string) (AbsActionInstance, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1})
diags = diags.Append(parseDiags)
if parseDiags.HasErrors() {
return AbsActionInstance{}, diags
}
addr, addrDiags := ParseAbsActionInstance(traversal)
diags = diags.Append(addrDiags)
return addr, diags
}
// ParseAbsActionInstance attempts to interpret the given traversal as an
// absolute action instance address, using the same syntax as expected by
// ParseTarget.
//
// If no error diagnostics are returned, the returned target includes the
// address that was extracted and the source range it was extracted from.
//
// If error diagnostics are returned then the AbsResource value is invalid and
// must not be used.
func ParseAbsActionInstance(traversal hcl.Traversal) (AbsActionInstance, tfdiags.Diagnostics) {
moduleAddr, remain, diags := parseModuleInstancePrefix(traversal, false)
if diags.HasErrors() {
return AbsActionInstance{}, diags
}
if remain.IsRelative() {
// (relative means that there's either nothing left or what's next isn't an identifier)
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid action address",
Detail: "Module path must be followed by an action instance address.",
Subject: remain.SourceRange().Ptr(),
})
return AbsActionInstance{}, diags
}
if remain.RootName() != "action" {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address",
Detail: "Action address must start with \"action.\".",
Subject: remain[0].SourceRange().Ptr(),
})
return AbsActionInstance{}, diags
}
remain = remain[1:]
if len(remain) < 2 {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address",
Detail: "Action specification must include an action type and name.",
Subject: remain.SourceRange().Ptr(),
})
return AbsActionInstance{}, diags
}
var actionType, name string
switch tt := remain[0].(type) {
case hcl.TraverseRoot:
actionType = tt.Name
case hcl.TraverseAttr:
actionType = tt.Name
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid action address",
Detail: "An action name is required.",
Subject: remain[0].SourceRange().Ptr(),
})
return AbsActionInstance{}, diags
}
switch tt := remain[1].(type) {
case hcl.TraverseAttr:
name = tt.Name
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address",
Detail: "An action name is required.",
Subject: remain[1].SourceRange().Ptr(),
})
return AbsActionInstance{}, diags
}
remain = remain[2:]
switch len(remain) {
case 0:
return moduleAddr.ActionInstance(actionType, name, NoKey), diags
case 1:
switch tt := remain[0].(type) {
case hcl.TraverseIndex:
key, err := ParseInstanceKey(tt.Key)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address",
Detail: fmt.Sprintf("Invalid resource instance key: %s.", err),
Subject: remain[0].SourceRange().Ptr(),
})
return AbsActionInstance{}, diags
}
return moduleAddr.ActionInstance(actionType, name, key), diags
case hcl.TraverseSplat:
// Not yet supported!
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address",
Detail: "Action instance key must be given in square brackets.",
Subject: remain[0].SourceRange().Ptr(),
})
return AbsActionInstance{}, diags
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address",
Detail: "Action instance key must be given in square brackets.",
Subject: remain[0].SourceRange().Ptr(),
})
return AbsActionInstance{}, diags
}
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address",
Detail: "Unexpected extra operators after address.",
Subject: remain[1].SourceRange().Ptr(),
})
return AbsActionInstance{}, diags
}
}
// ParseAbsAction attempts to interpret the given traversal as an absolute
// action address, using the same syntax as expected by ParseTarget.
//
// If no error diagnostics are returned, the returned target includes the
// address that was extracted and the source range it was extracted from.
//
// If error diagnostics are returned then the AbsAction value is invalid and
// must not be used.
func ParseAbsAction(traversal hcl.Traversal) (AbsAction, tfdiags.Diagnostics) {
addr, diags := ParseTargetAction(traversal)
if diags.HasErrors() {
return AbsAction{}, diags
}
switch tt := addr.Subject.(type) {
case AbsAction:
return tt, diags
case AbsActionInstance: // Catch likely user error with specialized message
// Assume that the last element of the traversal must be the index,
// since that's required for a valid resource instance address.
indexStep := traversal[len(traversal)-1]
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address",
Detail: "An action address is required. This instance key identifies a specific action instance, which is not expected here.",
Subject: indexStep.SourceRange().Ptr(),
})
return AbsAction{}, diags
case ModuleInstance: // Catch likely user error with specialized message
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address",
Detail: "An action address is required here. The module path must be followed by an action specification.",
Subject: traversal.SourceRange().Ptr(),
})
return AbsAction{}, diags
default: // Generic message for other address types
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address",
Detail: "An action address is required here.",
Subject: traversal.SourceRange().Ptr(),
})
return AbsAction{}, diags
}
}
// ParseAbsActionStr is a helper wrapper around ParseAbsAction that takes a
// string and parses it with the HCL native syntax traversal parser before
// interpreting it.
//
// Error diagnostics are returned if either the parsing fails or the analysis
// of the traversal fails. There is no way for the caller to distinguish the
// two kinds of diagnostics programmatically. If error diagnostics are returned
// the returned address may be incomplete.
//
// Since this function has no context about the source of the given string,
// any returned diagnostics will not have meaningful source location
// information.
func ParseAbsActionStr(str string) (AbsAction, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1})
diags = diags.Append(parseDiags)
if parseDiags.HasErrors() {
return AbsAction{}, diags
}
addr, addrDiags := ParseAbsAction(traversal)
diags = diags.Append(addrDiags)
return addr, diags
}