mirror of
https://github.com/rancher/steve.git
synced 2025-09-16 07:18:28 +00:00
Fix non-vai sorting involving string arrays, add tests. (#618)
- Bump wrangler to get fix for 'GetValueFromAny'. This adds string arrays to the types it supports. - Use a newer go library sort function to maintain a stable sort - Use the map-sort-map pattern to avoid repeatedly calculating the same underlying value for `object[arg1][arg2][...]`
This commit is contained in:
@@ -4,6 +4,7 @@ package listprocessor
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"github.com/rancher/wrangler/v3/pkg/data"
|
||||
"github.com/rancher/wrangler/v3/pkg/data/convert"
|
||||
corecontrollers "github.com/rancher/wrangler/v3/pkg/generated/controllers/core/v1"
|
||||
"github.com/sirupsen/logrus"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
@@ -353,26 +355,61 @@ func matchesAll(obj map[string]interface{}, filters []OrFilter) bool {
|
||||
}
|
||||
|
||||
// SortList sorts the slice by the provided sort criteria.
|
||||
// Convert the sorted list so we calculate the sort values only once per node,
|
||||
// making this an O(n) operation rather than an O(n**2) one.
|
||||
//
|
||||
// list of u.U => list of { *u.U, []stringValues } =>
|
||||
// sorted list of { *u.U, []stringValues } =>
|
||||
// final list of u.U from the above sorted list
|
||||
//
|
||||
// We do this because it's much faster to compare two string arrays then to compare
|
||||
// the string arrays based on walking objects repeatedly
|
||||
func SortList(list []unstructured.Unstructured, s Sort) []unstructured.Unstructured {
|
||||
if len(s.Fields) == 0 {
|
||||
return list
|
||||
}
|
||||
sort.Slice(list, func(i, j int) bool {
|
||||
leftNode := list[i].Object
|
||||
rightNode := list[j].Object
|
||||
for i, field := range s.Fields {
|
||||
leftValue := convert.ToString(data.GetValueN(leftNode, field...))
|
||||
rightValue := convert.ToString(data.GetValueN(rightNode, field...))
|
||||
if leftValue != rightValue {
|
||||
if s.Orders[i] == ASC {
|
||||
return leftValue < rightValue
|
||||
}
|
||||
return rightValue < leftValue
|
||||
type transformedData struct {
|
||||
originalItem *unstructured.Unstructured
|
||||
stringValues []string
|
||||
}
|
||||
transformedList := make([]transformedData, len(list))
|
||||
for i := range list {
|
||||
td := &transformedList[i]
|
||||
td.originalItem = &list[i]
|
||||
td.stringValues = make([]string, len(s.Fields))
|
||||
for j, fields := range s.Fields {
|
||||
val, ok := data.GetValueFromAny(list[i].Object, fields...)
|
||||
if ok {
|
||||
td.stringValues[j] = val.(string)
|
||||
} else {
|
||||
logrus.Debugf("Failed to walk list item %d with fields %s", i, fields)
|
||||
td.stringValues[j] = ""
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
return list
|
||||
}
|
||||
// We can't use slices.Compare(leftNode.stringValues, rightNode.stringValues)
|
||||
// because some comparisons might use ASC-order, and others DESC-order.
|
||||
sortFunc := func(leftNode, rightNode transformedData) int {
|
||||
for i := range leftNode.stringValues {
|
||||
diff := strings.Compare(leftNode.stringValues[i], rightNode.stringValues[i])
|
||||
if diff == 0 {
|
||||
// the two substrings are the same, so continue walking the arrays
|
||||
continue
|
||||
}
|
||||
if s.Orders[i] == DESC {
|
||||
// reversing sort order just means inverting the comparison value
|
||||
diff *= -1
|
||||
}
|
||||
return diff
|
||||
}
|
||||
return 0
|
||||
}
|
||||
slices.SortStableFunc(transformedList, sortFunc)
|
||||
newList := make([]unstructured.Unstructured, len(list))
|
||||
for i := range transformedList {
|
||||
newList[i] = *transformedList[i].originalItem
|
||||
}
|
||||
return newList
|
||||
}
|
||||
|
||||
// PaginateList returns a subset of the result based on the pagination criteria as well as the total number of pages the caller can expect.
|
||||
|
@@ -2362,6 +2362,309 @@ func TestSortList(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sort metadata.fields[1]",
|
||||
objects: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "fuji",
|
||||
"fields": []string{
|
||||
"a1",
|
||||
"position3", // this is where we expect to see this block after sorting
|
||||
},
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "pink",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "honeycrisp",
|
||||
"fields": []string{
|
||||
"a2",
|
||||
"position1",
|
||||
},
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "pink",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "granny-smith",
|
||||
"fields": []string{
|
||||
"a3",
|
||||
"position2",
|
||||
},
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "green",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sort: Sort{
|
||||
Fields: [][]string{{"metadata", "fields", "1"}},
|
||||
Orders: []SortOrder{ASC},
|
||||
},
|
||||
want: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "honeycrisp",
|
||||
"fields": []string{
|
||||
"a2",
|
||||
"position1",
|
||||
},
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "pink",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "granny-smith",
|
||||
"fields": []string{
|
||||
"a3",
|
||||
"position2",
|
||||
},
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "green",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "fuji",
|
||||
"fields": []string{
|
||||
"a1",
|
||||
"position3",
|
||||
},
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "pink",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sort metadata.fields[1] reverse order",
|
||||
objects: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "fuji",
|
||||
"fields": []string{
|
||||
"a1",
|
||||
"position3", // this is where we expect to see this block after sorting, but in reverse
|
||||
},
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "pink",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "honeycrisp",
|
||||
"fields": []string{
|
||||
"a2",
|
||||
"position1",
|
||||
},
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "pink",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "granny-smith",
|
||||
"fields": []string{
|
||||
"a3",
|
||||
"position2",
|
||||
},
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "green",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sort: Sort{
|
||||
Fields: [][]string{{"metadata", "fields", "1"}},
|
||||
Orders: []SortOrder{DESC},
|
||||
},
|
||||
want: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "fuji",
|
||||
"fields": []string{
|
||||
"a1",
|
||||
"position3",
|
||||
},
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "pink",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "granny-smith",
|
||||
"fields": []string{
|
||||
"a3",
|
||||
"position2",
|
||||
},
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "green",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "honeycrisp",
|
||||
"fields": []string{
|
||||
"a2",
|
||||
"position1",
|
||||
},
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "pink",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sort metadata.fields[1], preserve ties",
|
||||
objects: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "fuji",
|
||||
"fields": []string{
|
||||
"a1",
|
||||
"position3", // this is where we expect to see this block after sorting
|
||||
},
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "pink",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "honeycrisp",
|
||||
"fields": []string{
|
||||
"a2",
|
||||
"position1",
|
||||
},
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "pink",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "granny-smith",
|
||||
"fields": []string{
|
||||
"a3",
|
||||
"position1", // but should be second
|
||||
},
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "green",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sort: Sort{
|
||||
Fields: [][]string{{"metadata", "fields", "1"}},
|
||||
Orders: []SortOrder{ASC},
|
||||
},
|
||||
want: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "honeycrisp",
|
||||
"fields": []string{
|
||||
"a2",
|
||||
"position1",
|
||||
},
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "pink",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "granny-smith",
|
||||
"fields": []string{
|
||||
"a3",
|
||||
"position1",
|
||||
},
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "green",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "apple",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "fuji",
|
||||
"fields": []string{
|
||||
"a1",
|
||||
"position3",
|
||||
},
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"color": "pink",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
Reference in New Issue
Block a user