github.com/mponton/terratest@v0.44.0/modules/terraform/plan_struct.go (about) 1 package terraform 2 3 import ( 4 "encoding/json" 5 6 tfjson "github.com/hashicorp/terraform-json" 7 "github.com/mponton/terratest/modules/testing" 8 "github.com/stretchr/testify/assert" 9 "github.com/stretchr/testify/require" 10 ) 11 12 // PlanStruct is a Go Struct representation of the plan object returned from Terraform (after running `terraform show`). 13 // Unlike the raw plan representation returned by terraform-json, this struct provides a map that maps the resource 14 // addresses to the changes and planned values to make it easier to navigate the raw plan struct. 15 type PlanStruct struct { 16 // The raw representation of the plan. See 17 // https://www.terraform.io/docs/internals/json-format.html#plan-representation for details on the structure of the 18 // plan output. 19 RawPlan tfjson.Plan 20 21 // A map that maps full resource addresses (e.g., module.foo.null_resource.test) to the planned values of that 22 // resource. 23 ResourcePlannedValuesMap map[string]*tfjson.StateResource 24 25 // A map that maps full resource addresses (e.g., module.foo.null_resource.test) to the planned actions terraform 26 // will take on that resource. 27 ResourceChangesMap map[string]*tfjson.ResourceChange 28 } 29 30 // parsePlanJson takes in the json string representation of the terraform plan and returns a go struct representation 31 // for easy introspection. 32 func parsePlanJson(jsonStr string) (*PlanStruct, error) { 33 plan := &PlanStruct{} 34 35 if err := json.Unmarshal([]byte(jsonStr), &plan.RawPlan); err != nil { 36 return nil, err 37 } 38 39 plan.ResourcePlannedValuesMap = parsePlannedValues(plan) 40 plan.ResourceChangesMap = parseResourceChanges(plan) 41 return plan, nil 42 } 43 44 // parseResourceChanges takes a plan and returns a map that maps resource addresses to the planned changes for that 45 // resource. If there are no changes, this returns an empty map instead of erroring. 46 func parseResourceChanges(plan *PlanStruct) map[string]*tfjson.ResourceChange { 47 out := map[string]*tfjson.ResourceChange{} 48 for _, change := range plan.RawPlan.ResourceChanges { 49 out[change.Address] = change 50 } 51 return out 52 } 53 54 // parsePlannedValues takes a plan and walks through the planned values to return a map that maps the full resource 55 // addresses to the planned resources. If there are no planned values, this returns an empty map instead of erroring. 56 func parsePlannedValues(plan *PlanStruct) map[string]*tfjson.StateResource { 57 plannedValues := plan.RawPlan.PlannedValues 58 if plannedValues == nil { 59 // No planned values, so return empty map. 60 return map[string]*tfjson.StateResource{} 61 } 62 63 rootModule := plannedValues.RootModule 64 if rootModule == nil { 65 // No module resources, so return empty map. 66 return map[string]*tfjson.StateResource{} 67 } 68 return parseModulePlannedValues(rootModule) 69 } 70 71 // parseModulePlannedValues will recursively walk through the modules in the planned_values of the plan struct to 72 // construct a map that maps the full resource addresses to the planned resource. 73 func parseModulePlannedValues(module *tfjson.StateModule) map[string]*tfjson.StateResource { 74 out := map[string]*tfjson.StateResource{} 75 for _, resource := range module.Resources { 76 // NOTE: the Address attribute of the module resource always returns the full address, even when the resource is 77 // nested within sub modules. 78 out[resource.Address] = resource 79 } 80 81 // NOTE: base case of recursion is when ChildModules is empty list. 82 for _, child := range module.ChildModules { 83 // Recurse in to the child module. We take a recursive approach here despite limitations of the recursion stack 84 // in golang due to the fact that it is rare to have heavily deep module calls in Terraform. So we optimize for 85 // code readability as opposed to performance. 86 childMap := parseModulePlannedValues(child) 87 for k, v := range childMap { 88 out[k] = v 89 } 90 } 91 return out 92 } 93 94 // AssertPlannedValuesMapKeyExists checks if the given key exists in the map, failing the test if it does not. 95 func AssertPlannedValuesMapKeyExists(t testing.TestingT, plan *PlanStruct, keyQuery string) { 96 _, hasKey := plan.ResourcePlannedValuesMap[keyQuery] 97 assert.Truef(t, hasKey, "Given planned values map does not have key %s", keyQuery) 98 } 99 100 // RequirePlannedValuesMapKeyExists checks if the given key exists in the map, failing and halting the test if it does not. 101 func RequirePlannedValuesMapKeyExists(t testing.TestingT, plan *PlanStruct, keyQuery string) { 102 _, hasKey := plan.ResourcePlannedValuesMap[keyQuery] 103 require.Truef(t, hasKey, "Given planned values map does not have key %s", keyQuery) 104 } 105 106 // AssertResourceChangesMapKeyExists checks if the given key exists in the map, failing the test if it does not. 107 func AssertResourceChangesMapKeyExists(t testing.TestingT, plan *PlanStruct, keyQuery string) { 108 _, hasKey := plan.ResourceChangesMap[keyQuery] 109 assert.Truef(t, hasKey, "Given resource changes map does not have key %s", keyQuery) 110 } 111 112 // RequireResourceChangesMapKeyExists checks if the given key exists in the map, failing the test if it does not. 113 func RequireResourceChangesMapKeyExists(t testing.TestingT, plan *PlanStruct, keyQuery string) { 114 _, hasKey := plan.ResourceChangesMap[keyQuery] 115 require.Truef(t, hasKey, "Given resource changes map does not have key %s", keyQuery) 116 }