Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for validating Output Changes #1459

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions modules/terraform/plan_struct.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ type PlanStruct struct {
// A map that maps full resource addresses (e.g., module.foo.null_resource.test) to the planned actions terraform
// will take on that resource.
ResourceChangesMap map[string]*tfjson.ResourceChange

// A map that maps the output name to the planned values of that output
OutputChangesMap map[string]*tfjson.Change
}

// ParsePlanJSON takes in the json string representation of the terraform plan and returns a go struct representation
Expand All @@ -36,11 +39,22 @@ func ParsePlanJSON(jsonStr string) (*PlanStruct, error) {
return nil, err
}

plan.OutputChangesMap = parseOutputChanges(plan)
plan.ResourcePlannedValuesMap = parsePlannedValues(plan)
plan.ResourceChangesMap = parseResourceChanges(plan)
return plan, nil
}

// parseOutputChanges takes a plan and returns a maps that maps output names to the planned changes for that output.
// If there are no changes, this returns an empty map instead of erroring
func parseOutputChanges(plan *PlanStruct) map[string]*tfjson.Change {
out := map[string]*tfjson.Change{}
for outputName, change := range plan.RawPlan.OutputChanges {
out[outputName] = change
}
return out
}

// parseResourceChanges takes a plan and returns a map that maps resource addresses to the planned changes for that
// resource. If there are no changes, this returns an empty map instead of erroring.
func parseResourceChanges(plan *PlanStruct) map[string]*tfjson.ResourceChange {
Expand Down Expand Up @@ -91,6 +105,18 @@ func parseModulePlannedValues(module *tfjson.StateModule) map[string]*tfjson.Sta
return out
}

// AssertOutputChangesMapKeyExists checks if the given key exists in the map, failing the test if it does not.
func AssertOutputChangesMapKeyExists(t testing.TestingT, plan *PlanStruct, keyQuery string) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AssertOutputChangesMapKeyExists - seems to be unused, can be added a test for it?

_, hasKey := plan.OutputChangesMap[keyQuery]
assert.Truef(t, hasKey, "Given output changes map does not have key %s", keyQuery)
}

// RequireOutputChangesMapKeyExists checks if the given key exists in the map, failing and halting the test if it does not.
func RequireOutputChangesMapKeyExists(t testing.TestingT, plan *PlanStruct, keyQuery string) {
_, hasKey := plan.OutputChangesMap[keyQuery]
require.Truef(t, hasKey, "Given output changes map does not have key %s", keyQuery)
}

// AssertPlannedValuesMapKeyExists checks if the given key exists in the map, failing the test if it does not.
func AssertPlannedValuesMapKeyExists(t testing.TestingT, plan *PlanStruct, keyQuery string) {
_, hasKey := plan.ResourcePlannedValuesMap[keyQuery]
Expand Down
46 changes: 40 additions & 6 deletions modules/terraform/plan_struct_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"testing"

http_helper "github.com/gruntwork-io/terratest/modules/http-helper"
tfjson "github.com/hashicorp/terraform-json"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand All @@ -12,17 +13,23 @@ const (
// NOTE: We pull down the json files from github during test runtime as opposed to checking it in as these source
// files are licensed under MPL and we want to avoid a dual license scenario where some source files in terratest
// are licensed under a different license.
basicJsonUrl = "https://raw.githubusercontent.com/hashicorp/terraform-json/v0.8.0/testdata/basic/plan.json"
deepModuleJsonUrl = "https://raw.githubusercontent.com/hashicorp/terraform-json/v0.8.0/testdata/deep_module/plan.json"
basicJsonUrl = "https://raw.githubusercontent.com/hashicorp/terraform-json/v0.13.0/testdata/basic/plan.json"
deepModuleJsonUrl = "https://raw.githubusercontent.com/hashicorp/terraform-json/v0.13.0/testdata/deep_module/plan.json"

changesJsonUrl = "https://raw.githubusercontent.com/hashicorp/terraform-json/v0.8.0/testdata/has_changes/plan.json"
changesJsonUrl = "https://raw.githubusercontent.com/hashicorp/terraform-json/v0.13.0/testdata/has_changes/plan.json"
)

func validateHTTPSuccess(statusCode int) bool {
return statusCode >= 200 && statusCode < 300
}

func TestPlannedValuesMapWithBasicJson(t *testing.T) {
t.Parallel()

// Retrieve test data from the terraform-json project.
_, jsonData := http_helper.HttpGet(t, basicJsonUrl, nil)
statusCode, jsonData := http_helper.HttpGet(t, basicJsonUrl, nil)
require.True(t, validateHTTPSuccess(statusCode))

plan, err := ParsePlanJSON(jsonData)
require.NoError(t, err)

Expand All @@ -47,7 +54,9 @@ func TestPlannedValuesMapWithDeepModuleJson(t *testing.T) {
t.Parallel()

// Retrieve test data from the terraform-json project.
_, jsonData := http_helper.HttpGet(t, deepModuleJsonUrl, nil)
statusCode, jsonData := http_helper.HttpGet(t, deepModuleJsonUrl, nil)
require.True(t, validateHTTPSuccess(statusCode))

plan, err := ParsePlanJSON(jsonData)
require.NoError(t, err)

Expand All @@ -63,7 +72,9 @@ func TestResourceChangesJson(t *testing.T) {
t.Parallel()

// Retrieve test data from the terraform-json project.
_, jsonData := http_helper.HttpGet(t, changesJsonUrl, nil)
statusCode, jsonData := http_helper.HttpGet(t, changesJsonUrl, nil)
require.True(t, validateHTTPSuccess(statusCode))

plan, err := ParsePlanJSON(jsonData)
require.NoError(t, err)

Expand All @@ -77,5 +88,28 @@ func TestResourceChangesJson(t *testing.T) {
barChanges := plan.ResourceChangesMap["null_resource.bar"]
require.NotNil(t, barChanges.Change)
assert.Equal(t, barChanges.Change.After.(map[string]interface{})["triggers"].(map[string]interface{})["foo_id"].(string), "424881806176056736")
}

func TestOutputChangesJson(t *testing.T) {
t.Parallel()

// Retrieve test data from the terraform-json project.
statusCode, jsonData := http_helper.HttpGet(t, changesJsonUrl, nil)
require.True(t, validateHTTPSuccess(statusCode))

plan, err := ParsePlanJSON(jsonData)
require.NoError(t, err)

// Spot check a few changes to make sure the right address was registered
RequireOutputChangesMapKeyExists(t, plan, "foo")
fooChanges := plan.OutputChangesMap["foo"]
require.NotNil(t, fooChanges)
assert.Equal(t, fooChanges.Actions, tfjson.Actions{"create"})
assert.Equal(t, fooChanges.After, "bar")

AssertOutputChangesMapKeyExists(t, plan, "map")
mapChanges := plan.OutputChangesMap["map"]
require.NotNil(t, mapChanges)
assert.Equal(t, mapChanges.Actions, tfjson.Actions{"create"})
assert.Equal(t, mapChanges.After.(map[string]interface{}), map[string]interface{}{"foo": "bar", "number": float64(42)})
}