2018-03-31 05:18:59 +02:00
package addrs
import (
"fmt"
2019-09-10 00:58:44 +02:00
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
2021-05-17 19:11:06 +02:00
"github.com/hashicorp/terraform/internal/tfdiags"
2018-03-31 05:18:59 +02:00
)
// Reference describes a reference to an address with source location
// information.
type Reference struct {
Subject Referenceable
SourceRange tfdiags . SourceRange
Remaining hcl . Traversal
}
// ParseRef attempts to extract a referencable address from the prefix of the
// 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-11 00:59:47 +02: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
}
// 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
}
func parseRef ( traversal hcl . Traversal ) ( * Reference , tfdiags . Diagnostics ) {
2018-03-31 05:18:59 +02:00
var diags tfdiags . Diagnostics
root := traversal . RootName ( )
rootRange := traversal [ 0 ] . SourceRange ( )
switch root {
case "count" :
name , rng , remain , diags := parseSingleAttrRef ( traversal )
return & Reference {
Subject : CountAttr { Name : name } ,
SourceRange : tfdiags . SourceRangeFromHCL ( rng ) ,
Remaining : remain ,
} , diags
2019-06-12 17:07:32 +02:00
case "each" :
name , rng , remain , diags := parseSingleAttrRef ( traversal )
return & Reference {
Subject : ForEachAttr { Name : name } ,
SourceRange : tfdiags . SourceRangeFromHCL ( rng ) ,
Remaining : remain ,
} , diags
2018-03-31 05:18:59 +02: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-15 01:42:30 +02: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
// if a later edition of the Terraform language introduces a new
// 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-31 05:18:59 +02:00
case "local" :
name , rng , remain , diags := parseSingleAttrRef ( traversal )
return & Reference {
Subject : LocalValue { Name : name } ,
SourceRange : tfdiags . SourceRangeFromHCL ( rng ) ,
Remaining : remain ,
} , diags
case "module" :
callName , callRange , remain , diags := parseSingleAttrRef ( traversal )
if diags . HasErrors ( ) {
return nil , diags
}
2020-04-12 17:26:44 +02:00
// 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.
2018-03-31 05:18:59 +02:00
callInstance := ModuleCallInstance {
Call : ModuleCall {
Name : callName ,
} ,
Key : NoKey ,
}
if len ( remain ) == 0 {
2020-04-12 17:26:44 +02:00
// 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.
2018-03-31 05:18:59 +02:00
return & Reference {
2020-04-12 17:26:44 +02:00
Subject : callInstance . Call ,
2018-03-31 05:18:59 +02:00
SourceRange : tfdiags . SourceRangeFromHCL ( 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 , idxTrav . SrcRange ) ) ,
Remaining : remain ,
} , diags
}
}
if attrTrav , ok := remain [ 0 ] . ( hcl . TraverseAttr ) ; ok {
remain = remain [ 1 : ]
return & Reference {
2021-06-30 00:06:00 +02:00
Subject : ModuleCallInstanceOutput {
2018-03-31 05:18:59 +02:00
Name : attrTrav . Name ,
Call : callInstance ,
} ,
SourceRange : tfdiags . SourceRangeFromHCL ( hcl . RangeBetween ( callRange , 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
case "path" :
name , rng , remain , diags := parseSingleAttrRef ( traversal )
return & Reference {
Subject : PathAttr { Name : name } ,
SourceRange : tfdiags . SourceRangeFromHCL ( rng ) ,
Remaining : remain ,
} , diags
case "self" :
return & Reference {
Subject : Self ,
SourceRange : tfdiags . SourceRangeFromHCL ( rootRange ) ,
Remaining : traversal [ 1 : ] ,
} , diags
case "terraform" :
name , rng , remain , diags := parseSingleAttrRef ( traversal )
return & Reference {
Subject : TerraformAttr { Name : name } ,
SourceRange : tfdiags . SourceRangeFromHCL ( rng ) ,
Remaining : remain ,
} , diags
case "var" :
name , rng , remain , diags := parseSingleAttrRef ( traversal )
return & Reference {
Subject : InputVariable { Name : name } ,
SourceRange : tfdiags . SourceRangeFromHCL ( rng ) ,
Remaining : remain ,
} , diags
2021-05-15 01:58:02 +02: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" ,
Detail : fmt . Sprintf ( "The symbol name %q is reserved for use in a future Terraform 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 ) ,
Subject : rootRange . Ptr ( ) ,
} )
return nil , diags
2018-03-31 05:18:59 +02:00
default :
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 15:53:23 +02:00
Subject : resourceAddr ,
2018-03-31 05:18:59 +02: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
}
func parseSingleAttrRef ( traversal hcl . Traversal ) ( string , hcl . Range , hcl . Traversal , tfdiags . Diagnostics ) {
var diags tfdiags . Diagnostics
root := traversal . RootName ( )
rootRange := traversal [ 0 ] . SourceRange ( )
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 ,
} )
return "" , hcl . Range { } , nil , diags
}
if attrTrav , ok := traversal [ 1 ] . ( hcl . TraverseAttr ) ; ok {
return attrTrav . Name , hcl . RangeBetween ( rootRange , attrTrav . SrcRange ) , traversal [ 2 : ] , diags
}
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 ( ) ,
} )
return "" , hcl . Range { } , nil , diags
}