mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-28 18:01:01 -06:00
df578afd7e
In historical versions of Terraform the responsibility to check this was inside the terraform.NewContext function, along with various other assorted concerns that made that function particularly complicated. More recently, we reduced the responsibility of the "terraform" package only to instantiating particular named plugins, assuming that its caller is responsible for selecting appropriate versions of any providers that _are_ external. However, until this commit we were just assuming that "terraform init" had correctly selected appropriate plugins and recorded them in the lock file, and so nothing was dealing with the problem of ensuring that there haven't been any changes to the lock file or config since the most recent "terraform init" which would cause us to need to re-evaluate those decisions. Part of the game here is to slightly extend the role of the dependency locks object to also carry information about a subset of provider addresses whose lock entries we're intentionally disregarding as part of the various little edge-case features we have for overridding providers: dev_overrides, "unmanaged providers", and the testing overrides in our own unit tests. This is an in-memory-only annotation, never included in the serialized plan files on disk. I had originally intended to create a new package to encapsulate all of this plugin-selection logic, including both the version constraint checking here and also the handling of the provider factory functions, but as an interim step I've just made version constraint consistency checks the responsibility of the backend/local package, which means that we'll always catch problems as part of preparing for local operations, while not imposing these additional checks on commands that _don't_ run local operations, such as "terraform apply" when in remote operations mode.
416 lines
16 KiB
Go
416 lines
16 KiB
Go
package configs
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/go-test/deep"
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
version "github.com/hashicorp/go-version"
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
|
svchost "github.com/hashicorp/terraform-svchost"
|
|
"github.com/hashicorp/terraform/internal/addrs"
|
|
"github.com/hashicorp/terraform/internal/depsfile"
|
|
"github.com/hashicorp/terraform/internal/getproviders"
|
|
)
|
|
|
|
func TestConfigProviderTypes(t *testing.T) {
|
|
// nil cfg should return an empty map
|
|
got := NewEmptyConfig().ProviderTypes()
|
|
if len(got) != 0 {
|
|
t.Fatal("expected empty result from empty config")
|
|
}
|
|
|
|
cfg, diags := testModuleConfigFromFile("testdata/valid-files/providers-explicit-implied.tf")
|
|
if diags.HasErrors() {
|
|
t.Fatal(diags.Error())
|
|
}
|
|
|
|
got = cfg.ProviderTypes()
|
|
want := []addrs.Provider{
|
|
addrs.NewDefaultProvider("aws"),
|
|
addrs.NewDefaultProvider("null"),
|
|
addrs.NewDefaultProvider("template"),
|
|
addrs.NewDefaultProvider("test"),
|
|
}
|
|
for _, problem := range deep.Equal(got, want) {
|
|
t.Error(problem)
|
|
}
|
|
}
|
|
|
|
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, and one implicit (default) provider
|
|
cfg, diags := testNestedModuleConfigFromDir(t, "testdata/valid-modules/nested-providers-fqns")
|
|
if diags.HasErrors() {
|
|
t.Fatal(diags.Error())
|
|
}
|
|
|
|
got = cfg.ProviderTypes()
|
|
want := []addrs.Provider{
|
|
addrs.NewProvider(addrs.DefaultProviderRegistryHost, "bar", "test"),
|
|
addrs.NewProvider(addrs.DefaultProviderRegistryHost, "foo", "test"),
|
|
addrs.NewDefaultProvider("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())
|
|
}
|
|
|
|
t.Run("already absolute", func(t *testing.T) {
|
|
addr := addrs.AbsProviderConfig{
|
|
Module: addrs.RootModule,
|
|
Provider: addrs.NewDefaultProvider("test"),
|
|
Alias: "boop",
|
|
}
|
|
got := cfg.ResolveAbsProviderAddr(addr, addrs.RootModule)
|
|
if got, want := got.String(), addr.String(); got != want {
|
|
t.Errorf("wrong result\ngot: %s\nwant: %s", got, want)
|
|
}
|
|
})
|
|
t.Run("local, implied mapping", func(t *testing.T) {
|
|
addr := addrs.LocalProviderConfig{
|
|
LocalName: "implied",
|
|
Alias: "boop",
|
|
}
|
|
got := cfg.ResolveAbsProviderAddr(addr, addrs.RootModule)
|
|
want := addrs.AbsProviderConfig{
|
|
Module: addrs.RootModule,
|
|
Provider: addrs.NewDefaultProvider("implied"),
|
|
Alias: "boop",
|
|
}
|
|
if got, want := got.String(), want.String(); got != want {
|
|
t.Errorf("wrong result\ngot: %s\nwant: %s", got, want)
|
|
}
|
|
})
|
|
t.Run("local, explicit mapping", func(t *testing.T) {
|
|
addr := addrs.LocalProviderConfig{
|
|
LocalName: "foo-test", // this is explicitly set in the config
|
|
Alias: "boop",
|
|
}
|
|
got := cfg.ResolveAbsProviderAddr(addr, addrs.RootModule)
|
|
want := addrs.AbsProviderConfig{
|
|
Module: addrs.RootModule,
|
|
Provider: addrs.NewProvider(addrs.DefaultProviderRegistryHost, "foo", "test"),
|
|
Alias: "boop",
|
|
}
|
|
if got, want := got.String(), want.String(); got != want {
|
|
t.Errorf("wrong result\ngot: %s\nwant: %s", got, want)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestConfigProviderRequirements(t *testing.T) {
|
|
cfg, diags := testNestedModuleConfigFromDir(t, "testdata/provider-reqs")
|
|
// TODO: Version Constraint Deprecation.
|
|
// Once we've removed the version argument from provider configuration
|
|
// blocks, this can go back to expected 0 diagnostics.
|
|
// assertNoDiagnostics(t, diags)
|
|
assertDiagnosticCount(t, diags, 1)
|
|
assertDiagnosticSummary(t, diags, "Version constraints inside provider configuration blocks are deprecated")
|
|
|
|
tlsProvider := addrs.NewProvider(
|
|
addrs.DefaultProviderRegistryHost,
|
|
"hashicorp", "tls",
|
|
)
|
|
happycloudProvider := addrs.NewProvider(
|
|
svchost.Hostname("tf.example.com"),
|
|
"awesomecorp", "happycloud",
|
|
)
|
|
nullProvider := addrs.NewDefaultProvider("null")
|
|
randomProvider := addrs.NewDefaultProvider("random")
|
|
impliedProvider := addrs.NewDefaultProvider("implied")
|
|
terraformProvider := addrs.NewBuiltInProvider("terraform")
|
|
configuredProvider := addrs.NewDefaultProvider("configured")
|
|
grandchildProvider := addrs.NewDefaultProvider("grandchild")
|
|
|
|
got, diags := cfg.ProviderRequirements()
|
|
assertNoDiagnostics(t, diags)
|
|
want := getproviders.Requirements{
|
|
// the nullProvider constraints from the two modules are merged
|
|
nullProvider: getproviders.MustParseVersionConstraints("~> 2.0.0, 2.0.1"),
|
|
randomProvider: getproviders.MustParseVersionConstraints("~> 1.2.0"),
|
|
tlsProvider: getproviders.MustParseVersionConstraints("~> 3.0"),
|
|
configuredProvider: getproviders.MustParseVersionConstraints("~> 1.4"),
|
|
impliedProvider: nil,
|
|
happycloudProvider: nil,
|
|
terraformProvider: nil,
|
|
grandchildProvider: nil,
|
|
}
|
|
|
|
if diff := cmp.Diff(want, got); diff != "" {
|
|
t.Errorf("wrong result\n%s", diff)
|
|
}
|
|
}
|
|
|
|
func TestConfigProviderRequirementsShallow(t *testing.T) {
|
|
cfg, diags := testNestedModuleConfigFromDir(t, "testdata/provider-reqs")
|
|
// TODO: Version Constraint Deprecation.
|
|
// Once we've removed the version argument from provider configuration
|
|
// blocks, this can go back to expected 0 diagnostics.
|
|
// assertNoDiagnostics(t, diags)
|
|
assertDiagnosticCount(t, diags, 1)
|
|
assertDiagnosticSummary(t, diags, "Version constraints inside provider configuration blocks are deprecated")
|
|
|
|
tlsProvider := addrs.NewProvider(
|
|
addrs.DefaultProviderRegistryHost,
|
|
"hashicorp", "tls",
|
|
)
|
|
nullProvider := addrs.NewDefaultProvider("null")
|
|
randomProvider := addrs.NewDefaultProvider("random")
|
|
impliedProvider := addrs.NewDefaultProvider("implied")
|
|
terraformProvider := addrs.NewBuiltInProvider("terraform")
|
|
configuredProvider := addrs.NewDefaultProvider("configured")
|
|
|
|
got, diags := cfg.ProviderRequirementsShallow()
|
|
assertNoDiagnostics(t, diags)
|
|
want := getproviders.Requirements{
|
|
// the nullProvider constraint is only from the root module
|
|
nullProvider: getproviders.MustParseVersionConstraints("~> 2.0.0"),
|
|
randomProvider: getproviders.MustParseVersionConstraints("~> 1.2.0"),
|
|
tlsProvider: getproviders.MustParseVersionConstraints("~> 3.0"),
|
|
configuredProvider: getproviders.MustParseVersionConstraints("~> 1.4"),
|
|
impliedProvider: nil,
|
|
terraformProvider: nil,
|
|
}
|
|
|
|
if diff := cmp.Diff(want, got); diff != "" {
|
|
t.Errorf("wrong result\n%s", diff)
|
|
}
|
|
}
|
|
|
|
func TestConfigProviderRequirementsByModule(t *testing.T) {
|
|
cfg, diags := testNestedModuleConfigFromDir(t, "testdata/provider-reqs")
|
|
// TODO: Version Constraint Deprecation.
|
|
// Once we've removed the version argument from provider configuration
|
|
// blocks, this can go back to expected 0 diagnostics.
|
|
// assertNoDiagnostics(t, diags)
|
|
assertDiagnosticCount(t, diags, 1)
|
|
assertDiagnosticSummary(t, diags, "Version constraints inside provider configuration blocks are deprecated")
|
|
|
|
tlsProvider := addrs.NewProvider(
|
|
addrs.DefaultProviderRegistryHost,
|
|
"hashicorp", "tls",
|
|
)
|
|
happycloudProvider := addrs.NewProvider(
|
|
svchost.Hostname("tf.example.com"),
|
|
"awesomecorp", "happycloud",
|
|
)
|
|
nullProvider := addrs.NewDefaultProvider("null")
|
|
randomProvider := addrs.NewDefaultProvider("random")
|
|
impliedProvider := addrs.NewDefaultProvider("implied")
|
|
terraformProvider := addrs.NewBuiltInProvider("terraform")
|
|
configuredProvider := addrs.NewDefaultProvider("configured")
|
|
grandchildProvider := addrs.NewDefaultProvider("grandchild")
|
|
|
|
got, diags := cfg.ProviderRequirementsByModule()
|
|
assertNoDiagnostics(t, diags)
|
|
want := &ModuleRequirements{
|
|
Name: "",
|
|
SourceAddr: nil,
|
|
SourceDir: "testdata/provider-reqs",
|
|
Requirements: getproviders.Requirements{
|
|
// Only the root module's version is present here
|
|
nullProvider: getproviders.MustParseVersionConstraints("~> 2.0.0"),
|
|
randomProvider: getproviders.MustParseVersionConstraints("~> 1.2.0"),
|
|
tlsProvider: getproviders.MustParseVersionConstraints("~> 3.0"),
|
|
configuredProvider: getproviders.MustParseVersionConstraints("~> 1.4"),
|
|
impliedProvider: nil,
|
|
terraformProvider: nil,
|
|
},
|
|
Children: map[string]*ModuleRequirements{
|
|
"kinder": {
|
|
Name: "kinder",
|
|
SourceAddr: addrs.ModuleSourceLocal("./child"),
|
|
SourceDir: "testdata/provider-reqs/child",
|
|
Requirements: getproviders.Requirements{
|
|
nullProvider: getproviders.MustParseVersionConstraints("= 2.0.1"),
|
|
happycloudProvider: nil,
|
|
},
|
|
Children: map[string]*ModuleRequirements{
|
|
"nested": {
|
|
Name: "nested",
|
|
SourceAddr: addrs.ModuleSourceLocal("./grandchild"),
|
|
SourceDir: "testdata/provider-reqs/child/grandchild",
|
|
Requirements: getproviders.Requirements{
|
|
grandchildProvider: nil,
|
|
},
|
|
Children: map[string]*ModuleRequirements{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
ignore := cmpopts.IgnoreUnexported(version.Constraint{}, cty.Value{}, hclsyntax.Body{})
|
|
if diff := cmp.Diff(want, got, ignore); diff != "" {
|
|
t.Errorf("wrong result\n%s", diff)
|
|
}
|
|
}
|
|
|
|
func TestVerifyDependencySelections(t *testing.T) {
|
|
cfg, diags := testNestedModuleConfigFromDir(t, "testdata/provider-reqs")
|
|
// TODO: Version Constraint Deprecation.
|
|
// Once we've removed the version argument from provider configuration
|
|
// blocks, this can go back to expected 0 diagnostics.
|
|
// assertNoDiagnostics(t, diags)
|
|
assertDiagnosticCount(t, diags, 1)
|
|
assertDiagnosticSummary(t, diags, "Version constraints inside provider configuration blocks are deprecated")
|
|
|
|
tlsProvider := addrs.NewProvider(
|
|
addrs.DefaultProviderRegistryHost,
|
|
"hashicorp", "tls",
|
|
)
|
|
happycloudProvider := addrs.NewProvider(
|
|
svchost.Hostname("tf.example.com"),
|
|
"awesomecorp", "happycloud",
|
|
)
|
|
nullProvider := addrs.NewDefaultProvider("null")
|
|
randomProvider := addrs.NewDefaultProvider("random")
|
|
impliedProvider := addrs.NewDefaultProvider("implied")
|
|
configuredProvider := addrs.NewDefaultProvider("configured")
|
|
grandchildProvider := addrs.NewDefaultProvider("grandchild")
|
|
|
|
tests := map[string]struct {
|
|
PrepareLocks func(*depsfile.Locks)
|
|
WantErrs []string
|
|
}{
|
|
"empty locks": {
|
|
func(*depsfile.Locks) {
|
|
// Intentionally blank
|
|
},
|
|
[]string{
|
|
`provider registry.terraform.io/hashicorp/configured: required by this configuration but no version is selected`,
|
|
`provider registry.terraform.io/hashicorp/grandchild: required by this configuration but no version is selected`,
|
|
`provider registry.terraform.io/hashicorp/implied: required by this configuration but no version is selected`,
|
|
`provider registry.terraform.io/hashicorp/null: required by this configuration but no version is selected`,
|
|
`provider registry.terraform.io/hashicorp/random: required by this configuration but no version is selected`,
|
|
`provider registry.terraform.io/hashicorp/tls: required by this configuration but no version is selected`,
|
|
`provider tf.example.com/awesomecorp/happycloud: required by this configuration but no version is selected`,
|
|
},
|
|
},
|
|
"suitable locks": {
|
|
func(locks *depsfile.Locks) {
|
|
locks.SetProvider(configuredProvider, getproviders.MustParseVersion("1.4.0"), nil, nil)
|
|
locks.SetProvider(grandchildProvider, getproviders.MustParseVersion("0.1.0"), nil, nil)
|
|
locks.SetProvider(impliedProvider, getproviders.MustParseVersion("0.2.0"), nil, nil)
|
|
locks.SetProvider(nullProvider, getproviders.MustParseVersion("2.0.1"), nil, nil)
|
|
locks.SetProvider(randomProvider, getproviders.MustParseVersion("1.2.2"), nil, nil)
|
|
locks.SetProvider(tlsProvider, getproviders.MustParseVersion("3.0.1"), nil, nil)
|
|
locks.SetProvider(happycloudProvider, getproviders.MustParseVersion("0.0.1"), nil, nil)
|
|
},
|
|
nil,
|
|
},
|
|
"null provider constraints changed": {
|
|
func(locks *depsfile.Locks) {
|
|
locks.SetProvider(configuredProvider, getproviders.MustParseVersion("1.4.0"), nil, nil)
|
|
locks.SetProvider(grandchildProvider, getproviders.MustParseVersion("0.1.0"), nil, nil)
|
|
locks.SetProvider(impliedProvider, getproviders.MustParseVersion("0.2.0"), nil, nil)
|
|
locks.SetProvider(nullProvider, getproviders.MustParseVersion("3.0.0"), nil, nil)
|
|
locks.SetProvider(randomProvider, getproviders.MustParseVersion("1.2.2"), nil, nil)
|
|
locks.SetProvider(tlsProvider, getproviders.MustParseVersion("3.0.1"), nil, nil)
|
|
locks.SetProvider(happycloudProvider, getproviders.MustParseVersion("0.0.1"), nil, nil)
|
|
},
|
|
[]string{
|
|
`provider registry.terraform.io/hashicorp/null: locked version selection 3.0.0 doesn't match the updated version constraints "~> 2.0.0, 2.0.1"`,
|
|
},
|
|
},
|
|
"null provider lock changed": {
|
|
func(locks *depsfile.Locks) {
|
|
// In this case, we set the lock file version constraints to
|
|
// match the configuration, and so our error message changes
|
|
// to not assume the configuration changed anymore.
|
|
locks.SetProvider(nullProvider, getproviders.MustParseVersion("3.0.0"), getproviders.MustParseVersionConstraints("~> 2.0.0, 2.0.1"), nil)
|
|
|
|
locks.SetProvider(configuredProvider, getproviders.MustParseVersion("1.4.0"), nil, nil)
|
|
locks.SetProvider(grandchildProvider, getproviders.MustParseVersion("0.1.0"), nil, nil)
|
|
locks.SetProvider(impliedProvider, getproviders.MustParseVersion("0.2.0"), nil, nil)
|
|
locks.SetProvider(randomProvider, getproviders.MustParseVersion("1.2.2"), nil, nil)
|
|
locks.SetProvider(tlsProvider, getproviders.MustParseVersion("3.0.1"), nil, nil)
|
|
locks.SetProvider(happycloudProvider, getproviders.MustParseVersion("0.0.1"), nil, nil)
|
|
},
|
|
[]string{
|
|
`provider registry.terraform.io/hashicorp/null: version constraints "~> 2.0.0, 2.0.1" don't match the locked version selection 3.0.0`,
|
|
},
|
|
},
|
|
"overridden provider": {
|
|
func(locks *depsfile.Locks) {
|
|
locks.SetProviderOverridden(happycloudProvider)
|
|
},
|
|
[]string{
|
|
// We still catch all of the other ones, because only happycloud was overridden
|
|
`provider registry.terraform.io/hashicorp/configured: required by this configuration but no version is selected`,
|
|
`provider registry.terraform.io/hashicorp/grandchild: required by this configuration but no version is selected`,
|
|
`provider registry.terraform.io/hashicorp/implied: required by this configuration but no version is selected`,
|
|
`provider registry.terraform.io/hashicorp/null: required by this configuration but no version is selected`,
|
|
`provider registry.terraform.io/hashicorp/random: required by this configuration but no version is selected`,
|
|
`provider registry.terraform.io/hashicorp/tls: required by this configuration but no version is selected`,
|
|
},
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
depLocks := depsfile.NewLocks()
|
|
test.PrepareLocks(depLocks)
|
|
gotErrs := cfg.VerifyDependencySelections(depLocks)
|
|
|
|
var gotErrsStr []string
|
|
if gotErrs != nil {
|
|
gotErrsStr = make([]string, len(gotErrs))
|
|
for i, err := range gotErrs {
|
|
gotErrsStr[i] = err.Error()
|
|
}
|
|
}
|
|
|
|
if diff := cmp.Diff(test.WantErrs, gotErrsStr); diff != "" {
|
|
t.Errorf("wrong errors\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConfigProviderForConfigAddr(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.DefaultProviderRegistryHost, "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 DefaultProvider.
|
|
got = cfg.ProviderForConfigAddr(addrs.NewDefaultLocalProviderConfig("bar-test"))
|
|
want = addrs.NewDefaultProvider("bar-test")
|
|
if !got.Equals(want) {
|
|
t.Errorf("wrong result\ngot: %s\nwant: %s", got, want)
|
|
}
|
|
}
|
|
|
|
func TestConfigAddProviderRequirements(t *testing.T) {
|
|
cfg, diags := testModuleConfigFromFile("testdata/valid-files/providers-explicit-implied.tf")
|
|
assertNoDiagnostics(t, diags)
|
|
|
|
reqs := getproviders.Requirements{
|
|
addrs.NewDefaultProvider("null"): nil,
|
|
}
|
|
diags = cfg.addProviderRequirements(reqs, true)
|
|
assertNoDiagnostics(t, diags)
|
|
}
|