Skip to content

Commit

Permalink
Feat: refactor resources/data to use the new serialization/deserializ… (
Browse files Browse the repository at this point in the history
#410)

* Feat: refactor resources/data to use the new serialization/deserialization logic

* improved code
  • Loading branch information
TomerHeber authored Jun 7, 2022
1 parent ada0b0f commit 79568b0
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 49 deletions.
11 changes: 10 additions & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,18 @@ The writeResourceData function receives a golang struct and a Terraform configur
Check [resource_module.go](./env0/resource_module.go) that uses the utilities vs [resource_environment.go](./env0/resource_environment.go) that does not.

Pay attention to the following caveats:
* The utilities leverage golang reflection. And work well for most simple types. Complex types may need additional code to be implemented.
* The golang fields are in CamalCase, while the terraform fields are in snake_case. They must match. E.g., ProjectName (golang) == project_name (Terraform). To override the default CamalCase to snake_case conversion you may use the tag `tfschema`. To ignore a field set the `tfschema` tag value to `-`.

#### writeResourceDataSlice

The writeResourceDataSlice function receives a golang slice, a field name (of type list) and a terraform configuration.
It will try to automatically write the slice structs to the terraform configuration under the field name.

#### Important Notes

When using any of these functions be sure to test them well.
These are "best-effort" helpers that leverage golang's refelection. They will work well for most basic cases, but may fall short in complex scenarios.

### Handling drifts

If ReadContext is called and the resource isn't found by the current ID, it's required to reset the ID.
Expand Down
166 changes: 118 additions & 48 deletions env0/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,29 +117,38 @@ func readResourceData(i interface{}, d *schema.ResourceData) error {
return nil
}

// Returns the field name or skip.
func getFieldName(field reflect.StructField) (string, bool) {
name := field.Name
// Assumes golang is CamalCase and Terraform is snake_case.
// This behavior can be overrided be used in the 'tfschema' tag.
name = toSnakeCase(name)
if tag, ok := field.Tag.Lookup("tfschema"); ok {
if tag == "-" {
return "", true
}

// 'resource' tag found. Override to tag value.
name = tag
}

return name, false
}

// Extracts values from the interface, and writes it to resourcedata.
func writeResourceData(i interface{}, d *schema.ResourceData) error {
val := reflect.ValueOf(i).Elem()

for i := 0; i < val.NumField(); i++ {
fieldName := val.Type().Field(i).Name
// Assumes golang is CamalCase and Terraform is snake_case.
// This behavior can be overrided be used in the 'tfschema' tag.
fieldNameSC := toSnakeCase(fieldName)
if resFieldName, ok := val.Type().Field(i).Tag.Lookup("tfschema"); ok {
if resFieldName == "-" {
continue
}

// 'resource' tag found. Override to tag value.
fieldNameSC = resFieldName
fieldName, skip := getFieldName(val.Type().Field(i))
if skip {
continue
}

field := val.Field(i)

fieldType := field.Type()

if fieldName == "Id" {
if fieldName == "id" {
id := field.String()
if len(id) == 0 {
return errors.New("id is empty")
Expand All @@ -148,12 +157,12 @@ func writeResourceData(i interface{}, d *schema.ResourceData) error {
continue
}

if d.Get(fieldNameSC) == nil {
if d.Get(fieldName) == nil {
continue
}

if customField, ok := field.Interface().(CustomResourceDataField); ok {
if err := customField.WriteResourceData(fieldNameSC, d); err != nil {
if err := customField.WriteResourceData(fieldName, d); err != nil {
return err
}
continue
Expand All @@ -170,48 +179,20 @@ func writeResourceData(i interface{}, d *schema.ResourceData) error {

switch fieldType.Kind() {
case reflect.String:
if err := d.Set(fieldNameSC, field.String()); err != nil {
if err := d.Set(fieldName, field.String()); err != nil {
return err
}
case reflect.Int:
if err := d.Set(fieldNameSC, field.Int()); err != nil {
if err := d.Set(fieldName, field.Int()); err != nil {
return err
}
case reflect.Bool:
if err := d.Set(fieldNameSC, field.Bool()); err != nil {
if err := d.Set(fieldName, field.Bool()); err != nil {
return err
}
case reflect.Slice:
switch field.Type() {
case reflect.TypeOf([]client.ModuleSshKey{}):
var rawSshKeys []map[string]string
for i := 0; i < field.Len(); i++ {
sshKey := field.Index(i).Interface().(client.ModuleSshKey)
rawSshKeys = append(rawSshKeys, map[string]string{"id": sshKey.Id, "name": sshKey.Name})
}
if err := d.Set(fieldNameSC, rawSshKeys); err != nil {
return err
}
case reflect.TypeOf([]client.Agent{}):
var agents []map[string]string
for i := 0; i < field.Len(); i++ {
agent := field.Index(i).Interface().(client.Agent)
agents = append(agents, map[string]string{"agent_key": agent.AgentKey})
}
if err := d.Set(fieldNameSC, agents); err != nil {
return err
}
case reflect.TypeOf([]string{}):
var strs []interface{}
for i := 0; i < field.Len(); i++ {
str := field.Index(i).Interface().(string)
strs = append(strs, str)
}
if err := d.Set(fieldNameSC, strs); err != nil {
return err
}
default:
return fmt.Errorf("internal error - unhandled slice type %v", field.Type())
if err := writeResourceDataSlice(field.Interface(), fieldName, d); err != nil {
return err
}
default:
return fmt.Errorf("internal error - unhandled field kind %v", field.Kind())
Expand All @@ -221,6 +202,95 @@ func writeResourceData(i interface{}, d *schema.ResourceData) error {
return nil
}

func getInterfaceSliceValues(i interface{}) []interface{} {
var result []interface{}

val := reflect.ValueOf(i)

for i := 0; i < val.Len(); i++ {
field := val.Index(i)
result = append(result, field.Interface())
}

return result
}

func getResourceDataSliceStructValue(val reflect.Value, name string, d *schema.ResourceData) (map[string]interface{}, error) {
value := make(map[string]interface{})

for i := 0; i < val.NumField(); i++ {
fieldName, skip := getFieldName(val.Type().Field(i))
if skip {
continue
}

field := val.Field(i)
fieldType := field.Type()

if fieldType.Kind() == reflect.Ptr {
if field.IsNil() {
continue
}

field = field.Elem()
}

// Check if the field exist in the schema. `*` is for any index.
if d.Get(name+".*."+fieldName) == nil {
continue
}

value[fieldName] = field.Interface()
}

return value, nil
}

// Extracts values from a slice of interfaces, and writes it to resourcedata at name.
func writeResourceDataSlice(i interface{}, name string, d *schema.ResourceData) error {
ivalues := getInterfaceSliceValues(i)
var values []interface{}

// Iterate over the slice of values and build a slice of terraform values.
for _, ivalue := range ivalues {
val := reflect.ValueOf(ivalue)
valType := val.Type()

if valType.Kind() == reflect.Ptr {
if val.IsNil() {
continue
}

val = val.Elem()
valType = val.Type()
}

switch valType.Kind() {
case reflect.String:
values = append(values, val.String())
case reflect.Int:
values = append(values, val.Int())
case reflect.Bool:
values = append(values, val.Bool())
case reflect.Struct:
// Slice of structs.
value, err := getResourceDataSliceStructValue(val, name, d)
if err != nil {
return err
}
values = append(values, value)
default:
return fmt.Errorf("internal error - unhandled slice kind %v", valType.Kind())
}
}

if values != nil {
return d.Set(name, values)
}

return nil
}

func safeSet(d *schema.ResourceData, k string, v interface{}) {
// Checks that the key exist in the schema before setting the value.
if test := d.Get(k); test != nil {
Expand Down
18 changes: 18 additions & 0 deletions env0/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,21 @@ func TestWriteCustomResourceData(t *testing.T) {
assert.Equal(t, *configurationVariable.IsRequired, d.Get("is_required"))
assert.Equal(t, configurationVariable.Regex, d.Get("regex"))
}

func TestWriteResourceDataSliceVariables(t *testing.T) {
d := schema.TestResourceDataRaw(t, dataAgents().Schema, map[string]interface{}{})

agent1 := client.Agent{
AgentKey: "key1",
}

agent2 := client.Agent{
AgentKey: "key1",
}

vars := []client.Agent{agent1, agent2}

assert.Nil(t, writeResourceDataSlice(vars, "agents", d))
assert.Equal(t, agent1.AgentKey, d.Get("agents.0.agent_key"))
assert.Equal(t, agent2.AgentKey, d.Get("agents.1.agent_key"))
}

0 comments on commit 79568b0

Please sign in to comment.