Skip to content

Commit 4ce385a

Browse files
author
Liam Cervante
authored
testing framework: implement overrides in terraform graph (#34169)
* testing framework: implement overrides in terraform graph * fix bug for modules and module instances * add test for ModuleInstance.ContainingModule()
1 parent 1974c9e commit 4ce385a

20 files changed

+1393
-91
lines changed

internal/addrs/module_instance.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,18 @@ func (m ModuleInstance) Module() Module {
504504
return ret
505505
}
506506

507+
// ContainingModule returns the address of the module instance as if the last
508+
// step wasn't instanced. For example, it turns module.child[0] into
509+
// module.child and module[0].child[0] into module[0].child.
510+
func (m ModuleInstance) ContainingModule() ModuleInstance {
511+
if len(m) == 0 {
512+
return nil
513+
}
514+
515+
ret := m.Parent()
516+
return ret.Child(m[len(m)-1].Name, NoKey)
517+
}
518+
507519
func (m ModuleInstance) AddrType() TargetableAddrType {
508520
return ModuleInstanceAddrType
509521
}

internal/addrs/module_instance_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,47 @@ func TestModuleInstance_IsDeclaredByCall(t *testing.T) {
164164
}
165165
}
166166

167+
func TestModuleInstance_ContainingModule(t *testing.T) {
168+
tcs := map[string]struct {
169+
module string
170+
expected string
171+
}{
172+
"no_instances": {
173+
module: "module.parent.module.child",
174+
expected: "module.parent.module.child",
175+
},
176+
"last_instance": {
177+
module: "module.parent.module.child[0]",
178+
expected: "module.parent.module.child",
179+
},
180+
"middle_instance": {
181+
module: "module.parent[0].module.child",
182+
expected: "module.parent[0].module.child",
183+
},
184+
"all_instances": {
185+
module: "module.parent[0].module.child[0]",
186+
expected: "module.parent[0].module.child",
187+
},
188+
"single_no_instance": {
189+
module: "module.parent",
190+
expected: "module.parent",
191+
},
192+
"single_instance": {
193+
module: "module.parent[0]",
194+
expected: "module.parent",
195+
},
196+
}
197+
for name, tc := range tcs {
198+
t.Run(name, func(t *testing.T) {
199+
module := mustParseModuleInstanceStr(tc.module)
200+
actual, expected := module.ContainingModule().String(), tc.expected
201+
if actual != expected {
202+
t.Errorf("expected: %s\nactual: %s", expected, actual)
203+
}
204+
})
205+
}
206+
}
207+
167208
func mustParseModuleInstanceStr(str string) ModuleInstance {
168209
mi, diags := ParseModuleInstanceStr(str)
169210
if diags.HasErrors() {
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package mocking
5+
6+
import (
7+
"fmt"
8+
9+
"github.com/hashicorp/terraform/internal/addrs"
10+
"github.com/hashicorp/terraform/internal/configs"
11+
)
12+
13+
// Overrides contains a summary of all the overrides that should apply for a
14+
// test run.
15+
//
16+
// This requires us to deduplicate between run blocks and test files, and mock
17+
// providers.
18+
type Overrides struct {
19+
providerOverrides map[string]addrs.Map[addrs.Targetable, *configs.Override]
20+
localOverrides addrs.Map[addrs.Targetable, *configs.Override]
21+
}
22+
23+
func PackageOverrides(run *configs.TestRun, file *configs.TestFile, config *configs.Config) *Overrides {
24+
overrides := &Overrides{
25+
providerOverrides: make(map[string]addrs.Map[addrs.Targetable, *configs.Override]),
26+
localOverrides: addrs.MakeMap[addrs.Targetable, *configs.Override](),
27+
}
28+
29+
// The run block overrides have the highest priority, we always include all
30+
// of them.
31+
for _, elem := range run.Overrides.Elems {
32+
overrides.localOverrides.PutElement(elem)
33+
}
34+
35+
// The file overrides are second, we include these as long as there isn't
36+
// a direct replacement in the current run block or the run block doesn't
37+
// override an entire module that a file override would be inside.
38+
for _, elem := range file.Overrides.Elems {
39+
target := elem.Key
40+
41+
if overrides.localOverrides.Has(target) {
42+
// The run block provided a value already.
43+
continue
44+
}
45+
46+
overrides.localOverrides.PutElement(elem)
47+
}
48+
49+
// Finally, we want to include the overrides for any mock providers we have.
50+
for key, provider := range config.Module.ProviderConfigs {
51+
if !provider.Mock {
52+
// Only mock providers can supply overrides.
53+
continue
54+
}
55+
56+
for _, elem := range provider.MockData.Overrides.Elems {
57+
target := elem.Key
58+
59+
if overrides.localOverrides.Has(target) {
60+
// Then the file or the run block is providing an override with
61+
// higher precedence.
62+
continue
63+
}
64+
65+
if _, exists := overrides.providerOverrides[key]; !exists {
66+
overrides.providerOverrides[key] = addrs.MakeMap[addrs.Targetable, *configs.Override]()
67+
}
68+
overrides.providerOverrides[key].PutElement(elem)
69+
}
70+
}
71+
72+
return overrides
73+
}
74+
75+
// IsOverridden returns true if the module is either overridden directly or
76+
// nested within another module that is already being overridden.
77+
//
78+
// For this function, we know that overrides defined within mock providers
79+
// cannot target modules directly. Therefore, we only need to check the local
80+
// overrides within this function.
81+
func (overrides *Overrides) IsOverridden(module addrs.ModuleInstance) bool {
82+
if overrides.localOverrides.Has(module) {
83+
// Short circuit things, if we have an exact match just return now.
84+
return true
85+
}
86+
87+
// Otherwise, check for parents.
88+
for _, elem := range overrides.localOverrides.Elems {
89+
if elem.Key.TargetContains(module) {
90+
// Then we have an ancestor of module being overridden instead of
91+
// module being overridden directly.
92+
return true
93+
}
94+
}
95+
96+
return false
97+
}
98+
99+
// IsDeeplyOverridden returns true if an ancestor of this module is overridden
100+
// but not if the module is overridden directly.
101+
//
102+
// This function doesn't consider an instanced module to be deeply overridden
103+
// by the uninstanced reference to the same module. So,
104+
// IsDeeplyOverridden("mod.child[0]") would return false if "mod.child" has been
105+
// overridden.
106+
//
107+
// For this function, we know that overrides defined within mock providers
108+
// cannot target modules directly. Therefore, we only need to check the local
109+
// overrides within this function.
110+
func (overrides *Overrides) IsDeeplyOverridden(module addrs.ModuleInstance) bool {
111+
for _, elem := range overrides.localOverrides.Elems {
112+
target := elem.Key
113+
114+
if target.TargetContains(module) {
115+
// So we do think it contains it, but it could be matching here
116+
// because of equality or because we have an instanced module.
117+
if instance, ok := target.(addrs.ModuleInstance); ok {
118+
if instance.Equal(module) {
119+
// Then we're exactly equal, so not deeply nested.
120+
continue
121+
}
122+
123+
if instance.Module().Equal(module.Module()) {
124+
// Then we're an instanced version of they other one, so
125+
// also not deeply nested by our definition of deeply.
126+
continue
127+
}
128+
129+
}
130+
131+
// Otherwise, it's deeply nested.
132+
return true
133+
}
134+
}
135+
return false
136+
}
137+
138+
// GetOverrideInclProviders retrieves the override for target if it exists.
139+
//
140+
// This function also checks the provider specific overrides using the provider
141+
// argument.
142+
func (overrides *Overrides) GetOverrideInclProviders(target addrs.Targetable, provider addrs.AbsProviderConfig) (*configs.Override, bool) {
143+
// If we have a local override, then apply that first.
144+
if override, ok := overrides.GetOverride(target); ok {
145+
return override, true
146+
}
147+
148+
// Otherwise, check if we have overrides for this provider.
149+
providerOverrides, ok := overrides.ProviderMatch(provider)
150+
if ok {
151+
if override, ok := providerOverrides.GetOk(target); ok {
152+
return override, true
153+
}
154+
}
155+
156+
// If we have no overrides, that's okay.
157+
return nil, false
158+
}
159+
160+
// GetOverride retrieves the override for target from the local overrides if
161+
// it exists.
162+
func (overrides *Overrides) GetOverride(target addrs.Targetable) (*configs.Override, bool) {
163+
return overrides.localOverrides.GetOk(target)
164+
}
165+
166+
// ProviderMatch returns true if we have overrides for the given provider.
167+
//
168+
// This is so that we can selectively apply overrides to resources that are
169+
// being supplied by a given provider.
170+
func (overrides *Overrides) ProviderMatch(provider addrs.AbsProviderConfig) (addrs.Map[addrs.Targetable, *configs.Override], bool) {
171+
if !provider.Module.IsRoot() {
172+
// We can only set mock providers within the root module.
173+
return addrs.Map[addrs.Targetable, *configs.Override]{}, false
174+
}
175+
176+
name := provider.Provider.Type
177+
if len(provider.Alias) > 0 {
178+
name = fmt.Sprintf("%s.%s", name, provider.Alias)
179+
}
180+
181+
data, exists := overrides.providerOverrides[name]
182+
return data, exists
183+
}
184+
185+
// Empty returns true if we have no actual overrides.
186+
//
187+
// This is just a convenience function to make checking for overrides easier.
188+
func (overrides *Overrides) Empty() bool {
189+
if overrides == nil {
190+
return true
191+
}
192+
193+
if overrides.localOverrides.Len() > 0 {
194+
return false
195+
}
196+
197+
for _, value := range overrides.providerOverrides {
198+
if value.Len() > 0 {
199+
return false
200+
}
201+
}
202+
203+
return true
204+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package mocking
5+
6+
import (
7+
"testing"
8+
9+
"github.com/hashicorp/terraform/internal/addrs"
10+
"github.com/hashicorp/terraform/internal/configs"
11+
)
12+
13+
func TestPackageOverrides(t *testing.T) {
14+
mustResourceInstance := func(s string) addrs.AbsResourceInstance {
15+
addr, diags := addrs.ParseAbsResourceInstanceStr(s)
16+
if len(diags) > 0 {
17+
t.Fatal(diags)
18+
}
19+
return addr
20+
}
21+
22+
primary := mustResourceInstance("test_instance.primary")
23+
secondary := mustResourceInstance("test_instance.secondary")
24+
tertiary := mustResourceInstance("test_instance.tertiary")
25+
26+
testrun := mustResourceInstance("test_instance.test_run")
27+
testfile := mustResourceInstance("test_instance.test_file")
28+
provider := mustResourceInstance("test_instance.provider")
29+
30+
// Add a single override to the test run.
31+
run := &configs.TestRun{
32+
Overrides: addrs.MakeMap[addrs.Targetable, *configs.Override](),
33+
}
34+
run.Overrides.Put(primary, &configs.Override{
35+
Target: &addrs.Target{
36+
Subject: testrun,
37+
},
38+
})
39+
40+
// Add a unique item to the test file, and duplicate the test run data.
41+
file := &configs.TestFile{
42+
Overrides: addrs.MakeMap[addrs.Targetable, *configs.Override](),
43+
}
44+
file.Overrides.Put(primary, &configs.Override{
45+
Target: &addrs.Target{
46+
Subject: testfile,
47+
},
48+
})
49+
file.Overrides.Put(secondary, &configs.Override{
50+
Target: &addrs.Target{
51+
Subject: testfile,
52+
},
53+
})
54+
55+
// Add all data from the file and run block are duplicating here, and then
56+
// a unique one.
57+
config := &configs.Config{
58+
Module: &configs.Module{
59+
ProviderConfigs: map[string]*configs.Provider{
60+
"mock": {
61+
Mock: true,
62+
MockData: &configs.MockData{
63+
Overrides: addrs.MakeMap[addrs.Targetable, *configs.Override](),
64+
},
65+
},
66+
"real": {},
67+
},
68+
},
69+
}
70+
config.Module.ProviderConfigs["mock"].MockData.Overrides.Put(primary, &configs.Override{
71+
Target: &addrs.Target{
72+
Subject: provider,
73+
},
74+
})
75+
config.Module.ProviderConfigs["mock"].MockData.Overrides.Put(secondary, &configs.Override{
76+
Target: &addrs.Target{
77+
Subject: provider,
78+
},
79+
})
80+
config.Module.ProviderConfigs["mock"].MockData.Overrides.Put(tertiary, &configs.Override{
81+
Target: &addrs.Target{
82+
Subject: provider,
83+
},
84+
})
85+
86+
overrides := PackageOverrides(run, file, config)
87+
88+
// We now expect that the run and file overrides took precedence.
89+
first, pOk := overrides.GetOverride(primary)
90+
second, sOk := overrides.GetOverride(secondary)
91+
third, tOk := overrides.GetOverrideInclProviders(tertiary, addrs.AbsProviderConfig{
92+
Provider: addrs.Provider{
93+
Type: "mock",
94+
},
95+
})
96+
97+
if !pOk || !sOk || !tOk {
98+
t.Fatalf("expected to find all overrides, but got %t %t %t", pOk, sOk, tOk)
99+
}
100+
101+
if !first.Target.Subject.(addrs.AbsResourceInstance).Equal(testrun) {
102+
t.Errorf("expected %s but got %s for primary", testrun, first.Target.Subject)
103+
}
104+
105+
if !second.Target.Subject.(addrs.AbsResourceInstance).Equal(testfile) {
106+
t.Errorf("expected %s but got %s for primary", testfile, second.Target.Subject)
107+
}
108+
109+
if !third.Target.Subject.(addrs.AbsResourceInstance).Equal(provider) {
110+
t.Errorf("expected %s but got %s for primary", provider, third.Target.Subject)
111+
}
112+
113+
// Also, final sanity check.
114+
_, ok := overrides.providerOverrides["real"]
115+
if ok {
116+
t.Errorf("shouldn't have stored the real provider but did")
117+
}
118+
119+
}

0 commit comments

Comments
 (0)