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

helper/schema: Default hashing function for sets #3018

Merged
merged 1 commit into from
Oct 5, 2015
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
10 changes: 1 addition & 9 deletions helper/schema/field_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,7 @@ func (r *FieldReadResult) ValueOrZero(s *Schema) interface{} {
return r.Value
}

result := s.Type.Zero()

// The zero value of a set is nil, but we want it
// to actually be an empty set object...
if set, ok := result.(*Set); ok && set.F == nil {
set.F = s.Set
}

return result
return s.ZeroValue()
}

// addrToSchema finds the final element schema for the given address
Expand Down
2 changes: 1 addition & 1 deletion helper/schema/field_reader_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ func (r *ConfigFieldReader) readSet(
address []string, schema *Schema) (FieldReadResult, map[int]int, error) {
indexMap := make(map[int]int)
// Create the set that will be our result
set := &Set{F: schema.Set}
set := schema.ZeroValue().(*Set)

raw, err := readListField(&nestedConfigFieldReader{r}, address, schema)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion helper/schema/field_reader_diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ func (r *DiffFieldReader) readSet(
prefix := strings.Join(address, ".") + "."

// Create the set that will be our result
set := &Set{F: schema.Set}
set := schema.ZeroValue().(*Set)

// Go through the map and find all the set items
for k, d := range r.Diff.Attributes {
Expand Down
2 changes: 1 addition & 1 deletion helper/schema/field_reader_map.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func (r *MapFieldReader) readSet(
}

// Create the set that will be our result
set := &Set{F: schema.Set}
set := schema.ZeroValue().(*Set)

// If we have an empty list, then return an empty list
if countRaw.Computed || countRaw.Value.(int) == 0 {
Expand Down
32 changes: 27 additions & 5 deletions helper/schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,30 @@ func (s *Schema) DefaultValue() (interface{}, error) {
return nil, nil
}

// Returns a zero value for the schema.
func (s *Schema) ZeroValue() interface{} {
// If it's a set then we'll do a bit of extra work to provide the
// right hashing function in our empty value.
if s.Type == TypeSet {
setFunc := s.Set
if setFunc == nil {
// Default set function uses the schema to hash the whole value
elem := s.Elem
switch t := elem.(type) {
case *Schema:
setFunc = HashSchema(t)
case *Resource:
setFunc = HashResource(t)
default:
panic("invalid set element type")
}
}
return &Set{F: setFunc}
} else {
return s.Type.Zero()
}
}

func (s *Schema) finalizeDiff(
d *terraform.ResourceAttrDiff) *terraform.ResourceAttrDiff {
if d == nil {
Expand Down Expand Up @@ -496,10 +520,8 @@ func (m schemaMap) InternalValidate(topSchemaMap schemaMap) error {
return fmt.Errorf("%s: Default is not valid for lists or sets", k)
}

if v.Type == TypeList && v.Set != nil {
if v.Type != TypeSet && v.Set != nil {
return fmt.Errorf("%s: Set can only be set for TypeSet", k)
} else if v.Type == TypeSet && v.Set == nil {
return fmt.Errorf("%s: Set must be set", k)
}

switch t := v.Elem.(type) {
Expand Down Expand Up @@ -782,10 +804,10 @@ func (m schemaMap) diffSet(
}

if o == nil {
o = &Set{F: schema.Set}
o = schema.ZeroValue().(*Set)
}
if n == nil {
n = &Set{F: schema.Set}
n = schema.ZeroValue().(*Set)
}
os := o.(*Set)
ns := n.(*Set)
Expand Down
2 changes: 1 addition & 1 deletion helper/schema/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2789,7 +2789,7 @@ func TestSchemaMap_InternalValidate(t *testing.T) {
Optional: true,
},
},
true,
false,
},

// Required but computed
Expand Down
105 changes: 105 additions & 0 deletions helper/schema/serialize.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package schema

import (
"bytes"
"sort"
"strconv"
)

func SerializeValueForHash(buf *bytes.Buffer, val interface{}, schema *Schema) {
if val == nil {
buf.WriteRune(';')
return
}

switch schema.Type {
case TypeBool:
if val.(bool) {
buf.WriteRune('1')
} else {
buf.WriteRune('0')
}
case TypeInt:
buf.WriteString(strconv.Itoa(val.(int)))
case TypeFloat:
buf.WriteString(strconv.FormatFloat(val.(float64), 'g', -1, 64))
case TypeString:
buf.WriteString(val.(string))
case TypeList:
buf.WriteRune('(')
l := val.([]interface{})
for _, innerVal := range l {
serializeCollectionMemberForHash(buf, innerVal, schema.Elem)
}
buf.WriteRune(')')
case TypeMap:
m := val.(map[string]interface{})
var keys []string
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
buf.WriteRune('[')
for _, k := range keys {
innerVal := m[k]
buf.WriteString(k)
buf.WriteRune(':')
serializeCollectionMemberForHash(buf, innerVal, schema.Elem)
}
buf.WriteRune(']')
case TypeSet:
buf.WriteRune('{')
s := val.(*Set)
for _, innerVal := range s.List() {
serializeCollectionMemberForHash(buf, innerVal, schema.Elem)
}
buf.WriteRune('}')
default:
panic("unknown schema type to serialize")
}
buf.WriteRune(';')
}

// SerializeValueForHash appends a serialization of the given resource config
// to the given buffer, guaranteeing deterministic results given the same value
// and schema.
//
// Its primary purpose is as input into a hashing function in order
// to hash complex substructures when used in sets, and so the serialization
// is not reversible.
func SerializeResourceForHash(buf *bytes.Buffer, val interface{}, resource *Resource) {
sm := resource.Schema
m := val.(map[string]interface{})
var keys []string
for k := range sm {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
innerSchema := sm[k]
// Skip attributes that are not user-provided. Computed attributes
// do not contribute to the hash since their ultimate value cannot
// be known at plan/diff time.
if !(innerSchema.Required || innerSchema.Optional) {
continue
}

buf.WriteString(k)
buf.WriteRune(':')
innerVal := m[k]
SerializeValueForHash(buf, innerVal, innerSchema)
}
}

func serializeCollectionMemberForHash(buf *bytes.Buffer, val interface{}, elem interface{}) {
switch tElem := elem.(type) {
case *Schema:
SerializeValueForHash(buf, val, tElem)
case *Resource:
buf.WriteRune('<')
SerializeResourceForHash(buf, val, tElem)
buf.WriteString(">;")
default:
panic("invalid element type")
}
}
Loading