diff --git a/pkg/iac/scanners/terraform/parser/parser.go b/pkg/iac/scanners/terraform/parser/parser.go index 3427fa08f72f..d47e241b93c6 100644 --- a/pkg/iac/scanners/terraform/parser/parser.go +++ b/pkg/iac/scanners/terraform/parser/parser.go @@ -83,6 +83,7 @@ func (p *Parser) newModuleParser(moduleFS fs.FS, moduleSource, modulePath, modul mp.logger = log.WithPrefix("terraform parser").With("module", moduleName) mp.projectRoot = p.projectRoot mp.skipPaths = p.skipPaths + mp.options = p.options p.children = append(p.children, mp) for _, option := range p.options { option(mp) diff --git a/pkg/iac/scanners/terraform/parser/parser_test.go b/pkg/iac/scanners/terraform/parser/parser_test.go index e7ffa3230be3..08267c38dd3e 100644 --- a/pkg/iac/scanners/terraform/parser/parser_test.go +++ b/pkg/iac/scanners/terraform/parser/parser_test.go @@ -1648,9 +1648,10 @@ func TestNestedDynamicBlock(t *testing.T) { assert.Len(t, nested, 4) } -func parse(t *testing.T, files map[string]string) terraform.Modules { +func parse(t *testing.T, files map[string]string, opts ...Option) terraform.Modules { fs := testutil.CreateFS(t, files) - parser := New(fs, "", OptionStopOnHCLError(true)) + opts = append(opts, OptionStopOnHCLError(true)) + parser := New(fs, "", opts...) require.NoError(t, parser.ParseFS(context.TODO(), ".")) modules, _, err := parser.EvaluateAll(context.TODO()) @@ -1702,6 +1703,74 @@ resource "test_resource" "this" { assert.Equal(t, "test_value", attr.GetRawValue()) } +// TestNestedModulesOptions ensures parser options are carried to the nested +// submodule evaluators. +// The test will include an invalid module that will fail to download +// if it is attempted. +func TestNestedModulesOptions(t *testing.T) { + // reset the previous default logger + prevLog := slog.Default() + defer slog.SetDefault(prevLog) + var buf bytes.Buffer + slog.SetDefault(slog.New(log.NewHandler(&buf, nil))) + + files := map[string]string{ + "main.tf": ` +module "city" { + source = "./modules/city" +} + +resource "city" "neighborhoods" { + names = module.city.neighborhoods +} +`, + "modules/city/main.tf": ` +module "brooklyn" { + source = "./brooklyn" +} + +module "queens" { + source = "./queens" +} + +output "neighborhoods" { + value = [module.brooklyn.name, module.queens.name] +} +`, + "modules/city/brooklyn/main.tf": ` +output "name" { + value = "Brooklyn" +} +`, + "modules/city/queens/main.tf": ` +output "name" { + value = "Queens" +} + +module "invalid" { + source = "https://example.invaliddomain" +} +`, + } + + // Using the OptionWithDownloads(false) option will prevent the invalid + // module from being downloaded. If the log exists "failed to download" + // then the submodule evaluator attempted to download, which was disallowed. + modules := parse(t, files, OptionWithDownloads(false)) + require.Len(t, modules, 4) + + resources := modules.GetResourcesByType("city") + require.Len(t, resources, 1) + + for _, res := range resources { + attr, _ := res.GetNestedAttribute("names") + require.NotNil(t, attr, res.FullName()) + assert.Equal(t, []string{"Brooklyn", "Queens"}, attr.GetRawValue()) + } + + require.NotContains(t, buf.String(), "failed to download") +} + func TestCyclicModules(t *testing.T) { files := map[string]string{ "main.tf": `