configs: added map to configs.Module for provider local name lookup (#24039)

* configs: added map of ProviderLocalNames to configs.Module

We will need to lookup any user-supplied local names for a given FQN.
This PR adds a map of ProviderLocalNames to the Module, along with
adding tests for this and for decodeRequiredProvidersBlock.

This also introduces the appearance of support for a required_provider
"source" attribute, but ignores any user-supplied source and instead
continues to assume that addrs.NewLegacyProvider is the way to go.
This commit is contained in:
Kristin Laemmert 2020-02-11 13:17:37 -05:00 committed by GitHub
parent 7e76ce33a9
commit 851e6dcdbb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 263 additions and 12 deletions

View File

@ -31,6 +31,7 @@ type Module struct {
Backend *Backend
ProviderConfigs map[string]*Provider
ProviderRequirements map[string]ProviderRequirements
ProviderLocalNames map[addrs.Provider]string
Variables map[string]*Variable
Locals map[string]*Local
@ -85,6 +86,7 @@ func NewModule(primaryFiles, overrideFiles []*File) (*Module, hcl.Diagnostics) {
mod := &Module{
ProviderConfigs: map[string]*Provider{},
ProviderRequirements: map[string]ProviderRequirements{},
ProviderLocalNames: map[addrs.Provider]string{},
Variables: map[string]*Variable{},
Locals: map[string]*Local{},
Outputs: map[string]*Output{},
@ -105,6 +107,9 @@ func NewModule(primaryFiles, overrideFiles []*File) (*Module, hcl.Diagnostics) {
diags = append(diags, checkModuleExperiments(mod)...)
// Generate the FQN -> LocalProviderName map
mod.gatherProviderLocalNames()
return mod, diags
}
@ -170,11 +175,14 @@ func (m *Module) appendFile(file *File) hcl.Diagnostics {
}
for _, reqd := range file.RequiredProviders {
// TODO: once the remaining provider source functionality is
// implemented, get addrs.Provider from source if set, or
// addrs.NewDefaultProvider(name) if not
// As an interim *testing* step, we will accept a source argument
// but assume that the source is a legacy provider. This allows us to
// exercise the provider local names -> fqn logic without changing
// terraform's behavior.
if reqd.Source != "" {
panic("source is not yet supported")
// Fixme: once the rest of the provider source logic is implemented,
// update this to get the addrs.Provider by using
// addrs.ParseProviderSourceString()
}
fqn := addrs.NewLegacyProvider(reqd.Name)
if existing, exists := m.ProviderRequirements[reqd.Name]; exists {
@ -425,3 +433,27 @@ func (m *Module) mergeFile(file *File) hcl.Diagnostics {
return diags
}
// gatherProviderLocalNames is a helper function that populatesA a map of
// provider FQNs -> provider local names. This information is useful for
// user-facing output, which should include both the FQN and LocalName. It must
// only be populated after the module has been parsed.
func (m *Module) gatherProviderLocalNames() {
providers := make(map[addrs.Provider]string)
for k, v := range m.ProviderRequirements {
providers[v.Type] = k
}
m.ProviderLocalNames = providers
}
// LocalNameForProvider returns the module-specific user-supplied local name for
// a given provider FQN, or the default local name if none was supplied.
func (m *Module) LocalNameForProvider(p addrs.Provider) string {
if existing, exists := m.ProviderLocalNames[p]; exists {
return existing
} else {
// If there isn't a map entry, fall back to the default:
// Type = LocalName
return p.Type
}
}

34
configs/module_test.go Normal file
View File

@ -0,0 +1,34 @@
package configs
import (
"testing"
"github.com/hashicorp/terraform/addrs"
)
// TestNewModule_provider_fqns exercises module.gatherProviderLocalNames()
func TestNewModule_provider_local_name(t *testing.T) {
mod, diags := testModuleFromDir("testdata/providers-explicit-fqn")
if diags.HasErrors() {
t.Fatal(diags.Error())
}
// FIXME: while the provider source is set to "foo/test", terraform
// currently assumes everything is a legacy provider and the localname and
// type match. This test will be updated when provider source is fully
// implemented.
p := addrs.NewLegacyProvider("foo_test")
if name, exists := mod.ProviderLocalNames[p]; !exists {
t.Fatal("provider FQN foo/test not found")
} else {
if name != "foo_test" {
t.Fatalf("provider localname mismatch: got %s, want foo_test", name)
}
}
// ensure the reverse lookup (fqn to local name) works as well
localName := mod.LocalNameForProvider(p)
if localName != "foo_test" {
t.Fatal("provider local name not found")
}
}

View File

@ -9,8 +9,6 @@ import (
// RequiredProvider represents a declaration of a dependency on a particular
// provider version without actually configuring that provider. This is used in
// child modules that expect a provider to be passed in from their parent.
//
// TODO: "Source" is a placeholder for an attribute that is not yet supported.
type RequiredProvider struct {
Name string
Source string // TODO
@ -43,6 +41,7 @@ func decodeRequiredProvidersBlock(block *hcl.Block) ([]*RequiredProvider, hcl.Di
Requirement: vc,
})
case expr.Type().IsObjectType():
ret := &RequiredProvider{Name: name}
if expr.Type().HasAttribute("version") {
vc := VersionConstraint{
DeclRange: attr.Range,
@ -58,14 +57,15 @@ func decodeRequiredProvidersBlock(block *hcl.Block) ([]*RequiredProvider, hcl.Di
Detail: "This string does not use correct version constraint syntax.",
Subject: attr.Expr.Range().Ptr(),
})
reqs = append(reqs, &RequiredProvider{Name: name})
return reqs, diags
} else {
vc.Required = constraints
ret.Requirement = vc
}
vc.Required = constraints
reqs = append(reqs, &RequiredProvider{Name: name, Requirement: vc})
}
// No version
reqs = append(reqs, &RequiredProvider{Name: name})
if expr.Type().HasAttribute("source") {
ret.Source = expr.GetAttr("source").AsString()
}
reqs = append(reqs, ret)
default:
// should not happen
diags = append(diags, &hcl.Diagnostic{

View File

@ -0,0 +1,185 @@
package configs
import (
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcltest"
"github.com/zclconf/go-cty/cty"
)
var (
ignoreUnexported = cmpopts.IgnoreUnexported(version.Constraint{})
comparer = cmp.Comparer(func(x, y RequiredProvider) bool {
if x.Name != y.Name {
return false
}
if x.Source != y.Source {
return false
}
if x.Requirement.Required.String() != y.Requirement.Required.String() {
return false
}
return true
})
)
func TestDecodeRequiredProvidersBlock_legacy(t *testing.T) {
block := &hcl.Block{
Type: "required_providers",
Body: hcltest.MockBody(&hcl.BodyContent{
Attributes: hcl.Attributes{
"default": {
Name: "default",
Expr: hcltest.MockExprLiteral(cty.StringVal("1.0.0")),
},
},
}),
}
want := &RequiredProvider{
Name: "default",
Requirement: testVC("1.0.0"),
}
got, diags := decodeRequiredProvidersBlock(block)
if diags.HasErrors() {
t.Fatalf("unexpected error")
}
if len(got) != 1 {
t.Fatalf("wrong number of results, got %d, wanted 1", len(got))
}
if !cmp.Equal(got[0], want, ignoreUnexported, comparer) {
t.Fatalf("wrong result:\n %s", cmp.Diff(got[0], want, ignoreUnexported, comparer))
}
}
func TestDecodeRequiredProvidersBlock_provider_source(t *testing.T) {
block := &hcl.Block{
Type: "required_providers",
Body: hcltest.MockBody(&hcl.BodyContent{
Attributes: hcl.Attributes{
"my_test": {
Name: "my_test",
Expr: hcltest.MockExprLiteral(cty.ObjectVal(map[string]cty.Value{
"source": cty.StringVal("mycloud/test"),
"version": cty.StringVal("2.0.0"),
})),
},
},
}),
}
want := &RequiredProvider{
Name: "my_test",
Source: "mycloud/test",
Requirement: testVC("2.0.0"),
}
got, diags := decodeRequiredProvidersBlock(block)
if diags.HasErrors() {
t.Fatalf("unexpected error")
}
if len(got) != 1 {
t.Fatalf("wrong number of results, got %d, wanted 1", len(got))
}
if !cmp.Equal(got[0], want, ignoreUnexported, comparer) {
t.Fatalf("wrong result:\n %s", cmp.Diff(got[0], want, ignoreUnexported, comparer))
}
}
func TestDecodeRequiredProvidersBlock_mixed(t *testing.T) {
block := &hcl.Block{
Type: "required_providers",
Body: hcltest.MockBody(&hcl.BodyContent{
Attributes: hcl.Attributes{
"legacy": {
Name: "legacy",
Expr: hcltest.MockExprLiteral(cty.StringVal("1.0.0")),
},
"my_test": {
Name: "my_test",
Expr: hcltest.MockExprLiteral(cty.ObjectVal(map[string]cty.Value{
"source": cty.StringVal("mycloud/test"),
"version": cty.StringVal("2.0.0"),
})),
},
},
}),
}
want := []*RequiredProvider{
{
Name: "legacy",
Requirement: testVC("1.0.0"),
},
{
Name: "my_test",
Source: "mycloud/test",
Requirement: testVC("2.0.0"),
},
}
got, diags := decodeRequiredProvidersBlock(block)
if diags.HasErrors() {
t.Fatalf("unexpected error")
}
if len(got) != 2 {
t.Fatalf("wrong number of results, got %d, wanted 2", len(got))
}
for i, rp := range want {
if !cmp.Equal(got[i], rp, ignoreUnexported, comparer) {
t.Fatalf("wrong result:\n %s", cmp.Diff(got[0], rp, ignoreUnexported, comparer))
}
}
}
func TestDecodeRequiredProvidersBlock_version_error(t *testing.T) {
block := &hcl.Block{
Type: "required_providers",
Body: hcltest.MockBody(&hcl.BodyContent{
Attributes: hcl.Attributes{
"my_test": {
Name: "my_test",
Expr: hcltest.MockExprLiteral(cty.ObjectVal(map[string]cty.Value{
"source": cty.StringVal("mycloud/test"),
"version": cty.StringVal("invalid"),
})),
},
},
}),
}
want := []*RequiredProvider{
{
Name: "my_test",
Source: "mycloud/test",
},
}
got, diags := decodeRequiredProvidersBlock(block)
if !diags.HasErrors() {
t.Fatalf("expected error, got success")
} else {
fmt.Printf(diags[0].Summary)
}
if len(got) != 1 {
t.Fatalf("wrong number of results, got %d, wanted 1", len(got))
}
for i, rp := range want {
if !cmp.Equal(got[i], rp, ignoreUnexported, comparer) {
t.Fatalf("wrong result:\n %s", cmp.Diff(got[0], rp, ignoreUnexported, comparer))
}
}
}
func testVC(ver string) VersionConstraint {
constraint, _ := version.NewConstraint(ver)
return VersionConstraint{
Required: constraint,
DeclRange: hcl.Range{},
}
}