terraform: provider source test (#24342)

* configs: parse provider source string during module merge

This was the smallest unit of work needed to start writing provider
source tests!

* Update configs/parser_test.go

Co-Authored-By: Alisdair McDiarmid <alisdair@users.noreply.github.com>
This commit is contained in:
Kristin Laemmert 2020-03-12 12:00:00 -04:00 committed by GitHub
parent 33464568e8
commit 1c78b26012
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 366 additions and 22 deletions

View File

@ -126,6 +126,11 @@ func (pt Provider) LessThan(other Provider) bool {
}
}
// Equals returns true if the receiver and other provider have the same attributes.
func (pt Provider) Equals(other Provider) bool {
return pt == other
}
// ParseProviderSourceString parses the source attribute and returns a provider.
// This is intended primarily to parse the FQN-like strings returned by
// terraform-config-inspect.

View File

@ -101,6 +101,10 @@ func TestParseProviderSourceStr(t *testing.T) {
Provider{},
true,
},
"/ / /": { // empty strings
Provider{},
true,
},
"badhost!/hashicorp/aws": {
Provider{},
true,
@ -241,5 +245,41 @@ func TestParseProviderPart(t *testing.T) {
}
})
}
}
func TestProviderEquals(t *testing.T) {
tests := []struct {
InputP Provider
OtherP Provider
Want bool
}{
{
NewProvider(DefaultRegistryHost, "foo", "test"),
NewProvider(DefaultRegistryHost, "foo", "test"),
true,
},
{
NewProvider(DefaultRegistryHost, "foo", "test"),
NewProvider(DefaultRegistryHost, "bar", "test"),
false,
},
{
NewProvider(DefaultRegistryHost, "foo", "test"),
NewProvider(DefaultRegistryHost, "foo", "my-test"),
false,
},
{
NewProvider(DefaultRegistryHost, "foo", "test"),
NewProvider("example.com", "foo", "test"),
false,
},
}
for _, test := range tests {
t.Run(test.InputP.String(), func(t *testing.T) {
got := test.InputP.Equals(test.OtherP)
if got != test.Want {
t.Errorf("wrong result\ngot: %v\nwant: %v", got, test.Want)
}
})
}
}

View File

@ -9,12 +9,7 @@ import (
)
func TestConfigProviderTypes(t *testing.T) {
mod, diags := testModuleFromFile("testdata/valid-files/providers-explicit-implied.tf")
if diags.HasErrors() {
t.Fatal(diags.Error())
}
cfg, diags := BuildConfig(mod, nil)
cfg, diags := testModuleConfigFromFile("testdata/valid-files/providers-explicit-implied.tf")
if diags.HasErrors() {
t.Fatal(diags.Error())
}
@ -30,13 +25,33 @@ func TestConfigProviderTypes(t *testing.T) {
}
}
func TestConfigResolveAbsProviderAddr(t *testing.T) {
mod, diags := testModuleFromDir("testdata/providers-explicit-fqn")
func TestConfigProviderTypes_nested(t *testing.T) {
// basic test with a nil config
c := NewEmptyConfig()
got := c.ProviderTypes()
if len(got) != 0 {
t.Fatalf("wrong result!\ngot: %#v\nwant: nil\n", got)
}
// config with two provider sources
cfg, diags := testNestedModuleConfigFromDir(t, "testdata/valid-modules/nested-providers-fqns")
if diags.HasErrors() {
t.Fatal(diags.Error())
}
cfg, diags := BuildConfig(mod, nil)
got = cfg.ProviderTypes()
want := []addrs.Provider{
addrs.NewProvider(addrs.DefaultRegistryHost, "bar", "test"),
addrs.NewProvider(addrs.DefaultRegistryHost, "foo", "test"),
}
for _, problem := range deep.Equal(got, want) {
t.Error(problem)
}
}
func TestConfigResolveAbsProviderAddr(t *testing.T) {
cfg, diags := testModuleConfigFromDir("testdata/providers-explicit-fqn")
if diags.HasErrors() {
t.Fatal(diags.Error())
}
@ -89,3 +104,21 @@ func TestConfigResolveAbsProviderAddr(t *testing.T) {
}
})
}
func TestProviderForConfigAddr(t *testing.T) {
cfg, diags := testModuleConfigFromDir("testdata/valid-modules/providers-fqns")
assertNoDiagnostics(t, diags)
got := cfg.ProviderForConfigAddr(addrs.NewDefaultLocalProviderConfig("foo-test"))
want := addrs.NewProvider(addrs.DefaultRegistryHost, "foo", "test")
if !got.Equals(want) {
t.Errorf("wrong result\ngot: %s\nwant: %s", got, want)
}
// now check a provider that isn't in the configuration. It should return a NewLegacyProvider.
got = cfg.ProviderForConfigAddr(addrs.NewDefaultLocalProviderConfig("bar-test"))
want = addrs.NewLegacyProvider("bar-test")
if !got.Equals(want) {
t.Errorf("wrong result\ngot: %s\nwant: %s", got, want)
}
}

View File

@ -7,6 +7,7 @@ import (
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/experiments"
"github.com/hashicorp/terraform/tfdiags"
)
// Module is a container for a set of configuration constructs that are
@ -179,9 +180,21 @@ func (m *Module) appendFile(file *File) hcl.Diagnostics {
for _, reqd := range file.RequiredProviders {
var fqn addrs.Provider
if reqd.Source != "" {
// FIXME: capture errors
fqn, _ = addrs.ParseProviderSourceString(reqd.Source)
if reqd.Source.SourceStr != "" {
var sourceDiags tfdiags.Diagnostics
fqn, sourceDiags = addrs.ParseProviderSourceString(reqd.Source.SourceStr)
if sourceDiags.HasErrors() {
for i := range sourceDiags {
if sourceDiags[i].Severity() == tfdiags.Error {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid provider source string",
Detail: sourceDiags[i].Description().Detail,
Subject: &reqd.Source.DeclRange,
})
}
}
}
} else {
fqn = addrs.NewLegacyProvider(reqd.Name)
}

View File

@ -43,7 +43,13 @@ func mergeProviderVersionConstraints(recv map[string]ProviderRequirements, ovrd
delete(recv, reqd.Name)
}
for _, reqd := range ovrd {
fqn := addrs.NewLegacyProvider(reqd.Name)
var fqn addrs.Provider
if reqd.Source.SourceStr != "" {
// any errors parsing the source string will have already been captured.
fqn, _ = addrs.ParseProviderSourceString(reqd.Source.SourceStr)
} else {
fqn = addrs.NewLegacyProvider(reqd.Name)
}
recv[reqd.Name] = ProviderRequirements{Type: fqn, VersionConstraints: []VersionConstraint{reqd.Requirement}}
}
}

View File

@ -28,3 +28,17 @@ func TestNewModule_provider_local_name(t *testing.T) {
t.Fatal("provider local name not found")
}
}
func TestProviderForLocalConfig(t *testing.T) {
mod, diags := testModuleFromDir("testdata/providers-explicit-fqn")
if diags.HasErrors() {
t.Fatal(diags.Error())
}
lc := addrs.LocalProviderConfig{LocalName: "foo-test"}
got := mod.ProviderForLocalConfig(lc)
want := addrs.NewProvider(addrs.DefaultRegistryHost, "foo", "test")
if !got.Equals(want) {
t.Fatalf("wrong result! got %#v, want %#v\n", got, want)
}
}

View File

@ -1,13 +1,16 @@
package configs
import (
"fmt"
"os"
"path"
"path/filepath"
"reflect"
"testing"
"github.com/davecgh/go-spew/spew"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/hcl/v2"
"github.com/spf13/afero"
)
@ -45,6 +48,17 @@ func testModuleFromFile(filename string) (*Module, hcl.Diagnostics) {
return mod, modDiags
}
// testModuleConfigFrom File reads a single file from the given path as a
// module and returns its configuration. This is a helper for use in unit tests.
func testModuleConfigFromFile(filename string) (*Config, hcl.Diagnostics) {
parser := NewParser(nil)
f, diags := parser.LoadConfigFile(filename)
mod, modDiags := NewModule([]*File{f}, nil)
diags = append(diags, modDiags...)
cfg, moreDiags := BuildConfig(mod, nil)
return cfg, append(diags, moreDiags...)
}
// testModuleFromDir reads configuration from the given directory path as
// a module and returns it. This is a helper for use in unit tests.
func testModuleFromDir(path string) (*Module, hcl.Diagnostics) {
@ -52,6 +66,46 @@ func testModuleFromDir(path string) (*Module, hcl.Diagnostics) {
return parser.LoadConfigDir(path)
}
// testModuleFromDir reads configuration from the given directory path as a
// module and returns its configuration. This is a helper for use in unit tests.
func testModuleConfigFromDir(path string) (*Config, hcl.Diagnostics) {
parser := NewParser(nil)
mod, diags := parser.LoadConfigDir(path)
cfg, moreDiags := BuildConfig(mod, nil)
return cfg, append(diags, moreDiags...)
}
// testNestedModuleConfigFromDir reads configuration from the given directory path as
// a module with (optional) submodules and returns its configuration. This is a
// helper for use in unit tests.
func testNestedModuleConfigFromDir(t *testing.T, path string) (*Config, hcl.Diagnostics) {
t.Helper()
parser := NewParser(nil)
mod, diags := parser.LoadConfigDir(path)
assertNoDiagnostics(t, diags)
if mod == nil {
t.Fatal("got nil root module; want non-nil")
}
versionI := 0
cfg, diags := BuildConfig(mod, ModuleWalkerFunc(
func(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) {
// For the sake of this test we're going to just treat our
// SourceAddr as a path relative to our fixture directory.
// A "real" implementation of ModuleWalker should accept the
// various different source address syntaxes Terraform supports.
sourcePath := filepath.Join(path, req.SourceAddr)
mod, diags := parser.LoadConfigDir(sourcePath)
version, _ := version.NewVersion(fmt.Sprintf("1.0.%d", versionI))
versionI++
return mod, version, diags
},
))
return cfg, diags
}
func assertNoDiagnostics(t *testing.T, diags hcl.Diagnostics) bool {
t.Helper()
return assertDiagnosticCount(t, diags, 0)
@ -59,7 +113,7 @@ func assertNoDiagnostics(t *testing.T, diags hcl.Diagnostics) bool {
func assertDiagnosticCount(t *testing.T, diags hcl.Diagnostics, want int) bool {
t.Helper()
if len(diags) != 0 {
if len(diags) != want {
t.Errorf("wrong number of diagnostics %d; want %d", len(diags), want)
for _, diag := range diags {
t.Logf("- %s", diag)

View File

@ -7,14 +7,20 @@ 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.
// provider version or source without actually configuring that provider. This
// is used in child modules that expect a provider to be passed in from their
// parent.
type RequiredProvider struct {
Name string
Source string // TODO
Source Source
Requirement VersionConstraint
}
type Source struct {
SourceStr string
DeclRange hcl.Range
}
// ProviderRequirements represents merged provider version constraints.
// VersionConstraints come from terraform.require_providers blocks and provider
// blocks.
@ -63,7 +69,8 @@ func decodeRequiredProvidersBlock(block *hcl.Block) ([]*RequiredProvider, hcl.Di
}
}
if expr.Type().HasAttribute("source") {
ret.Source = expr.GetAttr("source").AsString()
ret.Source.SourceStr = expr.GetAttr("source").AsString()
ret.Source.DeclRange = attr.Range
}
reqs = append(reqs, ret)
default:

View File

@ -60,6 +60,12 @@ func TestDecodeRequiredProvidersBlock_legacy(t *testing.T) {
}
func TestDecodeRequiredProvidersBlock_provider_source(t *testing.T) {
mockRange := hcl.Range{
Filename: "mock.tf",
Start: hcl.Pos{Line: 3, Column: 12, Byte: 27},
End: hcl.Pos{Line: 3, Column: 19, Byte: 34},
}
block := &hcl.Block{
Type: "required_providers",
Body: hcltest.MockBody(&hcl.BodyContent{
@ -70,6 +76,7 @@ func TestDecodeRequiredProvidersBlock_provider_source(t *testing.T) {
"source": cty.StringVal("mycloud/test"),
"version": cty.StringVal("2.0.0"),
})),
Range: mockRange,
},
},
}),
@ -77,7 +84,7 @@ func TestDecodeRequiredProvidersBlock_provider_source(t *testing.T) {
want := &RequiredProvider{
Name: "my_test",
Source: "mycloud/test",
Source: Source{SourceStr: "mycloud/test", DeclRange: mockRange},
Requirement: testVC("2.0.0"),
}
got, diags := decodeRequiredProvidersBlock(block)
@ -119,7 +126,7 @@ func TestDecodeRequiredProvidersBlock_mixed(t *testing.T) {
},
{
Name: "my_test",
Source: "mycloud/test",
Source: Source{SourceStr: "mycloud/test", DeclRange: hcl.Range{}},
Requirement: testVC("2.0.0"),
},
}
@ -162,7 +169,7 @@ func TestDecodeRequiredProvidersBlock_version_error(t *testing.T) {
want := []*RequiredProvider{
{
Name: "my_test",
Source: "mycloud/test",
Source: Source{SourceStr: "mycloud/test", DeclRange: hcl.Range{}},
},
}

View File

@ -92,3 +92,57 @@ func TestParseProviderConfigCompact(t *testing.T) {
})
}
}
func TestParseProviderConfigCompactStr(t *testing.T) {
tests := []struct {
Input string
Want addrs.LocalProviderConfig
WantDiag string
}{
{
`aws`,
addrs.LocalProviderConfig{
LocalName: "aws",
},
``,
},
{
`aws.foo`,
addrs.LocalProviderConfig{
LocalName: "aws",
Alias: "foo",
},
``,
},
{
`aws["foo"]`,
addrs.LocalProviderConfig{},
`The provider type name must either stand alone or be followed by an alias name separated with a dot.`,
},
}
for _, test := range tests {
t.Run(test.Input, func(t *testing.T) {
got, diags := ParseProviderConfigCompactStr(test.Input)
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)
}
})
}
}

View File

@ -0,0 +1,9 @@
terraform {
required_providers {
bar-test = {
source = "bar/test"
}
}
}
provider "bar-test" {}

View File

@ -0,0 +1,13 @@
terraform {
required_providers {
foo-test = {
source = "foo/test"
}
}
}
provider "foo-test" {}
module "child" {
source = "./child"
}

View File

@ -0,0 +1,9 @@
terraform {
required_providers {
foo-test = {
source = "foo/test"
}
}
}
provider "foo-test" {}

View File

@ -0,0 +1,13 @@
terraform {
required_providers {
your_aws = {
// This is temporarily using the legacy provider namespace so that we can
// write tests without fully supporting provider source
source = "-/aws"
}
}
}
resource "aws_instance" "web" {
provider = "your_aws"
}

View File

@ -0,0 +1,13 @@
terraform {
required_providers {
my_aws = {
// This is temporarily using the legacy provider namespace so that we can
// write tests without fully supporting provider source
source = "-/aws"
}
}
}
resource "aws_instance" "web" {
provider = "my_aws"
}

View File

@ -0,0 +1,13 @@
terraform {
required_providers {
my_aws = {
// This is temporarily using the legacy provider namespace so that we can
// write tests without fully supporting provider source
source = "-/aws"
}
}
}
resource "aws_instance" "web" {
provider = "my_aws"
}

View File

@ -1,6 +1,7 @@
package terraform
import (
"fmt"
"strings"
"testing"
@ -96,6 +97,46 @@ func TestProviderTransformer_moduleChild(t *testing.T) {
}
}
// Test providers with FQNs that do not match the typeName
func TestProviderTransformer_fqns(t *testing.T) {
for _, mod := range []string{"fqns", "fqns-module"} {
mod := testModule(t, fmt.Sprintf("transform-provider-%s", mod))
g := Graph{Path: addrs.RootModuleInstance}
{
tf := &ConfigTransformer{Config: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
{
transform := &AttachResourceConfigTransformer{Config: mod}
if err := transform.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
{
transform := &MissingProviderTransformer{Providers: []string{"aws"}, Config: mod}
if err := transform.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
transform := &ProviderTransformer{Config: mod}
if err := transform.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformProviderBasicStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
}
func TestCloseProviderTransformer(t *testing.T) {
mod := testModule(t, "transform-provider-basic")