Skip to content

Commit

Permalink
feat(policies): allow merging by a complex key (#5650)
Browse files Browse the repository at this point in the history
Signed-off-by: Mike Beaumont <mjboamail@gmail.com>
  • Loading branch information
michaelbeaumont authored Jan 12, 2023
1 parent d1dcabc commit f53d7fe
Show file tree
Hide file tree
Showing 2 changed files with 193 additions and 8 deletions.
126 changes: 118 additions & 8 deletions pkg/core/xds/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package xds
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"strings"

Expand Down Expand Up @@ -33,7 +34,8 @@ func MergeConfs(confs []interface{}) (interface{}, error) {
}
}

result, err := newConf(reflect.TypeOf(confs[0]))
confType := reflect.TypeOf(confs[0])
result, err := newConf(confType)
if err != nil {
return nil, err
}
Expand All @@ -42,18 +44,124 @@ func MergeConfs(confs []interface{}) (interface{}, error) {
return nil, err
}

valueResult := reflect.ValueOf(result)
// clear appendable slices, so we won't duplicate values of the last conf
clearAppendSlices(reflect.ValueOf(result))
clearAppendSlices(valueResult)
for _, conf := range confs {
// call .Elem() to unwrap interface{}
appendSlices(reflect.ValueOf(result), reflect.ValueOf(&conf).Elem())
appendSlices(valueResult, reflect.ValueOf(&conf).Elem())
}

v := reflect.ValueOf(result).Elem().Interface()
if err := handleMergeByKeyFields(valueResult); err != nil {
return nil, err
}

v := valueResult.Elem().Interface()

return v, nil
}

type acc struct {
Key interface{}
Defaults []interface{}
}

const (
defaultFieldName = "Default"
policyMergeTag = "policyMerge"
mergeValuesByKey = "mergeValuesByKey"
mergeKey = "mergeKey"
)

func handleMergeByKeyFields(valueResult reflect.Value) error {
confType := valueResult.Elem().Type()
for fieldIndex := 0; fieldIndex < confType.NumField(); fieldIndex++ {
field := confType.Field(fieldIndex)
if !strings.Contains(field.Tag.Get(policyMergeTag), mergeValuesByKey) {
continue
}
if field.Type.Kind() != reflect.Slice && field.Type.Elem().Kind() != reflect.Struct {
return errors.New("a merge by key field must be a slice of structs")
}

entriesValue := valueResult.Elem().Field(fieldIndex)

merged, err := mergeByKey(entriesValue)
if err != nil {
return err
}
entriesValue.Set(merged)
}
return nil
}

func mergeByKey(vals reflect.Value) (reflect.Value, error) {
if vals.Len() == 0 {
return reflect.Value{}, nil
}
valType := vals.Index(0).Type()
key, ok := findKeyAndSpec(valType)
if !ok {
return reflect.Value{}, fmt.Errorf("a merge by key field must have a field tagged as %s and a Default field", mergeKey)
}
var defaultsByKey []acc
for valueIndex := 0; valueIndex < vals.Len(); valueIndex++ {
value := vals.Index(valueIndex)
mergeKeyValue := value.FieldByName(key.Name).Interface()
var found bool
for accIndex, accRule := range defaultsByKey {
if !reflect.DeepEqual(accRule.Key, mergeKeyValue) {
continue
}
defaultsByKey[accIndex] = acc{
Key: accRule.Key,
Defaults: append(accRule.Defaults, value.FieldByName(defaultFieldName).Interface()),
}
found = true
}
if !found {
defaultsByKey = append(defaultsByKey, acc{
Key: mergeKeyValue,
Defaults: []interface{}{value.FieldByName(defaultFieldName).Interface()},
})
}
}
keyValues := reflect.Zero(vals.Type())
for _, confs := range defaultsByKey {
merged, err := MergeConfs(confs.Defaults)
if err != nil {
return reflect.Value{}, err
}

keyValueP := reflect.New(valType)
keyValue := keyValueP.Elem()

keyValue.FieldByName(key.Name).Set(reflect.ValueOf(confs.Key))
keyValue.FieldByName(defaultFieldName).Set(reflect.ValueOf(merged))

keyValues = reflect.Append(keyValues, keyValue)
}
return keyValues, nil
}

func findKeyAndSpec(typ reflect.Type) (reflect.StructField, bool) {
var key *reflect.StructField
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
if strings.Contains(field.Tag.Get(policyMergeTag), mergeKey) {
key = &field
break
}
}
if key == nil {
return reflect.StructField{}, false
}
if _, ok := typ.FieldByName(defaultFieldName); !ok {
return reflect.StructField{}, false
}
return *key, true
}

func newConf(t reflect.Type) (interface{}, error) {
if t.Kind() == reflect.Pointer {
return nil, errors.New("conf is expected to have a non-pointer type")
Expand All @@ -68,10 +176,11 @@ func clearAppendSlices(val reflect.Value) {
}
for i := 0; i < strVal.NumField(); i++ {
valField := strVal.Field(i)
fieldName := strVal.Type().Field(i).Name
field := strVal.Type().Field(i)
switch valField.Kind() {
case reflect.Slice:
if strings.HasPrefix(fieldName, appendSlicesPrefix) {
mergeByKey := strings.Contains(field.Tag.Get(policyMergeTag), mergeValuesByKey)
if strings.HasPrefix(field.Name, appendSlicesPrefix) || mergeByKey {
valField.Set(reflect.Zero(valField.Type()))
}
case reflect.Struct:
Expand All @@ -95,10 +204,11 @@ func appendSlices(dst reflect.Value, src reflect.Value) {
dstField := strDst.Field(i)
srcField := strSrc.Field(i)

fieldName := strDst.Type().Field(i).Name
field := strDst.Type().Field(i)
switch dstField.Kind() {
case reflect.Slice:
if strings.HasPrefix(fieldName, appendSlicesPrefix) {
mergeByKey := strings.Contains(field.Tag.Get(policyMergeTag), mergeValuesByKey)
if strings.HasPrefix(field.Name, appendSlicesPrefix) || mergeByKey {
s := reflect.AppendSlice(dstField, srcField)
dstField.Set(s)
}
Expand Down
75 changes: 75 additions & 0 deletions pkg/core/xds/merge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,4 +199,79 @@ var _ = Describe("MergeConfs", func() {
},
}),
)

type mergeKey struct {
Left bool
Right bool
}
type mergeConf struct {
A *bool `json:"a,omitempty"`
B *bool `json:"b,omitempty"`
}
type mergeEntry struct {
Key mergeKey `json:"key" policyMerge:"mergeKey"`
Default mergeConf `json:"default"`
}
type nonMergeEntry struct {
Key mergeKey `json:"key"`
Default mergeConf `json:"default"`
}
type testPolicy struct {
MergeValues []mergeEntry `policyMerge:"mergeValuesByKey"`
OtherValues *[]nonMergeEntry `json:"otherValues,omitempty"`
}

type mergeValuesByKeyCase struct {
policies []testPolicy
expected testPolicy
}

t := true
DescribeTable("mergeValuesByKey",
func(given mergeValuesByKeyCase) {
var givens []interface{}
for _, p := range given.policies {
givens = append(givens, p)
}

merged, err := xds.MergeConfs(givens)
Expect(err).ToNot(HaveOccurred())
Expect(merged).To(Equal(given.expected))
},
Entry("should work with a basic policy", mergeValuesByKeyCase{
policies: []testPolicy{{
MergeValues: []mergeEntry{{
Key: mergeKey{Left: true}, Default: mergeConf{A: &t},
}, {
Key: mergeKey{Left: true}, Default: mergeConf{B: &t},
}},
OtherValues: &[]nonMergeEntry{{
Key: mergeKey{Left: true}, Default: mergeConf{A: &t},
}, {
Key: mergeKey{Left: true}, Default: mergeConf{B: &t},
}, {
Key: mergeKey{Right: true}, Default: mergeConf{B: &t},
}},
}, {
MergeValues: []mergeEntry{{
Key: mergeKey{Right: true}, Default: mergeConf{B: &t},
}},
}},
expected: testPolicy{
MergeValues: []mergeEntry{{
Key: mergeKey{Left: true}, Default: mergeConf{A: &t, B: &t},
}, {
Key: mergeKey{Right: true}, Default: mergeConf{B: &t},
}},
OtherValues: &[]nonMergeEntry{{
Key: mergeKey{Left: true}, Default: mergeConf{A: &t},
}, {
Key: mergeKey{Left: true}, Default: mergeConf{B: &t},
}, {
Key: mergeKey{Right: true}, Default: mergeConf{B: &t},
}},
},
}),
)

})

0 comments on commit f53d7fe

Please sign in to comment.