| // 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), |
| } |
| } |
| |
| // ConfigAction returns the unexpanded ConfigAction for this AbsAction. |
| func (a AbsAction) ConfigAction() 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()) |
| case ConfigAction: |
| return a.ConfigAction().Equal(to) |
| 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) |
| case ConfigAction: |
| return a.ConfigAction().Equal(to) |
| 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 { |
| targetable |
| 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, |
| } |
| } |
| |
| // AddrType implements Targetable |
| func (a ConfigAction) AddrType() TargetableAddrType { |
| return ActionAddrType |
| } |
| |
| 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) TargetContains(other Targetable) bool { |
| switch other := other.(type) { |
| case AbsAction: |
| return other.ConfigAction().Equal(a) |
| case AbsActionInstance: |
| return other.ContainingAction().ConfigAction().Equal(a) |
| } |
| return false |
| } |
| |
| 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 |
| } |