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  }