// Copyright (c) The OpenTofu Authors // SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2023 HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package configs import ( "strings" "testing" "github.com/opentofu/opentofu/internal/addrs" "github.com/zclconf/go-cty/cty" ) // TestNewModule_provider_local_name 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 { 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") } // if there is not a local name for a provider, it should return the type name 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_override(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_override_no_base(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_override_backend(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") } } // 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_override_no_base(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_cloud_override(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") } } func TestModule_cloud_duplicate_overrides(t *testing.T) { _, diags := testModuleFromDir("testdata/invalid-modules/override-cloud-duplicates") want := `Duplicate cloud configurations` if got := diags.Error(); !strings.Contains(got, want) { t.Fatalf("expected module error to contain %q\nerror was:\n%s", want, got) } }