mirror of
https://github.com/rancher/steve.git
synced 2025-09-08 02:39:26 +00:00
Support indexing on array-like fields (#673)
* Run tests using sqlite DB in a temp directory. I was running into write-file errors which happens when two sqlite processes try to update the DB at the same time. * Implement and test the extractBarredValue custom SQL function. * Explain the DB path constants better.
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -30,7 +31,7 @@ import (
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
func makeListOptionIndexer(ctx context.Context, opts ListOptionIndexerOptions) (*ListOptionIndexer, error) {
|
||||
func makeListOptionIndexer(ctx context.Context, opts ListOptionIndexerOptions) (*ListOptionIndexer, string, error) {
|
||||
gvk := schema.GroupVersionKind{
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
@@ -41,25 +42,31 @@ func makeListOptionIndexer(ctx context.Context, opts ListOptionIndexerOptions) (
|
||||
name := informerNameFromGVK(gvk)
|
||||
m, err := encryption.NewManager()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
db, err := db.NewClient(nil, m, m)
|
||||
db, dbPath, err := db.NewClient(nil, m, m, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
s, err := store.NewStore(ctx, example, cache.DeletionHandlingMetaNamespaceKeyFunc, db, false, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
listOptionIndexer, err := NewListOptionIndexer(ctx, s, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return listOptionIndexer, nil
|
||||
return listOptionIndexer, dbPath, nil
|
||||
}
|
||||
|
||||
func cleanTempFiles(basePath string) {
|
||||
os.Remove(basePath)
|
||||
os.Remove(basePath + "-shm")
|
||||
os.Remove(basePath + "-wal")
|
||||
}
|
||||
|
||||
func TestNewListOptionIndexer(t *testing.T) {
|
||||
@@ -920,7 +927,8 @@ func TestNewListOptionIndexerEasy(t *testing.T) {
|
||||
Fields: fields,
|
||||
IsNamespaced: true,
|
||||
}
|
||||
loi, err := makeListOptionIndexer(ctx, opts)
|
||||
loi, dbPath, err := makeListOptionIndexer(ctx, opts)
|
||||
defer cleanTempFiles(dbPath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
for _, item := range itemList.Items {
|
||||
@@ -941,6 +949,216 @@ func TestNewListOptionIndexerEasy(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserDefinedExtractFunction(t *testing.T) {
|
||||
makeObj := func(name string, barSeparatedHosts string) map[string]any {
|
||||
h1 := map[string]any{
|
||||
"metadata": map[string]any{
|
||||
"name": name,
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"rules": map[string]any{
|
||||
"host": barSeparatedHosts,
|
||||
},
|
||||
},
|
||||
}
|
||||
return h1
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
type testCase struct {
|
||||
description string
|
||||
listOptions sqltypes.ListOptions
|
||||
partitions []partition.Partition
|
||||
ns string
|
||||
|
||||
items []*unstructured.Unstructured
|
||||
|
||||
extraIndexedFields [][]string
|
||||
expectedList *unstructured.UnstructuredList
|
||||
expectedTotal int
|
||||
expectedContToken string
|
||||
expectedErr error
|
||||
}
|
||||
|
||||
obj01 := makeObj("obj01", "dogs|horses|humans")
|
||||
obj02 := makeObj("obj02", "dogs|cats|fish")
|
||||
obj03 := makeObj("obj03", "camels|clowns|zebras")
|
||||
obj04 := makeObj("obj04", "aardvarks|harps|zyphyrs")
|
||||
allObjects := []map[string]any{obj01, obj02, obj03, obj04}
|
||||
makeList := func(t *testing.T, objs ...map[string]any) *unstructured.UnstructuredList {
|
||||
t.Helper()
|
||||
|
||||
if len(objs) == 0 {
|
||||
return &unstructured.UnstructuredList{Object: map[string]any{"items": []any{}}, Items: []unstructured.Unstructured{}}
|
||||
}
|
||||
|
||||
var items []any
|
||||
for _, obj := range objs {
|
||||
items = append(items, obj)
|
||||
}
|
||||
|
||||
list := &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"items": items,
|
||||
},
|
||||
}
|
||||
|
||||
itemList, err := list.ToList()
|
||||
require.NoError(t, err)
|
||||
|
||||
return itemList
|
||||
}
|
||||
itemList := makeList(t, allObjects...)
|
||||
|
||||
var tests []testCase
|
||||
tests = append(tests, testCase{
|
||||
description: "find dogs in the first substring",
|
||||
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
|
||||
{
|
||||
[]sqltypes.Filter{
|
||||
{
|
||||
Field: []string{"spec", "rules", "0", "host"},
|
||||
Matches: []string{"dogs"},
|
||||
Op: sqltypes.Eq,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
partitions: []partition.Partition{{All: true}},
|
||||
ns: "",
|
||||
expectedList: makeList(t, obj01, obj02),
|
||||
expectedTotal: 2,
|
||||
expectedContToken: "",
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "extractBarredValue on item 0 should work",
|
||||
listOptions: sqltypes.ListOptions{
|
||||
SortList: sqltypes.SortList{
|
||||
SortDirectives: []sqltypes.Sort{
|
||||
{
|
||||
Fields: []string{"spec", "rules", "0", "host"},
|
||||
Order: sqltypes.ASC,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
partitions: []partition.Partition{{All: true}},
|
||||
ns: "",
|
||||
expectedList: makeList(t, obj04, obj03, obj01, obj02),
|
||||
expectedTotal: len(allObjects),
|
||||
expectedContToken: "",
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "extractBarredValue on item 1 should work",
|
||||
listOptions: sqltypes.ListOptions{
|
||||
SortList: sqltypes.SortList{
|
||||
SortDirectives: []sqltypes.Sort{
|
||||
{
|
||||
Fields: []string{"spec", "rules", "1", "host"},
|
||||
Order: sqltypes.ASC,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
partitions: []partition.Partition{{All: true}},
|
||||
ns: "",
|
||||
expectedList: makeList(t, obj02, obj03, obj04, obj01),
|
||||
expectedTotal: len(allObjects),
|
||||
expectedContToken: "",
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "extractBarredValue on item 2 should work",
|
||||
listOptions: sqltypes.ListOptions{
|
||||
SortList: sqltypes.SortList{
|
||||
SortDirectives: []sqltypes.Sort{
|
||||
{
|
||||
Fields: []string{"spec", "rules", "2", "host"},
|
||||
Order: sqltypes.ASC,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
partitions: []partition.Partition{{All: true}},
|
||||
ns: "",
|
||||
expectedList: makeList(t, obj02, obj01, obj03, obj04),
|
||||
expectedTotal: len(allObjects),
|
||||
expectedContToken: "",
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "extractBarredValue on item 3 should fall back to default sorting",
|
||||
listOptions: sqltypes.ListOptions{
|
||||
SortList: sqltypes.SortList{
|
||||
SortDirectives: []sqltypes.Sort{
|
||||
{
|
||||
Fields: []string{"spec", "rules", "3", "host"},
|
||||
Order: sqltypes.ASC,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
partitions: []partition.Partition{{All: true}},
|
||||
ns: "",
|
||||
expectedList: makeList(t, allObjects...),
|
||||
expectedTotal: len(allObjects),
|
||||
expectedContToken: "",
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "extractBarredValue on item -2 should result in a compile error",
|
||||
listOptions: sqltypes.ListOptions{
|
||||
SortList: sqltypes.SortList{
|
||||
SortDirectives: []sqltypes.Sort{
|
||||
{
|
||||
Fields: []string{"spec", "rules", "-2", "host"},
|
||||
Order: sqltypes.ASC,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
partitions: []partition.Partition{{All: true}},
|
||||
ns: "",
|
||||
expectedErr: errors.New("column is invalid [spec.rules.-2.host]: supplied column is invalid"),
|
||||
})
|
||||
t.Parallel()
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
fields := [][]string{
|
||||
{"spec", "rules", "host"},
|
||||
}
|
||||
fields = append(fields, test.extraIndexedFields...)
|
||||
|
||||
opts := ListOptionIndexerOptions{
|
||||
Fields: fields,
|
||||
IsNamespaced: true,
|
||||
}
|
||||
loi, dbPath, err := makeListOptionIndexer(ctx, opts)
|
||||
defer cleanTempFiles(dbPath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
for _, item := range itemList.Items {
|
||||
err = loi.Add(&item)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
list, total, contToken, err := loi.ListByOptions(ctx, &test.listOptions, test.partitions, test.ns)
|
||||
if test.expectedErr != nil {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.expectedList, list)
|
||||
assert.Equal(t, test.expectedTotal, total)
|
||||
assert.Equal(t, test.expectedContToken, contToken)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstructQuery(t *testing.T) {
|
||||
type testCase struct {
|
||||
description string
|
||||
@@ -1365,6 +1583,87 @@ func TestConstructQuery(t *testing.T) {
|
||||
expectedStmtArgs: []any{"numericThing", float64(35)},
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "TestConstructQuery: uses the extractBarredValue custom function for penultimate indexer",
|
||||
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
|
||||
{
|
||||
[]sqltypes.Filter{
|
||||
{
|
||||
Field: []string{"spec", "containers", "3", "image"},
|
||||
Matches: []string{"nginx-happy"},
|
||||
Op: sqltypes.Eq,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
partitions: []partition.Partition{},
|
||||
ns: "",
|
||||
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||
JOIN "something_fields" f ON o.key = f.key
|
||||
WHERE
|
||||
(extractBarredValue(f."spec.containers.image", "3") = ?) AND
|
||||
(FALSE)
|
||||
ORDER BY f."metadata.name" ASC `,
|
||||
expectedStmtArgs: []any{"nginx-happy"},
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "TestConstructQuery: uses the extractBarredValue custom function for penultimate indexer when sorting",
|
||||
listOptions: sqltypes.ListOptions{
|
||||
SortList: sqltypes.SortList{
|
||||
SortDirectives: []sqltypes.Sort{
|
||||
{
|
||||
Fields: []string{"spec", "containers", "16", "image"},
|
||||
Order: sqltypes.ASC,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
partitions: []partition.Partition{},
|
||||
ns: "",
|
||||
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||
JOIN "something_fields" f ON o.key = f.key
|
||||
WHERE
|
||||
(FALSE)
|
||||
ORDER BY extractBarredValue(f."spec.containers.image", "16") ASC`,
|
||||
expectedStmtArgs: []any{},
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "TestConstructQuery: uses the extractBarredValue custom function for penultimate indexer when both filtering and sorting",
|
||||
listOptions: sqltypes.ListOptions{
|
||||
Filters: []sqltypes.OrFilter{
|
||||
{
|
||||
[]sqltypes.Filter{
|
||||
{
|
||||
Field: []string{"spec", "containers", "3", "image"},
|
||||
Matches: []string{"nginx-happy"},
|
||||
Op: sqltypes.Eq,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
SortList: sqltypes.SortList{
|
||||
SortDirectives: []sqltypes.Sort{
|
||||
{
|
||||
Fields: []string{"spec", "containers", "16", "image"},
|
||||
Order: sqltypes.ASC,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
partitions: []partition.Partition{},
|
||||
ns: "",
|
||||
expectedStmt: `SELECT o.object, o.objectnonce, o.dekid FROM "something" o
|
||||
JOIN "something_fields" f ON o.key = f.key
|
||||
WHERE
|
||||
(extractBarredValue(f."spec.containers.image", "3") = ?) AND
|
||||
(FALSE)
|
||||
ORDER BY extractBarredValue(f."spec.containers.image", "16") ASC`,
|
||||
expectedStmtArgs: []any{"nginx-happy"},
|
||||
expectedErr: nil,
|
||||
})
|
||||
tests = append(tests, testCase{
|
||||
description: "multiple filters with a positive label test and a negative non-label test still outer-join",
|
||||
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
|
||||
@@ -1565,7 +1864,7 @@ func TestConstructQuery(t *testing.T) {
|
||||
}
|
||||
lii := &ListOptionIndexer{
|
||||
Indexer: i,
|
||||
indexedFields: []string{"metadata.queryField1", "status.queryField2"},
|
||||
indexedFields: []string{"metadata.queryField1", "status.queryField2", "spec.containers.image"},
|
||||
}
|
||||
queryInfo, err := lii.constructQuery(&test.listOptions, test.partitions, test.ns, "something")
|
||||
if test.expectedErr != nil {
|
||||
@@ -1862,7 +2161,8 @@ func TestWatchMany(t *testing.T) {
|
||||
},
|
||||
IsNamespaced: true,
|
||||
}
|
||||
loi, err := makeListOptionIndexer(ctx, opts)
|
||||
loi, dbPath, err := makeListOptionIndexer(ctx, opts)
|
||||
defer cleanTempFiles(dbPath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
startWatcher := func(ctx context.Context) (chan watch.Event, chan error) {
|
||||
@@ -2118,7 +2418,8 @@ func TestWatchFilter(t *testing.T) {
|
||||
Fields: [][]string{{"metadata", "somefield"}},
|
||||
IsNamespaced: true,
|
||||
}
|
||||
loi, err := makeListOptionIndexer(ctx, opts)
|
||||
loi, dbPath, err := makeListOptionIndexer(ctx, opts)
|
||||
defer cleanTempFiles(dbPath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
wCh, errCh := startWatcher(ctx, loi, WatchFilter{
|
||||
@@ -2209,7 +2510,8 @@ func TestWatchResourceVersion(t *testing.T) {
|
||||
opts := ListOptionIndexerOptions{
|
||||
IsNamespaced: true,
|
||||
}
|
||||
loi, err := makeListOptionIndexer(parentCtx, opts)
|
||||
loi, dbPath, err := makeListOptionIndexer(parentCtx, opts)
|
||||
defer cleanTempFiles(dbPath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
getRV := func(t *testing.T) string {
|
||||
@@ -2361,7 +2663,8 @@ func TestWatchGarbageCollection(t *testing.T) {
|
||||
opts := ListOptionIndexerOptions{
|
||||
MaximumEventsCount: 2,
|
||||
}
|
||||
loi, err := makeListOptionIndexer(parentCtx, opts)
|
||||
loi, dbPath, err := makeListOptionIndexer(parentCtx, opts)
|
||||
defer cleanTempFiles(dbPath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
getRV := func(t *testing.T) string {
|
||||
@@ -2465,7 +2768,8 @@ func TestNonNumberResourceVersion(t *testing.T) {
|
||||
Fields: [][]string{{"metadata", "somefield"}},
|
||||
IsNamespaced: true,
|
||||
}
|
||||
loi, err := makeListOptionIndexer(ctx, opts)
|
||||
loi, dbPath, err := makeListOptionIndexer(ctx, opts)
|
||||
defer cleanTempFiles(dbPath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
foo := &unstructured.Unstructured{
|
||||
|
Reference in New Issue
Block a user