// 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 ( "fmt" "path/filepath" "testing" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/gohcl" "github.com/opentofu/opentofu/internal/addrs" "github.com/zclconf/go-cty/cty" ) func TestModuleOverrideVariable(t *testing.T) { mod, diags := testModuleFromDir("testdata/valid-modules/override-variable") assertNoDiagnostics(t, diags) if mod == nil { t.Fatalf("module is nil") } got := mod.Variables want := map[string]*Variable{ "fully_overridden": { Name: "fully_overridden", Description: "b_override description", DescriptionSet: true, Default: cty.StringVal("b_override"), Nullable: false, NullableSet: true, Type: cty.String, ConstraintType: cty.String, ParsingMode: VariableParseLiteral, DeclRange: hcl.Range{ Filename: filepath.FromSlash("testdata/valid-modules/override-variable/primary.tf"), Start: hcl.Pos{ Line: 1, Column: 1, Byte: 0, }, End: hcl.Pos{ Line: 1, Column: 28, Byte: 27, }, }, }, "partially_overridden": { Name: "partially_overridden", Description: "base description", DescriptionSet: true, Default: cty.StringVal("b_override partial"), Nullable: true, NullableSet: false, Type: cty.String, ConstraintType: cty.String, ParsingMode: VariableParseLiteral, DeclRange: hcl.Range{ Filename: filepath.FromSlash("testdata/valid-modules/override-variable/primary.tf"), Start: hcl.Pos{ Line: 7, Column: 1, Byte: 103, }, End: hcl.Pos{ Line: 7, Column: 32, Byte: 134, }, }, }, } assertResultDeepEqual(t, got, want) } func TestModuleOverrideModule(t *testing.T) { mod, diags := testModuleFromDir("testdata/valid-modules/override-module") assertNoDiagnostics(t, diags) if mod == nil { t.Fatalf("module is nil") } if _, exists := mod.ModuleCalls["example"]; !exists { t.Fatalf("no module 'example'") } if len(mod.ModuleCalls) != 1 { t.Fatalf("wrong number of module calls in result %d; want 1", len(mod.ModuleCalls)) } got := mod.ModuleCalls["example"] want := &ModuleCall{ Name: "example", SourceAddr: addrs.ModuleSourceLocal("./example2-a_override"), SourceAddrRaw: "./example2-a_override", SourceAddrRange: hcl.Range{ Filename: filepath.FromSlash("testdata/valid-modules/override-module/a_override.tf"), Start: hcl.Pos{ Line: 3, Column: 12, Byte: 31, }, End: hcl.Pos{ Line: 3, Column: 35, Byte: 54, }, }, SourceSet: true, DeclRange: hcl.Range{ Filename: filepath.FromSlash("testdata/valid-modules/override-module/primary.tf"), Start: hcl.Pos{ Line: 2, Column: 1, Byte: 1, }, End: hcl.Pos{ Line: 2, Column: 17, Byte: 17, }, }, DependsOn: []hcl.Traversal{ { hcl.TraverseRoot{ Name: "null_resource", SrcRange: hcl.Range{ Filename: filepath.FromSlash("testdata/valid-modules/override-module/primary.tf"), Start: hcl.Pos{Line: 11, Column: 17, Byte: 149}, End: hcl.Pos{Line: 11, Column: 30, Byte: 162}, }, }, hcl.TraverseAttr{ Name: "test", SrcRange: hcl.Range{ Filename: filepath.FromSlash("testdata/valid-modules/override-module/primary.tf"), Start: hcl.Pos{Line: 11, Column: 30, Byte: 162}, End: hcl.Pos{Line: 11, Column: 35, Byte: 167}, }, }, }, }, Providers: []PassedProviderConfig{ { InChild: &ProviderConfigRef{ Name: "test", NameRange: hcl.Range{ Filename: filepath.FromSlash("testdata/valid-modules/override-module/b_override.tf"), Start: hcl.Pos{Line: 7, Column: 5, Byte: 97}, End: hcl.Pos{Line: 7, Column: 9, Byte: 101}, }, }, InParent: &ProviderConfigRef{ Name: "test", NameRange: hcl.Range{ Filename: filepath.FromSlash("testdata/valid-modules/override-module/b_override.tf"), Start: hcl.Pos{Line: 7, Column: 12, Byte: 104}, End: hcl.Pos{Line: 7, Column: 16, Byte: 108}, }, Alias: "b_override", AliasRange: &hcl.Range{ Filename: filepath.FromSlash("testdata/valid-modules/override-module/b_override.tf"), Start: hcl.Pos{Line: 7, Column: 16, Byte: 108}, End: hcl.Pos{Line: 7, Column: 27, Byte: 119}, }, }, }, }, } // We're going to extract and nil out our hcl.Body here because DeepEqual // is not a useful way to assert on that. gotConfig := got.Config got.Config = nil assertResultDeepEqual(t, got, want) type content struct { Kept *string `hcl:"kept"` Foo *string `hcl:"foo"` New *string `hcl:"new"` Newer *string `hcl:"newer"` } var gotArgs content diags = gohcl.DecodeBody(gotConfig, nil, &gotArgs) assertNoDiagnostics(t, diags) wantArgs := content{ Kept: stringPtr("primary kept"), Foo: stringPtr("a_override foo"), New: stringPtr("b_override new"), Newer: stringPtr("b_override newer"), } assertResultDeepEqual(t, gotArgs, wantArgs) } func TestModuleOverrideDynamic(t *testing.T) { schema := &hcl.BodySchema{ Blocks: []hcl.BlockHeaderSchema{ {Type: "foo"}, {Type: "dynamic", LabelNames: []string{"type"}}, }, } t.Run("base is dynamic", func(t *testing.T) { mod, diags := testModuleFromDir("testdata/valid-modules/override-dynamic-block-base") assertNoDiagnostics(t, diags) if mod == nil { t.Fatalf("module is nil") } if _, exists := mod.ManagedResources["test.foo"]; !exists { t.Fatalf("no module 'example'") } if len(mod.ManagedResources) != 1 { t.Fatalf("wrong number of managed resources in result %d; want 1", len(mod.ManagedResources)) } body := mod.ManagedResources["test.foo"].Config content, diags := body.Content(schema) assertNoDiagnostics(t, diags) if len(content.Blocks) != 1 { t.Fatalf("wrong number of blocks in result %d; want 1", len(content.Blocks)) } if got, want := content.Blocks[0].Type, "foo"; got != want { t.Fatalf("wrong block type %q; want %q", got, want) } }) t.Run("override is dynamic", func(t *testing.T) { mod, diags := testModuleFromDir("testdata/valid-modules/override-dynamic-block-override") assertNoDiagnostics(t, diags) if mod == nil { t.Fatalf("module is nil") } if _, exists := mod.ManagedResources["test.foo"]; !exists { t.Fatalf("no module 'example'") } if len(mod.ManagedResources) != 1 { t.Fatalf("wrong number of managed resources in result %d; want 1", len(mod.ManagedResources)) } body := mod.ManagedResources["test.foo"].Config content, diags := body.Content(schema) assertNoDiagnostics(t, diags) if len(content.Blocks) != 1 { t.Fatalf("wrong number of blocks in result %d; want 1", len(content.Blocks)) } if got, want := content.Blocks[0].Type, "dynamic"; got != want { t.Fatalf("wrong block type %q; want %q", got, want) } if got, want := content.Blocks[0].Labels[0], "foo"; got != want { t.Fatalf("wrong dynamic block label %q; want %q", got, want) } }) } func TestModuleOverrideSensitiveVariable(t *testing.T) { type testCase struct { sensitive bool sensitiveSet bool } cases := map[string]testCase{ "false_true": { sensitive: true, sensitiveSet: true, }, "true_false": { sensitive: false, sensitiveSet: true, }, "false_false_true": { sensitive: true, sensitiveSet: true, }, "true_true_false": { sensitive: false, sensitiveSet: true, }, "false_true_false": { sensitive: false, sensitiveSet: true, }, "true_false_true": { sensitive: true, sensitiveSet: true, }, } mod, diags := testModuleFromDir("testdata/valid-modules/override-variable-sensitive") assertNoDiagnostics(t, diags) if mod == nil { t.Fatalf("module is nil") } got := mod.Variables for v, want := range cases { t.Run(fmt.Sprintf("variable %s", v), func(t *testing.T) { if got[v].Sensitive != want.sensitive { t.Errorf("wrong result for sensitive\ngot: %t want: %t", got[v].Sensitive, want.sensitive) } if got[v].SensitiveSet != want.sensitiveSet { t.Errorf("wrong result for sensitive set\ngot: %t want: %t", got[v].Sensitive, want.sensitive) } }) } } func TestModuleOverrideResourceFQNs(t *testing.T) { mod, diags := testModuleFromDir("testdata/valid-modules/override-resource-provider") assertNoDiagnostics(t, diags) got := mod.ManagedResources["test_instance.explicit"] wantProvider := addrs.NewProvider(addrs.DefaultProviderRegistryHost, "bar", "test") wantProviderCfg := &ProviderConfigRef{ Name: "bar-test", NameRange: hcl.Range{ Filename: filepath.FromSlash("testdata/valid-modules/override-resource-provider/a_override.tf"), Start: hcl.Pos{Line: 2, Column: 14, Byte: 51}, End: hcl.Pos{Line: 2, Column: 22, Byte: 59}, }, } if !got.Provider.Equals(wantProvider) { t.Fatalf("wrong provider %s, want %s", got.Provider, wantProvider) } assertResultDeepEqual(t, got.ProviderConfigRef, wantProviderCfg) // now verify that a resource with no provider config falls back to default got = mod.ManagedResources["test_instance.default"] wantProvider = addrs.NewDefaultProvider("test") if !got.Provider.Equals(wantProvider) { t.Fatalf("wrong provider %s, want %s", got.Provider, wantProvider) } if got.ProviderConfigRef != nil { t.Fatalf("wrong result: found provider config ref %s, expected nil", got.ProviderConfigRef) } } func TestModuleOverrideIgnoreAllChanges(t *testing.T) { mod, diags := testModuleFromDir("testdata/valid-modules/override-ignore-changes") assertNoDiagnostics(t, diags) r := mod.ManagedResources["test_instance.foo"] if !r.Managed.IgnoreAllChanges { t.Fatalf("wrong result: expected r.Managed.IgnoreAllChanges to be true") } }