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:
parent
33464568e8
commit
1c78b26012
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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}}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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{}},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
terraform {
|
||||
required_providers {
|
||||
bar-test = {
|
||||
source = "bar/test"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "bar-test" {}
|
|
@ -0,0 +1,13 @@
|
|||
terraform {
|
||||
required_providers {
|
||||
foo-test = {
|
||||
source = "foo/test"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "foo-test" {}
|
||||
|
||||
module "child" {
|
||||
source = "./child"
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
terraform {
|
||||
required_providers {
|
||||
foo-test = {
|
||||
source = "foo/test"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "foo-test" {}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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")
|
||||
|
||||
|
|
Loading…
Reference in New Issue