terraform/internal/configs/module_test.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

684 lines
24 KiB
Go
Raw Permalink Normal View History

// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package configs
import (
"strings"
"testing"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/zclconf/go-cty/cty"
)
// 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())
}
p := addrs.NewProvider(addrs.DefaultProviderRegistryHost, "foo", "test")
if name, exists := mod.ProviderLocalNames[p]; !exists {
t.Fatal("provider FQN foo/test not found")
} else {
addrs: Stronger validation and normalization of provider namespace/type The provider FQN is becoming our primary identifier for a provider, so it's important that we are clear about the equality rules for these addresses and what characters are valid within them. We previously had a basic regex permitting ASCII letters and digits for validation and no normalization at all. We need to do at least case folding and UTF-8 normalization because these names will appear in file and directory names in case-insensitive filesystems and in repository names such as on GitHub. Since we're already using DNS-style normalization and validation rules for the hostname part, rather than defining an entirely new set of rules here we'll just treat the provider namespace and type as if they were single labels in a DNS name. Aside from some internal consistency, that also works out nicely because systems like GitHub use organization and repository names as part of hostnames (e.g. with GitHub Pages) and so tend to apply comparable constraints themselves. This introduces the possibility of names containing letters from alphabets other than the latin alphabet, and for latin letters with diacritics. That's consistent with our introduction of similar support for identifiers in the language in Terraform 0.12, and is intended to be more friendly to Terraform users throughout the world that might prefer to name their products using a different alphabet. This is also a further justification for using the DNS normalization rules: modern companies tend to choose product names that make good domain names, and now such names will be usable as Terraform provider names too.
2020-02-14 21:10:03 -05:00
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)
addrs: Stronger validation and normalization of provider namespace/type The provider FQN is becoming our primary identifier for a provider, so it's important that we are clear about the equality rules for these addresses and what characters are valid within them. We previously had a basic regex permitting ASCII letters and digits for validation and no normalization at all. We need to do at least case folding and UTF-8 normalization because these names will appear in file and directory names in case-insensitive filesystems and in repository names such as on GitHub. Since we're already using DNS-style normalization and validation rules for the hostname part, rather than defining an entirely new set of rules here we'll just treat the provider namespace and type as if they were single labels in a DNS name. Aside from some internal consistency, that also works out nicely because systems like GitHub use organization and repository names as part of hostnames (e.g. with GitHub Pages) and so tend to apply comparable constraints themselves. This introduces the possibility of names containing letters from alphabets other than the latin alphabet, and for latin letters with diacritics. That's consistent with our introduction of similar support for identifiers in the language in Terraform 0.12, and is intended to be more friendly to Terraform users throughout the world that might prefer to name their products using a different alphabet. This is also a further justification for using the DNS normalization rules: modern companies tend to choose product names that make good domain names, and now such names will be usable as Terraform provider names too.
2020-02-14 21:10:03 -05:00
if localName != "foo-test" {
t.Fatal("provider local name not found")
}
// if there is not a local name for a provider, it should return the type name
2020-10-05 08:33:49 -04:00
localName = mod.LocalNameForProvider(addrs.NewDefaultProvider("nonexist"))
if localName != "nonexist" {
t.Error("wrong local name returned for a non-local provider")
}
// can also look up the "terraform" provider and see that it sources is
// allowed to be overridden, even though there is a builtin provider
// called "terraform".
p = addrs.NewProvider(addrs.DefaultProviderRegistryHost, "not-builtin", "not-terraform")
if name, exists := mod.ProviderLocalNames[p]; !exists {
t.Fatal("provider FQN not-builtin/not-terraform not found")
} else {
if name != "terraform" {
t.Fatalf("provider localname mismatch: got %s, want terraform", name)
}
}
}
// This test validates the provider FQNs set in each Resource
func TestNewModule_resource_providers(t *testing.T) {
cfg, diags := testNestedModuleConfigFromDir(t, "testdata/valid-modules/nested-providers-fqns")
if diags.HasErrors() {
t.Fatal(diags.Error())
}
// both the root and child module have two resources, one which should use
// the default implied provider and one explicitly using a provider set in
// required_providers
wantImplicit := addrs.NewDefaultProvider("test")
wantFoo := addrs.NewProvider(addrs.DefaultProviderRegistryHost, "foo", "test")
wantBar := addrs.NewProvider(addrs.DefaultProviderRegistryHost, "bar", "test")
// root module
if !cfg.Module.ManagedResources["test_instance.explicit"].Provider.Equals(wantFoo) {
t.Fatalf("wrong provider for \"test_instance.explicit\"\ngot: %s\nwant: %s",
cfg.Module.ManagedResources["test_instance.explicit"].Provider,
wantFoo,
)
}
if !cfg.Module.ManagedResources["test_instance.implicit"].Provider.Equals(wantImplicit) {
t.Fatalf("wrong provider for \"test_instance.implicit\"\ngot: %s\nwant: %s",
cfg.Module.ManagedResources["test_instance.implicit"].Provider,
wantImplicit,
)
}
// a data source
if !cfg.Module.DataResources["data.test_resource.explicit"].Provider.Equals(wantFoo) {
t.Fatalf("wrong provider for \"module.child.test_instance.explicit\"\ngot: %s\nwant: %s",
cfg.Module.ManagedResources["test_instance.explicit"].Provider,
wantBar,
)
}
// child module
cm := cfg.Children["child"].Module
if !cm.ManagedResources["test_instance.explicit"].Provider.Equals(wantBar) {
t.Fatalf("wrong provider for \"module.child.test_instance.explicit\"\ngot: %s\nwant: %s",
cfg.Module.ManagedResources["test_instance.explicit"].Provider,
wantBar,
)
}
if !cm.ManagedResources["test_instance.implicit"].Provider.Equals(wantImplicit) {
t.Fatalf("wrong provider for \"module.child.test_instance.implicit\"\ngot: %s\nwant: %s",
cfg.Module.ManagedResources["test_instance.implicit"].Provider,
wantImplicit,
)
}
}
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.DefaultProviderRegistryHost, "foo", "test")
if !got.Equals(want) {
t.Fatalf("wrong result! got %#v, want %#v\n", got, want)
}
}
// At most one required_providers block per module is permitted.
func TestModule_required_providers_multiple(t *testing.T) {
_, diags := testModuleFromDir("testdata/invalid-modules/multiple-required-providers")
if !diags.HasErrors() {
t.Fatal("module should have error diags, but does not")
}
want := `Duplicate required providers configuration`
if got := diags.Error(); !strings.Contains(got, want) {
t.Fatalf("expected error to contain %q\nerror was:\n%s", want, got)
}
}
// A module may have required_providers configured in files loaded later than
// resources. These provider settings should still be reflected in the
// resources' configuration.
func TestModule_required_providers_after_resource(t *testing.T) {
mod, diags := testModuleFromDir("testdata/valid-modules/required-providers-after-resource")
if diags.HasErrors() {
t.Fatal(diags.Error())
}
want := addrs.NewProvider(addrs.DefaultProviderRegistryHost, "foo", "test")
req, exists := mod.ProviderRequirements.RequiredProviders["test"]
if !exists {
t.Fatal("no provider requirements found for \"test\"")
}
if req.Type != want {
t.Errorf("wrong provider addr for \"test\"\ngot: %s\nwant: %s",
req.Type, want,
)
}
if got := mod.ManagedResources["test_instance.my-instance"].Provider; !got.Equals(want) {
t.Errorf("wrong provider addr for \"test_instance.my-instance\"\ngot: %s\nwant: %s",
got, want,
)
}
}
// We support overrides for required_providers blocks, which should replace the
// entire block for each provider localname, leaving other blocks unaffected.
// This should also be reflected in any resources in the module using this
// provider.
func TestModule_required_provider_overrides(t *testing.T) {
mod, diags := testModuleFromDir("testdata/valid-modules/required-providers-overrides")
if diags.HasErrors() {
t.Fatal(diags.Error())
}
// The foo provider and resource should be unaffected
want := addrs.NewProvider(addrs.DefaultProviderRegistryHost, "acme", "foo")
req, exists := mod.ProviderRequirements.RequiredProviders["foo"]
if !exists {
t.Fatal("no provider requirements found for \"foo\"")
}
if req.Type != want {
t.Errorf("wrong provider addr for \"foo\"\ngot: %s\nwant: %s",
req.Type, want,
)
}
if got := mod.ManagedResources["foo_thing.ft"].Provider; !got.Equals(want) {
t.Errorf("wrong provider addr for \"foo_thing.ft\"\ngot: %s\nwant: %s",
got, want,
)
}
// The bar provider and resource should be using the override config
want = addrs.NewProvider(addrs.DefaultProviderRegistryHost, "blorp", "bar")
req, exists = mod.ProviderRequirements.RequiredProviders["bar"]
if !exists {
t.Fatal("no provider requirements found for \"bar\"")
}
if req.Type != want {
t.Errorf("wrong provider addr for \"bar\"\ngot: %s\nwant: %s",
req.Type, want,
)
}
if gotVer, wantVer := req.Requirement.Required.String(), "~>2.0.0"; gotVer != wantVer {
t.Errorf("wrong provider version constraint for \"bar\"\ngot: %s\nwant: %s",
gotVer, wantVer,
)
}
if got := mod.ManagedResources["bar_thing.bt"].Provider; !got.Equals(want) {
t.Errorf("wrong provider addr for \"bar_thing.bt\"\ngot: %s\nwant: %s",
got, want,
)
}
}
// Resources without explicit provider configuration are assigned a provider
// implied based on the resource type. For example, this resource:
//
// resource "foo_instance" "test" {}
//
// ...is assigned to whichever provider has local name "foo" in the current
// module.
//
// To find the correct provider, we first look in the module's provider
// requirements map for a local name matching the resource type, and fall back
// to a default provider if none is found. This applies to both managed and
// data resources.
func TestModule_implied_provider(t *testing.T) {
mod, diags := testModuleFromDir("testdata/valid-modules/implied-providers")
if diags.HasErrors() {
t.Fatal(diags.Error())
}
// The three providers used in the config resources
foo := addrs.NewProvider("registry.acme.corp", "acme", "foo")
whatever := addrs.NewProvider(addrs.DefaultProviderRegistryHost, "acme", "something")
bar := addrs.NewDefaultProvider("bar")
// Verify that the registry.acme.corp/acme/foo provider is defined in the
// module provider requirements with local name "foo"
req, exists := mod.ProviderRequirements.RequiredProviders["foo"]
if !exists {
t.Fatal("no provider requirements found for \"foo\"")
}
if req.Type != foo {
t.Errorf("wrong provider addr for \"foo\"\ngot: %s\nwant: %s",
req.Type, foo,
)
}
// Verify that the acme/something provider is defined in the
// module provider requirements with local name "whatever"
req, exists = mod.ProviderRequirements.RequiredProviders["whatever"]
if !exists {
t.Fatal("no provider requirements found for \"foo\"")
}
if req.Type != whatever {
t.Errorf("wrong provider addr for \"whatever\"\ngot: %s\nwant: %s",
req.Type, whatever,
)
}
// Check that resources are assigned the correct providers: foo_* resources
// should have the custom foo provider, bar_* resources the default bar
// provider.
tests := []struct {
Address string
Provider addrs.Provider
}{
{"foo_resource.a", foo},
{"data.foo_resource.b", foo},
{"bar_resource.c", bar},
{"data.bar_resource.d", bar},
{"whatever_resource.e", whatever},
{"data.whatever_resource.f", whatever},
}
for _, test := range tests {
resources := mod.ManagedResources
if strings.HasPrefix(test.Address, "data.") {
resources = mod.DataResources
}
resource, exists := resources[test.Address]
if !exists {
t.Errorf("could not find resource %q in %#v", test.Address, resources)
continue
}
if got := resource.Provider; !got.Equals(test.Provider) {
t.Errorf("wrong provider addr for %q\ngot: %s\nwant: %s",
test.Address, got, test.Provider,
)
}
}
}
func TestImpliedProviderForUnqualifiedType(t *testing.T) {
mod, diags := testModuleFromDir("testdata/valid-modules/implied-providers")
if diags.HasErrors() {
t.Fatal(diags.Error())
}
foo := addrs.NewProvider("registry.acme.corp", "acme", "foo")
whatever := addrs.NewProvider(addrs.DefaultProviderRegistryHost, "acme", "something")
bar := addrs.NewDefaultProvider("bar")
tf := addrs.NewBuiltInProvider("terraform")
tests := []struct {
Type string
Provider addrs.Provider
}{
{"foo", foo},
{"whatever", whatever},
{"bar", bar},
{"terraform", tf},
}
for _, test := range tests {
got := mod.ImpliedProviderForUnqualifiedType(test.Type)
if !got.Equals(test.Provider) {
t.Errorf("wrong result for %q: got %#v, want %#v\n", test.Type, got, test.Provider)
}
}
}
func TestModule_backend_overrides_a_backend(t *testing.T) {
t.Run("it can override a backend block with a different backend block", func(t *testing.T) {
mod, diags := testModuleFromDir("testdata/valid-modules/override-backend")
if diags.HasErrors() {
t.Fatal(diags.Error())
}
gotType := mod.Backend.Type
wantType := "bar"
if gotType != wantType {
t.Errorf("wrong result for backend type: got %#v, want %#v\n", gotType, wantType)
}
attrs, _ := mod.Backend.Config.JustAttributes()
gotAttr, diags := attrs["path"].Expr.Value(nil)
if diags.HasErrors() {
t.Fatal(diags.Error())
}
wantAttr := cty.StringVal("CHANGED/relative/path/to/terraform.tfstate")
if !gotAttr.RawEquals(wantAttr) {
t.Errorf("wrong result for backend 'path': got %#v, want %#v\n", gotAttr, wantAttr)
}
})
}
// Unlike most other overrides, backend blocks do not require a base configuration in a primary
// configuration file, as an omitted backend there implies the local backend.
func TestModule_backend_overrides_no_base(t *testing.T) {
t.Run("it can introduce a backend block via overrides when the base config has has no cloud or backend blocks", func(t *testing.T) {
mod, diags := testModuleFromDir("testdata/valid-modules/override-backend-no-base")
if diags.HasErrors() {
t.Fatal(diags.Error())
}
if mod.Backend == nil {
t.Errorf("expected module Backend not to be nil")
}
})
}
func TestModule_cloud_overrides_a_backend(t *testing.T) {
t.Run("it can override a backend block with a cloud block", func(t *testing.T) {
mod, diags := testModuleFromDir("testdata/valid-modules/override-backend-with-cloud")
if diags.HasErrors() {
t.Fatal(diags.Error())
}
if mod.Backend != nil {
t.Errorf("expected module Backend to be nil")
}
if mod.CloudConfig == nil {
t.Errorf("expected module CloudConfig not to be nil")
}
})
}
func TestModule_cloud_overrides_cloud(t *testing.T) {
t.Run("it can override a cloud block with a different cloud block", func(t *testing.T) {
mod, diags := testModuleFromDir("testdata/valid-modules/override-cloud")
if diags.HasErrors() {
t.Fatal(diags.Error())
}
attrs, _ := mod.CloudConfig.Config.JustAttributes()
gotAttr, diags := attrs["organization"].Expr.Value(nil)
if diags.HasErrors() {
t.Fatal(diags.Error())
}
wantAttr := cty.StringVal("CHANGED")
if !gotAttr.RawEquals(wantAttr) {
t.Errorf("wrong result for Cloud 'organization': got %#v, want %#v\n", gotAttr, wantAttr)
}
// The override should have completely replaced the cloud block in the primary file, no merging
if attrs["should_not_be_present_with_override"] != nil {
t.Errorf("expected 'should_not_be_present_with_override' attribute to be nil")
}
})
}
// Unlike most other overrides, cloud blocks do not require a base configuration in a primary
// configuration file, as an omitted backend there implies the local backend and cloud blocks
// override backends.
func TestModule_cloud_overrides_no_base(t *testing.T) {
t.Run("it can introduce a cloud block via overrides when the base config has no cloud or backend blocks", func(t *testing.T) {
mod, diags := testModuleFromDir("testdata/valid-modules/override-cloud-no-base")
if diags.HasErrors() {
t.Fatal(diags.Error())
}
if mod.CloudConfig == nil {
t.Errorf("expected module CloudConfig not to be nil")
}
})
}
func TestModule_backend_overrides_cloud(t *testing.T) {
t.Run("it can override a cloud block with a backend block", func(t *testing.T) {
mod, diags := testModuleFromDir("testdata/valid-modules/override-cloud-with-backend")
if diags.HasErrors() {
t.Fatal(diags.Error())
}
gotType := mod.Backend.Type
wantType := "override"
if gotType != wantType {
t.Errorf("wrong result for backend type: got %#v, want %#v\n", gotType, wantType)
}
attrs, _ := mod.Backend.Config.JustAttributes()
gotAttr, diags := attrs["path"].Expr.Value(nil)
if diags.HasErrors() {
t.Fatal(diags.Error())
}
wantAttr := cty.StringVal("value from override")
if !gotAttr.RawEquals(wantAttr) {
t.Errorf("wrong result for backend 'path': got %#v, want %#v\n", gotAttr, wantAttr)
}
})
}
func TestModule_cloud_duplicate_overrides(t *testing.T) {
t.Run("it raises an error when a override file contains multiple cloud blocks", func(t *testing.T) {
_, diags := testModuleFromDir("testdata/invalid-modules/override-cloud-duplicates")
want := `Duplicate HCP Terraform configurations`
if got := diags.Error(); !strings.Contains(got, want) {
t.Fatalf("expected module error to contain %q\nerror was:\n%s", want, got)
}
})
}
func TestModule_backend_multiple(t *testing.T) {
t.Run("it detects when two backend blocks are present within the same module in separate files", func(t *testing.T) {
_, diags := testModuleFromDir("testdata/invalid-modules/multiple-backends")
if !diags.HasErrors() {
t.Fatal("module should have error diags, but does not")
}
want := `Duplicate 'backend' configuration block`
if got := diags.Error(); !strings.Contains(got, want) {
t.Fatalf("expected error to contain %q\nerror was:\n%s", want, got)
}
})
}
func TestModule_cloud_multiple(t *testing.T) {
t.Run("it detects when two cloud blocks are present within the same module in separate files", func(t *testing.T) {
_, diags := testModuleFromDir("testdata/invalid-modules/multiple-cloud")
if !diags.HasErrors() {
t.Fatal("module should have error diags, but does not")
}
want := `Duplicate HCP Terraform configurations`
if got := diags.Error(); !strings.Contains(got, want) {
t.Fatalf("expected error to contain %q\nerror was:\n%s", want, got)
}
})
}
// Cannot combine use of backend, cloud, state_store blocks.
func TestModule_conflicting_backend_cloud_stateStore(t *testing.T) {
testCases := map[string]struct {
dir string
wantMsg string
allowExperiments bool
}{
"cloud backends conflict": {
// detects when both cloud and backend blocks are in the same terraform block
dir: "testdata/invalid-modules/conflict-cloud-backend",
wantMsg: `Conflicting 'cloud' and 'backend' configuration blocks are present`,
},
"cloud backends conflict separate": {
// it detects when both cloud and backend blocks are present in the same module in separate files
dir: "testdata/invalid-modules/conflict-cloud-backend-separate-files",
wantMsg: `Conflicting 'cloud' and 'backend' configuration blocks are present`,
},
"cloud state store conflict": {
// detects when both cloud and state_store blocks are in the same terraform block
dir: "testdata/invalid-modules/conflict-cloud-statestore",
wantMsg: `Conflicting 'cloud' and 'state_store' configuration blocks are present`,
allowExperiments: true,
},
"cloud state store conflict separate": {
// it detects when both cloud and state_store blocks are present in the same module in separate files
dir: "testdata/invalid-modules/conflict-cloud-statestore-separate-files",
wantMsg: `Conflicting 'cloud' and 'state_store' configuration blocks are present`,
allowExperiments: true,
},
"state store backend conflict": {
// it detects when both state_store and backend blocks are in the same terraform block
dir: "testdata/invalid-modules/conflict-statestore-backend",
wantMsg: `Conflicting 'state_store' and 'backend' configuration blocks are present`,
allowExperiments: true,
},
"state store backend conflict separate": {
// it detects when both state_store and backend blocks are present in the same module in separate files
dir: "testdata/invalid-modules/conflict-statestore-backend-separate-files",
wantMsg: `Conflicting 'state_store' and 'backend' configuration blocks are present`,
allowExperiments: true,
},
"cloud backend state store conflict": {
// it detects all 3 of cloud, state_storage and backend blocks are in the same terraform block
dir: "testdata/invalid-modules/conflict-cloud-backend-statestore",
wantMsg: `Only one of 'cloud', 'state_store', or 'backend' configuration blocks are allowed`,
allowExperiments: true,
},
}
for _, tc := range testCases {
t.Run(tc.dir, func(t *testing.T) {
var diags hcl.Diagnostics
if tc.allowExperiments {
PSS: Add initial (incomplete) version of code changes to the `init` command for using pluggable state storage (#37321) * Store the FQN of the provider used in PSS in representations of the parsed config. This can only be done once modules have been parsed and the required providers data is available. There are multiple places where config is parsed, into either Config or Module structs, so this needs to be implemented in multiple places. * Update affected tests, improve error diagnostic * Begin enabling method to return a backend.Backend made using state_store config. State store config can now be received via BackendOpts and there is rough validation of whether the config makes sense (does the provider offer a store with the given name?). * Update code's logic to include possibility of a state store being in use At this point there are no cases that actually handle the state store scenarios though! * Add empty cases for handle all broad init scenarios involving PSS * Update default case's error to report state store variables * Improve how we resolve the builtin terraform provider's address * Add test that hits the code path for adding a state store to a new (or implied local) project * Add test for use of `-reconfigure` flag; show that it hits the code path for adding a state store for the first time * Add test that hits the code path for removing use of a state store, migrating to (implied) local backend * Add test that hits the code path for changing a state store's configuration * Update existing test names to be backend-specific * Add tests that hits the code path for migrating between PSS and backends * Consolidate PSS-related tests at end of the file * Fix log text * Add test showing that using variables is disallowed with state_store and nested provider blocks * Update test name * Fix test cases * Add TODOs so we remember to remove experiments from tests * Update state store-related tests to use t.Cleanup * Remove use of `testChdir`
2025-07-18 09:08:18 -04:00
// TODO(SarahFrench/radeksimko) - disable experiments in this test once the feature is GA.
_, diags = testModuleFromDirWithExperiments(tc.dir)
} else {
_, diags = testModuleFromDir(tc.dir)
}
if !diags.HasErrors() {
t.Fatal("module should have error diags, but does not")
}
if got := diags.Error(); !strings.Contains(got, tc.wantMsg) {
t.Fatalf("expected error to contain %q\nerror was:\n%s", tc.wantMsg, got)
}
})
}
}
func TestModule_stateStore_overrides_stateStore(t *testing.T) {
t.Run("it can override a state_store block with a different state_store block", func(t *testing.T) {
PSS: Add initial (incomplete) version of code changes to the `init` command for using pluggable state storage (#37321) * Store the FQN of the provider used in PSS in representations of the parsed config. This can only be done once modules have been parsed and the required providers data is available. There are multiple places where config is parsed, into either Config or Module structs, so this needs to be implemented in multiple places. * Update affected tests, improve error diagnostic * Begin enabling method to return a backend.Backend made using state_store config. State store config can now be received via BackendOpts and there is rough validation of whether the config makes sense (does the provider offer a store with the given name?). * Update code's logic to include possibility of a state store being in use At this point there are no cases that actually handle the state store scenarios though! * Add empty cases for handle all broad init scenarios involving PSS * Update default case's error to report state store variables * Improve how we resolve the builtin terraform provider's address * Add test that hits the code path for adding a state store to a new (or implied local) project * Add test for use of `-reconfigure` flag; show that it hits the code path for adding a state store for the first time * Add test that hits the code path for removing use of a state store, migrating to (implied) local backend * Add test that hits the code path for changing a state store's configuration * Update existing test names to be backend-specific * Add tests that hits the code path for migrating between PSS and backends * Consolidate PSS-related tests at end of the file * Fix log text * Add test showing that using variables is disallowed with state_store and nested provider blocks * Update test name * Fix test cases * Add TODOs so we remember to remove experiments from tests * Update state store-related tests to use t.Cleanup * Remove use of `testChdir`
2025-07-18 09:08:18 -04:00
// TODO(SarahFrench/radeksimko) - disable experiments in this test once the feature is GA.
mod, diags := testModuleFromDirWithExperiments("testdata/valid-modules/override-state-store")
if diags.HasErrors() {
t.Fatal(diags.Error())
}
if mod.StateStore == nil {
t.Fatal("expected parsed module to include a state store, found none")
}
// Check type override
gotType := mod.StateStore.Type
wantType := "foo_override"
if gotType != wantType {
t.Errorf("wrong result for state_store type: got %#v, want %#v\n", gotType, wantType)
}
// Check custom attribute override
attrs, _ := mod.StateStore.Config.JustAttributes()
gotAttr, diags := attrs["custom_attr"].Expr.Value(nil)
if diags.HasErrors() {
t.Fatal(diags.Error())
}
wantAttr := cty.StringVal("override")
if !gotAttr.RawEquals(wantAttr) {
t.Errorf("wrong result for state_store 'custom_attr': got %#v, want %#v\n", gotAttr, wantAttr)
}
// Check provider reference override
wantLocalName := "bar"
if mod.StateStore.Provider.Name != wantLocalName {
t.Errorf("wrong result for state_store 'provider' value's local name: got %#v, want %#v\n", mod.StateStore.Provider.Name, wantLocalName)
}
})
}
// Unlike most other overrides, state_store blocks do not require a base configuration in a primary
// configuration file, as an omitted backend there implies the local backend.
func TestModule_stateStore_override_no_base(t *testing.T) {
t.Run("it can introduce a state_store block via overrides when the base config has has no cloud, backend, or state_store blocks", func(t *testing.T) {
PSS: Add initial (incomplete) version of code changes to the `init` command for using pluggable state storage (#37321) * Store the FQN of the provider used in PSS in representations of the parsed config. This can only be done once modules have been parsed and the required providers data is available. There are multiple places where config is parsed, into either Config or Module structs, so this needs to be implemented in multiple places. * Update affected tests, improve error diagnostic * Begin enabling method to return a backend.Backend made using state_store config. State store config can now be received via BackendOpts and there is rough validation of whether the config makes sense (does the provider offer a store with the given name?). * Update code's logic to include possibility of a state store being in use At this point there are no cases that actually handle the state store scenarios though! * Add empty cases for handle all broad init scenarios involving PSS * Update default case's error to report state store variables * Improve how we resolve the builtin terraform provider's address * Add test that hits the code path for adding a state store to a new (or implied local) project * Add test for use of `-reconfigure` flag; show that it hits the code path for adding a state store for the first time * Add test that hits the code path for removing use of a state store, migrating to (implied) local backend * Add test that hits the code path for changing a state store's configuration * Update existing test names to be backend-specific * Add tests that hits the code path for migrating between PSS and backends * Consolidate PSS-related tests at end of the file * Fix log text * Add test showing that using variables is disallowed with state_store and nested provider blocks * Update test name * Fix test cases * Add TODOs so we remember to remove experiments from tests * Update state store-related tests to use t.Cleanup * Remove use of `testChdir`
2025-07-18 09:08:18 -04:00
// TODO(SarahFrench/radeksimko) - disable experiments in this test once the feature is GA.
mod, diags := testModuleFromDirWithExperiments("testdata/valid-modules/override-state-store-no-base")
if diags.HasErrors() {
t.Fatal(diags.Error())
}
if mod.StateStore == nil {
t.Errorf("expected module StateStore not to be nil")
}
})
}
func TestModule_stateStore_overrides_backend(t *testing.T) {
t.Run("it can override a backend block with a state_store block", func(t *testing.T) {
PSS: Add initial (incomplete) version of code changes to the `init` command for using pluggable state storage (#37321) * Store the FQN of the provider used in PSS in representations of the parsed config. This can only be done once modules have been parsed and the required providers data is available. There are multiple places where config is parsed, into either Config or Module structs, so this needs to be implemented in multiple places. * Update affected tests, improve error diagnostic * Begin enabling method to return a backend.Backend made using state_store config. State store config can now be received via BackendOpts and there is rough validation of whether the config makes sense (does the provider offer a store with the given name?). * Update code's logic to include possibility of a state store being in use At this point there are no cases that actually handle the state store scenarios though! * Add empty cases for handle all broad init scenarios involving PSS * Update default case's error to report state store variables * Improve how we resolve the builtin terraform provider's address * Add test that hits the code path for adding a state store to a new (or implied local) project * Add test for use of `-reconfigure` flag; show that it hits the code path for adding a state store for the first time * Add test that hits the code path for removing use of a state store, migrating to (implied) local backend * Add test that hits the code path for changing a state store's configuration * Update existing test names to be backend-specific * Add tests that hits the code path for migrating between PSS and backends * Consolidate PSS-related tests at end of the file * Fix log text * Add test showing that using variables is disallowed with state_store and nested provider blocks * Update test name * Fix test cases * Add TODOs so we remember to remove experiments from tests * Update state store-related tests to use t.Cleanup * Remove use of `testChdir`
2025-07-18 09:08:18 -04:00
// TODO(SarahFrench/radeksimko) - disable experiments in this test once the feature is GA.
mod, diags := testModuleFromDirWithExperiments("testdata/valid-modules/override-backend-with-state-store")
if diags.HasErrors() {
t.Fatal(diags.Error())
}
// Backend not set
if mod.Backend != nil {
t.Errorf("backend should not be set: got %#v\n", mod.Backend)
}
// Check state_store
if mod.StateStore == nil {
t.Fatal("expected parsed module to include a state store, found none")
}
gotType := mod.StateStore.Type
wantType := "foo_override"
if gotType != wantType {
t.Errorf("wrong result for state_store type: got %#v, want %#v\n", gotType, wantType)
}
// Not necessary to assert all values in state_store
})
}
func TestModule_stateStore_overrides_cloud(t *testing.T) {
t.Run("it can override a cloud block with a state_store block", func(t *testing.T) {
PSS: Add initial (incomplete) version of code changes to the `init` command for using pluggable state storage (#37321) * Store the FQN of the provider used in PSS in representations of the parsed config. This can only be done once modules have been parsed and the required providers data is available. There are multiple places where config is parsed, into either Config or Module structs, so this needs to be implemented in multiple places. * Update affected tests, improve error diagnostic * Begin enabling method to return a backend.Backend made using state_store config. State store config can now be received via BackendOpts and there is rough validation of whether the config makes sense (does the provider offer a store with the given name?). * Update code's logic to include possibility of a state store being in use At this point there are no cases that actually handle the state store scenarios though! * Add empty cases for handle all broad init scenarios involving PSS * Update default case's error to report state store variables * Improve how we resolve the builtin terraform provider's address * Add test that hits the code path for adding a state store to a new (or implied local) project * Add test for use of `-reconfigure` flag; show that it hits the code path for adding a state store for the first time * Add test that hits the code path for removing use of a state store, migrating to (implied) local backend * Add test that hits the code path for changing a state store's configuration * Update existing test names to be backend-specific * Add tests that hits the code path for migrating between PSS and backends * Consolidate PSS-related tests at end of the file * Fix log text * Add test showing that using variables is disallowed with state_store and nested provider blocks * Update test name * Fix test cases * Add TODOs so we remember to remove experiments from tests * Update state store-related tests to use t.Cleanup * Remove use of `testChdir`
2025-07-18 09:08:18 -04:00
// TODO(SarahFrench/radeksimko) - disable experiments in this test once the feature is GA.
mod, diags := testModuleFromDirWithExperiments("testdata/valid-modules/override-cloud-with-state-store")
if diags.HasErrors() {
t.Fatal(diags.Error())
}
// CloudConfig not set
if mod.CloudConfig != nil {
t.Errorf("backend should not be set: got %#v\n", mod.Backend)
}
// Check state_store
if mod.StateStore == nil {
t.Fatal("expected parsed module to include a state store, found none")
}
gotType := mod.StateStore.Type
wantType := "foo_override"
if gotType != wantType {
t.Errorf("wrong result for state_store type: got %#v, want %#v\n", gotType, wantType)
}
// Not necessary to assert all values in state_store
})
}
func TestModule_state_store_multiple(t *testing.T) {
t.Run("it detects when two state_store blocks are present within the same module in separate files", func(t *testing.T) {
PSS: Add initial (incomplete) version of code changes to the `init` command for using pluggable state storage (#37321) * Store the FQN of the provider used in PSS in representations of the parsed config. This can only be done once modules have been parsed and the required providers data is available. There are multiple places where config is parsed, into either Config or Module structs, so this needs to be implemented in multiple places. * Update affected tests, improve error diagnostic * Begin enabling method to return a backend.Backend made using state_store config. State store config can now be received via BackendOpts and there is rough validation of whether the config makes sense (does the provider offer a store with the given name?). * Update code's logic to include possibility of a state store being in use At this point there are no cases that actually handle the state store scenarios though! * Add empty cases for handle all broad init scenarios involving PSS * Update default case's error to report state store variables * Improve how we resolve the builtin terraform provider's address * Add test that hits the code path for adding a state store to a new (or implied local) project * Add test for use of `-reconfigure` flag; show that it hits the code path for adding a state store for the first time * Add test that hits the code path for removing use of a state store, migrating to (implied) local backend * Add test that hits the code path for changing a state store's configuration * Update existing test names to be backend-specific * Add tests that hits the code path for migrating between PSS and backends * Consolidate PSS-related tests at end of the file * Fix log text * Add test showing that using variables is disallowed with state_store and nested provider blocks * Update test name * Fix test cases * Add TODOs so we remember to remove experiments from tests * Update state store-related tests to use t.Cleanup * Remove use of `testChdir`
2025-07-18 09:08:18 -04:00
// TODO(SarahFrench/radeksimko) - disable experiments in this test once the feature is GA.
_, diags := testModuleFromDirWithExperiments("testdata/invalid-modules/multiple-state-store")
if !diags.HasErrors() {
t.Fatal("module should have error diags, but does not")
}
want := `Duplicate 'state_store' configuration block`
if got := diags.Error(); !strings.Contains(got, want) {
t.Fatalf("expected error to contain %q\nerror was:\n%s", want, got)
}
})
}