From f3a7921444f46b582bc3688fb7ae5bbfd4c82563 Mon Sep 17 00:00:00 2001 From: Martijn van Schaardenburg Date: Tue, 7 Oct 2025 18:44:05 +0000 Subject: [PATCH 1/6] feat: Support for dispatching to isolated projects/service accounts --- cli/bpmetadata/tfconfig.go | 11 +- infra/blueprint-test/go.mod | 2 +- infra/blueprint-test/pkg/tft/module_walk.go | 189 ++++++++++++++++++ .../pkg/tft/module_walk_test.go | 86 ++++++++ infra/blueprint-test/pkg/tft/terraform.go | 108 +++++++++- 5 files changed, 386 insertions(+), 10 deletions(-) create mode 100644 infra/blueprint-test/pkg/tft/module_walk.go create mode 100644 infra/blueprint-test/pkg/tft/module_walk_test.go diff --git a/cli/bpmetadata/tfconfig.go b/cli/bpmetadata/tfconfig.go index 7cf1e452c85..56ac12c5803 100644 --- a/cli/bpmetadata/tfconfig.go +++ b/cli/bpmetadata/tfconfig.go @@ -22,11 +22,10 @@ import ( ) const ( - versionRegEx = "/v([0-9]+[.0-9]*)$" - modulePattern = `(?:^|/)modules/([^/]+)` - perModuleRoles = "per_module_roles" + versionRegEx = "/v([0-9]+[.0-9]*)$" + modulePattern = `(?:^|/)modules/([^/]+)` + perModuleRoles = "per_module_roles" perModuleServices = "per_module_services" - rootModuleName = "root" ) type blueprintVersion struct { @@ -656,14 +655,14 @@ func generateTFState(bpPath string) ([]byte, error) { func parseBpModuleName(bpPath string, blueprintRoot string) string { relPath, err := filepath.Rel(blueprintRoot, bpPath) if err != nil { - return rootModuleName + return tft.RootModuleName } matches := regexp.MustCompile(modulePattern).FindStringSubmatch(relPath) if len(matches) == 2 { return matches[1] } - return rootModuleName + return tft.RootModuleName } func extractModuleLocalList(file *hcl.File, localKey string, moduleName string) ([]string, error) { diff --git a/infra/blueprint-test/go.mod b/infra/blueprint-test/go.mod index 4472b6c4ece..47a02a805d5 100644 --- a/infra/blueprint-test/go.mod +++ b/infra/blueprint-test/go.mod @@ -5,6 +5,7 @@ go 1.23.0 require ( github.com/GoogleContainerTools/kpt-functions-sdk/go/api v0.0.0-20250702115044-9bfead305c54 github.com/alexflint/go-filemutex v1.3.0 + github.com/google/go-cmp v0.7.0 github.com/gruntwork-io/terratest v0.50.0 github.com/hashicorp/terraform-config-inspect v0.0.0-20250731202709-e8a84eebd3e7 github.com/hashicorp/terraform-json v0.26.0 @@ -73,7 +74,6 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.9 // indirect - github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect diff --git a/infra/blueprint-test/pkg/tft/module_walk.go b/infra/blueprint-test/pkg/tft/module_walk.go new file mode 100644 index 00000000000..5efc722833b --- /dev/null +++ b/infra/blueprint-test/pkg/tft/module_walk.go @@ -0,0 +1,189 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package tft + +import ( + "fmt" + "maps" + "path/filepath" + "regexp" + "strings" + + "github.com/hashicorp/terraform-config-inspect/tfconfig" +) + +const ( + RootModuleName = "root" +) + +var ( + rootModuleRegexp = regexp.MustCompile("^[./]*$") + modulesDirRegexp = regexp.MustCompile("^[./]*/modules/(.+)$") +) + +// findModulesUnderTest does a graph search starting from tfDir, looking +// for any transitively referenced modules that are considered "modules under test", +// which are modules in the "modules/" dir or the root module +// (the exact definition is in isModuleUnderTest() above). +// +// Any external modules encountered are ignored. +// +// The search doesn't continue to unpack any module under test, so for example, if +// modules/aaa sources modules/bbb, then only modules/aaa will be in the returned +// set (unless modules/bbb is reachable from tfDir without going through modules/aaa). +func findModulesUnderTest(tfDir string) (stringSet, error) { + tfDir = filepath.Clean(tfDir) + + modulesUnderTest := make(stringSet) + pathsToVisit := stringSet{tfDir: struct{}{}} + seen := make(stringSet) + + maxIters := 5 + for iterations := 0; iterations < maxIters; iterations++ { + // moduleRefs is a map from filesystem path to a set of modules sourced from there. + moduleRefs, err := findAllReferencedModules(pathsToVisit) + if err != nil { + return nil, err + } + + maps.Insert(seen, maps.All(pathsToVisit)) + + for _, refs := range moduleRefs { + for ref, _ := range refs { + if isModuleUnderTest(ref) { + name, err := localModuleName(ref) + if err != nil { + return nil, err + } + modulesUnderTest[name] = struct{}{} + } + } + } + + pathsToVisit = stripAlreadySeen(nextPathsToVisit(moduleRefs), seen) + + if len(pathsToVisit) == 0 { + return modulesUnderTest, nil + } + } + + return nil, fmt.Errorf("Exceeded %v iterations when searching for referenced modules starting from %q, pathsToVisit is currently %v", maxIters, tfDir, pathsToVisit) +} + +// localModuleName takes a local module source string (e.g. ../../modules/my_module) +// and extracts the short module name used to refer to the module in test/setup/*.tf +// when per_module_{roles,services} are defined (e.g. my_module). +// +// This function returns an error if the input is not a local module source +// (e.g. "terraform-google-modules/network/google"). +func localModuleName(moduleRef string) (string, error) { + if rootModuleRegexp.MatchString(moduleRef) { + return RootModuleName, nil + } + + matches := modulesDirRegexp.FindStringSubmatch(moduleRef) + if len(matches) < 2 { + // modulesDirRegexp couldn't find a match even though this function is supposed to + // be called with local module references only. + return "", fmt.Errorf("Couldn't extract module name from source %q using regexp %v", moduleRef, modulesDirRegexp) + } + return matches[1], nil +} + +// isLocalModule uses terraform's definition of what a local module source looks like. +func isLocalModule(moduleRef string) bool { + return strings.HasPrefix(moduleRef, "../") || + strings.HasPrefix(moduleRef, "./") || + strings.HasPrefix(moduleRef, "/") +} + +// isModuleUnderTest looks at the given module source string and returns true +// if it is the root module of this repo, or one of the modules inside the "modules/" dir. +// This is done by just checking whether the moduleRef is a local filesystem path leading +// up to the root and then potentially into the "modules/" dir. +func isModuleUnderTest(moduleRef string) bool { + return isLocalModule(moduleRef) && (rootModuleRegexp.MatchString(moduleRef) || modulesDirRegexp.MatchString(moduleRef)) +} + +type stringSet = map[string]struct{} + +// nextPathsToVisit looks at the given mapping of +// (module path) => (modules referenced from that path) and returns a set of +// paths to visit next. External modules and "modules under test" are excluded +// as they do not need further examination. +func nextPathsToVisit(moduleRefs map[string]stringSet) stringSet { + nextPaths := make(stringSet) + for modulePath, refs := range moduleRefs { + for ref, _ := range refs { + if isLocalModule(ref) && !isModuleUnderTest(ref) { + nextPaths[filepath.Clean(filepath.Join(modulePath, ref))] = struct{}{} + } + } + } + return nextPaths +} + +// stripAlreadySeen returns a new set that includes everything in "modulePaths" +// that is not in "seen". +func stripAlreadySeen(modulePaths stringSet, seen stringSet) stringSet { + newPaths := make(stringSet) + for path, _ := range modulePaths { + if _, ok := seen[path]; !ok { + newPaths[path] = struct{}{} + } + } + return newPaths +} + +// findAllReferencedModules takes a set of filesystem paths for terraform +// modules and returns a map from the path to a set of all modules referenced +// from the module at that path. +func findAllReferencedModules(modulePaths stringSet) (map[string]stringSet, error) { + moduleRefs := make(map[string]stringSet) + for path, _ := range modulePaths { + modules, err := findReferencedModules(path) + if err != nil { + return nil, err + } + moduleRefs[path] = modules + } + return moduleRefs, nil +} + +// findReferencedModules looks in tfDir and extracts the sources for all module +// blocks in that directory. +// The returned value is a set of module sources. Some possible examples: +// +// "../.." +// "../../modules/bar" +// "terraform-google-modules/kubernetes-engine/google" +// "terraform-google-modules/kubernetes-engine/google//modules/workload-identity" +// +// Overridden by tests. +var findReferencedModules = func(tfDir string) (stringSet, error) { + mod, diags := tfconfig.LoadModule(tfDir) + err := diags.Err() + if err != nil { + return nil, err + } + + sources := make(stringSet) + for _, moduleBlock := range mod.ModuleCalls { + sources[moduleBlock.Source] = struct{}{} + } + return sources, nil +} diff --git a/infra/blueprint-test/pkg/tft/module_walk_test.go b/infra/blueprint-test/pkg/tft/module_walk_test.go new file mode 100644 index 00000000000..a37b889d558 --- /dev/null +++ b/infra/blueprint-test/pkg/tft/module_walk_test.go @@ -0,0 +1,86 @@ +package tft + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func toSet(args ...string) stringSet { + result := make(stringSet) + for _, arg := range args { + result[arg] = struct{}{} + } + return result +} + +func TestFindModulesUnderTest(t *testing.T) { + // Monkey patch findReferencedModules() in module_walk.go + // to skip parsing real Terraform modules. + findReferencedModules = func(tfDir string) (stringSet, error) { + switch tfDir { + + // Group 1 (with external module references omitted): + // fixture1 -> [example1, example2] + // example1 -> [root, abc] + // example2 -> [xyz] + case "local/test/integration/fixture1": + return toSet("external1", "../../../examples/example1", "../../../examples/example2"), nil + case "local/examples/example1": + return toSet("../..", "../../modules/abc"), nil + case "local/examples/example2": + return toSet("../../modules/xyz", "external2"), nil + + // Group 2: fixture2 -> external_example, both point to external modules. + case "local/test/integration/fixture2": + return toSet("terraform-google-modules/network/google", "../../../examples/external_example"), nil + case "local/examples/external_example": + return toSet("terraform-google-modules/kubernetes-engine/google//modules/private-cluster"), nil + + default: + return nil, fmt.Errorf("no fake behavior configured for module path %q", tfDir) + } + } + + tests := []struct { + startPath string + want stringSet + }{ + // Group 1. + { + startPath: "local/test/integration/fixture1", + want: toSet("root", "abc", "xyz"), + }, + { + startPath: "local/examples/example1", + want: toSet("root", "abc"), + }, + { + startPath: "local/examples/example2", + want: toSet("xyz"), + }, + + // Group 2: no modules under test found. + { + startPath: "local/test/integration/fixture2", + want: toSet(), + }, + { + startPath: "local/examples/external_example", + want: toSet(), + }, + } + for _, tt := range tests { + t.Run(filepath.Base(tt.startPath), func(t *testing.T) { + got, err := findModulesUnderTest(tt.startPath) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("findModulesUnderTest(%q) returned unexpected result (-want +got):\n%s", tt.startPath, diff) + } + }) + } +} diff --git a/infra/blueprint-test/pkg/tft/terraform.go b/infra/blueprint-test/pkg/tft/terraform.go index dadd8484267..f6979ca8c6c 100644 --- a/infra/blueprint-test/pkg/tft/terraform.go +++ b/infra/blueprint-test/pkg/tft/terraform.go @@ -20,9 +20,11 @@ package tft import ( b64 "encoding/base64" "fmt" + "maps" "os" "path" "path/filepath" + "slices" "strings" gotest "testing" "time" @@ -41,6 +43,11 @@ import ( const ( setupKeyOutputName = "sa_key" + setupKeyMapOutputName = "sa_keys_per_module" + + setupProjectOutputName = "project_id" + setupProjectMapOutputName = "project_ids_per_module" + tftCacheMutexFilename = "bpt-tft-cache.lock" planFilename = "plan.tfplan" ) @@ -244,9 +251,33 @@ func NewTFBlueprintTest(t testing.TB, opts ...tftOption) *TFBlueprintTest { gcloud.ActivateCredsAndEnvVars(tft.t, tft.saKey) } // load TFEnvVars from setup outputs + if tft.setupOutputOverrides == nil { + tft.setupOutputOverrides = make(map[string]interface{}) + } if tft.setupDir != "" { tft.logger.Logf(tft.t, "Loading env vars from setup %s", tft.setupDir) outputs := tft.getOutputs(tft.sensitiveOutputs(tft.setupDir)) + + modulesUnderTest, err := findModulesUnderTest(tft.tfDir) + if err != nil { + t.Fatal(err) + } + + // Pick a specific project_id and sa_key from project_ids_per_module and sa_keys_per_module + // if available. + overrides, err := resolveProjectAndKey(outputs, modulesUnderTest) + if err != nil { + t.Fatalf("Problem looking up overrides for project_id and sa_key from setup outputs: %v", err) + } + for k, v := range overrides { + tft.logger.Logf(tft.t, "Overriding var %q from per-module isolation settings", k) + outputs[k] = v + tft.setupOutputOverrides[k] = v + } + if _, hasProjectID := outputs[setupProjectMapOutputName]; hasProjectID && len(modulesUnderTest) == 0 { + tft.logger.Logf(tft.t, `*** No local modules were transitively referenced from %q (did you forget to run module-swapper?) and no default project_id output var is available. A later "failed to find project_id" error is likely. ***`, tft.tfDir) + } + loadTFEnvVar(tft.tfEnvVars, tft.getTFOutputsAsInputs(outputs)) if credsEnc, exists := tft.tfEnvVars[fmt.Sprintf("TF_VAR_%s", setupKeyOutputName)]; tft.saKey == "" && exists { if credDec, err := b64.StdEncoding.DecodeString(credsEnc); err == nil { @@ -260,9 +291,6 @@ func NewTFBlueprintTest(t testing.TB, opts ...tftOption) *TFBlueprintTest { } // Load env vars to supplement/override setup tft.logger.Logf(tft.t, "Loading setup from environment") - if tft.setupOutputOverrides == nil { - tft.setupOutputOverrides = make(map[string]interface{}) - } for k, v := range extractFromEnv("CFT_SETUP_") { tft.setupOutputOverrides[k] = v } @@ -452,6 +480,80 @@ func (b *TFBlueprintTest) GetTFSetupJsonOutput(key string) gjson.Result { return gjson.Parse(jsonString) } +// fetchRelevantFromOutputs looks in the given map of terraform outputs for +// outputs[key][subkey]. It returns: +// +// key_present, item_if_present, error +// +// where it is considered OK for `key` to be missing from `outputs`, resulting +// a return value of: +// +// false, "", nil +// +// But it is considered an error if `subkey` is missing from `outputs[key]` or +// if outputs[key][subkey] is not a string. +func fetchRelevantFromOutputs(outputs map[string]interface{}, key string, subkey string) (bool, string, error) { + val, found := outputs[key] + if !found { + return false, "", nil + } + + subMap, ok := val.(map[string]interface{}) + if !ok { + return false, "", fmt.Errorf("wrong data type for %q, expected map[string]interface, got %T", key, val) + } + + relevantItem, ok := subMap[subkey] + if !ok { + return false, "", fmt.Errorf("could not find key %q in map %q, which had keys %v", subkey, key, slices.Collect(maps.Keys(subMap))) + } + relevantItemStr, ok := relevantItem.(string) + if !ok { + return false, "", fmt.Errorf("value for key %q in map %q, had type %T, expected string", subkey, key, relevantItem) + } + return true, relevantItemStr, nil +} + +// resolveProjectAndKey picks a specific project ID and service account key +// to use, given the full map of the test setup outputs and a set of modules +// under test referenced by the current tfDir. +// +// The test setup outputs are taken in as an argument and are not modified. +// Instead, a map of overriding outputs is returned. +// +// If the modulesUnderTest argument contains only one module, then +// project_ids_per_module and sa_keys_per_module are resolved to the +// relevant project ID and key and the relevant ones are returned in the overrides map. +func resolveProjectAndKey(outputs map[string]interface{}, modulesUnderTest stringSet) (map[string]interface{}, error) { + overrides := make(map[string]interface{}) + + if len(modulesUnderTest) != 1 { + return overrides, nil + } + loneModuleName := "" + for module, _ := range modulesUnderTest { + loneModuleName = module + break + } + + foundProjectIDMap, relevantProjectID, err := fetchRelevantFromOutputs(outputs, setupProjectMapOutputName, loneModuleName) + if err != nil { + return nil, err + } + if foundProjectIDMap { + overrides[setupProjectOutputName] = relevantProjectID + } + + foundKeyMap, relevantKey, err := fetchRelevantFromOutputs(outputs, setupKeyMapOutputName, loneModuleName) + if err != nil { + return nil, err + } + if foundKeyMap { + overrides[setupKeyOutputName] = relevantKey + } + return overrides, nil +} + // loadTFEnvVar adds new env variables prefixed with TF_VAR_ to an existing map of variables. func loadTFEnvVar(m map[string]string, new map[string]string) { for k, v := range new { From ab71babf4aaad786a35435dd07ebe9133511b62a Mon Sep 17 00:00:00 2001 From: Martijn van Schaardenburg Date: Tue, 7 Oct 2025 19:04:14 +0000 Subject: [PATCH 2/6] revert CLI changes due to RootModuleName not being visible yet --- cli/bpmetadata/tfconfig.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/cli/bpmetadata/tfconfig.go b/cli/bpmetadata/tfconfig.go index 56ac12c5803..7cf1e452c85 100644 --- a/cli/bpmetadata/tfconfig.go +++ b/cli/bpmetadata/tfconfig.go @@ -22,10 +22,11 @@ import ( ) const ( - versionRegEx = "/v([0-9]+[.0-9]*)$" - modulePattern = `(?:^|/)modules/([^/]+)` - perModuleRoles = "per_module_roles" + versionRegEx = "/v([0-9]+[.0-9]*)$" + modulePattern = `(?:^|/)modules/([^/]+)` + perModuleRoles = "per_module_roles" perModuleServices = "per_module_services" + rootModuleName = "root" ) type blueprintVersion struct { @@ -655,14 +656,14 @@ func generateTFState(bpPath string) ([]byte, error) { func parseBpModuleName(bpPath string, blueprintRoot string) string { relPath, err := filepath.Rel(blueprintRoot, bpPath) if err != nil { - return tft.RootModuleName + return rootModuleName } matches := regexp.MustCompile(modulePattern).FindStringSubmatch(relPath) if len(matches) == 2 { return matches[1] } - return tft.RootModuleName + return rootModuleName } func extractModuleLocalList(file *hcl.File, localKey string, moduleName string) ([]string, error) { From 5fde31cafe76e477ca0a96d2b2908936df9d49c1 Mon Sep 17 00:00:00 2001 From: Martijn van Schaardenburg Date: Tue, 7 Oct 2025 19:11:53 +0000 Subject: [PATCH 3/6] make linter happy --- infra/blueprint-test/pkg/tft/module_walk.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/infra/blueprint-test/pkg/tft/module_walk.go b/infra/blueprint-test/pkg/tft/module_walk.go index 5efc722833b..6ca2d2197fa 100644 --- a/infra/blueprint-test/pkg/tft/module_walk.go +++ b/infra/blueprint-test/pkg/tft/module_walk.go @@ -63,7 +63,7 @@ func findModulesUnderTest(tfDir string) (stringSet, error) { maps.Insert(seen, maps.All(pathsToVisit)) for _, refs := range moduleRefs { - for ref, _ := range refs { + for ref := range refs { if isModuleUnderTest(ref) { name, err := localModuleName(ref) if err != nil { @@ -81,7 +81,7 @@ func findModulesUnderTest(tfDir string) (stringSet, error) { } } - return nil, fmt.Errorf("Exceeded %v iterations when searching for referenced modules starting from %q, pathsToVisit is currently %v", maxIters, tfDir, pathsToVisit) + return nil, fmt.Errorf("exceeded %v iterations when searching for referenced modules starting from %q, pathsToVisit is currently %v", maxIters, tfDir, pathsToVisit) } // localModuleName takes a local module source string (e.g. ../../modules/my_module) @@ -99,7 +99,7 @@ func localModuleName(moduleRef string) (string, error) { if len(matches) < 2 { // modulesDirRegexp couldn't find a match even though this function is supposed to // be called with local module references only. - return "", fmt.Errorf("Couldn't extract module name from source %q using regexp %v", moduleRef, modulesDirRegexp) + return "", fmt.Errorf("couldn't extract module name from source %q using regexp %v", moduleRef, modulesDirRegexp) } return matches[1], nil } @@ -128,7 +128,7 @@ type stringSet = map[string]struct{} func nextPathsToVisit(moduleRefs map[string]stringSet) stringSet { nextPaths := make(stringSet) for modulePath, refs := range moduleRefs { - for ref, _ := range refs { + for ref := range refs { if isLocalModule(ref) && !isModuleUnderTest(ref) { nextPaths[filepath.Clean(filepath.Join(modulePath, ref))] = struct{}{} } @@ -141,7 +141,7 @@ func nextPathsToVisit(moduleRefs map[string]stringSet) stringSet { // that is not in "seen". func stripAlreadySeen(modulePaths stringSet, seen stringSet) stringSet { newPaths := make(stringSet) - for path, _ := range modulePaths { + for path := range modulePaths { if _, ok := seen[path]; !ok { newPaths[path] = struct{}{} } From ed8f9359dbc94c98c6185c0b7efea78cfca191a7 Mon Sep 17 00:00:00 2001 From: Martijn van Schaardenburg Date: Tue, 7 Oct 2025 19:22:08 +0000 Subject: [PATCH 4/6] more lint, also tweak log message --- infra/blueprint-test/pkg/tft/module_walk.go | 2 +- infra/blueprint-test/pkg/tft/terraform.go | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/infra/blueprint-test/pkg/tft/module_walk.go b/infra/blueprint-test/pkg/tft/module_walk.go index 6ca2d2197fa..439eeedc793 100644 --- a/infra/blueprint-test/pkg/tft/module_walk.go +++ b/infra/blueprint-test/pkg/tft/module_walk.go @@ -154,7 +154,7 @@ func stripAlreadySeen(modulePaths stringSet, seen stringSet) stringSet { // from the module at that path. func findAllReferencedModules(modulePaths stringSet) (map[string]stringSet, error) { moduleRefs := make(map[string]stringSet) - for path, _ := range modulePaths { + for path := range modulePaths { modules, err := findReferencedModules(path) if err != nil { return nil, err diff --git a/infra/blueprint-test/pkg/tft/terraform.go b/infra/blueprint-test/pkg/tft/terraform.go index f6979ca8c6c..8f3849229f6 100644 --- a/infra/blueprint-test/pkg/tft/terraform.go +++ b/infra/blueprint-test/pkg/tft/terraform.go @@ -275,7 +275,12 @@ func NewTFBlueprintTest(t testing.TB, opts ...tftOption) *TFBlueprintTest { tft.setupOutputOverrides[k] = v } if _, hasProjectID := outputs[setupProjectMapOutputName]; hasProjectID && len(modulesUnderTest) == 0 { - tft.logger.Logf(tft.t, `*** No local modules were transitively referenced from %q (did you forget to run module-swapper?) and no default project_id output var is available. A later "failed to find project_id" error is likely. ***`, tft.tfDir) + tft.logger.Logf(tft.t, ` +*** + No local modules were transitively referenced from %q + (did you forget to run module-swapper?) and no default project_id output var is available. + A later ""project_id" is not set" error is likely. +***`, tft.tfDir) } loadTFEnvVar(tft.tfEnvVars, tft.getTFOutputsAsInputs(outputs)) @@ -531,7 +536,7 @@ func resolveProjectAndKey(outputs map[string]interface{}, modulesUnderTest strin return overrides, nil } loneModuleName := "" - for module, _ := range modulesUnderTest { + for module := range modulesUnderTest { loneModuleName = module break } From ad94f4dccedf2b85ae3b1d0fb7ef9be1895c91b3 Mon Sep 17 00:00:00 2001 From: Martijn van Schaardenburg Date: Tue, 7 Oct 2025 19:23:21 +0000 Subject: [PATCH 5/6] whitespace --- infra/blueprint-test/pkg/tft/terraform.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/infra/blueprint-test/pkg/tft/terraform.go b/infra/blueprint-test/pkg/tft/terraform.go index 8f3849229f6..6a0d59b2331 100644 --- a/infra/blueprint-test/pkg/tft/terraform.go +++ b/infra/blueprint-test/pkg/tft/terraform.go @@ -278,8 +278,8 @@ func NewTFBlueprintTest(t testing.TB, opts ...tftOption) *TFBlueprintTest { tft.logger.Logf(tft.t, ` *** No local modules were transitively referenced from %q - (did you forget to run module-swapper?) and no default project_id output var is available. - A later ""project_id" is not set" error is likely. + (did you forget to run module-swapper?) and no default project_id output var is available. + A later ""project_id" is not set" error is likely. ***`, tft.tfDir) } From 46ed8392333ac51cc219f332f7ad307a15e9cc91 Mon Sep 17 00:00:00 2001 From: Martijn van Schaardenburg Date: Wed, 8 Oct 2025 15:58:00 +0000 Subject: [PATCH 6/6] fix unit test error --- infra/blueprint-test/pkg/tft/module_walk_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infra/blueprint-test/pkg/tft/module_walk_test.go b/infra/blueprint-test/pkg/tft/module_walk_test.go index a37b889d558..c7f0361cd9c 100644 --- a/infra/blueprint-test/pkg/tft/module_walk_test.go +++ b/infra/blueprint-test/pkg/tft/module_walk_test.go @@ -19,6 +19,8 @@ func toSet(args ...string) stringSet { func TestFindModulesUnderTest(t *testing.T) { // Monkey patch findReferencedModules() in module_walk.go // to skip parsing real Terraform modules. + findOrig := findReferencedModules + defer func() { findReferencedModules = findOrig }() findReferencedModules = func(tfDir string) (stringSet, error) { switch tfDir {