mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-26 00:41:27 -06:00
a7507e140d
Module references, like resource references, need to always return the and object containing all instances in order to handle modules as single values, and to postpone index evaluation to when the expression as whole is evaluated.
346 lines
10 KiB
Go
346 lines
10 KiB
Go
package addrs
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
|
"github.com/hashicorp/terraform/tfdiags"
|
|
)
|
|
|
|
// 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) {
|
|
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) {
|
|
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
|
|
|
|
case "each":
|
|
name, rng, remain, diags := parseSingleAttrRef(traversal)
|
|
return &Reference{
|
|
Subject: ForEachAttr{Name: name},
|
|
SourceRange: tfdiags.SourceRangeFromHCL(rng),
|
|
Remaining: remain,
|
|
}, diags
|
|
|
|
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)
|
|
|
|
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
|
|
}
|
|
|
|
// 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 := ModuleCallInstance{
|
|
Call: ModuleCall{
|
|
Name: callName,
|
|
},
|
|
Key: NoKey,
|
|
}
|
|
|
|
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: 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{
|
|
Subject: AbsModuleCallOutput{
|
|
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
|
|
|
|
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{
|
|
Subject: resourceAddr,
|
|
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
|
|
}
|