Skip to content

Commit

Permalink
Merge branch 'use-tftypes-everywhere' into feature/app
Browse files Browse the repository at this point in the history
  • Loading branch information
mgyucht committed Dec 9, 2024
2 parents 0619e2e + 0d49760 commit a0297e6
Show file tree
Hide file tree
Showing 25 changed files with 831 additions and 60 deletions.
3 changes: 3 additions & 0 deletions .codegen/model.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@ func (o *{{ $parent.PascalName}}) Set{{.PascalName}}(ctx context.Context, v {{te
}
{{- end}}
t := o.Type(ctx).(basetypes.ObjectType).AttrTypes["{{template "tfsdk-name" (dict "field" .)}}"]
{{- if or .Entity.ArrayValue .Entity.MapValue }}
t = t.(attr.TypeWithElementType).ElementType()
{{- end }}
o.{{.PascalName}}{{if eq .PascalName "Type"}}_{{end}} = types.{{if .Entity.MapValue}}Map{{else}}List{{end}}ValueMust(t, vs)
}
{{end}}
Expand Down
130 changes: 82 additions & 48 deletions internal/providers/pluginfw/converters/converters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/stretchr/testify/assert"
)

Expand All @@ -32,6 +33,10 @@ type DummyTfSdk struct {
SliceStructPtr types.List `tfsdk:"slice_struct_ptr" tf:"optional"`
Irrelevant types.String `tfsdk:"-"`
Object types.Object `tfsdk:"object" tf:"optional"`
ObjectPtr types.Object `tfsdk:"object_ptr" tf:"optional"`
Type_ types.String `tfsdk:"type" tf:""` // Test Type_ renaming
EmptyStructList types.List `tfsdk:"empty_struct_list" tf:"optional"`
EmptyStructObject types.Object `tfsdk:"empty_struct_object" tf:"optional"`
}

func (DummyTfSdk) GetComplexFieldTypes(ctx context.Context) map[string]reflect.Type {
Expand All @@ -43,9 +48,11 @@ func (DummyTfSdk) GetComplexFieldTypes(ctx context.Context) map[string]reflect.T
"nested_map": reflect.TypeOf(DummyNestedTfSdk{}),
"repeated": reflect.TypeOf(types.Int64{}),
"attributes": reflect.TypeOf(types.String{}),
"slice_struct": reflect.TypeOf(DummyNestedTfSdk{}),
"slice_struct_ptr": reflect.TypeOf(DummyNestedTfSdk{}),
"object": reflect.TypeOf(DummyNestedTfSdk{}),
"object_ptr": reflect.TypeOf(DummyNestedTfSdk{}),
"empty_struct_list": reflect.TypeOf(DummyNestedTfSdkEmpty{}),
"empty_struct_object": reflect.TypeOf(DummyNestedTfSdkEmpty{}),
}
}

Expand Down Expand Up @@ -81,6 +88,8 @@ type DummyNestedTfSdk struct {
Enabled types.Bool `tfsdk:"enabled" tf:"optional"`
}

type DummyNestedTfSdkEmpty struct{}

type DummyGoSdk struct {
Enabled bool `json:"enabled"`
Workers int64 `json:"workers"`
Expand All @@ -98,8 +107,12 @@ type DummyGoSdk struct {
AdditionalField string `json:"additional_field"`
DistinctField string `json:"distinct_field"` // distinct field that the tfsdk struct doesn't have
SliceStructPtr *DummyNestedGoSdk `json:"slice_struct_ptr"`
ForceSendFields []string `json:"-"`
Object DummyNestedGoSdk `json:"object"`
ObjectPtr *DummyNestedGoSdk `json:"object_ptr"`
Type string `json:"type"` // Test Type_ renaming
EmptyStructList []DummyNestedGoSdkEmpty `json:"empty_struct_list"`
EmptyStructObject *DummyNestedGoSdkEmpty `json:"empty_struct_object"`
ForceSendFields []string `json:"-"`
}

type DummyNestedGoSdk struct {
Expand All @@ -108,57 +121,54 @@ type DummyNestedGoSdk struct {
ForceSendFields []string `json:"-"`
}

type DummyNestedGoSdkEmpty struct{}

// This function is used to populate empty fields in the tfsdk struct with null values.
// This is required because the Go->TF conversion function instantiates list, map, and
// object fields with empty values, which are not equal to null values in the tfsdk struct.
func populateEmptyFields(c DummyTfSdk) DummyTfSdk {
complexFields := c.GetComplexFieldTypes(context.Background())
v := reflect.ValueOf(&c).Elem()
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
// If the field is a simple type, the zero value is OK.
switch field.Type() {
case reflect.TypeOf(types.Bool{}), reflect.TypeOf(types.Int64{}), reflect.TypeOf(types.Float64{}), reflect.TypeOf(types.String{}):
continue
}
if !field.IsZero() {
continue
}

tfsdkName := v.Type().Field(i).Tag.Get("tfsdk")
complexType, ok := complexFields[tfsdkName]
if !ok {
continue
}

var typ attr.Type
switch complexType {
case reflect.TypeOf(types.Bool{}):
typ = types.BoolType
case reflect.TypeOf(types.Int64{}):
typ = types.Int64Type
case reflect.TypeOf(types.Float64{}):
typ = types.Float64Type
case reflect.TypeOf(types.String{}):
typ = types.StringType
default:
innerVal := reflect.New(complexType).Elem().Interface()
typ = tfcommon.NewObjectTyper(innerVal).Type(context.Background())
}
switch field.Type() {
case reflect.TypeOf(types.List{}):
value := types.ListNull(typ)
field.Set(reflect.ValueOf(value))
case reflect.TypeOf(types.Map{}):
value := types.MapNull(typ)
field.Set(reflect.ValueOf(value))
case reflect.TypeOf(types.Object{}):
objectType := typ.(types.ObjectType)
value := types.ObjectNull(objectType.AttrTypes)
field.Set(reflect.ValueOf(value))
}
if c.NoPointerNested.IsNull() {
c.NoPointerNested = types.ListNull(dummyType)
}
if c.NestedList.IsNull() {
c.NestedList = types.ListNull(dummyType)
}
if c.NestedPointerList.IsNull() {
c.NestedPointerList = types.ListNull(dummyType)
}
if c.Map.IsNull() {
c.Map = types.MapNull(types.StringType)
}
if c.NestedMap.IsNull() {
c.NestedMap = types.MapNull(dummyType)
}
if c.Repeated.IsNull() {
c.Repeated = types.ListNull(types.Int64Type)
}
if c.Attributes.IsNull() {
c.Attributes = types.MapNull(types.StringType)
}
if c.SliceStructPtr.IsNull() {
c.SliceStructPtr = types.ListNull(dummyType)
}
return v.Interface().(DummyTfSdk)
if c.Object.IsNull() {
// type.Object fields that correspond to structs are considered never to be null.
c.Object = types.ObjectValueMust(dummyType.AttrTypes, map[string]attr.Value{
"name": types.StringNull(),
"enabled": types.BoolNull(),
})
}
if c.ObjectPtr.IsNull() {
// type.Object fields that correspond to pointers are considered null when the Go SDK value is nil.
c.ObjectPtr = types.ObjectNull(dummyType.AttrTypes)
}
if c.EmptyStructList.IsNull() {
c.EmptyStructList = types.ListNull(basetypes.ObjectType{AttrTypes: map[string]attr.Type{}})
}
if c.EmptyStructObject.IsNull() {
c.EmptyStructObject = types.ObjectNull(map[string]attr.Type{})
}
return c
}

// Function to construct individual test case with a pair of matching tfSdkStruct and gosdkStruct.
Expand Down Expand Up @@ -198,6 +208,7 @@ func TestGoSdkToTfSdkStructConversionFailure(t *testing.T) {
}

var dummyType = tfcommon.NewObjectTyper(DummyNestedTfSdk{}).Type(context.Background()).(types.ObjectType)
var emptyType = basetypes.ObjectType{AttrTypes: map[string]attr.Type{}}

var tests = []struct {
name string
Expand Down Expand Up @@ -360,6 +371,29 @@ var tests = []struct {
ForceSendFields: []string{"Name", "Enabled"},
}, ForceSendFields: []string{"Object"}},
},
{
"type name",
DummyTfSdk{Type_: types.StringValue("abc")},
DummyGoSdk{Type: "abc", ForceSendFields: []string{"Type"}},
},
{
"empty list of empty struct to list conversion",
DummyTfSdk{EmptyStructList: types.ListValueMust(emptyType, []attr.Value{})},
DummyGoSdk{EmptyStructList: []DummyNestedGoSdkEmpty{}},
},
{
"non-empty list empty struct to list conversion",
DummyTfSdk{EmptyStructList: types.ListValueMust(emptyType, []attr.Value{
types.ObjectValueMust(map[string]attr.Type{}, map[string]attr.Value{}),
types.ObjectValueMust(map[string]attr.Type{}, map[string]attr.Value{}),
})},
DummyGoSdk{EmptyStructList: []DummyNestedGoSdkEmpty{{}, {}}},
},
{
"non-nil pointer of empty struct to object conversion",
DummyTfSdk{EmptyStructObject: types.ObjectValueMust(emptyType.AttrTypes, map[string]attr.Value{})},
DummyGoSdk{EmptyStructObject: &DummyNestedGoSdkEmpty{}, ForceSendFields: []string{"EmptyStructObject"}},
},
}

func TestConverter(t *testing.T) {
Expand Down
34 changes: 23 additions & 11 deletions internal/providers/pluginfw/converters/go_to_tf.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,9 @@ func GoSdkToTfSdkStruct(ctx context.Context, gosdk interface{}, tfsdk interface{
if srcFieldTag == "-" {
continue
}
destField := destVal.FieldByName(toTfSdkName(srcFieldName))
destFieldType, ok := destVal.Type().FieldByName(field.StructField.Name)
destFieldStructName := toTfSdkName(srcFieldName)
destField := destVal.FieldByName(destFieldStructName)
destFieldType, ok := destVal.Type().FieldByName(destFieldStructName)
if !ok {
d.AddError(goSdkToTfSdkStructConversionFailureMessage, fmt.Sprintf("destination struct does not have field %s. %s", srcFieldName, common.TerraformBugErrorMessage))
return
Expand Down Expand Up @@ -136,9 +137,19 @@ func goSdkToTfSdkSingleField(
return
}

// Otherwise, dereference the pointer and continue.
srcField = srcField.Elem()
d.Append(goSdkToTfSdkSingleField(ctx, srcField, destField, forceSendField, tfType, innerType)...)
// Otherwise, the source field is a non-nil pointer to a struct.
// If the target is a list, we treat the source field as a slice with length 1
// containing only the dereferenced pointer.
if destField.Type() == reflect.TypeOf(types.List{}) {
listSrc := reflect.MakeSlice(reflect.SliceOf(srcField.Type().Elem()), 1, 1)
listSrc.Index(0).Set(srcField.Elem())
d.Append(goSdkToTfSdkSingleField(ctx, listSrc, destField, forceSendField, tfType, innerType)...)
return
}

// Otherwise, the target is an object. Dereference the pointer and convert the underlying struct.
d.Append(goSdkToTfSdkSingleField(ctx, srcField.Elem(), destField, forceSendField, tfType, innerType)...)
return
case reflect.Bool:
boolVal := srcField.Interface().(bool)
// check if the value is non-zero or if the field is in the forceSendFields list
Expand Down Expand Up @@ -186,15 +197,16 @@ func goSdkToTfSdkSingleField(
}
case reflect.Struct:
// This corresponds to either a types.List or types.Object.
// If the struct is zero value, set the destination field to the null value of the appropriate type.
if srcField.IsZero() {
setFieldToNull(destField, tfType)
return
}

// If the destination field is a types.List, treat the source field as a slice with length 1
// containing only this struct.
if destField.Type() == reflect.TypeOf(types.List{}) {
// For compatibility, a field consisting of a zero-valued struct that is mapped to lists is treated as an
// empty list.
if srcField.IsZero() {
setFieldToNull(destField, tfType)
return
}

listSrc := reflect.MakeSlice(reflect.SliceOf(srcField.Type()), 1, 1)
listSrc.Index(0).Set(srcField)
d.Append(goSdkToTfSdkSingleField(ctx, listSrc, destField, forceSendField, tfType, innerType)...)
Expand Down
15 changes: 14 additions & 1 deletion internal/providers/pluginfw/converters/tf_to_go.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,20 @@ func tfsdkToGoSdkStructField(
return
}

d.Append(TfSdkToGoSdkStruct(ctx, innerValue.Interface(), destField.Addr().Interface())...)
destType := destField.Type()
if destType.Kind() == reflect.Ptr {
destType = destType.Elem()
}
destValue := reflect.New(destType)
d.Append(TfSdkToGoSdkStruct(ctx, innerValue.Interface(), destValue.Interface())...)
if d.HasError() {
return
}
if destField.Type().Kind() == reflect.Ptr {
destField.Set(destValue)
} else {
destField.Set(destValue.Elem())
}
default:
d.AddError(tfSdkToGoSdkFieldConversionFailureMessage, fmt.Sprintf("%T is not currently supported as a source field. %s", v, common.TerraformBugErrorMessage))
return
Expand Down
3 changes: 3 additions & 0 deletions internal/providers/pluginfw/products/cluster/data_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ func (d *ClusterDataSource) Read(ctx context.Context, req datasource.ReadRequest

var tfCluster compute_tf.ClusterDetails
resp.Diagnostics.Append(converters.GoSdkToTfSdkStruct(ctx, cluster, &tfCluster)...)
if resp.Diagnostics.HasError() {
return
}

clusterInfo.ClusterId = tfCluster.ClusterId
clusterInfo.Name = tfCluster.ClusterName
Expand Down
23 changes: 23 additions & 0 deletions internal/providers/pluginfw/tfschema/struct_to_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,30 @@ func typeToSchema(ctx context.Context, v reflect.Value) NestedBlockObject {
}
}
}
// No other types are supported. Instead, we provide helpful error messages to help users writing
// custom TFSDK structures to use the appropriate types.
case int, int32, int64:
panic(fmt.Errorf("unsupported type %T in tfsdk structs. Use types.Int intead. %s", value, common.TerraformBugErrorMessage))
case float32, float64:
panic(fmt.Errorf("unsupported type %T in tfsdk structs. Use types.Float64 instead. %s", value, common.TerraformBugErrorMessage))
case string:
panic(fmt.Errorf("unsupported type %T in tfsdk structs. Use types.String instead. %s", value, common.TerraformBugErrorMessage))
case bool:
panic(fmt.Errorf("unsupported type %T in tfsdk structs. Use types.Bool instead. %s", value, common.TerraformBugErrorMessage))
default:
fieldType := field.Value.Type()
if fieldType.Kind() == reflect.Slice {
fieldElemType := fieldType.Elem()
panic(fmt.Errorf("unsupported type %T in tfsdk structs. Use types.List instead. To capture the element type, implement the ComplexFieldTypeProvider interface and add the following mapping: \"%s\": reflect.TypeOf(%s). %s", value, fieldName, fieldElemType.Name(), common.TerraformBugErrorMessage))
}
if fieldType.Kind() == reflect.Map {
fieldElemType := fieldType.Elem()
panic(fmt.Errorf("unsupported type %T in tfsdk structs. Use types.Map instead. To capture the element type, implement the ComplexFieldTypeProvider interface and add the following mapping: \"%s\": reflect.TypeOf(%s). %s", value, fieldName, fieldElemType.Name(), common.TerraformBugErrorMessage))
}
if fieldType.Kind() == reflect.Struct {
// TODO: change the recommendation to use types.Object when support is added.
panic(fmt.Errorf("unsupported type %T in tfsdk structs. Use types.List instead, and treat the nested object as a list of length 1. To capture the element type, implement the ComplexFieldTypeProvider interface and add the following mapping: \"%s\": reflect.TypeOf(%s). %s", value, fieldName, fieldType.Name(), common.TerraformBugErrorMessage))
}
panic(fmt.Errorf("unexpected type %T in tfsdk structs, expected a plugin framework value type. %s", value, common.TerraformBugErrorMessage))
}
// types.List fields of complex types correspond to ListNestedBlock, which don't have optional/required/computed flags.
Expand Down
8 changes: 8 additions & 0 deletions internal/service/apps_tf/model.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit a0297e6

Please sign in to comment.