1
0
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:
Eric Promislow
2025-06-13 10:18:48 -07:00
committed by GitHub
parent 5de54d6a4a
commit 2cd7997e6b
4 changed files with 357 additions and 17 deletions

View File

@@ -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.

View File

@@ -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) {