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

fix - terraform complex variables are not getting resolved #657

Merged
Merged
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
49 changes: 35 additions & 14 deletions pkg/iac-providers/terraform/commons/cty-converters.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,8 @@ import (
"github.com/zclconf/go-cty/cty/gocty"
)

// list of available cty to golang type converters
// list of available primitive cty to golang type converters
var (
ctyConverterFuncs = []func(cty.Value) (interface{}, error){ctyToStr, ctyToInt, ctyToFloat, ctyToBool, ctyToSlice, ctyToMap}
ctyNativeConverterFuncs = []func(cty.Value) (interface{}, error){ctyToStr, ctyToInt, ctyToFloat, ctyToBool}
)

Expand Down Expand Up @@ -66,14 +65,12 @@ func ctyToSlice(ctyVal cty.Value) (interface{}, error) {

if ctyVal.Type().IsListType() || ctyVal.Type().IsTupleType() || ctyVal.Type().IsSetType() {
for _, v := range ctyVal.AsValueSlice() {
for _, converter := range ctyNativeConverterFuncs {
resolved, err := converter(v)
if err == nil {
val = append(val, resolved)
break
}
nativeVal, err := convertCtyToGoNative(v)
if err != nil {
allErrs = errors.Wrap(allErrs, err.Error())
continue
}
val = append(val, nativeVal)
}
if allErrs != nil {
return nil, allErrs
Expand Down Expand Up @@ -107,14 +104,12 @@ func ctyToMap(ctyVal cty.Value) (interface{}, error) {
// golang value
for k, v := range ctyValMap {
// convert cty.Value to native golang type based on cty.Type
for _, converter := range ctyNativeConverterFuncs {
resolved, err := converter(v)
if err == nil {
val[k] = resolved
break
}
nativeVal, err := convertCtyToGoNative(v)
if err != nil {
allErrs = errors.Wrap(allErrs, err.Error())
continue
}
val[k] = nativeVal
}
if allErrs != nil {
return nil, allErrs
Expand All @@ -123,3 +118,29 @@ func ctyToMap(ctyVal cty.Value) (interface{}, error) {
// hopefully successful!
return val, nil
}

// convertCtyToGoNative converts a cty.Value to its go native type
func convertCtyToGoNative(ctyVal cty.Value) (interface{}, error) {
if ctyVal.Type().IsPrimitiveType() {
return convertPrimitiveType(ctyVal)
}
return convertComplexType(ctyVal)
}

// convertPrimitiveType converts a primitive cty.Value to its go native type
func convertPrimitiveType(ctyVal cty.Value) (interface{}, error) {
for _, converter := range ctyNativeConverterFuncs {
if val, err := converter(ctyVal); err == nil {
return val, err
}
}
return nil, fmt.Errorf("ctyVal could not be resolved to native go type")
}

// convertPrimitiveType converts a complex cty.Value (list, tuple, set, map, object) to its go native type
func convertComplexType(ctyVal cty.Value) (interface{}, error) {
if ctyVal.Type().IsListType() || ctyVal.Type().IsTupleType() || ctyVal.Type().IsSetType() {
return ctyToSlice(ctyVal)
}
return ctyToMap(ctyVal)
}
203 changes: 203 additions & 0 deletions pkg/iac-providers/terraform/commons/cty-converters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -503,3 +503,206 @@ func TestCtyToMap(t *testing.T) {
})
}
}

func TestConvertPrimitiveType(t *testing.T) {
intCtyVal := cty.NumberIntVal(-10)
floatCtyVal := cty.NumberFloatVal(3.141)
unsignedIntCtyVal := cty.NumberUIntVal(44)
stringCtyVal := cty.StringVal("Peter Parker")
boolCtyVal := cty.BoolVal(true)
nonPrimitiveCtyVal := cty.ListVal([]cty.Value{cty.StringVal("test")})

type args struct {
ctyVal cty.Value
}
tests := []struct {
name string
args args
want interface{}
wantErr bool
}{
{
name: "signed int input",
args: args{
ctyVal: intCtyVal,
},
want: -10,
},
{
name: "float input",
args: args{
ctyVal: floatCtyVal,
},
want: 3.141,
},
{
name: "unsigned int input",
args: args{
ctyVal: unsignedIntCtyVal,
},
want: 44,
},
{
name: "string input",
args: args{
ctyVal: stringCtyVal,
},
want: "Peter Parker",
Copy link
Contributor

Choose a reason for hiding this comment

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

I like the reference here :)

},
{
name: "boolean input",
args: args{
ctyVal: boolCtyVal,
},
want: true,
},
{
name: "non primitive input",
args: args{
ctyVal: nonPrimitiveCtyVal,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := convertPrimitiveType(tt.args.ctyVal)
if (err != nil) != tt.wantErr {
t.Errorf("convertPrimitiveType() got error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("convertPrimitiveType() got = %v, want %v", got, tt.want)
}
})
}
}

func TestConvertComplexType(t *testing.T) {

nonComplexCtyVal := cty.NumberIntVal(5)
complexListCtyVal := cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"application": cty.StringVal("app1"),
"port": cty.NumberIntVal(9010),
}),
cty.ObjectVal(map[string]cty.Value{
"application": cty.StringVal("app2"),
"port": cty.NumberIntVal(8000),
}),
})
complexMapCtyVal := cty.MapVal(map[string]cty.Value{
"first": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("Thor"),
"ID": cty.NumberIntVal(1),
}),
"second": cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("Spiderman"),
"ID": cty.NumberIntVal(2),
}),
})

type args struct {
ctyVal cty.Value
}
tests := []struct {
name string
args args
want interface{}
wantErr bool
}{
{
name: "non complex input",
args: args{
ctyVal: nonComplexCtyVal,
},
wantErr: true,
},
{
name: "list of objects input",
args: args{
ctyVal: complexListCtyVal,
},
want: []interface{}{
map[string]interface{}{
"application": "app1",
"port": 9010,
},
map[string]interface{}{
"application": "app2",
"port": 8000,
},
},
},
{
name: "map of objects input",
args: args{
ctyVal: complexMapCtyVal,
},
want: map[string]interface{}{
"first": map[string]interface{}{
"name": "Thor",
"ID": 1,
},
"second": map[string]interface{}{
"name": "Spiderman",
"ID": 2,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := convertComplexType(tt.args.ctyVal)
if (err != nil) != tt.wantErr {
t.Errorf("convertComplexType() got error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("convertComplexType() got = %v, want %v", got, tt.want)
}
})
}
}

func TestConvertCtyToGoNative(t *testing.T) {
primitiveCtyVal := cty.StringVal("primitive")
complexCtyVal := cty.ListVal([]cty.Value{cty.BoolVal(false)})

type args struct {
ctyVal cty.Value
}
tests := []struct {
name string
args args
want interface{}
wantErr bool
}{
{
name: "primitive cty input",
args: args{
ctyVal: primitiveCtyVal,
},
want: "primitive",
},
{
name: "complex cty input",
args: args{
ctyVal: complexCtyVal,
},
want: []interface{}{false},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := convertCtyToGoNative(tt.args.ctyVal)
if (err != nil) != tt.wantErr {
t.Errorf("convertCtyToGoNative() got error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("convertCtyToGoNative() want = %v, want %v", got, tt.want)
}
})
}
}
33 changes: 17 additions & 16 deletions pkg/iac-providers/terraform/commons/module-references.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,22 +93,23 @@ func (r *RefResolver) ResolveModuleRef(moduleRef string,

// default value is of cty.Value type, convert it to native golang type
// based on cty.Type, determine golang type
for _, converter := range ctyConverterFuncs {
if val, err := converter(hclVar.Default); err == nil {
zap.S().Debugf("resolved module variable ref: '%v', value: '%v'", moduleRef, val)

// replace the variable reference string with actual value
if reflect.TypeOf(val).Kind() == reflect.String {
valStr := val.(string)
resolvedVal := strings.Replace(moduleRef, moduleExpr, valStr, 1)
zap.S().Debugf("resolved str module value ref: '%v', value: '%v'", moduleRef, resolvedVal)
return r.ResolveStrRef(resolvedVal)
}
zap.S().Debugf("resolved module value ref: '%v', value: '%v'", moduleRef, val)
return val
val, err := convertCtyToGoNative(hclVar.Default)
if err != nil {
zap.S().Debugf(err.Error())
zap.S().Debugf("failed to convert cty.Value '%v' to golang native value", hclVar.Default.GoString())
return moduleRef
}
zap.S().Debugf("resolved variable ref '%v', value: '%v'", moduleRef, val)

if reflect.TypeOf(val).Kind() == reflect.String {
valStr := val.(string)
resolvedVal := strings.Replace(moduleRef, moduleExpr, valStr, 1)
if moduleRef == resolvedVal {
zap.S().Debugf("resolved str variable ref refers to self: '%v'", moduleRef)
return moduleRef
}
zap.S().Debugf("resolved str variable ref: '%v', value: '%v'", moduleRef, resolvedVal)
return r.ResolveStrRef(resolvedVal)
}
zap.S().Debugf("failed to convert cty.Value '%v' to golang native value", hclVar.Default.GoString())

return moduleRef
return val
}
38 changes: 17 additions & 21 deletions pkg/iac-providers/terraform/commons/variable-references.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,29 +79,25 @@ func (r *RefResolver) ResolveVarRef(varRef string) interface{} {

// default value is of cty.Value type, convert it to native golang type
// based on cty.Type, determine golang type
for _, converter := range ctyConverterFuncs {
if val, err := converter(hclVar.Default); err == nil {
zap.S().Debugf("resolved variable ref '%v', value: '%v'", varRef, val)

// replace the variable reference string with actual value
if reflect.TypeOf(val).Kind() == reflect.String {
valStr := val.(string)
resolvedVal := strings.Replace(varRef, varExpr, valStr, 1)
if varRef == resolvedVal {
zap.S().Debugf("resolved str variable ref refers to self: '%v'", varRef)
return varRef
}
zap.S().Debugf("resolved str variable ref: '%v', value: '%v'", varRef, resolvedVal)
return r.ResolveStrRef(resolvedVal)
}
zap.S().Debugf("resolved variable ref: '%v', value: '%v'", varRef, val)
return val
}
val, err := convertCtyToGoNative(hclVar.Default)
if err != nil {
zap.S().Debugf(err.Error())
zap.S().Debugf("failed to convert cty.Value '%v' to golang native value", hclVar.Default.GoString())
return varRef
}
zap.S().Debugf("failed to convert cty.Value '%v' to golang native value", hclVar.Default.GoString())
zap.S().Debugf("resolved variable ref '%v', value: '%v'", varRef, val)

// return original reference if cty conversion attempts fail
return varRef
if reflect.TypeOf(val).Kind() == reflect.String {
valStr := val.(string)
resolvedVal := strings.Replace(varRef, varExpr, valStr, 1)
if varRef == resolvedVal {
zap.S().Debugf("resolved str variable ref refers to self: '%v'", varRef)
return varRef
}
zap.S().Debugf("resolved str variable ref: '%v', value: '%v'", varRef, resolvedVal)
return r.ResolveStrRef(resolvedVal)
}
return val
}

// ResolveVarRefFromParentModuleCall returns the variable value as configured in
Expand Down
7 changes: 7 additions & 0 deletions pkg/iac-providers/terraform/v14/load-dir_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,13 @@ func TestLoadIacDir(t *testing.T) {
tfv14: TfV14{},
wantErr: nil,
},
{
name: "complex variables",
tfConfigDir: filepath.Join(testDataDir, "complex-variables"),
tfJSONFile: filepath.Join(tfJSONDir, "complex-variables.json"),
tfv14: TfV14{},
wantErr: nil,
},
}

for _, tt := range table2 {
Expand Down
Loading