addrs: ParseRef function, for parsing references in expressions
This function corresponds to terraform.NewInterpolatedVariable, but built with HCL2 primitives. It accepts a hcl.Traversal, which is what is returned from the HCL2 API functions to find which variables are referenced in a given expression.
This commit is contained in:
parent
b9d84f2944
commit
63041ffa09
|
@ -0,0 +1,296 @@
|
||||||
|
package addrs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/hcl2/hcl"
|
||||||
|
"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) {
|
||||||
|
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 "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 instance 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 instance. Might alternatively
|
||||||
|
// be a reference to a collection of instances 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,
|
||||||
|
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: ModuleCallOutput{
|
||||||
|
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: resourceInstAddr,
|
||||||
|
SourceRange: tfdiags.SourceRangeFromHCL(rng),
|
||||||
|
Remaining: remain,
|
||||||
|
}, 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
|
||||||
|
}
|
|
@ -0,0 +1,689 @@
|
||||||
|
package addrs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-test/deep"
|
||||||
|
"github.com/hashicorp/hcl2/hcl"
|
||||||
|
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
||||||
|
"github.com/hashicorp/terraform/tfdiags"
|
||||||
|
"github.com/zclconf/go-cty/cty"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseRef(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
Input string
|
||||||
|
Want *Reference
|
||||||
|
WantErr string
|
||||||
|
}{
|
||||||
|
|
||||||
|
// count
|
||||||
|
{
|
||||||
|
`count.index`,
|
||||||
|
&Reference{
|
||||||
|
Subject: CountAttr{
|
||||||
|
Name: "index",
|
||||||
|
},
|
||||||
|
SourceRange: tfdiags.SourceRange{
|
||||||
|
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||||
|
End: tfdiags.SourcePos{Line: 1, Column: 12, Byte: 11},
|
||||||
|
},
|
||||||
|
Remaining: hcl.Traversal{},
|
||||||
|
},
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`count.index.blah`,
|
||||||
|
&Reference{
|
||||||
|
Subject: CountAttr{
|
||||||
|
Name: "index",
|
||||||
|
},
|
||||||
|
SourceRange: tfdiags.SourceRange{
|
||||||
|
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||||
|
End: tfdiags.SourcePos{Line: 1, Column: 12, Byte: 11},
|
||||||
|
},
|
||||||
|
Remaining: hcl.Traversal{
|
||||||
|
hcl.TraverseAttr{
|
||||||
|
Name: "blah",
|
||||||
|
SrcRange: hcl.Range{
|
||||||
|
Start: hcl.Pos{Line: 1, Column: 12, Byte: 11},
|
||||||
|
End: hcl.Pos{Line: 1, Column: 17, Byte: 16},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
``, // valid at this layer, but will fail during eval because "index" is a number
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`count`,
|
||||||
|
nil,
|
||||||
|
`The "count" object cannot be accessed directly. Instead, access one of its attributes.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`count["hello"]`,
|
||||||
|
nil,
|
||||||
|
`The "count" object does not support this operation.`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// data
|
||||||
|
{
|
||||||
|
`data.external.foo`,
|
||||||
|
&Reference{
|
||||||
|
Subject: ResourceInstance{
|
||||||
|
Resource: Resource{
|
||||||
|
Mode: DataResourceMode,
|
||||||
|
Type: "external",
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SourceRange: tfdiags.SourceRange{
|
||||||
|
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||||
|
End: tfdiags.SourcePos{Line: 1, Column: 18, Byte: 17},
|
||||||
|
},
|
||||||
|
Remaining: hcl.Traversal{},
|
||||||
|
},
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`data.external.foo.bar`,
|
||||||
|
&Reference{
|
||||||
|
Subject: ResourceInstance{
|
||||||
|
Resource: Resource{
|
||||||
|
Mode: DataResourceMode,
|
||||||
|
Type: "external",
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SourceRange: tfdiags.SourceRange{
|
||||||
|
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||||
|
End: tfdiags.SourcePos{Line: 1, Column: 18, Byte: 17},
|
||||||
|
},
|
||||||
|
Remaining: hcl.Traversal{
|
||||||
|
hcl.TraverseAttr{
|
||||||
|
Name: "bar",
|
||||||
|
SrcRange: hcl.Range{
|
||||||
|
Start: hcl.Pos{Line: 1, Column: 18, Byte: 17},
|
||||||
|
End: hcl.Pos{Line: 1, Column: 22, Byte: 21},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`data.external.foo["baz"].bar`,
|
||||||
|
&Reference{
|
||||||
|
Subject: ResourceInstance{
|
||||||
|
Resource: Resource{
|
||||||
|
Mode: DataResourceMode,
|
||||||
|
Type: "external",
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
Key: StringKey("baz"),
|
||||||
|
},
|
||||||
|
SourceRange: tfdiags.SourceRange{
|
||||||
|
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||||
|
End: tfdiags.SourcePos{Line: 1, Column: 25, Byte: 24},
|
||||||
|
},
|
||||||
|
Remaining: hcl.Traversal{
|
||||||
|
hcl.TraverseAttr{
|
||||||
|
Name: "bar",
|
||||||
|
SrcRange: hcl.Range{
|
||||||
|
Start: hcl.Pos{Line: 1, Column: 25, Byte: 24},
|
||||||
|
End: hcl.Pos{Line: 1, Column: 29, Byte: 28},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`data.external.foo["baz"]`,
|
||||||
|
&Reference{
|
||||||
|
Subject: ResourceInstance{
|
||||||
|
Resource: Resource{
|
||||||
|
Mode: DataResourceMode,
|
||||||
|
Type: "external",
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
Key: StringKey("baz"),
|
||||||
|
},
|
||||||
|
SourceRange: tfdiags.SourceRange{
|
||||||
|
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||||
|
End: tfdiags.SourcePos{Line: 1, Column: 25, Byte: 24},
|
||||||
|
},
|
||||||
|
Remaining: hcl.Traversal{},
|
||||||
|
},
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`data`,
|
||||||
|
nil,
|
||||||
|
`The "data" object must be followed by two attribute names: the data source type and the resource name.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`data.external`,
|
||||||
|
nil,
|
||||||
|
`The "data" object must be followed by two attribute names: the data source type and the resource name.`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// local
|
||||||
|
{
|
||||||
|
`local.foo`,
|
||||||
|
&Reference{
|
||||||
|
Subject: LocalValue{
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
SourceRange: tfdiags.SourceRange{
|
||||||
|
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||||
|
End: tfdiags.SourcePos{Line: 1, Column: 10, Byte: 9},
|
||||||
|
},
|
||||||
|
Remaining: hcl.Traversal{},
|
||||||
|
},
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`local.foo.blah`,
|
||||||
|
&Reference{
|
||||||
|
Subject: LocalValue{
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
SourceRange: tfdiags.SourceRange{
|
||||||
|
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||||
|
End: tfdiags.SourcePos{Line: 1, Column: 10, Byte: 9},
|
||||||
|
},
|
||||||
|
Remaining: hcl.Traversal{
|
||||||
|
hcl.TraverseAttr{
|
||||||
|
Name: "blah",
|
||||||
|
SrcRange: hcl.Range{
|
||||||
|
Start: hcl.Pos{Line: 1, Column: 10, Byte: 9},
|
||||||
|
End: hcl.Pos{Line: 1, Column: 15, Byte: 14},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`local.foo["blah"]`,
|
||||||
|
&Reference{
|
||||||
|
Subject: LocalValue{
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
SourceRange: tfdiags.SourceRange{
|
||||||
|
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||||
|
End: tfdiags.SourcePos{Line: 1, Column: 10, Byte: 9},
|
||||||
|
},
|
||||||
|
Remaining: hcl.Traversal{
|
||||||
|
hcl.TraverseIndex{
|
||||||
|
Key: cty.StringVal("blah"),
|
||||||
|
SrcRange: hcl.Range{
|
||||||
|
Start: hcl.Pos{Line: 1, Column: 10, Byte: 9},
|
||||||
|
End: hcl.Pos{Line: 1, Column: 18, Byte: 17},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`local`,
|
||||||
|
nil,
|
||||||
|
`The "local" object cannot be accessed directly. Instead, access one of its attributes.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`local["foo"]`,
|
||||||
|
nil,
|
||||||
|
`The "local" object does not support this operation.`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// module
|
||||||
|
{
|
||||||
|
`module.foo`,
|
||||||
|
&Reference{
|
||||||
|
Subject: ModuleCallInstance{
|
||||||
|
Call: ModuleCall{
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SourceRange: tfdiags.SourceRange{
|
||||||
|
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||||
|
End: tfdiags.SourcePos{Line: 1, Column: 11, Byte: 10},
|
||||||
|
},
|
||||||
|
Remaining: hcl.Traversal{},
|
||||||
|
},
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`module.foo.bar`,
|
||||||
|
&Reference{
|
||||||
|
Subject: ModuleCallOutput{
|
||||||
|
Call: ModuleCallInstance{
|
||||||
|
Call: ModuleCall{
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Name: "bar",
|
||||||
|
},
|
||||||
|
SourceRange: tfdiags.SourceRange{
|
||||||
|
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||||
|
End: tfdiags.SourcePos{Line: 1, Column: 15, Byte: 14},
|
||||||
|
},
|
||||||
|
Remaining: hcl.Traversal{},
|
||||||
|
},
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`module.foo.bar.baz`,
|
||||||
|
&Reference{
|
||||||
|
Subject: ModuleCallOutput{
|
||||||
|
Call: ModuleCallInstance{
|
||||||
|
Call: ModuleCall{
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Name: "bar",
|
||||||
|
},
|
||||||
|
SourceRange: tfdiags.SourceRange{
|
||||||
|
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||||
|
End: tfdiags.SourcePos{Line: 1, Column: 15, Byte: 14},
|
||||||
|
},
|
||||||
|
Remaining: hcl.Traversal{
|
||||||
|
hcl.TraverseAttr{
|
||||||
|
Name: "baz",
|
||||||
|
SrcRange: hcl.Range{
|
||||||
|
Start: hcl.Pos{Line: 1, Column: 15, Byte: 14},
|
||||||
|
End: hcl.Pos{Line: 1, Column: 19, Byte: 18},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`module.foo["baz"]`,
|
||||||
|
&Reference{
|
||||||
|
Subject: ModuleCallInstance{
|
||||||
|
Call: ModuleCall{
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
Key: StringKey("baz"),
|
||||||
|
},
|
||||||
|
SourceRange: tfdiags.SourceRange{
|
||||||
|
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||||
|
End: tfdiags.SourcePos{Line: 1, Column: 18, Byte: 17},
|
||||||
|
},
|
||||||
|
Remaining: hcl.Traversal{},
|
||||||
|
},
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`module.foo["baz"].bar`,
|
||||||
|
&Reference{
|
||||||
|
Subject: ModuleCallOutput{
|
||||||
|
Call: ModuleCallInstance{
|
||||||
|
Call: ModuleCall{
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
Key: StringKey("baz"),
|
||||||
|
},
|
||||||
|
Name: "bar",
|
||||||
|
},
|
||||||
|
SourceRange: tfdiags.SourceRange{
|
||||||
|
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||||
|
End: tfdiags.SourcePos{Line: 1, Column: 22, Byte: 21},
|
||||||
|
},
|
||||||
|
Remaining: hcl.Traversal{},
|
||||||
|
},
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`module.foo["baz"].bar.boop`,
|
||||||
|
&Reference{
|
||||||
|
Subject: ModuleCallOutput{
|
||||||
|
Call: ModuleCallInstance{
|
||||||
|
Call: ModuleCall{
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
Key: StringKey("baz"),
|
||||||
|
},
|
||||||
|
Name: "bar",
|
||||||
|
},
|
||||||
|
SourceRange: tfdiags.SourceRange{
|
||||||
|
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||||
|
End: tfdiags.SourcePos{Line: 1, Column: 22, Byte: 21},
|
||||||
|
},
|
||||||
|
Remaining: hcl.Traversal{
|
||||||
|
hcl.TraverseAttr{
|
||||||
|
Name: "boop",
|
||||||
|
SrcRange: hcl.Range{
|
||||||
|
Start: hcl.Pos{Line: 1, Column: 22, Byte: 21},
|
||||||
|
End: hcl.Pos{Line: 1, Column: 27, Byte: 26},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`module`,
|
||||||
|
nil,
|
||||||
|
`The "module" object cannot be accessed directly. Instead, access one of its attributes.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`module["foo"]`,
|
||||||
|
nil,
|
||||||
|
`The "module" object does not support this operation.`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// path
|
||||||
|
{
|
||||||
|
`path.module`,
|
||||||
|
&Reference{
|
||||||
|
Subject: PathAttr{
|
||||||
|
Name: "module",
|
||||||
|
},
|
||||||
|
SourceRange: tfdiags.SourceRange{
|
||||||
|
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||||
|
End: tfdiags.SourcePos{Line: 1, Column: 12, Byte: 11},
|
||||||
|
},
|
||||||
|
Remaining: hcl.Traversal{},
|
||||||
|
},
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`path.module.blah`,
|
||||||
|
&Reference{
|
||||||
|
Subject: PathAttr{
|
||||||
|
Name: "module",
|
||||||
|
},
|
||||||
|
SourceRange: tfdiags.SourceRange{
|
||||||
|
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||||
|
End: tfdiags.SourcePos{Line: 1, Column: 12, Byte: 11},
|
||||||
|
},
|
||||||
|
Remaining: hcl.Traversal{
|
||||||
|
hcl.TraverseAttr{
|
||||||
|
Name: "blah",
|
||||||
|
SrcRange: hcl.Range{
|
||||||
|
Start: hcl.Pos{Line: 1, Column: 12, Byte: 11},
|
||||||
|
End: hcl.Pos{Line: 1, Column: 17, Byte: 16},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
``, // valid at this layer, but will fail during eval because "module" is a string
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`path`,
|
||||||
|
nil,
|
||||||
|
`The "path" object cannot be accessed directly. Instead, access one of its attributes.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`path["module"]`,
|
||||||
|
nil,
|
||||||
|
`The "path" object does not support this operation.`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// self
|
||||||
|
{
|
||||||
|
`self`,
|
||||||
|
&Reference{
|
||||||
|
Subject: Self,
|
||||||
|
SourceRange: tfdiags.SourceRange{
|
||||||
|
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||||
|
End: tfdiags.SourcePos{Line: 1, Column: 5, Byte: 4},
|
||||||
|
},
|
||||||
|
Remaining: hcl.Traversal{},
|
||||||
|
},
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`self.blah`,
|
||||||
|
&Reference{
|
||||||
|
Subject: Self,
|
||||||
|
SourceRange: tfdiags.SourceRange{
|
||||||
|
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||||
|
End: tfdiags.SourcePos{Line: 1, Column: 5, Byte: 4},
|
||||||
|
},
|
||||||
|
Remaining: hcl.Traversal{
|
||||||
|
hcl.TraverseAttr{
|
||||||
|
Name: "blah",
|
||||||
|
SrcRange: hcl.Range{
|
||||||
|
Start: hcl.Pos{Line: 1, Column: 5, Byte: 4},
|
||||||
|
End: hcl.Pos{Line: 1, Column: 10, Byte: 9},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
|
||||||
|
// terraform
|
||||||
|
{
|
||||||
|
`terraform.workspace`,
|
||||||
|
&Reference{
|
||||||
|
Subject: TerraformAttr{
|
||||||
|
Name: "workspace",
|
||||||
|
},
|
||||||
|
SourceRange: tfdiags.SourceRange{
|
||||||
|
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||||
|
End: tfdiags.SourcePos{Line: 1, Column: 20, Byte: 19},
|
||||||
|
},
|
||||||
|
Remaining: hcl.Traversal{},
|
||||||
|
},
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`terraform.workspace.blah`,
|
||||||
|
&Reference{
|
||||||
|
Subject: TerraformAttr{
|
||||||
|
Name: "workspace",
|
||||||
|
},
|
||||||
|
SourceRange: tfdiags.SourceRange{
|
||||||
|
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||||
|
End: tfdiags.SourcePos{Line: 1, Column: 20, Byte: 19},
|
||||||
|
},
|
||||||
|
Remaining: hcl.Traversal{
|
||||||
|
hcl.TraverseAttr{
|
||||||
|
Name: "blah",
|
||||||
|
SrcRange: hcl.Range{
|
||||||
|
Start: hcl.Pos{Line: 1, Column: 20, Byte: 19},
|
||||||
|
End: hcl.Pos{Line: 1, Column: 25, Byte: 24},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
``, // valid at this layer, but will fail during eval because "workspace" is a string
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`terraform`,
|
||||||
|
nil,
|
||||||
|
`The "terraform" object cannot be accessed directly. Instead, access one of its attributes.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`terraform["workspace"]`,
|
||||||
|
nil,
|
||||||
|
`The "terraform" object does not support this operation.`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// var
|
||||||
|
{
|
||||||
|
`var.foo`,
|
||||||
|
&Reference{
|
||||||
|
Subject: InputVariable{
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
SourceRange: tfdiags.SourceRange{
|
||||||
|
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||||
|
End: tfdiags.SourcePos{Line: 1, Column: 8, Byte: 7},
|
||||||
|
},
|
||||||
|
Remaining: hcl.Traversal{},
|
||||||
|
},
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`var.foo.blah`,
|
||||||
|
&Reference{
|
||||||
|
Subject: InputVariable{
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
SourceRange: tfdiags.SourceRange{
|
||||||
|
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||||
|
End: tfdiags.SourcePos{Line: 1, Column: 8, Byte: 7},
|
||||||
|
},
|
||||||
|
Remaining: hcl.Traversal{
|
||||||
|
hcl.TraverseAttr{
|
||||||
|
Name: "blah",
|
||||||
|
SrcRange: hcl.Range{
|
||||||
|
Start: hcl.Pos{Line: 1, Column: 8, Byte: 7},
|
||||||
|
End: hcl.Pos{Line: 1, Column: 13, Byte: 12},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
``, // valid at this layer, but will fail during eval because "module" is a string
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`var`,
|
||||||
|
nil,
|
||||||
|
`The "var" object cannot be accessed directly. Instead, access one of its attributes.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`var["foo"]`,
|
||||||
|
nil,
|
||||||
|
`The "var" object does not support this operation.`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// anything else, interpreted as a managed resource reference
|
||||||
|
{
|
||||||
|
`boop_instance.foo`,
|
||||||
|
&Reference{
|
||||||
|
Subject: ResourceInstance{
|
||||||
|
Resource: Resource{
|
||||||
|
Mode: ManagedResourceMode,
|
||||||
|
Type: "boop_instance",
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SourceRange: tfdiags.SourceRange{
|
||||||
|
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||||
|
End: tfdiags.SourcePos{Line: 1, Column: 18, Byte: 17},
|
||||||
|
},
|
||||||
|
Remaining: hcl.Traversal{},
|
||||||
|
},
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`boop_instance.foo.bar`,
|
||||||
|
&Reference{
|
||||||
|
Subject: ResourceInstance{
|
||||||
|
Resource: Resource{
|
||||||
|
Mode: ManagedResourceMode,
|
||||||
|
Type: "boop_instance",
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SourceRange: tfdiags.SourceRange{
|
||||||
|
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||||
|
End: tfdiags.SourcePos{Line: 1, Column: 18, Byte: 17},
|
||||||
|
},
|
||||||
|
Remaining: hcl.Traversal{
|
||||||
|
hcl.TraverseAttr{
|
||||||
|
Name: "bar",
|
||||||
|
SrcRange: hcl.Range{
|
||||||
|
Start: hcl.Pos{Line: 1, Column: 18, Byte: 17},
|
||||||
|
End: hcl.Pos{Line: 1, Column: 22, Byte: 21},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`boop_instance.foo["baz"].bar`,
|
||||||
|
&Reference{
|
||||||
|
Subject: ResourceInstance{
|
||||||
|
Resource: Resource{
|
||||||
|
Mode: ManagedResourceMode,
|
||||||
|
Type: "boop_instance",
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
Key: StringKey("baz"),
|
||||||
|
},
|
||||||
|
SourceRange: tfdiags.SourceRange{
|
||||||
|
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||||
|
End: tfdiags.SourcePos{Line: 1, Column: 25, Byte: 24},
|
||||||
|
},
|
||||||
|
Remaining: hcl.Traversal{
|
||||||
|
hcl.TraverseAttr{
|
||||||
|
Name: "bar",
|
||||||
|
SrcRange: hcl.Range{
|
||||||
|
Start: hcl.Pos{Line: 1, Column: 25, Byte: 24},
|
||||||
|
End: hcl.Pos{Line: 1, Column: 29, Byte: 28},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`boop_instance.foo["baz"]`,
|
||||||
|
&Reference{
|
||||||
|
Subject: ResourceInstance{
|
||||||
|
Resource: Resource{
|
||||||
|
Mode: ManagedResourceMode,
|
||||||
|
Type: "boop_instance",
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
Key: StringKey("baz"),
|
||||||
|
},
|
||||||
|
SourceRange: tfdiags.SourceRange{
|
||||||
|
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
|
||||||
|
End: tfdiags.SourcePos{Line: 1, Column: 25, Byte: 24},
|
||||||
|
},
|
||||||
|
Remaining: hcl.Traversal{},
|
||||||
|
},
|
||||||
|
``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`boop_instance`,
|
||||||
|
nil,
|
||||||
|
`A reference to a resource type must be followed by at least one attribute access, specifying the resource name.`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.Input, func(t *testing.T) {
|
||||||
|
traversal, travDiags := hclsyntax.ParseTraversalAbs([]byte(test.Input), "", hcl.Pos{Line: 1, Column: 1})
|
||||||
|
if travDiags.HasErrors() {
|
||||||
|
t.Fatal(travDiags.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
got, diags := ParseRef(traversal)
|
||||||
|
|
||||||
|
switch len(diags) {
|
||||||
|
case 0:
|
||||||
|
if test.WantErr != "" {
|
||||||
|
t.Fatalf("succeeded; want error: %s", test.WantErr)
|
||||||
|
}
|
||||||
|
case 1:
|
||||||
|
if test.WantErr == "" {
|
||||||
|
t.Fatalf("unexpected diagnostics: %s", diags.Err())
|
||||||
|
}
|
||||||
|
if got, want := diags[0].Description().Detail, test.WantErr; got != want {
|
||||||
|
t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatalf("too many diagnostics: %s", diags.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
if diags.HasErrors() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, problem := range deep.Equal(got, test.Want) {
|
||||||
|
t.Errorf(problem)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue