2024-02-08 03:48:59 -06:00
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
2023-05-02 10:33:06 -05:00
// SPDX-License-Identifier: MPL-2.0
2018-03-30 22:18:59 -05:00
package addrs
import (
"fmt"
2021-06-09 19:24:10 -05:00
"strings"
2018-03-30 22:18:59 -05:00
2019-09-09 17:58:44 -05:00
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
2021-06-09 19:24:10 -05:00
"github.com/zclconf/go-cty/cty"
2023-06-28 02:47:24 -05:00
2023-09-20 06:35:35 -05:00
"github.com/opentofu/opentofu/internal/tfdiags"
2018-03-30 22:18:59 -05:00
)
// Reference describes a reference to an address with source location
// information.
type Reference struct {
Subject Referenceable
SourceRange tfdiags . SourceRange
Remaining hcl . Traversal
}
2021-06-09 19:24:10 -05:00
// DisplayString returns a string that approximates the subject and remaining
2024-08-29 12:20:33 -05:00
// traversal of the receiver in a way that resembles the OpenTofu language
2021-06-09 19:24:10 -05:00
// syntax that could've produced it.
//
2023-09-21 07:38:46 -05:00
// It's not guaranteed to actually be a valid OpenTofu language expression,
2021-06-09 19:24:10 -05:00
// since the intended use here is primarily for UI messages such as
// diagnostics.
func ( r * Reference ) DisplayString ( ) string {
if len ( r . Remaining ) == 0 {
// Easy case: we can just return the subject's string.
return r . Subject . String ( )
}
var ret strings . Builder
ret . WriteString ( r . Subject . String ( ) )
for _ , step := range r . Remaining {
switch tStep := step . ( type ) {
case hcl . TraverseRoot :
ret . WriteString ( tStep . Name )
case hcl . TraverseAttr :
ret . WriteByte ( '.' )
ret . WriteString ( tStep . Name )
case hcl . TraverseIndex :
ret . WriteByte ( '[' )
switch tStep . Key . Type ( ) {
case cty . String :
ret . WriteString ( fmt . Sprintf ( "%q" , tStep . Key . AsString ( ) ) )
case cty . Number :
bf := tStep . Key . AsBigFloat ( )
ret . WriteString ( bf . Text ( 'g' , 10 ) )
}
ret . WriteByte ( ']' )
}
}
return ret . String ( )
}
2024-08-29 12:20:33 -05:00
// ParseRef attempts to extract a referenceable address from the prefix of the
2018-03-30 22:18:59 -05:00
// given traversal, which must be an absolute traversal or this function
// will panic.
//
// If no error diagnostics are returned, the returned reference includes the
// address that was extracted, the source range it was extracted from, and any
// remaining relative traversal that was not consumed as part of the
// reference.
//
// If error diagnostics are returned then the Reference value is invalid and
// must not be used.
func ParseRef ( traversal hcl . Traversal ) ( * Reference , tfdiags . Diagnostics ) {
2018-05-10 17:59:47 -05:00
ref , diags := parseRef ( traversal )
// Normalize a little to make life easier for callers.
if ref != nil {
if len ( ref . Remaining ) == 0 {
ref . Remaining = nil
}
}
return ref , diags
}
2023-06-28 02:47:24 -05:00
// ParseRefFromTestingScope adds check blocks and outputs into the available
// references returned by ParseRef.
//
// The testing files and functionality have a slightly expanded referencing
// scope and so should use this function to retrieve references.
func ParseRefFromTestingScope ( traversal hcl . Traversal ) ( * Reference , tfdiags . Diagnostics ) {
root := traversal . RootName ( )
var diags tfdiags . Diagnostics
var reference * Reference
switch root {
case "output" :
2025-02-11 18:01:25 -06:00
reference , diags = parseSingleAttrRef ( traversal , func ( name string ) Referenceable {
return OutputValue { Name : name }
} )
2023-06-28 02:47:24 -05:00
case "check" :
2025-02-11 18:01:25 -06:00
reference , diags = parseSingleAttrRef ( traversal , func ( name string ) Referenceable {
return Check { Name : name }
} )
default :
// If it's not an output or a check block, then just parse it as normal.
return ParseRef ( traversal )
2023-06-28 02:47:24 -05:00
}
2025-02-11 18:01:25 -06:00
if reference != nil && len ( reference . Remaining ) == 0 {
reference . Remaining = nil
2023-06-28 02:47:24 -05:00
}
2025-02-11 18:01:25 -06:00
return reference , diags
2023-06-28 02:47:24 -05:00
}
2018-05-10 17:59:47 -05:00
// ParseRefStr is a helper wrapper around ParseRef that takes a string
// and parses it with the HCL native syntax traversal parser before
// interpreting it.
//
// This should be used only in specialized situations since it will cause the
// created references to not have any meaningful source location information.
// If a reference string is coming from a source that should be identified in
// error messages then the caller should instead parse it directly using a
// suitable function from the HCL API and pass the traversal itself to
// ParseRef.
//
// 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 reference may be nil or incomplete.
func ParseRefStr ( str string ) ( * Reference , 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 nil , diags
}
ref , targetDiags := ParseRef ( traversal )
diags = diags . Append ( targetDiags )
return ref , diags
}
2023-06-28 02:47:24 -05:00
// ParseRefStrFromTestingScope matches ParseRefStr except it supports the
// references supported by ParseRefFromTestingScope.
func ParseRefStrFromTestingScope ( str string ) ( * Reference , 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 nil , diags
}
ref , targetDiags := ParseRefFromTestingScope ( traversal )
diags = diags . Append ( targetDiags )
return ref , diags
}
2018-05-10 17:59:47 -05:00
func parseRef ( traversal hcl . Traversal ) ( * Reference , tfdiags . Diagnostics ) {
2018-03-30 22:18:59 -05:00
var diags tfdiags . Diagnostics
root := traversal . RootName ( )
rootRange := traversal [ 0 ] . SourceRange ( )
switch root {
case "count" :
2025-02-11 18:01:25 -06:00
return parseSingleAttrRef ( traversal , func ( name string ) Referenceable {
return CountAttr { Name : name }
} )
2019-06-12 10:07:32 -05:00
case "each" :
2025-02-11 18:01:25 -06:00
return parseSingleAttrRef ( traversal , func ( name string ) Referenceable {
return ForEachAttr { Name : name }
} )
2018-03-30 22:18:59 -05:00
case "data" :
if len ( traversal ) < 3 {
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid reference" ,
Detail : ` The "data" object must be followed by two attribute names: the data source type and the resource name. ` ,
Subject : traversal . SourceRange ( ) . Ptr ( ) ,
} )
return nil , diags
}
remain := traversal [ 1 : ] // trim off "data" so we can use our shared resource reference parser
return parseResourceRef ( DataResourceMode , rootRange , remain )
2021-05-14 18:42:30 -05:00
case "resource" :
// This is an alias for the normal case of just using a managed resource
// type as a top-level symbol, which will serve as an escape mechanism
2023-09-21 07:38:46 -05:00
// if a later edition of the OpenTofu language introduces a new
2021-05-14 18:42:30 -05:00
// reference prefix that conflicts with a resource type name in an
// existing provider. In that case, the edition upgrade tool can
// rewrite foo.bar into resource.foo.bar to ensure that "foo" remains
// interpreted as a resource type name rather than as the new reserved
// word.
if len ( traversal ) < 3 {
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid reference" ,
Detail : ` The "resource" object must be followed by two attribute names: the resource type and the resource name. ` ,
Subject : traversal . SourceRange ( ) . Ptr ( ) ,
} )
return nil , diags
}
remain := traversal [ 1 : ] // trim off "resource" so we can use our shared resource reference parser
return parseResourceRef ( ManagedResourceMode , rootRange , remain )
2018-03-30 22:18:59 -05:00
case "local" :
2025-02-11 18:01:25 -06:00
return parseSingleAttrRef ( traversal , func ( name string ) Referenceable {
return LocalValue { Name : name }
2018-03-30 22:18:59 -05:00
} )
2025-02-11 18:01:25 -06:00
case "module" :
return parseModuleCallRef ( traversal )
2018-03-30 22:18:59 -05:00
case "path" :
2025-02-11 18:01:25 -06:00
return parseSingleAttrRef ( traversal , func ( name string ) Referenceable {
return PathAttr { Name : name }
} )
2018-03-30 22:18:59 -05:00
case "self" :
return & Reference {
Subject : Self ,
SourceRange : tfdiags . SourceRangeFromHCL ( rootRange ) ,
Remaining : traversal [ 1 : ] ,
} , diags
case "terraform" :
2025-02-11 18:01:25 -06:00
return parseSingleAttrRef ( traversal , func ( name string ) Referenceable {
return NewTerraformAttr ( IdentTerraform , name )
} )
2024-08-01 07:14:34 -05:00
case "tofu" :
2025-02-11 18:01:25 -06:00
return parseSingleAttrRef ( traversal , func ( name string ) Referenceable {
return NewTerraformAttr ( IdentTofu , name )
} )
2018-03-30 22:18:59 -05:00
case "var" :
2025-02-11 18:01:25 -06:00
return parseSingleAttrRef ( traversal , func ( name string ) Referenceable {
return InputVariable { Name : name }
} )
2021-05-14 18:58:02 -05:00
case "template" , "lazy" , "arg" :
// These names are all pre-emptively reserved in the hope of landing
// some version of "template values" or "lazy expressions" feature
// before the next opt-in language edition, but don't yet do anything.
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Reserved symbol name" ,
2023-09-21 07:38:46 -05:00
Detail : fmt . Sprintf ( "The symbol name %q is reserved for use in a future OpenTofu version. If you are using a provider that already uses this as a resource type name, add the prefix \"resource.\" to force interpretation as a resource type name." , root ) ,
2021-05-14 18:58:02 -05:00
Subject : rootRange . Ptr ( ) ,
} )
return nil , diags
2018-03-30 22:18:59 -05:00
default :
2024-04-18 08:11:38 -05:00
function := ParseFunction ( root )
if function . IsNamespace ( FunctionNamespaceProvider ) {
pf , err := function . AsProviderFunction ( )
if err != nil {
return nil , diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Unable to parse provider function" ,
Detail : err . Error ( ) ,
Subject : rootRange . Ptr ( ) ,
} )
}
return & Reference {
Subject : pf ,
SourceRange : tfdiags . SourceRangeFromHCL ( rootRange ) ,
} , diags
}
2018-03-30 22:18:59 -05:00
return parseResourceRef ( ManagedResourceMode , rootRange , traversal )
}
}
func parseResourceRef ( mode ResourceMode , startRange hcl . Range , traversal hcl . Traversal ) ( * Reference , tfdiags . Diagnostics ) {
var diags tfdiags . Diagnostics
if len ( traversal ) < 2 {
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid reference" ,
Detail : ` A reference to a resource type must be followed by at least one attribute access, specifying the resource name. ` ,
Subject : hcl . RangeBetween ( traversal [ 0 ] . SourceRange ( ) , traversal [ len ( traversal ) - 1 ] . SourceRange ( ) ) . Ptr ( ) ,
} )
return nil , diags
}
var typeName , name string
switch tt := traversal [ 0 ] . ( type ) { // Could be either root or attr, depending on our resource mode
case hcl . TraverseRoot :
typeName = tt . Name
case hcl . TraverseAttr :
typeName = tt . Name
default :
// If it isn't a TraverseRoot then it must be a "data" reference.
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid reference" ,
Detail : ` The "data" object does not support this operation. ` ,
Subject : traversal [ 0 ] . SourceRange ( ) . Ptr ( ) ,
} )
return nil , diags
}
attrTrav , ok := traversal [ 1 ] . ( hcl . TraverseAttr )
if ! ok {
var what string
switch mode {
case DataResourceMode :
what = "data source"
default :
what = "resource type"
}
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid reference" ,
Detail : fmt . Sprintf ( ` A reference to a %s must be followed by at least one attribute access, specifying the resource name. ` , what ) ,
Subject : traversal [ 1 ] . SourceRange ( ) . Ptr ( ) ,
} )
return nil , diags
}
name = attrTrav . Name
rng := hcl . RangeBetween ( startRange , attrTrav . SrcRange )
remain := traversal [ 2 : ]
resourceAddr := Resource {
Mode : mode ,
Type : typeName ,
Name : name ,
}
resourceInstAddr := ResourceInstance {
Resource : resourceAddr ,
Key : NoKey ,
}
if len ( remain ) == 0 {
// This might actually be a reference to the collection of all instances
// of the resource, but we don't have enough context here to decide
// so we'll let the caller resolve that ambiguity.
return & Reference {
2019-09-19 08:53:23 -05:00
Subject : resourceAddr ,
2018-03-30 22:18:59 -05:00
SourceRange : tfdiags . SourceRangeFromHCL ( rng ) ,
} , diags
}
if idxTrav , ok := remain [ 0 ] . ( hcl . TraverseIndex ) ; ok {
var err error
resourceInstAddr . Key , err = ParseInstanceKey ( idxTrav . Key )
if err != nil {
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid index key" ,
Detail : fmt . Sprintf ( "Invalid index for resource instance: %s." , err ) ,
Subject : & idxTrav . SrcRange ,
} )
return nil , diags
}
remain = remain [ 1 : ]
rng = hcl . RangeBetween ( rng , idxTrav . SrcRange )
}
return & Reference {
Subject : resourceInstAddr ,
SourceRange : tfdiags . SourceRangeFromHCL ( rng ) ,
Remaining : remain ,
} , diags
}
2025-02-11 18:01:25 -06:00
func parseModuleCallRef ( traversal hcl . Traversal ) ( * Reference , tfdiags . Diagnostics ) {
// The following is a little circuitous just so we can reuse parseSingleAttrRef
// for this slightly-odd case while keeping it relatively simple for all of the
// other cases that use it: we first get the information we need wrapped up
// in a *Reference and then unpack it to perform further work below.
callRef , diags := parseSingleAttrRef ( traversal , func ( name string ) Referenceable {
return ModuleCallInstance {
Call : ModuleCall {
Name : name ,
} ,
Key : NoKey ,
}
} )
if diags . HasErrors ( ) {
return nil , diags
}
// A traversal starting with "module" can either be a reference to an
// entire module, or to a single output from a module instance,
// depending on what we find after this introducer.
callInstance := callRef . Subject . ( ModuleCallInstance ) //nolint:errcheck // This was constructed directly above by call to parseSingleAttrRef
callRange := callRef . SourceRange
remain := callRef . Remaining
if len ( remain ) == 0 {
// Reference to an entire module. Might alternatively be a
// reference to a single instance of a particular module, but the
// caller will need to deal with that ambiguity since we don't have
// enough context here.
return & Reference {
Subject : callInstance . Call ,
SourceRange : callRange ,
Remaining : remain ,
} , diags
}
if idxTrav , ok := remain [ 0 ] . ( hcl . TraverseIndex ) ; ok {
var err error
callInstance . Key , err = ParseInstanceKey ( idxTrav . Key )
if err != nil {
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid index key" ,
Detail : fmt . Sprintf ( "Invalid index for module instance: %s." , err ) ,
Subject : & idxTrav . SrcRange ,
} )
return nil , diags
}
remain = remain [ 1 : ]
if len ( remain ) == 0 {
// Also a reference to an entire module instance, but we have a key
// now.
return & Reference {
Subject : callInstance ,
SourceRange : tfdiags . SourceRangeFromHCL ( hcl . RangeBetween ( callRange . ToHCL ( ) , idxTrav . SrcRange ) ) ,
Remaining : remain ,
} , diags
}
}
if attrTrav , ok := remain [ 0 ] . ( hcl . TraverseAttr ) ; ok {
remain = remain [ 1 : ]
return & Reference {
Subject : ModuleCallInstanceOutput {
Name : attrTrav . Name ,
Call : callInstance ,
} ,
SourceRange : tfdiags . SourceRangeFromHCL ( hcl . RangeBetween ( callRange . ToHCL ( ) , attrTrav . SrcRange ) ) ,
Remaining : remain ,
} , diags
}
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid reference" ,
Detail : "Module instance objects do not support this operation." ,
Subject : remain [ 0 ] . SourceRange ( ) . Ptr ( ) ,
} )
return nil , diags
}
func parseSingleAttrRef ( traversal hcl . Traversal , makeAddr func ( name string ) Referenceable ) ( * Reference , tfdiags . Diagnostics ) {
2018-03-30 22:18:59 -05:00
var diags tfdiags . Diagnostics
root := traversal . RootName ( )
rootRange := traversal [ 0 ] . SourceRange ( )
2025-02-11 18:01:25 -06:00
// NOTE: In a previous version of this file parseSingleAttrRef only returned the component parts
// of a *Reference and then the callers assembled them, which caused the main parseRef function
// to return a non-nil result (with mostly-garbage field values) even in the error cases.
// We've preserved that oddity for now because our code complexity refactoring efforts should
// not change the externally-observable behavior, but to guarantee that we'd need to review
// all uses of parseRef to make sure that they aren't depending on getting a non-nil *Reference
// along with error diagnostics. :(
2018-03-30 22:18:59 -05:00
if len ( traversal ) < 2 {
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid reference" ,
Detail : fmt . Sprintf ( "The %q object cannot be accessed directly. Instead, access one of its attributes." , root ) ,
Subject : & rootRange ,
} )
2025-02-11 18:01:25 -06:00
return & Reference { Subject : makeAddr ( "" ) } , diags
2018-03-30 22:18:59 -05:00
}
if attrTrav , ok := traversal [ 1 ] . ( hcl . TraverseAttr ) ; ok {
2025-02-11 18:01:25 -06:00
subjectAddr := makeAddr ( attrTrav . Name )
return & Reference {
Subject : subjectAddr ,
SourceRange : tfdiags . SourceRangeFromHCL ( hcl . RangeBetween ( rootRange , attrTrav . SrcRange ) ) ,
Remaining : traversal [ 2 : ] ,
} , diags
2018-03-30 22:18:59 -05:00
}
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid reference" ,
Detail : fmt . Sprintf ( "The %q object does not support this operation." , root ) ,
Subject : traversal [ 1 ] . SourceRange ( ) . Ptr ( ) ,
} )
2025-02-11 18:01:25 -06:00
return & Reference { Subject : makeAddr ( "" ) } , diags
2018-03-30 22:18:59 -05:00
}