Skip to content

Commit

Permalink
Resolve Expressions when it references variables in called modules (#77)
Browse files Browse the repository at this point in the history
  • Loading branch information
obierlaire authored Jun 22, 2023
1 parent 1d5b47a commit ecfacb4
Show file tree
Hide file tree
Showing 4 changed files with 245 additions and 8 deletions.
107 changes: 107 additions & 0 deletions internal/terraform/aws/default_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import (
"path/filepath"
"testing"

_ "github.com/carboniferio/carbonifer/internal/testutils"

"github.com/carboniferio/carbonifer/internal/utils"
tfjson "github.com/hashicorp/terraform-json"
"github.com/stretchr/testify/assert"
)
Expand Down Expand Up @@ -116,3 +119,107 @@ func Test_getDefaultRegion_AWSConfigFile(t *testing.T) {
assert.Equal(t, "region_from_config_file", region)

}

func Test_getDefaultRegion_ModuleOutput(t *testing.T) {
awsConfigs := &tfjson.ProviderConfig{
Name: "aws",
Expressions: map[string]*tfjson.Expression{
"region": {
ExpressionData: &tfjson.ExpressionData{
References: []string{
"module.module1.region_output",
"module.globals"},
},
},
},
}

tfPlan := &tfjson.Plan{
Config: &tfjson.Config{
RootModule: &tfjson.ConfigModule{
ModuleCalls: map[string]*tfjson.ModuleCall{
"module1": {
Module: &tfjson.ConfigModule{
Outputs: map[string]*tfjson.ConfigOutput{
"region_output": {
Expression: &tfjson.Expression{
ExpressionData: &tfjson.ExpressionData{
References: []string{"var.region"},
},
},
Description: "The AWS region to use for resources.",
Sensitive: false,
},
},
},
},
},
},
},
Variables: map[string]*tfjson.PlanVariable{
"region": {
Value: "region_from_module_output",
},
},
}

region := getDefaultRegion(awsConfigs, tfPlan)
assert.Equal(t, "region_from_module_output", region)
}

func Test_getDefaultRegion_ModuleVariable(t *testing.T) {
awsConfigs := &tfjson.ProviderConfig{
Name: "aws",
Expressions: map[string]*tfjson.Expression{
"region": {
ExpressionData: &tfjson.ExpressionData{
References: []string{"module.globals.common_region"},
},
},
},
}

tfPlan := &tfjson.Plan{
Config: &tfjson.Config{
RootModule: &tfjson.ConfigModule{
ModuleCalls: map[string]*tfjson.ModuleCall{
"globals": {
Module: &tfjson.ConfigModule{
Outputs: map[string]*tfjson.ConfigOutput{
"common_region": {
Expression: &tfjson.Expression{
ExpressionData: &tfjson.ExpressionData{
References: []string{"var.region"},
},
},
Description: "The AWS region to use for resources.",
},
},
Variables: map[string]*tfjson.ConfigVariable{
"region": {
Default: "region_module_variable",
},
},
},
},
},
},
},
}

region := getDefaultRegion(awsConfigs, tfPlan)
assert.Equal(t, "region_module_variable", region)
}

func TestGetValueOfExpression_ModuleCalls(t *testing.T) {
plan := utils.LoadPlan("test/terraform/planJson/plan_with_module_calls.json") // Replace with the path to your plan JSON
expr := &tfjson.Expression{
ExpressionData: &tfjson.ExpressionData{
References: []string{"module.module2.module1_region"},
},
}

value, err := utils.GetValueOfExpression(expr, plan)
assert.NoError(t, err)
assert.Equal(t, "region_from_module_calls", value)
}
63 changes: 55 additions & 8 deletions internal/utils/expressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,68 @@ import (
tfjson "github.com/hashicorp/terraform-json"
)

func GetValueOfExpression(expression *tfjson.Expression, tfPlan *tfjson.Plan) (interface{}, error) {
if fmt.Sprintf("%T", expression.ConstantValue) != "*tfjson.unknownConstantValue" && expression.ConstantValue != nil {
func GetValueOfExpression(expression *tfjson.Expression, tfPlan *tfjson.Plan, configModuleOptional ...*tfjson.ConfigModule) (interface{}, error) {
var rootModule *tfjson.ConfigModule
if len(configModuleOptional) > 0 {
rootModule = configModuleOptional[0]
} else {
if tfPlan.Config != nil && tfPlan.Config.RootModule != nil {
rootModule = tfPlan.Config.RootModule
}
}

if expression.ConstantValue != nil && fmt.Sprintf("%T", expression.ConstantValue) != "*tfjson.unknownConstantValue" {
// It's a known value, return it as is
return expression.ConstantValue, nil
}

// Constant value is not set or unknown, look up references
for _, reference := range expression.References {
ref := strings.TrimPrefix(reference, "var.")
if val, ok := tfPlan.Variables[ref]; ok {
return val.Value, nil
refType, ref := splitModuleReference(reference)
switch refType {
case "var":
// First, check in the plan variables
if val, ok := tfPlan.Variables[ref]; ok {
return val.Value, nil
}

// If rootModule is not nil, check in the root module and the called module variables
if rootModule != nil {
// If not found in plan variables, check in the root module variables
if moduleVariable, ok := rootModule.Variables[ref]; ok {
return moduleVariable.Default, nil
}

// If not found in root module variables, check in the called module variables
for _, moduleCall := range rootModule.ModuleCalls {
if moduleVariable, ok := moduleCall.Module.Variables[ref]; ok {
return moduleVariable.Default, nil
}
}
}
case "module":
if rootModule != nil {
moduleCall, ok := rootModule.ModuleCalls[ref]
if ok {
moduleOutput := strings.Split(reference, ".")
if len(moduleOutput) >= 3 {
outputKey := moduleOutput[2]
output, ok := moduleCall.Module.Outputs[outputKey]
if ok {
// Recursive call with the new module config
return GetValueOfExpression(output.Expression, tfPlan, moduleCall.Module)
}
}
}
}
}
}

// No variables were found
return nil, errors.New("no value found for expression")
}

func splitModuleReference(reference string) (string, string) {
parts := strings.Split(reference, ".")
if len(parts) > 1 {
return parts[0], parts[1]
}
return parts[0], ""
}
25 changes: 25 additions & 0 deletions internal/utils/plan.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package utils

import (
"encoding/json"
"os"

log "github.com/sirupsen/logrus"

tfjson "github.com/hashicorp/terraform-json"
)

func LoadPlan(planFilePath string) *tfjson.Plan {
planBytes, err := os.ReadFile(planFilePath)
if err != nil {
log.Fatalf("Unable to read JSON plan file: %s", err)
}

var plan tfjson.Plan
err = json.Unmarshal(planBytes, &plan)
if err != nil {
log.Fatalf("Unable to unmarshal JSON plan file: %s", err)
}

return &plan
}
58 changes: 58 additions & 0 deletions test/terraform/planJson/plan_with_module_calls.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"format_version": "0.1",
"terraform_version": "0.12.24",
"configuration": {
"provider_config": {
"aws": {
"name": "aws",
"expressions": {
"region": {
"references": [
"module.module2.module1_region"
]
}
}
}
},
"root_module": {
"module_calls": {
"module2": {
"source": "../module2",
"module": {
"outputs": {
"module1_region": {
"expression": {
"references": [
"module.module1.region"
]
}
}
},
"module_calls": {
"module1": {
"source": "../module1",
"module": {
"outputs": {
"region": {
"expression": {
"references": [
"var.region"
]
}
}
},
"variables": {
"region": {
"default": "region_from_module_calls"
}
}
}
}
}
}
}
}
}
},
"variables": {}
}

0 comments on commit ecfacb4

Please sign in to comment.