Repository: GoogleCloudPlatform/terraform-validator
Pull request #111 (2019-11-26 20:49:04)

support nested child modules
This fixes https://github.com/GoogleCloudPlatform/terraform-validator/issues/110


Patch:
diff --git a/tfplan/json_plan.go b/tfplan/json_plan.go
index e35c23f2..37430758 100644
--- a/tfplan/json_plan.go
+++ b/tfplan/json_plan.go
@@ -16,6 +16,7 @@ package tfplan
 import (
 	"encoding/json"
 	"fmt"
+	"log"
 	"strconv"
 	"strings"
 
@@ -23,28 +24,33 @@ import (
 	"github.com/pkg/errors"
 )
 
+const maxChildModuleLevel = 10000
+
 // jsonPlan structure used to parse Terraform 12 plan exported in json format by 'terraform show -json ./binary_plan.tfplan' command.
 type jsonPlan struct {
 	PlannedValues struct {
 		RootModules struct {
-			Resources    []jsonResource
-			ChildModules []struct {
-				Address   string
-				Resources []jsonResource
-			} `json:"child_modules"`
+			Resources    []jsonResource `json:"resources"`
+			ChildModules []childModule  `json:"child_modules"`
 		} `json:"root_module"`
 	} `json:"planned_values"`
 }
 
+type childModule struct {
+	Address      string         `json:"address"`
+	Resources    []jsonResource `json:"resources"`
+	ChildModules []childModule  `json:"child_modules"`
+}
+
 // jsonResource represent single Terraform resource definition.
 type jsonResource struct {
-	Module       string
-	Name         string
-	Address      string
-	Mode         string
-	Kind         string `json:"type"`
-	ProviderName string `json:"provider_name"`
-	Values       map[string]interface{}
+	Module       string                 `json:"module"`
+	Name         string                 `json:"name"`
+	Address      string                 `json:"address"`
+	Mode         string                 `json:"mode"`
+	Kind         string                 `json:"type"`
+	ProviderName string                 `json:"provider_name"`
+	Values       map[string]interface{} `json:"values"`
 }
 
 // ComposeTF12Resources inspects a plan and returns the planned resources that match the provided resource schema map.
@@ -54,7 +60,6 @@ func ComposeTF12Resources(data []byte, schemas map[string]*schema.Resource) ([]R
 	if err != nil {
 		return nil, errors.Wrap(err, "read resources")
 	}
-
 	var instances []Resource
 	for _, r := range resources {
 		sch, ok := schemas[r.Kind]
@@ -167,25 +172,37 @@ func attributes(value interface{}, address []string, res map[string]string, sche
 func readJSONResources(data []byte) ([]jsonResource, error) {
 	plan := jsonPlan{}
 	err := json.Unmarshal(data, &plan)
-
 	if err != nil {
 		return nil, err
 	}
-
 	var result []jsonResource
-
 	for _, resource := range plan.PlannedValues.RootModules.Resources {
 		resource.Module = "root"
 		result = append(result, resource)
 	}
-
 	for _, module := range plan.PlannedValues.RootModules.ChildModules {
-		name := strings.SplitAfterN(module.Address, ".", 2)[1]
-		for _, resource := range module.Resources {
-			resource.Module = name
-			result = append(result, resource)
-		}
+		result = append(result, resourcesFromModule(&module, 0)...)
 	}
-
 	return result, nil
 }
+
+func resourcesFromModule(module *childModule, level int) []jsonResource {
+	if level > maxChildModuleLevel {
+		log.Printf("The configuration has more than %d level of modules. Modules with a depth more than %d will be ignored.", maxChildModuleLevel, maxChildModuleLevel)
+		return nil
+	}
+	pcs := strings.SplitAfterN(module.Address, ".", 2)
+	if len(pcs) < 2 {
+		return nil
+	}
+	name := pcs[1]
+	var result []jsonResource
+	for _, resource := range module.Resources {
+		resource.Module = name
+		result = append(result, resource)
+	}
+	for _, c := range module.ChildModules {
+		result = append(result, resourcesFromModule(&c, level+1)...)
+	}
+	return result
+}


Test patch:
diff --git a/tfplan/json_plan_test.go b/tfplan/json_plan_test.go
new file mode 100644
index 00000000..1e158a71
--- /dev/null
+++ b/tfplan/json_plan_test.go
@@ -0,0 +1,70 @@
+package tfplan
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestReadJSONResources(t *testing.T) {
+	data := []byte(`
+{
+  "planned_values": {
+    "root_module": {
+      "child_modules": [
+        {
+          "address": "module.foo",
+          "resources": [
+            {
+              "address": "module.foo.google_compute_instance.quz1"
+            }
+          ],
+          "child_modules": [
+            {
+              "address": "module.foo.bar",
+              "resources": [
+                {
+                  "address": "module.foo.bar.google_compute_instance.quz2"
+                }
+              ]
+            }
+          ]
+        }
+      ]
+    }
+  }
+}
+`)
+	wantJSON := []byte(`
+[
+  {
+    "address": "module.foo.google_compute_instance.quz1",
+    "mode": "",
+    "module": "foo",
+    "name": "",
+    "provider_name": "",
+    "type": "",
+    "values": null
+  },
+  {
+    "address": "module.foo.bar.google_compute_instance.quz2",
+    "mode": "",
+    "module": "foo.bar",
+    "name": "",
+    "provider_name": "",
+    "type": "",
+    "values": null
+  }
+]
+`)
+	got, err := readJSONResources(data)
+	if err != nil {
+		t.Fatalf("got error: %v", err)
+	}
+	gotJSON, err := json.Marshal(got)
+	if err != nil {
+		t.Fatalf("marshaling: %v", err)
+	}
+	require.JSONEq(t, string(wantJSON), string(gotJSON))
+}