mirror of
https://github.com/rancher/steve.git
synced 2025-07-13 22:54:57 +00:00
* Remove unused method * Add basic watch functionality * Remove TestWatchNamesErrReceive test
1932 lines
59 KiB
Go
1932 lines
59 KiB
Go
/*
|
|
Copyright 2023 SUSE LLC
|
|
|
|
Adapted from client-go, Copyright 2014 The Kubernetes Authors.
|
|
*/
|
|
|
|
package informer
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rancher/steve/pkg/sqlcache/db"
|
|
"github.com/rancher/steve/pkg/sqlcache/encryption"
|
|
"github.com/rancher/steve/pkg/sqlcache/partition"
|
|
"github.com/rancher/steve/pkg/sqlcache/sqltypes"
|
|
"github.com/rancher/steve/pkg/sqlcache/store"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/mock/gomock"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/apimachinery/pkg/util/sets"
|
|
watch "k8s.io/apimachinery/pkg/watch"
|
|
"k8s.io/client-go/tools/cache"
|
|
)
|
|
|
|
func makeListOptionIndexer(ctx context.Context, fields [][]string) (*ListOptionIndexer, error) {
|
|
gvk := schema.GroupVersionKind{
|
|
Group: "",
|
|
Version: "v1",
|
|
Kind: "ConfigMap",
|
|
}
|
|
example := &unstructured.Unstructured{}
|
|
example.SetGroupVersionKind(gvk)
|
|
name := informerNameFromGVK(gvk)
|
|
m, err := encryption.NewManager()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
db, err := db.NewClient(nil, m, m)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s, err := store.NewStore(ctx, example, cache.DeletionHandlingMetaNamespaceKeyFunc, db, false, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
listOptionIndexer, err := NewListOptionIndexer(ctx, fields, s, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return listOptionIndexer, nil
|
|
}
|
|
|
|
func TestNewListOptionIndexer(t *testing.T) {
|
|
type testCase struct {
|
|
description string
|
|
test func(t *testing.T)
|
|
}
|
|
|
|
var tests []testCase
|
|
tests = append(tests, testCase{description: "NewListOptionIndexer() with no errors returned, should return no error", test: func(t *testing.T) {
|
|
txClient := NewMockTXClient(gomock.NewController(t))
|
|
store := NewMockStore(gomock.NewController(t))
|
|
fields := [][]string{{"something"}}
|
|
id := "somename"
|
|
stmt := &sql.Stmt{}
|
|
// logic for NewIndexer(), only interested in if this results in error or not
|
|
store.EXPECT().GetName().Return(id).AnyTimes()
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
store.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(nil).Do(
|
|
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
|
err := f(txClient)
|
|
if err != nil {
|
|
t.Fail()
|
|
}
|
|
})
|
|
store.EXPECT().RegisterAfterAdd(gomock.Any())
|
|
store.EXPECT().RegisterAfterUpdate(gomock.Any())
|
|
store.EXPECT().Prepare(gomock.Any()).Return(stmt).AnyTimes()
|
|
// end NewIndexer() logic
|
|
|
|
store.EXPECT().RegisterAfterAdd(gomock.Any()).Times(3)
|
|
store.EXPECT().RegisterAfterUpdate(gomock.Any()).Times(3)
|
|
store.EXPECT().RegisterAfterDelete(gomock.Any()).Times(3)
|
|
store.EXPECT().RegisterAfterDeleteAll(gomock.Any()).Times(2)
|
|
|
|
// create field table
|
|
txClient.EXPECT().Exec(fmt.Sprintf(createFieldsTableFmt, id, `"metadata.name" TEXT, "metadata.creationTimestamp" TEXT, "metadata.namespace" TEXT, "something" TEXT`)).Return(nil, nil)
|
|
// create field table indexes
|
|
txClient.EXPECT().Exec(fmt.Sprintf(createFieldsIndexFmt, id, "metadata.name", id, "metadata.name")).Return(nil, nil)
|
|
txClient.EXPECT().Exec(fmt.Sprintf(createFieldsIndexFmt, id, "metadata.namespace", id, "metadata.namespace")).Return(nil, nil)
|
|
txClient.EXPECT().Exec(fmt.Sprintf(createFieldsIndexFmt, id, "metadata.creationTimestamp", id, "metadata.creationTimestamp")).Return(nil, nil)
|
|
txClient.EXPECT().Exec(fmt.Sprintf(createFieldsIndexFmt, id, fields[0][0], id, fields[0][0])).Return(nil, nil)
|
|
txClient.EXPECT().Exec(fmt.Sprintf(createLabelsTableFmt, id, id)).Return(nil, nil)
|
|
txClient.EXPECT().Exec(fmt.Sprintf(createLabelsTableIndexFmt, id, id)).Return(nil, nil)
|
|
store.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(nil).Do(
|
|
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
|
err := f(txClient)
|
|
if err != nil {
|
|
t.Fail()
|
|
}
|
|
})
|
|
|
|
loi, err := NewListOptionIndexer(context.Background(), fields, store, true)
|
|
assert.Nil(t, err)
|
|
assert.NotNil(t, loi)
|
|
}})
|
|
tests = append(tests, testCase{description: "NewListOptionIndexer() with error returned from NewIndexer(), should return an error", test: func(t *testing.T) {
|
|
txClient := NewMockTXClient(gomock.NewController(t))
|
|
store := NewMockStore(gomock.NewController(t))
|
|
fields := [][]string{{"something"}}
|
|
id := "somename"
|
|
// logic for NewIndexer(), only interested in if this results in error or not
|
|
store.EXPECT().GetName().Return(id).AnyTimes()
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
store.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(fmt.Errorf("error")).Do(
|
|
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
|
err := f(txClient)
|
|
if err != nil {
|
|
t.Fail()
|
|
}
|
|
})
|
|
|
|
_, err := NewListOptionIndexer(context.Background(), fields, store, false)
|
|
assert.NotNil(t, err)
|
|
}})
|
|
tests = append(tests, testCase{description: "NewListOptionIndexer() with error returned from Begin(), should return an error", test: func(t *testing.T) {
|
|
txClient := NewMockTXClient(gomock.NewController(t))
|
|
store := NewMockStore(gomock.NewController(t))
|
|
fields := [][]string{{"something"}}
|
|
id := "somename"
|
|
stmt := &sql.Stmt{}
|
|
// logic for NewIndexer(), only interested in if this results in error or not
|
|
store.EXPECT().GetName().Return(id).AnyTimes()
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
store.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(nil).Do(
|
|
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
|
err := f(txClient)
|
|
if err != nil {
|
|
t.Fail()
|
|
}
|
|
})
|
|
store.EXPECT().RegisterAfterAdd(gomock.Any())
|
|
store.EXPECT().RegisterAfterUpdate(gomock.Any())
|
|
store.EXPECT().Prepare(gomock.Any()).Return(stmt).AnyTimes()
|
|
// end NewIndexer() logic
|
|
|
|
store.EXPECT().RegisterAfterAdd(gomock.Any()).Times(3)
|
|
store.EXPECT().RegisterAfterUpdate(gomock.Any()).Times(3)
|
|
store.EXPECT().RegisterAfterDelete(gomock.Any()).Times(3)
|
|
store.EXPECT().RegisterAfterDeleteAll(gomock.Any()).Times(2)
|
|
|
|
store.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(fmt.Errorf("error"))
|
|
|
|
_, err := NewListOptionIndexer(context.Background(), fields, store, false)
|
|
assert.NotNil(t, err)
|
|
}})
|
|
tests = append(tests, testCase{description: "NewListOptionIndexer() with error from Exec() when creating fields table, should return an error", test: func(t *testing.T) {
|
|
txClient := NewMockTXClient(gomock.NewController(t))
|
|
store := NewMockStore(gomock.NewController(t))
|
|
fields := [][]string{{"something"}}
|
|
id := "somename"
|
|
stmt := &sql.Stmt{}
|
|
// logic for NewIndexer(), only interested in if this results in error or not
|
|
store.EXPECT().GetName().Return(id).AnyTimes()
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
store.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(nil).Do(
|
|
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
|
err := f(txClient)
|
|
if err != nil {
|
|
t.Fail()
|
|
}
|
|
})
|
|
store.EXPECT().RegisterAfterAdd(gomock.Any())
|
|
store.EXPECT().RegisterAfterUpdate(gomock.Any())
|
|
store.EXPECT().Prepare(gomock.Any()).Return(stmt).AnyTimes()
|
|
// end NewIndexer() logic
|
|
|
|
store.EXPECT().RegisterAfterAdd(gomock.Any()).Times(3)
|
|
store.EXPECT().RegisterAfterUpdate(gomock.Any()).Times(3)
|
|
store.EXPECT().RegisterAfterDelete(gomock.Any()).Times(3)
|
|
store.EXPECT().RegisterAfterDeleteAll(gomock.Any()).Times(2)
|
|
|
|
txClient.EXPECT().Exec(fmt.Sprintf(createFieldsTableFmt, id, `"metadata.name" TEXT, "metadata.creationTimestamp" TEXT, "metadata.namespace" TEXT, "something" TEXT`)).Return(nil, nil)
|
|
txClient.EXPECT().Exec(fmt.Sprintf(createFieldsIndexFmt, id, "metadata.name", id, "metadata.name")).Return(nil, fmt.Errorf("error"))
|
|
store.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(fmt.Errorf("error")).Do(
|
|
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
|
err := f(txClient)
|
|
if err == nil {
|
|
t.Fail()
|
|
}
|
|
})
|
|
|
|
_, err := NewListOptionIndexer(context.Background(), fields, store, true)
|
|
assert.NotNil(t, err)
|
|
}})
|
|
tests = append(tests, testCase{description: "NewListOptionIndexer() with error from create-labels, should return an error", test: func(t *testing.T) {
|
|
txClient := NewMockTXClient(gomock.NewController(t))
|
|
store := NewMockStore(gomock.NewController(t))
|
|
fields := [][]string{{"something"}}
|
|
id := "somename"
|
|
stmt := &sql.Stmt{}
|
|
// logic for NewIndexer(), only interested in if this results in error or not
|
|
store.EXPECT().GetName().Return(id).AnyTimes()
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
store.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(nil).Do(
|
|
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
|
err := f(txClient)
|
|
if err != nil {
|
|
t.Fail()
|
|
}
|
|
})
|
|
store.EXPECT().RegisterAfterAdd(gomock.Any())
|
|
store.EXPECT().RegisterAfterUpdate(gomock.Any())
|
|
store.EXPECT().Prepare(gomock.Any()).Return(stmt).AnyTimes()
|
|
// end NewIndexer() logic
|
|
|
|
store.EXPECT().RegisterAfterAdd(gomock.Any()).Times(3)
|
|
store.EXPECT().RegisterAfterUpdate(gomock.Any()).Times(3)
|
|
store.EXPECT().RegisterAfterDelete(gomock.Any()).Times(3)
|
|
store.EXPECT().RegisterAfterDeleteAll(gomock.Any()).Times(2)
|
|
|
|
txClient.EXPECT().Exec(fmt.Sprintf(createFieldsTableFmt, id, `"metadata.name" TEXT, "metadata.creationTimestamp" TEXT, "metadata.namespace" TEXT, "something" TEXT`)).Return(nil, nil)
|
|
txClient.EXPECT().Exec(fmt.Sprintf(createFieldsIndexFmt, id, "metadata.name", id, "metadata.name")).Return(nil, nil)
|
|
txClient.EXPECT().Exec(fmt.Sprintf(createFieldsIndexFmt, id, "metadata.namespace", id, "metadata.namespace")).Return(nil, nil)
|
|
txClient.EXPECT().Exec(fmt.Sprintf(createFieldsIndexFmt, id, "metadata.creationTimestamp", id, "metadata.creationTimestamp")).Return(nil, nil)
|
|
txClient.EXPECT().Exec(fmt.Sprintf(createFieldsIndexFmt, id, fields[0][0], id, fields[0][0])).Return(nil, nil)
|
|
txClient.EXPECT().Exec(fmt.Sprintf(createLabelsTableFmt, id, id)).Return(nil, fmt.Errorf("error"))
|
|
store.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(fmt.Errorf("error")).Do(
|
|
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
|
err := f(txClient)
|
|
if err == nil {
|
|
t.Fail()
|
|
}
|
|
})
|
|
|
|
_, err := NewListOptionIndexer(context.Background(), fields, store, true)
|
|
assert.NotNil(t, err)
|
|
}})
|
|
tests = append(tests, testCase{description: "NewListOptionIndexer() with error from Commit(), should return an error", test: func(t *testing.T) {
|
|
txClient := NewMockTXClient(gomock.NewController(t))
|
|
store := NewMockStore(gomock.NewController(t))
|
|
fields := [][]string{{"something"}}
|
|
id := "somename"
|
|
stmt := &sql.Stmt{}
|
|
// logic for NewIndexer(), only interested in if this results in error or not
|
|
store.EXPECT().GetName().Return(id).AnyTimes()
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
txClient.EXPECT().Exec(gomock.Any()).Return(nil, nil)
|
|
store.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(nil).Do(
|
|
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
|
err := f(txClient)
|
|
if err != nil {
|
|
t.Fail()
|
|
}
|
|
})
|
|
store.EXPECT().RegisterAfterAdd(gomock.Any())
|
|
store.EXPECT().RegisterAfterUpdate(gomock.Any())
|
|
store.EXPECT().Prepare(gomock.Any()).Return(stmt).AnyTimes()
|
|
// end NewIndexer() logic
|
|
|
|
store.EXPECT().RegisterAfterAdd(gomock.Any()).Times(3)
|
|
store.EXPECT().RegisterAfterUpdate(gomock.Any()).Times(3)
|
|
store.EXPECT().RegisterAfterDelete(gomock.Any()).Times(3)
|
|
store.EXPECT().RegisterAfterDeleteAll(gomock.Any()).Times(2)
|
|
|
|
txClient.EXPECT().Exec(fmt.Sprintf(createFieldsTableFmt, id, `"metadata.name" TEXT, "metadata.creationTimestamp" TEXT, "metadata.namespace" TEXT, "something" TEXT`)).Return(nil, nil)
|
|
txClient.EXPECT().Exec(fmt.Sprintf(createFieldsIndexFmt, id, "metadata.name", id, "metadata.name")).Return(nil, nil)
|
|
txClient.EXPECT().Exec(fmt.Sprintf(createFieldsIndexFmt, id, "metadata.namespace", id, "metadata.namespace")).Return(nil, nil)
|
|
txClient.EXPECT().Exec(fmt.Sprintf(createFieldsIndexFmt, id, "metadata.creationTimestamp", id, "metadata.creationTimestamp")).Return(nil, nil)
|
|
txClient.EXPECT().Exec(fmt.Sprintf(createFieldsIndexFmt, id, fields[0][0], id, fields[0][0])).Return(nil, nil)
|
|
txClient.EXPECT().Exec(fmt.Sprintf(createLabelsTableFmt, id, id)).Return(nil, nil)
|
|
txClient.EXPECT().Exec(fmt.Sprintf(createLabelsTableIndexFmt, id, id)).Return(nil, nil)
|
|
store.EXPECT().WithTransaction(gomock.Any(), true, gomock.Any()).Return(fmt.Errorf("error")).Do(
|
|
func(ctx context.Context, shouldEncrypt bool, f db.WithTransactionFunction) {
|
|
err := f(txClient)
|
|
if err != nil {
|
|
t.Fail()
|
|
}
|
|
})
|
|
|
|
_, err := NewListOptionIndexer(context.Background(), fields, store, true)
|
|
assert.NotNil(t, err)
|
|
}})
|
|
|
|
t.Parallel()
|
|
for _, test := range tests {
|
|
t.Run(test.description, func(t *testing.T) { test.test(t) })
|
|
}
|
|
}
|
|
|
|
func TestNewListOptionIndexerEasy(t *testing.T) {
|
|
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
|
|
}
|
|
foo := map[string]any{
|
|
"metadata": map[string]any{
|
|
"name": "obj1",
|
|
"namespace": "ns-a",
|
|
"somefield": "foo",
|
|
"sortfield": "4",
|
|
},
|
|
}
|
|
bar := map[string]any{
|
|
"metadata": map[string]any{
|
|
"name": "obj2",
|
|
"namespace": "ns-a",
|
|
"somefield": "bar",
|
|
"sortfield": "1",
|
|
"labels": map[string]any{
|
|
"cows": "milk",
|
|
"horses": "saddles",
|
|
},
|
|
},
|
|
}
|
|
baz := map[string]any{
|
|
"metadata": map[string]any{
|
|
"name": "obj3",
|
|
"namespace": "ns-a",
|
|
"somefield": "baz",
|
|
"sortfield": "2",
|
|
"labels": map[string]any{
|
|
"horses": "saddles",
|
|
},
|
|
},
|
|
"status": map[string]any{
|
|
"someotherfield": "helloworld",
|
|
},
|
|
}
|
|
toto := map[string]any{
|
|
"metadata": map[string]any{
|
|
"name": "obj4",
|
|
"namespace": "ns-a",
|
|
"somefield": "toto",
|
|
"sortfield": "2",
|
|
"labels": map[string]any{
|
|
"cows": "milk",
|
|
},
|
|
},
|
|
}
|
|
lodgePole := map[string]any{
|
|
"metadata": map[string]any{
|
|
"name": "obj5",
|
|
"namespace": "ns-b",
|
|
"unknown": "hi",
|
|
"labels": map[string]any{
|
|
"guard.cattle.io": "lodgepole",
|
|
},
|
|
},
|
|
}
|
|
|
|
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, foo, bar, baz, toto, lodgePole)
|
|
|
|
var tests []testCase
|
|
tests = append(tests, testCase{
|
|
description: "ListByOptions() with no errors returned, should not return an error",
|
|
listOptions: sqltypes.ListOptions{},
|
|
partitions: []partition.Partition{},
|
|
ns: "",
|
|
expectedList: makeList(t),
|
|
expectedTotal: 0,
|
|
expectedContToken: "",
|
|
expectedErr: nil,
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "ListByOptions() with an empty filter, should not return an error",
|
|
listOptions: sqltypes.ListOptions{
|
|
Filters: []sqltypes.OrFilter{{[]sqltypes.Filter{}}},
|
|
},
|
|
partitions: []partition.Partition{},
|
|
ns: "",
|
|
expectedList: makeList(t),
|
|
expectedTotal: 0,
|
|
expectedContToken: "",
|
|
expectedErr: nil,
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "ListByOptions with 1 OrFilter set with 1 filter should select where that filter is true in prepared sql.Stmt",
|
|
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
|
|
{
|
|
[]sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "somefield"},
|
|
Matches: []string{"foo"},
|
|
Op: sqltypes.Eq,
|
|
Partial: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{{All: true}},
|
|
ns: "",
|
|
expectedList: makeList(t, foo),
|
|
expectedTotal: 1,
|
|
expectedContToken: "",
|
|
expectedErr: nil,
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "ListByOptions with 1 OrFilter set with 1 filter with Op set top NotEq should select where that filter is not true in prepared sql.Stmt",
|
|
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
|
|
{
|
|
[]sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "somefield"},
|
|
Matches: []string{"foo"},
|
|
Op: sqltypes.NotEq,
|
|
Partial: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{{All: true}},
|
|
ns: "",
|
|
expectedList: makeList(t, bar, baz, toto, lodgePole),
|
|
expectedTotal: 4,
|
|
expectedContToken: "",
|
|
expectedErr: nil,
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "ListByOptions with 1 OrFilter set with 1 filter with Partial set to true should select where that partial match on that filter's value is true in prepared sql.Stmt",
|
|
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
|
|
{
|
|
[]sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "somefield"},
|
|
Matches: []string{"o"},
|
|
Op: sqltypes.Eq,
|
|
Partial: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{{All: true}},
|
|
ns: "",
|
|
expectedList: makeList(t, foo, toto),
|
|
expectedTotal: 2,
|
|
expectedContToken: "",
|
|
expectedErr: nil,
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "ListByOptions with 1 OrFilter set with multiple filters should select where any of those filters are true in prepared sql.Stmt",
|
|
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
|
|
{
|
|
[]sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "somefield"},
|
|
Matches: []string{"foo"},
|
|
Op: sqltypes.Eq,
|
|
Partial: true,
|
|
},
|
|
{
|
|
Field: []string{"metadata", "somefield"},
|
|
Matches: []string{"bar"},
|
|
Op: sqltypes.Eq,
|
|
Partial: true,
|
|
},
|
|
{
|
|
Field: []string{"metadata", "somefield"},
|
|
Matches: []string{"toto"},
|
|
Op: sqltypes.NotEq,
|
|
Partial: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{{All: true}},
|
|
ns: "",
|
|
expectedList: makeList(t, foo, bar, baz, lodgePole),
|
|
expectedTotal: 4,
|
|
expectedContToken: "",
|
|
expectedErr: nil,
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "ListByOptions with multiple OrFilters set should select where all OrFilters contain one filter that is true in prepared sql.Stmt",
|
|
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
|
|
{
|
|
Filters: []sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "somefield"},
|
|
Matches: []string{"foo"},
|
|
Op: sqltypes.Eq,
|
|
Partial: false,
|
|
},
|
|
{
|
|
Field: []string{"status", "someotherfield"},
|
|
Matches: []string{"helloworld"},
|
|
Op: sqltypes.NotEq,
|
|
Partial: false,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Filters: []sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "somefield"},
|
|
Matches: []string{"toto"},
|
|
Op: sqltypes.Eq,
|
|
Partial: false,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{{All: true}},
|
|
ns: "",
|
|
expectedList: makeList(t, toto),
|
|
expectedTotal: 1,
|
|
expectedContToken: "",
|
|
expectedErr: nil,
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "ListByOptions with labels filter should select the label in the prepared sql.Stmt",
|
|
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
|
|
{
|
|
Filters: []sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "labels", "guard.cattle.io"},
|
|
Matches: []string{"lodgepole"},
|
|
Op: sqltypes.Eq,
|
|
Partial: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{{All: true}},
|
|
ns: "",
|
|
expectedList: makeList(t, lodgePole),
|
|
expectedTotal: 1,
|
|
expectedContToken: "",
|
|
expectedErr: nil,
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "ListByOptions with two labels filters should use a self-join",
|
|
listOptions: sqltypes.ListOptions{
|
|
Filters: []sqltypes.OrFilter{
|
|
{
|
|
Filters: []sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "labels", "cows"},
|
|
Matches: []string{"milk"},
|
|
Op: sqltypes.Eq,
|
|
Partial: false,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Filters: []sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "labels", "horses"},
|
|
Matches: []string{"saddles"},
|
|
Op: sqltypes.Eq,
|
|
Partial: false,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{{All: true}},
|
|
ns: "",
|
|
expectedList: makeList(t, bar),
|
|
expectedTotal: 1,
|
|
expectedContToken: "",
|
|
expectedErr: nil,
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "ListByOptions with a mix of one label and one non-label query can still self-join",
|
|
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
|
|
{
|
|
Filters: []sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "labels", "cows"},
|
|
Matches: []string{"milk"},
|
|
Op: sqltypes.Eq,
|
|
Partial: false,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Filters: []sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "somefield"},
|
|
Matches: []string{"toto"},
|
|
Op: sqltypes.Eq,
|
|
Partial: false,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{{All: true}},
|
|
ns: "",
|
|
expectedList: makeList(t, toto),
|
|
expectedTotal: 1,
|
|
expectedContToken: "",
|
|
expectedErr: nil,
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "ListByOptions with only one Sort.Field set should sort on that field only, in ascending order in prepared sql.Stmt",
|
|
listOptions: sqltypes.ListOptions{
|
|
SortList: sqltypes.SortList{
|
|
SortDirectives: []sqltypes.Sort{
|
|
{
|
|
Fields: []string{"metadata", "somefield"},
|
|
Order: sqltypes.ASC,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{{All: true}},
|
|
ns: "",
|
|
expectedList: makeList(t, lodgePole, bar, baz, foo, toto),
|
|
expectedTotal: 5,
|
|
expectedContToken: "",
|
|
expectedErr: nil,
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "sort one field descending",
|
|
listOptions: sqltypes.ListOptions{
|
|
SortList: sqltypes.SortList{
|
|
SortDirectives: []sqltypes.Sort{
|
|
{
|
|
Fields: []string{"metadata", "somefield"},
|
|
Order: sqltypes.DESC,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{{All: true}},
|
|
ns: "",
|
|
expectedList: makeList(t, toto, foo, baz, bar, lodgePole),
|
|
expectedTotal: 5,
|
|
expectedContToken: "",
|
|
expectedErr: nil,
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "sort one unbound field descending",
|
|
listOptions: sqltypes.ListOptions{
|
|
SortList: sqltypes.SortList{
|
|
SortDirectives: []sqltypes.Sort{
|
|
{
|
|
Fields: []string{"metadata", "unknown"},
|
|
Order: sqltypes.DESC,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{{All: true}},
|
|
ns: "",
|
|
expectedList: makeList(t, lodgePole, toto, baz, bar, foo),
|
|
expectedTotal: 5,
|
|
expectedContToken: "",
|
|
expectedErr: nil,
|
|
})
|
|
// tests = append(tests, testCase{
|
|
// description: "sort one unbound label descending",
|
|
// listOptions: sqltypes.ListOptions{
|
|
// SortList: sqltypes.SortList{
|
|
// SortDirectives: []sqltypes.Sort{
|
|
// {
|
|
// Fields: []string{"metadata", "labels", "flip"},
|
|
// Order: sqltypes.DESC,
|
|
// },
|
|
// },
|
|
// },
|
|
// },
|
|
// partitions: []partition.Partition{{All: true}},
|
|
// ns: "",
|
|
// expectedList: makeList(t, lodgePole, toto, baz, bar, foo),
|
|
// expectedTotal: 5,
|
|
// expectedContToken: "",
|
|
// expectedErr: nil,
|
|
// })
|
|
// tests = append(tests, testCase{
|
|
// description: "ListByOptions sorting on two complex fields should sort on the first field in ascending order first and then sort on the second labels field in ascending order in prepared sql.Stmt",
|
|
// listOptions: sqltypes.ListOptions{
|
|
// SortList: sqltypes.SortList{
|
|
// SortDirectives: []sqltypes.Sort{
|
|
// {
|
|
// Fields: []string{"metadata", "sortfield"},
|
|
// Order: sqltypes.ASC,
|
|
// },
|
|
// {
|
|
// Fields: []string{"metadata", "labels", "cows"},
|
|
// Order: sqltypes.ASC,
|
|
// },
|
|
// },
|
|
// },
|
|
// },
|
|
// partitions: []partition.Partition{{All: true}},
|
|
// ns: "",
|
|
// expectedList: makeList(t),
|
|
// expectedTotal: 5,
|
|
// expectedContToken: "",
|
|
// expectedErr: nil,
|
|
// })
|
|
tests = append(tests, testCase{
|
|
description: "ListByOptions sorting on two fields should sort on the first field in ascending order first and then sort on the second field in ascending order in prepared sql.Stmt",
|
|
listOptions: sqltypes.ListOptions{
|
|
SortList: sqltypes.SortList{
|
|
SortDirectives: []sqltypes.Sort{
|
|
{
|
|
Fields: []string{"metadata", "sortfield"},
|
|
Order: sqltypes.ASC,
|
|
},
|
|
{
|
|
Fields: []string{"metadata", "somefield"},
|
|
Order: sqltypes.ASC,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{{All: true}},
|
|
ns: "",
|
|
expectedList: makeList(t, lodgePole, bar, baz, toto, foo),
|
|
expectedTotal: 5,
|
|
expectedContToken: "",
|
|
expectedErr: nil,
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "ListByOptions sorting on two fields should sort on the first field in descending order first and then sort on the second field in ascending order in prepared sql.Stmt",
|
|
listOptions: sqltypes.ListOptions{
|
|
SortList: sqltypes.SortList{
|
|
SortDirectives: []sqltypes.Sort{
|
|
{
|
|
Fields: []string{"metadata", "sortfield"},
|
|
Order: sqltypes.DESC,
|
|
},
|
|
{
|
|
Fields: []string{"metadata", "somefield"},
|
|
Order: sqltypes.ASC,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{{All: true}},
|
|
ns: "",
|
|
expectedList: makeList(t, foo, baz, toto, bar, lodgePole),
|
|
expectedTotal: 5,
|
|
expectedContToken: "",
|
|
expectedErr: nil,
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "ListByOptions with Pagination.PageSize set should set limit to PageSize in prepared sql.Stmt",
|
|
listOptions: sqltypes.ListOptions{
|
|
Pagination: sqltypes.Pagination{
|
|
PageSize: 3,
|
|
},
|
|
},
|
|
partitions: []partition.Partition{{All: true}},
|
|
ns: "",
|
|
expectedList: makeList(t, foo, bar, baz),
|
|
expectedTotal: 5,
|
|
expectedContToken: "3",
|
|
expectedErr: nil,
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "ListByOptions with Pagination.Page and no PageSize set should not add anything to prepared sql.Stmt",
|
|
listOptions: sqltypes.ListOptions{
|
|
Pagination: sqltypes.Pagination{
|
|
Page: 2,
|
|
},
|
|
},
|
|
partitions: []partition.Partition{{All: true}},
|
|
ns: "",
|
|
expectedList: makeList(t, foo, bar, baz, toto, lodgePole),
|
|
expectedTotal: 5,
|
|
expectedContToken: "",
|
|
expectedErr: nil,
|
|
})
|
|
// tests = append(tests, testCase{
|
|
// description: "ListByOptions with a Namespace Partition should select only items where metadata.namespace is equal to Namespace and all other conditions are met in prepared sql.Stmt",
|
|
// partitions: []partition.Partition{
|
|
// {
|
|
// Namespace: "ns-b",
|
|
// },
|
|
// },
|
|
// // XXX: Why do I need to specify the namespace here too?
|
|
// ns: "ns-b",
|
|
// expectedList: makeList(t, lodgePole),
|
|
// expectedTotal: 1,
|
|
// expectedContToken: "",
|
|
// expectedErr: nil,
|
|
// })
|
|
tests = append(tests, testCase{
|
|
description: "ListByOptions with a All Partition should select all items that meet all other conditions in prepared sql.Stmt",
|
|
partitions: []partition.Partition{
|
|
{
|
|
All: true,
|
|
},
|
|
},
|
|
ns: "",
|
|
expectedList: makeList(t, foo, bar, baz, toto, lodgePole),
|
|
expectedTotal: 5,
|
|
expectedContToken: "",
|
|
expectedErr: nil,
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "ListByOptions with a Passthrough Partition should select all items that meet all other conditions prepared sql.Stmt",
|
|
partitions: []partition.Partition{
|
|
{
|
|
Passthrough: true,
|
|
},
|
|
},
|
|
ns: "",
|
|
expectedList: makeList(t, foo, bar, baz, toto, lodgePole),
|
|
expectedTotal: 5,
|
|
expectedContToken: "",
|
|
expectedErr: nil,
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "ListByOptions with a Names Partition should select only items where metadata.name equals an items in Names and all other conditions are met in prepared sql.Stmt",
|
|
partitions: []partition.Partition{
|
|
{
|
|
Names: sets.New("obj1", "obj2"),
|
|
},
|
|
},
|
|
ns: "",
|
|
expectedList: makeList(t, foo, bar),
|
|
expectedTotal: 2,
|
|
expectedContToken: "",
|
|
expectedErr: nil,
|
|
})
|
|
t.Parallel()
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.description, func(t *testing.T) {
|
|
fields := [][]string{
|
|
{"metadata", "somefield"},
|
|
{"status", "someotherfield"},
|
|
{"metadata", "unknown"},
|
|
{"metadata", "sortfield"},
|
|
}
|
|
fields = append(fields, test.extraIndexedFields...)
|
|
|
|
loi, err := makeListOptionIndexer(ctx, fields)
|
|
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
|
|
}
|
|
|
|
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
|
|
listOptions sqltypes.ListOptions
|
|
partitions []partition.Partition
|
|
ns string
|
|
expectedCountStmt string
|
|
expectedCountStmtArgs []any
|
|
expectedStmt string
|
|
expectedStmtArgs []any
|
|
expectedErr error
|
|
}
|
|
|
|
var tests []testCase
|
|
tests = append(tests, testCase{
|
|
description: "TestConstructQuery: handles IN statements",
|
|
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
|
|
{
|
|
[]sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "queryField1"},
|
|
Matches: []string{"somevalue"},
|
|
Op: sqltypes.In,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
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
|
|
(f."metadata.queryField1" IN (?)) AND
|
|
(FALSE)
|
|
ORDER BY f."metadata.name" ASC `,
|
|
expectedStmtArgs: []any{"somevalue"},
|
|
expectedErr: nil,
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "TestConstructQuery: handles NOT-IN statements",
|
|
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
|
|
{
|
|
[]sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "queryField1"},
|
|
Matches: []string{"somevalue"},
|
|
Op: sqltypes.NotIn,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
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
|
|
(f."metadata.queryField1" NOT IN (?)) AND
|
|
(FALSE)
|
|
ORDER BY f."metadata.name" ASC `,
|
|
expectedStmtArgs: []any{"somevalue"},
|
|
expectedErr: nil,
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "TestConstructQuery: handles EXISTS statements",
|
|
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
|
|
{
|
|
[]sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "queryField1"},
|
|
Op: sqltypes.Exists,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{},
|
|
ns: "",
|
|
expectedErr: errors.New("NULL and NOT NULL tests aren't supported for non-label queries"),
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "TestConstructQuery: handles NOT-EXISTS statements",
|
|
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
|
|
{
|
|
[]sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "queryField1"},
|
|
Op: sqltypes.NotExists,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{},
|
|
ns: "",
|
|
expectedErr: errors.New("NULL and NOT NULL tests aren't supported for non-label queries"),
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "TestConstructQuery: handles == statements for label statements",
|
|
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
|
|
{
|
|
[]sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "labels", "labelEqualFull"},
|
|
Matches: []string{"somevalue"},
|
|
Op: sqltypes.Eq,
|
|
Partial: false,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{},
|
|
ns: "",
|
|
expectedStmt: `SELECT DISTINCT o.object, o.objectnonce, o.dekid FROM "something" o
|
|
JOIN "something_fields" f ON o.key = f.key
|
|
LEFT OUTER JOIN "something_labels" lt1 ON o.key = lt1.key
|
|
WHERE
|
|
(lt1.label = ? AND lt1.value = ?) AND
|
|
(FALSE)
|
|
ORDER BY f."metadata.name" ASC `,
|
|
expectedStmtArgs: []any{"labelEqualFull", "somevalue"},
|
|
expectedErr: nil,
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "TestConstructQuery: handles == statements for label statements, match partial",
|
|
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
|
|
{
|
|
[]sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "labels", "labelEqualPartial"},
|
|
Matches: []string{"somevalue"},
|
|
Op: sqltypes.Eq,
|
|
Partial: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{},
|
|
ns: "",
|
|
expectedStmt: `SELECT DISTINCT o.object, o.objectnonce, o.dekid FROM "something" o
|
|
JOIN "something_fields" f ON o.key = f.key
|
|
LEFT OUTER JOIN "something_labels" lt1 ON o.key = lt1.key
|
|
WHERE
|
|
(lt1.label = ? AND lt1.value LIKE ? ESCAPE '\') AND
|
|
(FALSE)
|
|
ORDER BY f."metadata.name" ASC `,
|
|
expectedStmtArgs: []any{"labelEqualPartial", "%somevalue%"},
|
|
expectedErr: nil,
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "TestConstructQuery: handles != statements for label statements",
|
|
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
|
|
{
|
|
[]sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "labels", "labelNotEqualFull"},
|
|
Matches: []string{"somevalue"},
|
|
Op: sqltypes.NotEq,
|
|
Partial: false,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{},
|
|
ns: "",
|
|
expectedStmt: `SELECT DISTINCT o.object, o.objectnonce, o.dekid FROM "something" o
|
|
JOIN "something_fields" f ON o.key = f.key
|
|
LEFT OUTER JOIN "something_labels" lt1 ON o.key = lt1.key
|
|
WHERE
|
|
((o.key NOT IN (SELECT o1.key FROM "something" o1
|
|
JOIN "something_fields" f1 ON o1.key = f1.key
|
|
LEFT OUTER JOIN "something_labels" lt1i1 ON o1.key = lt1i1.key
|
|
WHERE lt1i1.label = ?)) OR (lt1.label = ? AND lt1.value != ?)) AND
|
|
(FALSE)
|
|
ORDER BY f."metadata.name" ASC `,
|
|
expectedStmtArgs: []any{"labelNotEqualFull", "labelNotEqualFull", "somevalue"},
|
|
expectedErr: nil,
|
|
})
|
|
|
|
tests = append(tests, testCase{
|
|
description: "TestConstructQuery: handles != statements for label statements, match partial",
|
|
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
|
|
{
|
|
[]sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "labels", "labelNotEqualPartial"},
|
|
Matches: []string{"somevalue"},
|
|
Op: sqltypes.NotEq,
|
|
Partial: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{},
|
|
ns: "",
|
|
expectedStmt: `SELECT DISTINCT o.object, o.objectnonce, o.dekid FROM "something" o
|
|
JOIN "something_fields" f ON o.key = f.key
|
|
LEFT OUTER JOIN "something_labels" lt1 ON o.key = lt1.key
|
|
WHERE
|
|
((o.key NOT IN (SELECT o1.key FROM "something" o1
|
|
JOIN "something_fields" f1 ON o1.key = f1.key
|
|
LEFT OUTER JOIN "something_labels" lt1i1 ON o1.key = lt1i1.key
|
|
WHERE lt1i1.label = ?)) OR (lt1.label = ? AND lt1.value NOT LIKE ? ESCAPE '\')) AND
|
|
(FALSE)
|
|
ORDER BY f."metadata.name" ASC `,
|
|
expectedStmtArgs: []any{"labelNotEqualPartial", "labelNotEqualPartial", "%somevalue%"},
|
|
expectedErr: nil,
|
|
})
|
|
|
|
tests = append(tests, testCase{
|
|
description: "TestConstructQuery: handles multiple != statements for label statements",
|
|
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
|
|
{
|
|
[]sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "labels", "notEqual1"},
|
|
Matches: []string{"value1"},
|
|
Op: sqltypes.NotEq,
|
|
Partial: false,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
[]sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "labels", "notEqual2"},
|
|
Matches: []string{"value2"},
|
|
Op: sqltypes.NotEq,
|
|
Partial: false,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{},
|
|
ns: "",
|
|
expectedStmt: `SELECT DISTINCT o.object, o.objectnonce, o.dekid FROM "something" o
|
|
JOIN "something_fields" f ON o.key = f.key
|
|
LEFT OUTER JOIN "something_labels" lt1 ON o.key = lt1.key
|
|
LEFT OUTER JOIN "something_labels" lt2 ON o.key = lt2.key
|
|
WHERE
|
|
((o.key NOT IN (SELECT o1.key FROM "something" o1
|
|
JOIN "something_fields" f1 ON o1.key = f1.key
|
|
LEFT OUTER JOIN "something_labels" lt1i1 ON o1.key = lt1i1.key
|
|
WHERE lt1i1.label = ?)) OR (lt1.label = ? AND lt1.value != ?)) AND
|
|
((o.key NOT IN (SELECT o1.key FROM "something" o1
|
|
JOIN "something_fields" f1 ON o1.key = f1.key
|
|
LEFT OUTER JOIN "something_labels" lt2i1 ON o1.key = lt2i1.key
|
|
WHERE lt2i1.label = ?)) OR (lt2.label = ? AND lt2.value != ?)) AND
|
|
(FALSE)
|
|
ORDER BY f."metadata.name" ASC `,
|
|
expectedStmtArgs: []any{"notEqual1", "notEqual1", "value1", "notEqual2", "notEqual2", "value2"},
|
|
expectedErr: nil,
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "TestConstructQuery: handles IN statements for label statements",
|
|
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
|
|
{
|
|
[]sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "labels", "labelIN"},
|
|
Matches: []string{"somevalue1", "someValue2"},
|
|
Op: sqltypes.In,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{},
|
|
ns: "",
|
|
expectedStmt: `SELECT DISTINCT o.object, o.objectnonce, o.dekid FROM "something" o
|
|
JOIN "something_fields" f ON o.key = f.key
|
|
LEFT OUTER JOIN "something_labels" lt1 ON o.key = lt1.key
|
|
WHERE
|
|
(lt1.label = ? AND lt1.value IN (?, ?)) AND
|
|
(FALSE)
|
|
ORDER BY f."metadata.name" ASC `,
|
|
expectedStmtArgs: []any{"labelIN", "somevalue1", "someValue2"},
|
|
expectedErr: nil,
|
|
})
|
|
|
|
tests = append(tests, testCase{
|
|
description: "TestConstructQuery: handles NOTIN statements for label statements",
|
|
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
|
|
{
|
|
[]sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "labels", "labelNOTIN"},
|
|
Matches: []string{"somevalue1", "someValue2"},
|
|
Op: sqltypes.NotIn,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{},
|
|
ns: "",
|
|
expectedStmt: `SELECT DISTINCT o.object, o.objectnonce, o.dekid FROM "something" o
|
|
JOIN "something_fields" f ON o.key = f.key
|
|
LEFT OUTER JOIN "something_labels" lt1 ON o.key = lt1.key
|
|
WHERE
|
|
((o.key NOT IN (SELECT o1.key FROM "something" o1
|
|
JOIN "something_fields" f1 ON o1.key = f1.key
|
|
LEFT OUTER JOIN "something_labels" lt1i1 ON o1.key = lt1i1.key
|
|
WHERE lt1i1.label = ?)) OR (lt1.label = ? AND lt1.value NOT IN (?, ?))) AND
|
|
(FALSE)
|
|
ORDER BY f."metadata.name" ASC `,
|
|
expectedStmtArgs: []any{"labelNOTIN", "labelNOTIN", "somevalue1", "someValue2"},
|
|
expectedErr: nil,
|
|
})
|
|
|
|
tests = append(tests, testCase{
|
|
description: "TestConstructQuery: handles EXISTS statements for label statements",
|
|
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
|
|
{
|
|
[]sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "labels", "labelEXISTS"},
|
|
Matches: []string{},
|
|
Op: sqltypes.Exists,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{},
|
|
ns: "",
|
|
expectedStmt: `SELECT DISTINCT o.object, o.objectnonce, o.dekid FROM "something" o
|
|
JOIN "something_fields" f ON o.key = f.key
|
|
LEFT OUTER JOIN "something_labels" lt1 ON o.key = lt1.key
|
|
WHERE
|
|
(lt1.label = ?) AND
|
|
(FALSE)
|
|
ORDER BY f."metadata.name" ASC `,
|
|
expectedStmtArgs: []any{"labelEXISTS"},
|
|
expectedErr: nil,
|
|
})
|
|
|
|
tests = append(tests, testCase{
|
|
description: "TestConstructQuery: handles NOTEXISTS statements for label statements",
|
|
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
|
|
{
|
|
[]sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "labels", "labelNOTEXISTS"},
|
|
Matches: []string{},
|
|
Op: sqltypes.NotExists,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{},
|
|
ns: "",
|
|
expectedStmt: `SELECT DISTINCT o.object, o.objectnonce, o.dekid FROM "something" o
|
|
JOIN "something_fields" f ON o.key = f.key
|
|
LEFT OUTER JOIN "something_labels" lt1 ON o.key = lt1.key
|
|
WHERE
|
|
(o.key NOT IN (SELECT o1.key FROM "something" o1
|
|
JOIN "something_fields" f1 ON o1.key = f1.key
|
|
LEFT OUTER JOIN "something_labels" lt1i1 ON o1.key = lt1i1.key
|
|
WHERE lt1i1.label = ?)) AND
|
|
(FALSE)
|
|
ORDER BY f."metadata.name" ASC `,
|
|
expectedStmtArgs: []any{"labelNOTEXISTS"},
|
|
expectedErr: nil,
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "TestConstructQuery: handles LessThan statements",
|
|
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
|
|
{
|
|
[]sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "labels", "numericThing"},
|
|
Matches: []string{"5"},
|
|
Op: sqltypes.Lt,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{},
|
|
ns: "",
|
|
expectedStmt: `SELECT DISTINCT o.object, o.objectnonce, o.dekid FROM "something" o
|
|
JOIN "something_fields" f ON o.key = f.key
|
|
LEFT OUTER JOIN "something_labels" lt1 ON o.key = lt1.key
|
|
WHERE
|
|
(lt1.label = ? AND lt1.value < ?) AND
|
|
(FALSE)
|
|
ORDER BY f."metadata.name" ASC `,
|
|
expectedStmtArgs: []any{"numericThing", float64(5)},
|
|
expectedErr: nil,
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "TestConstructQuery: handles GreaterThan statements",
|
|
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
|
|
{
|
|
[]sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "labels", "numericThing"},
|
|
Matches: []string{"35"},
|
|
Op: sqltypes.Gt,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{},
|
|
ns: "",
|
|
expectedStmt: `SELECT DISTINCT o.object, o.objectnonce, o.dekid FROM "something" o
|
|
JOIN "something_fields" f ON o.key = f.key
|
|
LEFT OUTER JOIN "something_labels" lt1 ON o.key = lt1.key
|
|
WHERE
|
|
(lt1.label = ? AND lt1.value > ?) AND
|
|
(FALSE)
|
|
ORDER BY f."metadata.name" ASC `,
|
|
expectedStmtArgs: []any{"numericThing", float64(35)},
|
|
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{
|
|
{
|
|
Filters: []sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "labels", "junta"},
|
|
Matches: []string{"esther"},
|
|
Op: sqltypes.Eq,
|
|
Partial: true,
|
|
},
|
|
{
|
|
Field: []string{"metadata", "queryField1"},
|
|
Matches: []string{"golgi"},
|
|
Op: sqltypes.NotEq,
|
|
Partial: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{},
|
|
ns: "",
|
|
expectedStmt: `SELECT DISTINCT o.object, o.objectnonce, o.dekid FROM "something" o
|
|
JOIN "something_fields" f ON o.key = f.key
|
|
LEFT OUTER JOIN "something_labels" lt1 ON o.key = lt1.key
|
|
WHERE
|
|
((lt1.label = ? AND lt1.value LIKE ? ESCAPE '\') OR (f."metadata.queryField1" NOT LIKE ? ESCAPE '\')) AND
|
|
(FALSE)
|
|
ORDER BY f."metadata.name" ASC `,
|
|
expectedStmtArgs: []any{"junta", "%esther%", "%golgi%"},
|
|
expectedErr: nil,
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "multiple filters and or-filters with a positive label test and a negative non-label test still outer-join and have correct AND/ORs",
|
|
listOptions: sqltypes.ListOptions{Filters: []sqltypes.OrFilter{
|
|
{
|
|
Filters: []sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "labels", "nectar"},
|
|
Matches: []string{"stash"},
|
|
Op: sqltypes.Eq,
|
|
Partial: true,
|
|
},
|
|
{
|
|
Field: []string{"metadata", "queryField1"},
|
|
Matches: []string{"landlady"},
|
|
Op: sqltypes.NotEq,
|
|
Partial: false,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Filters: []sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "labels", "lawn"},
|
|
Matches: []string{"reba", "coil"},
|
|
Op: sqltypes.In,
|
|
},
|
|
{
|
|
Field: []string{"metadata", "queryField1"},
|
|
Op: sqltypes.Gt,
|
|
Matches: []string{"2"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{},
|
|
ns: "",
|
|
expectedStmt: `SELECT DISTINCT o.object, o.objectnonce, o.dekid FROM "something" o
|
|
JOIN "something_fields" f ON o.key = f.key
|
|
LEFT OUTER JOIN "something_labels" lt1 ON o.key = lt1.key
|
|
LEFT OUTER JOIN "something_labels" lt2 ON o.key = lt2.key
|
|
WHERE
|
|
((lt1.label = ? AND lt1.value LIKE ? ESCAPE '\') OR (f."metadata.queryField1" != ?)) AND
|
|
((lt2.label = ? AND lt2.value IN (?, ?)) OR (f."metadata.queryField1" > ?)) AND
|
|
(FALSE)
|
|
ORDER BY f."metadata.name" ASC `,
|
|
expectedStmtArgs: []any{"nectar", "%stash%", "landlady", "lawn", "reba", "coil", float64(2)},
|
|
expectedErr: nil,
|
|
})
|
|
|
|
tests = append(tests, testCase{
|
|
description: "TestConstructQuery: handles == statements for label statements, match partial, sort on metadata.queryField1",
|
|
listOptions: sqltypes.ListOptions{
|
|
Filters: []sqltypes.OrFilter{
|
|
{
|
|
[]sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "labels", "labelEqualPartial"},
|
|
Matches: []string{"somevalue"},
|
|
Op: sqltypes.Eq,
|
|
Partial: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
SortList: sqltypes.SortList{
|
|
SortDirectives: []sqltypes.Sort{
|
|
{
|
|
Fields: []string{"metadata", "queryField1"},
|
|
Order: sqltypes.ASC,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{},
|
|
ns: "",
|
|
expectedStmt: `SELECT DISTINCT o.object, o.objectnonce, o.dekid FROM "something" o
|
|
JOIN "something_fields" f ON o.key = f.key
|
|
LEFT OUTER JOIN "something_labels" lt1 ON o.key = lt1.key
|
|
WHERE
|
|
(lt1.label = ? AND lt1.value LIKE ? ESCAPE '\') AND
|
|
(FALSE)
|
|
ORDER BY f."metadata.queryField1" ASC`,
|
|
expectedStmtArgs: []any{"labelEqualPartial", "%somevalue%"},
|
|
expectedErr: nil,
|
|
})
|
|
|
|
tests = append(tests, testCase{
|
|
description: "TestConstructQuery: sort on label statements with no query",
|
|
listOptions: sqltypes.ListOptions{
|
|
SortList: sqltypes.SortList{
|
|
SortDirectives: []sqltypes.Sort{
|
|
{
|
|
Fields: []string{"metadata", "labels", "this"},
|
|
Order: sqltypes.ASC,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{},
|
|
ns: "",
|
|
expectedStmt: `SELECT DISTINCT o.object, o.objectnonce, o.dekid FROM "something" o
|
|
JOIN "something_fields" f ON o.key = f.key
|
|
LEFT OUTER JOIN "something_labels" lt1 ON o.key = lt1.key
|
|
WHERE
|
|
(lt1.label = ?) AND
|
|
(FALSE)
|
|
ORDER BY (CASE lt1.label WHEN ? THEN lt1.value ELSE NULL END) ASC NULLS LAST`,
|
|
expectedStmtArgs: []any{"this", "this"},
|
|
expectedErr: nil,
|
|
})
|
|
|
|
tests = append(tests, testCase{
|
|
description: "TestConstructQuery: sort and query on both labels and non-labels without overlap",
|
|
listOptions: sqltypes.ListOptions{
|
|
Filters: []sqltypes.OrFilter{
|
|
{
|
|
[]sqltypes.Filter{
|
|
{
|
|
Field: []string{"metadata", "queryField1"},
|
|
Matches: []string{"toys"},
|
|
Op: sqltypes.Eq,
|
|
},
|
|
{
|
|
Field: []string{"metadata", "labels", "jamb"},
|
|
Matches: []string{"juice"},
|
|
Op: sqltypes.Eq,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
SortList: sqltypes.SortList{
|
|
SortDirectives: []sqltypes.Sort{
|
|
{
|
|
Fields: []string{"metadata", "labels", "this"},
|
|
Order: sqltypes.ASC,
|
|
},
|
|
{
|
|
Fields: []string{"status", "queryField2"},
|
|
Order: sqltypes.DESC,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
partitions: []partition.Partition{},
|
|
ns: "",
|
|
expectedStmt: `SELECT DISTINCT o.object, o.objectnonce, o.dekid FROM "something" o
|
|
JOIN "something_fields" f ON o.key = f.key
|
|
LEFT OUTER JOIN "something_labels" lt2 ON o.key = lt2.key
|
|
LEFT OUTER JOIN "something_labels" lt3 ON o.key = lt3.key
|
|
WHERE
|
|
((f."metadata.queryField1" = ?) OR (lt2.label = ? AND lt2.value = ?) OR (lt3.label = ?)) AND
|
|
(FALSE)
|
|
ORDER BY (CASE lt3.label WHEN ? THEN lt3.value ELSE NULL END) ASC NULLS LAST, f."status.queryField2" DESC`,
|
|
expectedStmtArgs: []any{"toys", "jamb", "juice", "this", "this"},
|
|
expectedErr: nil,
|
|
})
|
|
|
|
t.Parallel()
|
|
for _, test := range tests {
|
|
t.Run(test.description, func(t *testing.T) {
|
|
store := NewMockStore(gomock.NewController(t))
|
|
i := &Indexer{
|
|
Store: store,
|
|
}
|
|
lii := &ListOptionIndexer{
|
|
Indexer: i,
|
|
indexedFields: []string{"metadata.queryField1", "status.queryField2"},
|
|
}
|
|
queryInfo, err := lii.constructQuery(&test.listOptions, test.partitions, test.ns, "something")
|
|
if test.expectedErr != nil {
|
|
assert.Equal(t, test.expectedErr, err)
|
|
return
|
|
}
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, test.expectedStmt, queryInfo.query)
|
|
assert.Equal(t, test.expectedStmtArgs, queryInfo.params)
|
|
assert.Equal(t, test.expectedCountStmt, queryInfo.countQuery)
|
|
assert.Equal(t, test.expectedCountStmtArgs, queryInfo.countParams)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSmartJoin(t *testing.T) {
|
|
type testCase struct {
|
|
description string
|
|
fieldArray []string
|
|
expectedFieldName string
|
|
}
|
|
|
|
var tests []testCase
|
|
tests = append(tests, testCase{
|
|
description: "single-letter names should be dotted",
|
|
fieldArray: []string{"metadata", "labels", "a"},
|
|
expectedFieldName: "metadata.labels.a",
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "underscore should be dotted",
|
|
fieldArray: []string{"metadata", "labels", "_"},
|
|
expectedFieldName: "metadata.labels._",
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "simple names should be dotted",
|
|
fieldArray: []string{"metadata", "labels", "queryField2"},
|
|
expectedFieldName: "metadata.labels.queryField2",
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "a numeric field should be bracketed",
|
|
fieldArray: []string{"metadata", "fields", "43"},
|
|
expectedFieldName: "metadata.fields[43]",
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "a field starting with a number should be bracketed",
|
|
fieldArray: []string{"metadata", "fields", "46days"},
|
|
expectedFieldName: "metadata.fields[46days]",
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "compound names should be bracketed",
|
|
fieldArray: []string{"metadata", "labels", "rancher.cattle.io/moo"},
|
|
expectedFieldName: "metadata.labels[rancher.cattle.io/moo]",
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "space-separated names should be bracketed",
|
|
fieldArray: []string{"metadata", "labels", "space here"},
|
|
expectedFieldName: "metadata.labels[space here]",
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "already-bracketed terms cause double-bracketing and should never be used",
|
|
fieldArray: []string{"metadata", "labels[k8s.io/deepcode]"},
|
|
expectedFieldName: "metadata[labels[k8s.io/deepcode]]",
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "an empty array should be an empty string",
|
|
fieldArray: []string{},
|
|
expectedFieldName: "",
|
|
})
|
|
t.Parallel()
|
|
for _, test := range tests {
|
|
t.Run(test.description, func(t *testing.T) {
|
|
gotFieldName := smartJoin(test.fieldArray)
|
|
assert.Equal(t, test.expectedFieldName, gotFieldName)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildSortLabelsClause(t *testing.T) {
|
|
type testCase struct {
|
|
description string
|
|
labelName string
|
|
joinTableIndexByLabelName map[string]int
|
|
direction bool
|
|
expectedStmt string
|
|
expectedParam string
|
|
expectedErr string
|
|
}
|
|
|
|
var tests []testCase
|
|
tests = append(tests, testCase{
|
|
description: "TestBuildSortClause: empty index list errors",
|
|
labelName: "emptyListError",
|
|
expectedErr: `internal error: no join-table index given for labelName "emptyListError"`,
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "TestBuildSortClause: hit ascending",
|
|
labelName: "testBSL1",
|
|
joinTableIndexByLabelName: map[string]int{"testBSL1": 3},
|
|
direction: true,
|
|
expectedStmt: `(CASE lt3.label WHEN ? THEN lt3.value ELSE NULL END) ASC NULLS LAST`,
|
|
expectedParam: "testBSL1",
|
|
})
|
|
tests = append(tests, testCase{
|
|
description: "TestBuildSortClause: hit descending",
|
|
labelName: "testBSL2",
|
|
joinTableIndexByLabelName: map[string]int{"testBSL2": 4},
|
|
direction: false,
|
|
expectedStmt: `(CASE lt4.label WHEN ? THEN lt4.value ELSE NULL END) DESC NULLS FIRST`,
|
|
expectedParam: "testBSL2",
|
|
})
|
|
t.Parallel()
|
|
for _, test := range tests {
|
|
t.Run(test.description, func(t *testing.T) {
|
|
stmt, param, err := buildSortLabelsClause(test.labelName, test.joinTableIndexByLabelName, test.direction)
|
|
if test.expectedErr != "" {
|
|
assert.Equal(t, test.expectedErr, err.Error())
|
|
} else {
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, test.expectedStmt, stmt)
|
|
assert.Equal(t, test.expectedParam, param)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetField(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
obj any
|
|
field string
|
|
expectedResult any
|
|
expectedErr bool
|
|
}{
|
|
{
|
|
name: "simple",
|
|
obj: &unstructured.Unstructured{
|
|
Object: map[string]any{
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
field: "foo",
|
|
expectedResult: "bar",
|
|
},
|
|
{
|
|
name: "nested",
|
|
obj: &unstructured.Unstructured{
|
|
Object: map[string]any{
|
|
"foo": map[string]any{
|
|
"bar": "baz",
|
|
},
|
|
},
|
|
},
|
|
field: "foo.bar",
|
|
expectedResult: "baz",
|
|
},
|
|
{
|
|
name: "array",
|
|
obj: &unstructured.Unstructured{
|
|
Object: map[string]any{
|
|
"theList": []any{
|
|
"foo", "bar", "baz",
|
|
},
|
|
},
|
|
},
|
|
field: "theList[1]",
|
|
expectedResult: "bar",
|
|
},
|
|
{
|
|
name: "array of object",
|
|
obj: &unstructured.Unstructured{
|
|
Object: map[string]any{
|
|
"theList": []any{
|
|
map[string]any{
|
|
"name": "foo",
|
|
},
|
|
map[string]any{
|
|
"name": "bar",
|
|
},
|
|
map[string]any{
|
|
"name": "baz",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
field: "theList.name",
|
|
expectedResult: []string{"foo", "bar", "baz"},
|
|
},
|
|
{
|
|
name: "annotation",
|
|
obj: &unstructured.Unstructured{
|
|
Object: map[string]any{
|
|
"annotations": map[string]any{
|
|
"with.dot.in.it/and-slash": "foo",
|
|
},
|
|
},
|
|
},
|
|
field: "annotations[with.dot.in.it/and-slash]",
|
|
expectedResult: "foo",
|
|
},
|
|
{
|
|
name: "field not found",
|
|
obj: &unstructured.Unstructured{
|
|
Object: map[string]any{
|
|
"spec": map[string]any{
|
|
"rules": []any{
|
|
map[string]any{},
|
|
map[string]any{
|
|
"host": "example.com",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
field: "spec.rules.host",
|
|
expectedResult: []string{"", "example.com"},
|
|
},
|
|
{
|
|
name: "array index invalid",
|
|
obj: &unstructured.Unstructured{
|
|
Object: map[string]any{
|
|
"theList": []any{
|
|
"foo", "bar", "baz",
|
|
},
|
|
},
|
|
},
|
|
field: "theList[a]",
|
|
expectedErr: true,
|
|
},
|
|
{
|
|
name: "array index out of bound",
|
|
obj: &unstructured.Unstructured{
|
|
Object: map[string]any{
|
|
"theList": []any{
|
|
"foo", "bar", "baz",
|
|
},
|
|
},
|
|
},
|
|
field: "theList[3]",
|
|
expectedErr: true,
|
|
},
|
|
{
|
|
name: "invalid array",
|
|
obj: &unstructured.Unstructured{
|
|
Object: map[string]any{
|
|
"spec": map[string]any{
|
|
"rules": []any{
|
|
1,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
field: "spec.rules.host",
|
|
expectedErr: true,
|
|
},
|
|
{
|
|
name: "invalid array nested",
|
|
obj: &unstructured.Unstructured{
|
|
Object: map[string]any{
|
|
"spec": map[string]any{
|
|
"rules": []any{
|
|
map[string]any{
|
|
"host": 1,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
field: "spec.rules.host",
|
|
expectedErr: true,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
result, err := getField(test.obj, test.field)
|
|
if test.expectedErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, test.expectedResult, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWatchMany(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
loi, err := makeListOptionIndexer(ctx, [][]string{{"metadata", "somefield"}})
|
|
assert.NoError(t, err)
|
|
|
|
startWatcher := func(ctx context.Context) (chan watch.Event, chan error) {
|
|
errCh := make(chan error, 1)
|
|
eventsCh := make(chan watch.Event, 100)
|
|
go func() {
|
|
watchErr := loi.Watch(ctx, WatchOptions{}, eventsCh)
|
|
errCh <- watchErr
|
|
}()
|
|
time.Sleep(100 * time.Millisecond)
|
|
return eventsCh, errCh
|
|
}
|
|
|
|
waitStopWatcher := func(errCh chan error) error {
|
|
select {
|
|
case <-time.After(time.Second * 5):
|
|
return fmt.Errorf("not finished in time")
|
|
case err := <-errCh:
|
|
return err
|
|
}
|
|
}
|
|
|
|
receiveEvents := func(eventsCh chan watch.Event) []watch.Event {
|
|
timer := time.NewTimer(time.Millisecond * 50)
|
|
var events []watch.Event
|
|
for {
|
|
select {
|
|
case <-timer.C:
|
|
return events
|
|
case ev := <-eventsCh:
|
|
events = append(events, ev)
|
|
}
|
|
}
|
|
}
|
|
watcher1, errCh1 := startWatcher(ctx)
|
|
events := receiveEvents(watcher1)
|
|
assert.Len(t, events, 0)
|
|
|
|
foo := &unstructured.Unstructured{
|
|
Object: map[string]any{
|
|
"metadata": map[string]any{
|
|
"name": "foo",
|
|
},
|
|
},
|
|
}
|
|
fooUpdated := foo.DeepCopy()
|
|
fooUpdated.SetLabels(map[string]string{
|
|
"hello": "world",
|
|
})
|
|
|
|
err = loi.Add(foo)
|
|
assert.NoError(t, err)
|
|
|
|
events = receiveEvents(watcher1)
|
|
assert.Equal(t, []watch.Event{{Type: watch.Added, Object: foo}}, events)
|
|
|
|
ctx2, cancel2 := context.WithCancel(context.Background())
|
|
watcher2, errCh2 := startWatcher(ctx2)
|
|
|
|
err = loi.Update(fooUpdated)
|
|
assert.NoError(t, err)
|
|
|
|
events = receiveEvents(watcher1)
|
|
assert.Equal(t, []watch.Event{{Type: watch.Modified, Object: fooUpdated}}, events)
|
|
|
|
events = receiveEvents(watcher2)
|
|
assert.Equal(t, []watch.Event{{Type: watch.Modified, Object: fooUpdated}}, events)
|
|
|
|
watcher3, errCh3 := startWatcher(ctx)
|
|
|
|
cancel2()
|
|
err = waitStopWatcher(errCh2)
|
|
assert.NoError(t, err)
|
|
|
|
err = loi.Delete(fooUpdated)
|
|
assert.NoError(t, err)
|
|
err = loi.Add(foo)
|
|
assert.NoError(t, err)
|
|
err = loi.Update(fooUpdated)
|
|
assert.NoError(t, err)
|
|
|
|
events = receiveEvents(watcher3)
|
|
assert.Equal(t, []watch.Event{
|
|
{Type: watch.Deleted, Object: fooUpdated},
|
|
{Type: watch.Added, Object: foo},
|
|
{Type: watch.Modified, Object: fooUpdated},
|
|
}, events)
|
|
|
|
// Verify cancelled watcher don't receive anything anymore
|
|
events = receiveEvents(watcher2)
|
|
assert.Len(t, events, 0)
|
|
|
|
events = receiveEvents(watcher1)
|
|
assert.Equal(t, []watch.Event{
|
|
{Type: watch.Deleted, Object: fooUpdated},
|
|
{Type: watch.Added, Object: foo},
|
|
{Type: watch.Modified, Object: fooUpdated},
|
|
}, events)
|
|
|
|
cancel()
|
|
err = waitStopWatcher(errCh1)
|
|
assert.NoError(t, err)
|
|
|
|
err = waitStopWatcher(errCh3)
|
|
assert.NoError(t, err)
|
|
}
|