chore(terraform): assign *terraform.Module 'parent' field (#8444)

This commit is contained in:
Steven Masley
2025-03-04 20:23:09 -06:00
committed by GitHub
parent dd54f80d3f
commit f670602091
11 changed files with 294 additions and 3 deletions

View File

@@ -7,6 +7,7 @@ import (
"log/slog"
"os"
"path/filepath"
"slices"
"sort"
"testing"
"testing/fstest"
@@ -18,6 +19,7 @@ import (
"github.com/aquasecurity/trivy/internal/testutil"
"github.com/aquasecurity/trivy/pkg/iac/terraform"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/set"
)
func Test_BasicParsing(t *testing.T) {
@@ -1714,6 +1716,20 @@ func TestNestedModulesOptions(t *testing.T) {
var buf bytes.Buffer
slog.SetDefault(slog.New(log.NewHandler(&buf, nil)))
// Folder structure
// ./
// ├── main.tf
// └── modules
// ├── city
// │ └── main.tf
// ├── queens
// │ └── main.tf
// └── brooklyn
// └── main.tf
//
// Modules referenced
// main -> city ├─> brooklyn
// └─> queens
files := map[string]string{
"main.tf": `
module "city" {
@@ -1769,6 +1785,140 @@ module "invalid" {
}
require.NotContains(t, buf.String(), "failed to download")
// Verify module parents are set correctly.
expectedParents := map[string]string{
".": "",
"modules/city": ".",
"modules/city/brooklyn": "modules/city",
"modules/city/queens": "modules/city",
}
for _, mod := range modules {
expected, exists := expectedParents[mod.ModulePath()]
require.Truef(t, exists, "module %s does not exist in assertion", mod.ModulePath())
if expected == "" {
require.Nil(t, mod.Parent())
} else {
require.Equal(t, expected, mod.Parent().ModulePath(), "parent of module %q", mod.ModulePath())
}
}
}
// TestModuleParents sets up a nested module structure and verifies the
// parent-child relationships are correctly set.
func TestModuleParents(t *testing.T) {
// The setup is a list of continents, some countries, some cities, etc.
dirfs := os.DirFS("./testdata/nested")
parser := New(dirfs, "",
OptionStopOnHCLError(true),
OptionWithDownloads(false),
)
require.NoError(t, parser.ParseFS(context.TODO(), "."))
modules, _, err := parser.EvaluateAll(context.TODO())
require.NoError(t, err)
// modules only have 'parent'. They do not have children, so create
// a structure that allows traversal from the root to the leafs.
modChildren := make(map[*terraform.Module][]*terraform.Module)
// Keep track of every module that exists
modSet := set.New[*terraform.Module]()
var root *terraform.Module
for _, mod := range modules {
mod := mod
modChildren[mod] = make([]*terraform.Module, 0)
modSet.Append(mod)
if mod.Parent() == nil {
// Only 1 root should exist
require.Nil(t, root, "root module already set")
root = mod
}
modChildren[mod.Parent()] = append(modChildren[mod.Parent()], mod)
}
type node struct {
prefix string
modulePath string
children []node
}
// expectedTree is the full module tree structure.
expectedTree := node{
modulePath: ".",
children: []node{
{
modulePath: "north-america",
children: []node{
{
modulePath: "north-america/united-states",
children: []node{
{modulePath: "north-america/united-states/springfield", prefix: "illinois-"},
{modulePath: "north-america/united-states/springfield", prefix: "idaho-"},
{modulePath: "north-america/united-states/new-york", children: []node{
{modulePath: "north-america/united-states/new-york/new-york-city"},
}},
},
},
{
modulePath: "north-america/canada",
children: []node{
{modulePath: "north-america/canada/springfield", prefix: "ontario-"},
},
},
},
},
},
}
var assertChild func(t *testing.T, n node, mod *terraform.Module)
assertChild = func(t *testing.T, n node, mod *terraform.Module) {
modSet.Remove(mod)
children := modChildren[mod]
t.Run(n.modulePath, func(t *testing.T) {
if !assert.Equal(t, len(n.children), len(children), "modChildren count for %s", n.modulePath) {
return
}
for _, child := range children {
// Find the child module that we are expecting.
idx := slices.IndexFunc(n.children, func(node node) bool {
outputBlocks := child.GetBlocks().OfType("output")
outIdx := slices.IndexFunc(outputBlocks, func(outputBlock *terraform.Block) bool {
return outputBlock.Labels()[0] == "name"
})
if outIdx == -1 {
return false
}
output := outputBlocks[outIdx]
outVal := output.GetAttribute("value").Value()
if !outVal.Type().Equals(cty.String) {
return false
}
modName := filepath.Base(node.modulePath)
if outVal.AsString() != node.prefix+modName {
return false
}
return node.modulePath == child.ModulePath()
})
if !assert.NotEqualf(t, -1, idx, "module prefix=%s path=%s not found in %s", n.prefix, child.ModulePath(), n.modulePath) {
continue
}
assertChild(t, n.children[idx], child)
}
})
}
assertChild(t, expectedTree, root)
// If any module was not asserted, the test will fail. This ensures the
// entire module tree is checked.
require.Equal(t, 0, modSet.Size(), "all modules asserted")
}
func TestCyclicModules(t *testing.T) {