addrs: ParseAbsProviderConfig function

This is for parsing the type of provider configuration address we write
into state in order to remember which provider configuration is
responsible for each resource.
This commit is contained in:
Martin Atkins 2018-04-23 16:58:01 -07:00
parent 8ca174b133
commit c6598a3f86
3 changed files with 411 additions and 1 deletions

View File

@ -1,6 +1,16 @@
package addrs
import "bytes"
import (
"bytes"
"fmt"
"github.com/zclconf/go-cty/cty/gocty"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/terraform/tfdiags"
)
// ModuleInstance is an address for a particular module instance within the
// dynamic module tree. This is an extension of the static traversals
@ -12,6 +22,141 @@ import "bytes"
// creation.
type ModuleInstance []ModuleInstanceStep
func ParseModuleInstance(traversal hcl.Traversal) (ModuleInstance, tfdiags.Diagnostics) {
mi, remain, diags := parseModuleInstancePrefix(traversal)
if len(remain) != 0 {
if len(remain) == len(traversal) {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid module instance address",
Detail: "A module instance address must begin with \"module.\".",
Subject: remain.SourceRange().Ptr(),
})
} else {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid module instance address",
Detail: "The module instance address is followed by additional invalid content.",
Subject: remain.SourceRange().Ptr(),
})
}
}
return mi, diags
}
func parseModuleInstancePrefix(traversal hcl.Traversal) (ModuleInstance, hcl.Traversal, tfdiags.Diagnostics) {
remain := traversal
var mi ModuleInstance
var diags tfdiags.Diagnostics
for len(remain) > 0 {
var next string
switch tt := remain[0].(type) {
case hcl.TraverseRoot:
next = tt.Name
case hcl.TraverseAttr:
next = tt.Name
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address operator",
Detail: "Module address prefix must be followed by dot and then a name.",
Subject: remain[0].SourceRange().Ptr(),
})
break
}
if next != "module" {
break
}
kwRange := remain[0].SourceRange()
remain = remain[1:]
// If we have the prefix "module" then we should be followed by an
// module call name, as an attribute, and then optionally an index step
// giving the instance key.
if len(remain) == 0 {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address operator",
Detail: "Prefix \"module.\" must be followed by a module name.",
Subject: &kwRange,
})
break
}
var moduleName string
switch tt := remain[0].(type) {
case hcl.TraverseAttr:
moduleName = tt.Name
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address operator",
Detail: "Prefix \"module.\" must be followed by a module name.",
Subject: remain[0].SourceRange().Ptr(),
})
break
}
remain = remain[1:]
step := ModuleInstanceStep{
Name: moduleName,
}
if len(remain) > 0 {
if idx, ok := remain[0].(hcl.TraverseIndex); ok {
remain = remain[1:]
switch idx.Key.Type() {
case cty.String:
step.InstanceKey = StringKey(idx.Key.AsString())
case cty.Number:
var idxInt int
err := gocty.FromCtyValue(idx.Key, &idxInt)
if err == nil {
step.InstanceKey = IntKey(idxInt)
} else {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address operator",
Detail: fmt.Sprintf("Invalid module index: %s.", err),
Subject: idx.SourceRange().Ptr(),
})
}
default:
// Should never happen, because no other types are allowed in traversal indices.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address operator",
Detail: "Invalid module key: must be either a string or an integer.",
Subject: idx.SourceRange().Ptr(),
})
}
}
}
mi = append(mi, step)
}
var retRemain hcl.Traversal
if len(remain) > 0 {
retRemain = make(hcl.Traversal, len(remain))
copy(retRemain, remain)
// The first element here might be either a TraverseRoot or a
// TraverseAttr, depending on whether we had a module address on the
// front. To make life easier for callers, we'll normalize to always
// start with a TraverseRoot.
if tt, ok := retRemain[0].(hcl.TraverseAttr); ok {
retRemain[0] = hcl.TraverseRoot{
Name: tt.Name,
SrcRange: tt.SrcRange,
}
}
}
return mi, retRemain, diags
}
// ModuleInstanceStep is a single traversal step through the dynamic module
// tree. It is used only as part of ModuleInstance.
type ModuleInstanceStep struct {

View File

@ -2,6 +2,10 @@ package addrs
import (
"fmt"
"github.com/hashicorp/terraform/tfdiags"
"github.com/hashicorp/hcl2/hcl"
)
// ProviderConfig is the address of a provider configuration.
@ -37,6 +41,76 @@ type AbsProviderConfig struct {
ProviderConfig ProviderConfig
}
// ParseAbsProviderConfig parses the given traversal as an absolute provider
// address. The following are examples of traversals that can be successfully
// parsed as absolute provider configuration addresses:
//
// provider.aws
// provider.aws.foo
// module.bar.provider.aws
// module.bar.module.baz.provider.aws.foo
// module.foo[1].provider.aws.foo
//
// This type of address is used, for example, to record the relationships
// between resources and provider configurations in the state structure.
// This type of address is not generally used in the UI, except in error
// messages that refer to provider configurations.
func ParseAbsProviderConfig(traversal hcl.Traversal) (AbsProviderConfig, tfdiags.Diagnostics) {
modInst, remain, diags := parseModuleInstancePrefix(traversal)
ret := AbsProviderConfig{
Module: modInst,
}
if len(remain) < 2 || remain.RootName() != "provider" {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid provider configuration address",
Detail: "Provider address must begin with \"provider.\", followed by a provider type name.",
Subject: remain.SourceRange().Ptr(),
})
return ret, diags
}
if len(remain) > 3 {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid provider configuration address",
Detail: "Extraneous operators after provider configuration alias.",
Subject: hcl.Traversal(remain[3:]).SourceRange().Ptr(),
})
return ret, diags
}
if tt, ok := remain[1].(hcl.TraverseAttr); ok {
ret.ProviderConfig.Type = tt.Name
} else {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid provider configuration address",
Detail: "The prefix \"provider.\" must be followed by a provider type name.",
Subject: remain[1].SourceRange().Ptr(),
})
return ret, diags
}
if len(remain) == 3 {
if tt, ok := remain[2].(hcl.TraverseAttr); ok {
ret.ProviderConfig.Alias = tt.Name
} else {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid provider configuration address",
Detail: "Provider type name must be followed by a configuration alias name.",
Subject: remain[2].SourceRange().Ptr(),
})
return ret, diags
}
}
return ret, diags
}
func (pc AbsProviderConfig) String() string {
if len(pc.Module) == 0 {
return pc.ProviderConfig.String()
}
return fmt.Sprintf("%s.%s", pc.Module.String(), pc.ProviderConfig.String())
}

View File

@ -0,0 +1,191 @@
package addrs
import (
"testing"
"github.com/go-test/deep"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
)
func TestParseAbsProviderConfig(t *testing.T) {
tests := []struct {
Input string
Want AbsProviderConfig
WantDiag string
}{
{
`provider.aws`,
AbsProviderConfig{
Module: RootModuleInstance,
ProviderConfig: ProviderConfig{
Type: "aws",
},
},
``,
},
{
`provider.aws.foo`,
AbsProviderConfig{
Module: RootModuleInstance,
ProviderConfig: ProviderConfig{
Type: "aws",
Alias: "foo",
},
},
``,
},
{
`module.baz.provider.aws`,
AbsProviderConfig{
Module: ModuleInstance{
{
Name: "baz",
},
},
ProviderConfig: ProviderConfig{
Type: "aws",
},
},
``,
},
{
`module.baz.provider.aws.foo`,
AbsProviderConfig{
Module: ModuleInstance{
{
Name: "baz",
},
},
ProviderConfig: ProviderConfig{
Type: "aws",
Alias: "foo",
},
},
``,
},
{
`module.baz["foo"].provider.aws`,
AbsProviderConfig{
Module: ModuleInstance{
{
Name: "baz",
InstanceKey: StringKey("foo"),
},
},
ProviderConfig: ProviderConfig{
Type: "aws",
},
},
``,
},
{
`module.baz[1].provider.aws`,
AbsProviderConfig{
Module: ModuleInstance{
{
Name: "baz",
InstanceKey: IntKey(1),
},
},
ProviderConfig: ProviderConfig{
Type: "aws",
},
},
``,
},
{
`module.baz[1].module.bar.provider.aws`,
AbsProviderConfig{
Module: ModuleInstance{
{
Name: "baz",
InstanceKey: IntKey(1),
},
{
Name: "bar",
},
},
ProviderConfig: ProviderConfig{
Type: "aws",
},
},
``,
},
{
`aws`,
AbsProviderConfig{},
`Provider address must begin with "provider.", followed by a provider type name.`,
},
{
`aws.foo`,
AbsProviderConfig{},
`Provider address must begin with "provider.", followed by a provider type name.`,
},
{
`provider`,
AbsProviderConfig{},
`Provider address must begin with "provider.", followed by a provider type name.`,
},
{
`provider.aws.foo.bar`,
AbsProviderConfig{},
`Extraneous operators after provider configuration alias.`,
},
{
`provider["aws"]`,
AbsProviderConfig{},
`The prefix "provider." must be followed by a provider type name.`,
},
{
`provider.aws["foo"]`,
AbsProviderConfig{},
`Provider type name must be followed by a configuration alias name.`,
},
{
`module.foo`,
AbsProviderConfig{},
`Provider address must begin with "provider.", followed by a provider type name.`,
},
{
`module.foo["provider"]`,
AbsProviderConfig{},
`Provider address must begin with "provider.", followed by a provider type name.`,
},
}
for _, test := range tests {
t.Run(test.Input, func(t *testing.T) {
traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(test.Input), "", hcl.Pos{})
if len(parseDiags) != 0 {
t.Errorf("unexpected diagnostics during parse")
for _, diag := range parseDiags {
t.Logf("- %s", diag)
}
return
}
got, diags := ParseAbsProviderConfig(traversal)
if test.WantDiag != "" {
if len(diags) != 1 {
t.Fatalf("got %d diagnostics; want 1", len(diags))
}
gotDetail := diags[0].Description().Detail
if gotDetail != test.WantDiag {
t.Fatalf("wrong diagnostic detail\ngot: %s\nwant: %s", gotDetail, test.WantDiag)
}
return
} else {
if len(diags) != 0 {
t.Fatalf("got %d diagnostics; want 0", len(diags))
}
}
for _, problem := range deep.Equal(got, test.Want) {
t.Error(problem)
}
})
}
}