Skip to content

Commit f670602

Browse files
authored
chore(terraform): assign *terraform.Module 'parent' field (#8444)
1 parent dd54f80 commit f670602

File tree

11 files changed

+294
-3
lines changed

11 files changed

+294
-3
lines changed

pkg/iac/scanners/terraform/parser/evaluator.go

+13-3
Original file line numberDiff line numberDiff line change
@@ -139,17 +139,19 @@ func (e *evaluator) EvaluateAll(ctx context.Context) (terraform.Modules, map[str
139139
e.blocks = e.expandBlocks(e.blocks)
140140
e.blocks = e.expandBlocks(e.blocks)
141141

142-
submodules := e.evaluateSubmodules(ctx, fsMap)
142+
// rootModule is initialized here, but not fully evaluated until all submodules are evaluated.
143+
// Initializing it up front to keep the module hierarchy of parents correct.
144+
rootModule := terraform.NewModule(e.projectRootPath, e.modulePath, e.blocks, e.ignores)
145+
submodules := e.evaluateSubmodules(ctx, rootModule, fsMap)
143146

144147
e.logger.Debug("Starting post-submodules evaluation...")
145148
e.evaluateSteps()
146149

147150
e.logger.Debug("Module evaluation complete.")
148-
rootModule := terraform.NewModule(e.projectRootPath, e.modulePath, e.blocks, e.ignores)
149151
return append(terraform.Modules{rootModule}, submodules...), fsMap
150152
}
151153

152-
func (e *evaluator) evaluateSubmodules(ctx context.Context, fsMap map[string]fs.FS) terraform.Modules {
154+
func (e *evaluator) evaluateSubmodules(ctx context.Context, parent *terraform.Module, fsMap map[string]fs.FS) terraform.Modules {
153155
submodules := e.loadSubmodules(ctx)
154156

155157
if len(submodules) == 0 {
@@ -174,6 +176,14 @@ func (e *evaluator) evaluateSubmodules(ctx context.Context, fsMap map[string]fs.
174176

175177
var modules terraform.Modules
176178
for _, sm := range submodules {
179+
// Assign the parent placeholder to any submodules without a parent. Any modules
180+
// with a parent already have their correct parent placeholder assigned.
181+
for _, submod := range sm.modules {
182+
if submod.Parent() == nil {
183+
submod.SetParent(parent)
184+
}
185+
}
186+
177187
modules = append(modules, sm.modules...)
178188
for k, v := range sm.fsMap {
179189
fsMap[k] = v

pkg/iac/scanners/terraform/parser/parser_test.go

+150
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"log/slog"
88
"os"
99
"path/filepath"
10+
"slices"
1011
"sort"
1112
"testing"
1213
"testing/fstest"
@@ -18,6 +19,7 @@ import (
1819
"github.com/aquasecurity/trivy/internal/testutil"
1920
"github.com/aquasecurity/trivy/pkg/iac/terraform"
2021
"github.com/aquasecurity/trivy/pkg/log"
22+
"github.com/aquasecurity/trivy/pkg/set"
2123
)
2224

2325
func Test_BasicParsing(t *testing.T) {
@@ -1714,6 +1716,20 @@ func TestNestedModulesOptions(t *testing.T) {
17141716
var buf bytes.Buffer
17151717
slog.SetDefault(slog.New(log.NewHandler(&buf, nil)))
17161718

1719+
// Folder structure
1720+
// ./
1721+
// ├── main.tf
1722+
// └── modules
1723+
// ├── city
1724+
// │ └── main.tf
1725+
// ├── queens
1726+
// │ └── main.tf
1727+
// └── brooklyn
1728+
// └── main.tf
1729+
//
1730+
// Modules referenced
1731+
// main -> city ├─> brooklyn
1732+
// └─> queens
17171733
files := map[string]string{
17181734
"main.tf": `
17191735
module "city" {
@@ -1769,6 +1785,140 @@ module "invalid" {
17691785
}
17701786

17711787
require.NotContains(t, buf.String(), "failed to download")
1788+
1789+
// Verify module parents are set correctly.
1790+
expectedParents := map[string]string{
1791+
".": "",
1792+
"modules/city": ".",
1793+
"modules/city/brooklyn": "modules/city",
1794+
"modules/city/queens": "modules/city",
1795+
}
1796+
1797+
for _, mod := range modules {
1798+
expected, exists := expectedParents[mod.ModulePath()]
1799+
require.Truef(t, exists, "module %s does not exist in assertion", mod.ModulePath())
1800+
if expected == "" {
1801+
require.Nil(t, mod.Parent())
1802+
} else {
1803+
require.Equal(t, expected, mod.Parent().ModulePath(), "parent of module %q", mod.ModulePath())
1804+
}
1805+
}
1806+
}
1807+
1808+
// TestModuleParents sets up a nested module structure and verifies the
1809+
// parent-child relationships are correctly set.
1810+
func TestModuleParents(t *testing.T) {
1811+
// The setup is a list of continents, some countries, some cities, etc.
1812+
dirfs := os.DirFS("./testdata/nested")
1813+
parser := New(dirfs, "",
1814+
OptionStopOnHCLError(true),
1815+
OptionWithDownloads(false),
1816+
)
1817+
require.NoError(t, parser.ParseFS(context.TODO(), "."))
1818+
1819+
modules, _, err := parser.EvaluateAll(context.TODO())
1820+
require.NoError(t, err)
1821+
1822+
// modules only have 'parent'. They do not have children, so create
1823+
// a structure that allows traversal from the root to the leafs.
1824+
modChildren := make(map[*terraform.Module][]*terraform.Module)
1825+
// Keep track of every module that exists
1826+
modSet := set.New[*terraform.Module]()
1827+
var root *terraform.Module
1828+
for _, mod := range modules {
1829+
mod := mod
1830+
modChildren[mod] = make([]*terraform.Module, 0)
1831+
modSet.Append(mod)
1832+
1833+
if mod.Parent() == nil {
1834+
// Only 1 root should exist
1835+
require.Nil(t, root, "root module already set")
1836+
root = mod
1837+
}
1838+
modChildren[mod.Parent()] = append(modChildren[mod.Parent()], mod)
1839+
}
1840+
1841+
type node struct {
1842+
prefix string
1843+
modulePath string
1844+
children []node
1845+
}
1846+
1847+
// expectedTree is the full module tree structure.
1848+
expectedTree := node{
1849+
modulePath: ".",
1850+
children: []node{
1851+
{
1852+
modulePath: "north-america",
1853+
children: []node{
1854+
{
1855+
modulePath: "north-america/united-states",
1856+
children: []node{
1857+
{modulePath: "north-america/united-states/springfield", prefix: "illinois-"},
1858+
{modulePath: "north-america/united-states/springfield", prefix: "idaho-"},
1859+
{modulePath: "north-america/united-states/new-york", children: []node{
1860+
{modulePath: "north-america/united-states/new-york/new-york-city"},
1861+
}},
1862+
},
1863+
},
1864+
{
1865+
modulePath: "north-america/canada",
1866+
children: []node{
1867+
{modulePath: "north-america/canada/springfield", prefix: "ontario-"},
1868+
},
1869+
},
1870+
},
1871+
},
1872+
},
1873+
}
1874+
1875+
var assertChild func(t *testing.T, n node, mod *terraform.Module)
1876+
assertChild = func(t *testing.T, n node, mod *terraform.Module) {
1877+
modSet.Remove(mod)
1878+
children := modChildren[mod]
1879+
1880+
t.Run(n.modulePath, func(t *testing.T) {
1881+
if !assert.Equal(t, len(n.children), len(children), "modChildren count for %s", n.modulePath) {
1882+
return
1883+
}
1884+
for _, child := range children {
1885+
// Find the child module that we are expecting.
1886+
idx := slices.IndexFunc(n.children, func(node node) bool {
1887+
outputBlocks := child.GetBlocks().OfType("output")
1888+
outIdx := slices.IndexFunc(outputBlocks, func(outputBlock *terraform.Block) bool {
1889+
return outputBlock.Labels()[0] == "name"
1890+
})
1891+
if outIdx == -1 {
1892+
return false
1893+
}
1894+
1895+
output := outputBlocks[outIdx]
1896+
outVal := output.GetAttribute("value").Value()
1897+
if !outVal.Type().Equals(cty.String) {
1898+
return false
1899+
}
1900+
1901+
modName := filepath.Base(node.modulePath)
1902+
if outVal.AsString() != node.prefix+modName {
1903+
return false
1904+
}
1905+
1906+
return node.modulePath == child.ModulePath()
1907+
})
1908+
if !assert.NotEqualf(t, -1, idx, "module prefix=%s path=%s not found in %s", n.prefix, child.ModulePath(), n.modulePath) {
1909+
continue
1910+
}
1911+
1912+
assertChild(t, n.children[idx], child)
1913+
}
1914+
})
1915+
1916+
}
1917+
1918+
assertChild(t, expectedTree, root)
1919+
// If any module was not asserted, the test will fail. This ensures the
1920+
// entire module tree is checked.
1921+
require.Equal(t, 0, modSet.Size(), "all modules asserted")
17721922
}
17731923

17741924
func TestCyclicModules(t *testing.T) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module "north-america" {
2+
source = "./north-america"
3+
}
4+
5+
output "all" {
6+
value = [
7+
module.north-america,
8+
]
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
variable "prefix" {
2+
type = string
3+
default = ""
4+
}
5+
6+
output "name" {
7+
value = "${var.prefix}canada"
8+
}
9+
10+
module "ontario-springfield" {
11+
source = "./springfield"
12+
prefix = "ontario-"
13+
}
14+
15+
output "ontario-springfield" {
16+
value = module.ontario-springfield.name
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
variable "prefix" {
2+
type = string
3+
default = ""
4+
}
5+
6+
output "name" {
7+
value = "${var.prefix}springfield"
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
variable "prefix" {
2+
type = string
3+
default = ""
4+
}
5+
6+
output "name" {
7+
value = "${var.prefix}north-america"
8+
}
9+
10+
module "canada" {
11+
source = "./canada"
12+
prefix = ""
13+
}
14+
15+
module "united-states" {
16+
source = "./united-states"
17+
prefix = ""
18+
}
19+
20+
output "united-states" {
21+
value = module.united-states.name
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
variable "prefix" {
2+
type = string
3+
default = ""
4+
}
5+
6+
output "name" {
7+
value = "${var.prefix}new-york-city"
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
variable "prefix" {
2+
type = string
3+
default = ""
4+
}
5+
6+
output "name" {
7+
value = "${var.prefix}new-york"
8+
}
9+
10+
module "new-york-city" {
11+
source = "./new-york-city"
12+
prefix = ""
13+
}
14+
15+
output "new-york-city" {
16+
value = module.new-york-city.name
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
variable "prefix" {
2+
type = string
3+
default = ""
4+
}
5+
6+
output "name" {
7+
value = "${var.prefix}springfield"
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
variable "prefix" {
2+
type = string
3+
default = ""
4+
}
5+
6+
output "name" {
7+
value = "${var.prefix}united-states"
8+
}
9+
10+
// Same module twice, with different variables
11+
module "illinois-springfield" {
12+
source = "./springfield"
13+
prefix = "illinois-"
14+
}
15+
16+
output "illinois-springfield" {
17+
value = module.illinois-springfield.name
18+
}
19+
20+
module "idaho-springfield" {
21+
source = "./springfield"
22+
prefix = "idaho-"
23+
}
24+
25+
output "idaho-springfield" {
26+
value = module.idaho-springfield.name
27+
}
28+
29+
module "new-york" {
30+
source = "./new-york"
31+
prefix = ""
32+
}
33+
34+
output "new-york" {
35+
value = module.new-york.name
36+
}
37+
38+

pkg/iac/terraform/module.go

+4
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ func (c *Module) ModulePath() string {
4747
return c.modulePath
4848
}
4949

50+
func (c *Module) Parent() *Module {
51+
return c.parent
52+
}
53+
5054
func (c *Module) Ignores() ignore.Rules {
5155
return c.ignores
5256
}

0 commit comments

Comments
 (0)