mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-30 10:47:14 -06:00
2453025a1a
We've ended up implementing something approximately like this in a few places now, so this is a centralized version that we can consolidate on moving forward, gradually removing that duplication.
418 lines
13 KiB
Go
418 lines
13 KiB
Go
package addrs
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
// Reference describes a reference to an address with source location
|
|
// information.
|
|
type Reference struct {
|
|
Subject Referenceable
|
|
SourceRange tfdiags.SourceRange
|
|
Remaining hcl.Traversal
|
|
}
|
|
|
|
// DisplayString returns a string that approximates the subject and remaining
|
|
// traversal of the reciever in a way that resembles the Terraform language
|
|
// syntax that could've produced it.
|
|
//
|
|
// It's not guaranteed to actually be a valid Terraform language expression,
|
|
// 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()
|
|
}
|
|
|
|
// 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 "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)
|
|
|
|
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: ModuleCallInstanceOutput{
|
|
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
|
|
|
|
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
|
|
|
|
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
|
|
}
|