diff --git a/pkg/sqlcache/informer/listoption_indexer.go b/pkg/sqlcache/informer/listoption_indexer.go index 0e1158ef..70653363 100644 --- a/pkg/sqlcache/informer/listoption_indexer.go +++ b/pkg/sqlcache/informer/listoption_indexer.go @@ -742,9 +742,28 @@ func formatMatchTargetWithFormatter(match string, format string) string { return fmt.Sprintf(format, match) } +// There are two kinds of string arrays to turn into a string, based on the last value in the array +// simple: ["a", "b", "conformsToIdentifier"] => "a.b.conformsToIdentifier" +// complex: ["a", "b", "foo.io/stuff"] => "a.b[foo.io/stuff]" + +func smartJoin(s []string) string { + if len(s) == 0 { + return "" + } + if len(s) == 1 { + return s[0] + } + lastBit := s[len(s)-1] + simpleName := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`) + if simpleName.MatchString(lastBit) { + return strings.Join(s, ".") + } + return fmt.Sprintf("%s[%s]", strings.Join(s[0:len(s)-1], "."), lastBit) +} + // toColumnName returns the column name corresponding to a field expressed as string slice func toColumnName(s []string) string { - return db.Sanitize(strings.Join(s, ".")) + return db.Sanitize(smartJoin(s)) } // getField extracts the value of a field expressed as a string path from an unstructured object diff --git a/pkg/sqlcache/informer/listoption_indexer_test.go b/pkg/sqlcache/informer/listoption_indexer_test.go index 16198144..6c59d303 100644 --- a/pkg/sqlcache/informer/listoption_indexer_test.go +++ b/pkg/sqlcache/informer/listoption_indexer_test.go @@ -262,6 +262,7 @@ func TestListByOptions(t *testing.T) { expectedCountStmtArgs []any expectedStmt string expectedStmtArgs []any + extraIndexedFields []string expectedList *unstructured.UnstructuredList returnList []any expectedContToken string @@ -688,6 +689,27 @@ func TestListByOptions(t *testing.T) { 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 field in ascending order in prepared sql.Stmt", + listOptions: ListOptions{ + Sort: Sort{ + Fields: [][]string{{"metadata", "fields", "3"}, {"metadata", "labels", "stub.io/candy"}}, + Orders: []SortOrder{ASC, ASC}, + }, + }, + extraIndexedFields: []string{"metadata.fields[3]", "metadata.labels[stub.io/candy]"}, + 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 f."metadata.fields[3]" ASC, f."metadata.labels[stub.io/candy]" ASC`, + returnList: []any{&unstructured.Unstructured{Object: unstrTestObjectMap}, &unstructured.Unstructured{Object: unstrTestObjectMap}}, + expectedList: &unstructured.UnstructuredList{Object: map[string]interface{}{"items": []map[string]interface{}{unstrTestObjectMap, unstrTestObjectMap}}, Items: []unstructured.Unstructured{{Object: unstrTestObjectMap}, {Object: unstrTestObjectMap}}}, + 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: ListOptions{ @@ -909,8 +931,8 @@ func TestListByOptions(t *testing.T) { Indexer: i, indexedFields: []string{"metadata.somefield", "status.someotherfield"}, } - if test.description == "ListByOptions with multiple OrFilters set should select where all OrFilters contain one filter that is true in prepared sql.Stmt" { - fmt.Printf("stop here") + if len(test.extraIndexedFields) > 0 { + lii.indexedFields = append(lii.indexedFields, test.extraIndexedFields...) } queryInfo, err := lii.constructQuery(test.listOptions, test.partitions, test.ns, "something") if test.expectedErr != nil { @@ -1536,3 +1558,65 @@ func TestConstructQuery(t *testing.T) { }) } } + +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) + }) + } +} diff --git a/pkg/sqlcache/integration_test.go b/pkg/sqlcache/integration_test.go index 8c7e1ea7..69183270 100644 --- a/pkg/sqlcache/integration_test.go +++ b/pkg/sqlcache/integration_test.go @@ -61,7 +61,7 @@ func (i *IntegrationSuite) TearDownSuite() { } func (i *IntegrationSuite) TestSQLCacheFilters() { - fields := [][]string{{`metadata`, `annotations[somekey]`}} + fields := [][]string{{"metadata", "annotations", "somekey"}} require := i.Require() configMapWithAnnotations := func(name string, annotations map[string]string) v1.ConfigMap { return v1.ConfigMap{ @@ -122,7 +122,7 @@ func (i *IntegrationSuite) TestSQLCacheFilters() { { name: "matches filter", filters: orFiltersForFilters(informer.Filter{ - Field: []string{`metadata`, `annotations[somekey]`}, + Field: []string{"metadata", "annotations", "somekey"}, Matches: []string{"somevalue"}, Op: informer.Eq, Partial: false, @@ -132,7 +132,7 @@ func (i *IntegrationSuite) TestSQLCacheFilters() { { name: "partial matches filter", filters: orFiltersForFilters(informer.Filter{ - Field: []string{`metadata`, `annotations[somekey]`}, + Field: []string{"metadata", "annotations", "somekey"}, Matches: []string{"somevalue"}, Op: informer.Eq, Partial: true, @@ -142,7 +142,7 @@ func (i *IntegrationSuite) TestSQLCacheFilters() { { name: "no matches for filter with underscore as it is interpreted literally", filters: orFiltersForFilters(informer.Filter{ - Field: []string{`metadata`, `annotations[somekey]`}, + Field: []string{"metadata", "annotations", "somekey"}, Matches: []string{"somevalu_"}, Op: informer.Eq, Partial: true, @@ -152,7 +152,7 @@ func (i *IntegrationSuite) TestSQLCacheFilters() { { name: "no matches for filter with percent sign as it is interpreted literally", filters: orFiltersForFilters(informer.Filter{ - Field: []string{`metadata`, `annotations[somekey]`}, + Field: []string{"metadata", "annotations", "somekey"}, Matches: []string{"somevalu%"}, Op: informer.Eq, Partial: true, @@ -162,7 +162,7 @@ func (i *IntegrationSuite) TestSQLCacheFilters() { { name: "match with special characters", filters: orFiltersForFilters(informer.Filter{ - Field: []string{`metadata`, `annotations[somekey]`}, + Field: []string{"metadata", "annotations", "somekey"}, Matches: []string{"c%%l_value"}, Op: informer.Eq, Partial: true, @@ -172,7 +172,7 @@ func (i *IntegrationSuite) TestSQLCacheFilters() { { name: "match with literal backslash character", filters: orFiltersForFilters(informer.Filter{ - Field: []string{`metadata`, `annotations[somekey]`}, + Field: []string{"metadata", "annotations", "somekey"}, Matches: []string{`my\windows\path`}, Op: informer.Eq, Partial: true, @@ -182,7 +182,7 @@ func (i *IntegrationSuite) TestSQLCacheFilters() { { name: "not eq filter", filters: orFiltersForFilters(informer.Filter{ - Field: []string{`metadata`, `annotations[somekey]`}, + Field: []string{"metadata", "annotations", "somekey"}, Matches: []string{"somevalue"}, Op: informer.NotEq, Partial: false, @@ -192,7 +192,7 @@ func (i *IntegrationSuite) TestSQLCacheFilters() { { name: "partial not eq filter", filters: orFiltersForFilters(informer.Filter{ - Field: []string{`metadata`, `annotations[somekey]`}, + Field: []string{"metadata", "annotations", "somekey"}, Matches: []string{"somevalue"}, Op: informer.NotEq, Partial: true, @@ -203,13 +203,13 @@ func (i *IntegrationSuite) TestSQLCacheFilters() { name: "multiple or filters match", filters: orFiltersForFilters( informer.Filter{ - Field: []string{`metadata`, `annotations[somekey]`}, + Field: []string{"metadata", "annotations", "somekey"}, Matches: []string{"somevalue"}, Op: informer.Eq, Partial: true, }, informer.Filter{ - Field: []string{`metadata`, `annotations[somekey]`}, + Field: []string{"metadata", "annotations", "somekey"}, Matches: []string{"notequal"}, Op: informer.Eq, Partial: false, @@ -221,7 +221,7 @@ func (i *IntegrationSuite) TestSQLCacheFilters() { name: "or filters on different fields", filters: orFiltersForFilters( informer.Filter{ - Field: []string{`metadata`, `annotations[somekey]`}, + Field: []string{"metadata", "annotations", "somekey"}, Matches: []string{"somevalue"}, Op: informer.Eq, Partial: true, @@ -241,7 +241,7 @@ func (i *IntegrationSuite) TestSQLCacheFilters() { { Filters: []informer.Filter{ { - Field: []string{`metadata`, `annotations[somekey]`}, + Field: []string{"metadata", "annotations", "somekey"}, Matches: []string{"somevalue"}, Op: informer.Eq, Partial: true, @@ -265,7 +265,7 @@ func (i *IntegrationSuite) TestSQLCacheFilters() { name: "no matches", filters: orFiltersForFilters( informer.Filter{ - Field: []string{`metadata`, `annotations[somekey]`}, + Field: []string{"metadata", "annotations", "somekey"}, Matches: []string{"valueNotRepresented"}, Op: informer.Eq, Partial: false, diff --git a/pkg/stores/sqlpartition/listprocessor/processor.go b/pkg/stores/sqlpartition/listprocessor/processor.go index 8eceae78..f1b89e7c 100644 --- a/pkg/stores/sqlpartition/listprocessor/processor.go +++ b/pkg/stores/sqlpartition/listprocessor/processor.go @@ -35,7 +35,7 @@ const ( notOp = "!" ) -var labelsRegex = regexp.MustCompile(`^(metadata)\.(labels)\[(.+)\]$`) +var endsWithBracket = regexp.MustCompile(`^(.+)\[(.+)]$`) var mapK8sOpToRancherOp = map[selection.Operator]informer.Op{ selection.Equals: informer.Eq, selection.DoubleEquals: informer.Eq, @@ -191,17 +191,16 @@ func getLimit(apiOp *types.APIRequest) int { return limit } -// splitQuery takes a single-string metadata-labels filter and converts it into an array of 3 accessor strings, -// where the first two strings are always "metadata" and "labels", and the third is the label name. -// This is more complex than doing something like `strings.Split(".", "metadata.labels.fieldName") -// because the fieldName can be more complex - in particular it can contain "."s) and needs to be -// bracketed, as in `metadata.labels[rancher.io/cattle.and.beef]". -// The `labelsRegex` looks for the bracketed form. +// splitQuery takes a single-string k8s object accessor and returns its separate fields in a slice. +// "Simple" accessors of the form `metadata.labels.foo` => ["metadata", "labels", "foo"] +// but accessors with square brackets need to be broken on the brackets, as in +// "metadata.annotations[k8s.io/this-is-fun]" => ["metadata", "annotations", "k8s.io/this-is-fun"] +// We assume in the kubernetes/rancher world json keys are always alphanumeric-underscorish, so +// we only look for square brackets at the end of the string. func splitQuery(query string) []string { - m := labelsRegex.FindStringSubmatch(query) - if m != nil && len(m) == 4 { - // m[0] contains the entire string, so just return all but that first item in `m` - return m[1:] + m := endsWithBracket.FindStringSubmatch(query) + if m != nil { + return append(strings.Split(m[1], "."), m[2]) } return strings.Split(query, ".") } @@ -219,7 +218,7 @@ func parseNamespaceOrProjectFilters(ctx context.Context, projOrNS string, op inf Op: informer.Eq, }, { - Field: []string{"metadata", "labels[field.cattle.io/projectId]"}, + Field: []string{"metadata", "labels", "field.cattle.io/projectId"}, Matches: []string{pn}, Op: informer.Eq, }, diff --git a/pkg/stores/sqlpartition/listprocessor/processor_test.go b/pkg/stores/sqlpartition/listprocessor/processor_test.go index ece6fb48..0674f82b 100644 --- a/pkg/stores/sqlpartition/listprocessor/processor_test.go +++ b/pkg/stores/sqlpartition/listprocessor/processor_test.go @@ -92,7 +92,7 @@ func TestParseQuery(t *testing.T) { Op: informer.Eq, }, { - Field: []string{"metadata", "labels[field.cattle.io/projectId]"}, + Field: []string{"metadata", "labels", "field.cattle.io/projectId"}, Matches: []string{"somethin"}, Op: informer.Eq, }, @@ -142,7 +142,7 @@ func TestParseQuery(t *testing.T) { Op: informer.Eq, }, { - Field: []string{"metadata", "labels[field.cattle.io/projectId]"}, + Field: []string{"metadata", "labels", "field.cattle.io/projectId"}, Matches: []string{"somethin"}, Op: informer.Eq, }, @@ -195,7 +195,7 @@ func TestParseQuery(t *testing.T) { Op: informer.Eq, }, { - Field: []string{"metadata", "labels[field.cattle.io/projectId]"}, + Field: []string{"metadata", "labels", "field.cattle.io/projectId"}, Matches: []string{"somethin"}, Op: informer.Eq, }, @@ -293,6 +293,83 @@ func TestParseQuery(t *testing.T) { }, }, }) + tests = append(tests, testCase{ + description: "ParseQuery() with an annotations filter param should split it correctly.", + req: &types.APIRequest{ + Request: &http.Request{ + URL: &url.URL{RawQuery: "filter=metadata.annotations[chumley.example.com/fish]=seals"}, + }, + }, + expectedLO: informer.ListOptions{ + ChunkSize: defaultLimit, + Filters: []informer.OrFilter{ + { + Filters: []informer.Filter{ + { + Field: []string{"metadata", "annotations", "chumley.example.com/fish"}, + Matches: []string{"seals"}, + Op: informer.Eq, + Partial: false, + }, + }, + }, + }, + Pagination: informer.Pagination{ + Page: 1, + }, + }, + }) + tests = append(tests, testCase{ + description: "ParseQuery() with a numeric filter index should split it correctly.", + req: &types.APIRequest{ + Request: &http.Request{ + URL: &url.URL{RawQuery: "filter=metadata.fields[3]<5"}, + }, + }, + expectedLO: informer.ListOptions{ + ChunkSize: defaultLimit, + Filters: []informer.OrFilter{ + { + Filters: []informer.Filter{ + { + Field: []string{"metadata", "fields", "3"}, + Matches: []string{"5"}, + Op: informer.Lt, + }, + }, + }, + }, + Pagination: informer.Pagination{ + Page: 1, + }, + }, + }) + tests = append(tests, testCase{ + description: "ParseQuery() with a labels filter param should create a labels-specific filter.", + req: &types.APIRequest{ + Request: &http.Request{ + URL: &url.URL{RawQuery: "filter=metadata.labels[grover.example.com/fish]~heads"}, + }, + }, + expectedLO: informer.ListOptions{ + ChunkSize: defaultLimit, + Filters: []informer.OrFilter{ + { + Filters: []informer.Filter{ + { + Field: []string{"metadata", "labels", "grover.example.com/fish"}, + Matches: []string{"heads"}, + Op: informer.Eq, + Partial: true, + }, + }, + }, + }, + Pagination: informer.Pagination{ + Page: 1, + }, + }, + }) tests = append(tests, testCase{ description: "ParseQuery() with multiple filter params, should include multiple or filters.", req: &types.APIRequest{ diff --git a/pkg/stores/sqlpartition/partitioner_test.go b/pkg/stores/sqlpartition/partitioner_test.go index cdc93ff1..a8eb43c2 100644 --- a/pkg/stores/sqlpartition/partitioner_test.go +++ b/pkg/stores/sqlpartition/partitioner_test.go @@ -6,8 +6,8 @@ import ( "go.uber.org/mock/gomock" "github.com/rancher/apiserver/pkg/types" - "github.com/rancher/steve/pkg/sqlcache/partition" "github.com/rancher/steve/pkg/accesscontrol" + "github.com/rancher/steve/pkg/sqlcache/partition" "github.com/rancher/wrangler/v3/pkg/schemas" "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/util/sets" diff --git a/pkg/stores/sqlpartition/store_test.go b/pkg/stores/sqlpartition/store_test.go index d98a8188..cb09b82f 100644 --- a/pkg/stores/sqlpartition/store_test.go +++ b/pkg/stores/sqlpartition/store_test.go @@ -15,8 +15,8 @@ import ( "go.uber.org/mock/gomock" "github.com/rancher/apiserver/pkg/types" - "github.com/rancher/steve/pkg/sqlcache/partition" "github.com/rancher/steve/pkg/accesscontrol" + "github.com/rancher/steve/pkg/sqlcache/partition" "github.com/rancher/steve/pkg/stores/sqlproxy" "github.com/rancher/wrangler/v3/pkg/generic" corev1 "k8s.io/api/core/v1" diff --git a/pkg/stores/sqlproxy/proxy_store.go b/pkg/stores/sqlproxy/proxy_store.go index f2849970..8c7c00bc 100644 --- a/pkg/stores/sqlproxy/proxy_store.go +++ b/pkg/stores/sqlproxy/proxy_store.go @@ -62,7 +62,7 @@ var ( // Please keep the gvkKey entries in alphabetical order, on a field-by-field basis typeSpecificIndexedFields = map[string][][]string{ gvkKey("", "v1", "ConfigMap"): { - {"metadata", "labels[harvesterhci.io/cloud-init-template]"}}, + {"metadata", "labels", "harvesterhci.io/cloud-init-template"}}, gvkKey("", "v1", "Event"): { {"_type"}, {"involvedObject", "kind"}, @@ -71,7 +71,7 @@ var ( {"reason"}, }, gvkKey("", "v1", "Namespace"): { - {"metadata", "labels[field.cattle.io/projectId]"}}, + {"metadata", "labels", "field.cattle.io/projectId"}}, gvkKey("", "v1", "Node"): { {"status", "nodeInfo", "kubeletVersion"}, {"status", "nodeInfo", "operatingSystem"}}, @@ -89,13 +89,13 @@ var ( {"spec", "type"}, }, gvkKey("apps", "v1", "DaemonSet"): { - {"metadata", "annotations[field.cattle.io/publicEndpoints]"}, + {"metadata", "annotations", "field.cattle.io/publicEndpoints"}, }, gvkKey("apps", "v1", "Deployment"): { - {"metadata", "annotations[field.cattle.io/publicEndpoints]"}, + {"metadata", "annotations", "field.cattle.io/publicEndpoints"}, }, gvkKey("apps", "v1", "StatefulSet"): { - {"metadata", "annotations[field.cattle.io/publicEndpoints]"}, + {"metadata", "annotations", "field.cattle.io/publicEndpoints"}, }, gvkKey("autoscaling", "v2", "HorizontalPodAutoscaler"): { {"spec", "scaleTargetRef", "name"}, @@ -104,16 +104,16 @@ var ( {"status", "currentReplicas"}, }, gvkKey("batch", "v1", "CronJob"): { - {"metadata", "annotations[field.cattle.io/publicEndpoints]"}, + {"metadata", "annotations", "field.cattle.io/publicEndpoints"}, }, gvkKey("batch", "v1", "Job"): { - {"metadata", "annotations[field.cattle.io/publicEndpoints]"}, + {"metadata", "annotations", "field.cattle.io/publicEndpoints"}, }, gvkKey("catalog.cattle.io", "v1", "App"): { {"spec", "chart", "metadata", "name"}, }, gvkKey("catalog.cattle.io", "v1", "ClusterRepo"): { - {"metadata", "annotations[clusterrepo.cattle.io/hidden]"}, + {"metadata", "annotations", "clusterrepo.cattle.io/hidden"}, {"spec", "gitBranch"}, {"spec", "gitRepo"}, }, @@ -125,7 +125,7 @@ var ( gvkKey("cluster.x-k8s.io", "v1beta1", "Machine"): { {"spec", "clusterName"}}, gvkKey("management.cattle.io", "v3", "Cluster"): { - {"metadata", "labels[provider.cattle.io]"}, + {"metadata", "labels", "provider.cattle.io"}, {"spec", "internal"}, {"spec", "displayName"}, {"status", "connected"}, @@ -142,13 +142,13 @@ var ( {"spec", "ingressClassName"}, }, gvkKey("provisioning.cattle.io", "v1", "Cluster"): { - {"metadata", "labels[provider.cattle.io]"}, + {"metadata", "labels", "provider.cattle.io"}, {"status", "clusterName"}, {"status", "provider"}, }, gvkKey("storage.k8s.io", "v1", "StorageClass"): { {"provisioner"}, - {"metadata", "annotations[storageclass.kubernetes.io/is-default-class]"}, + {"metadata", "annotations", "storageclass.kubernetes.io/is-default-class"}, }, } commonIndexFields = [][]string{ diff --git a/pkg/stores/sqlproxy/proxy_store_test.go b/pkg/stores/sqlproxy/proxy_store_test.go index 94026f30..a6f28d59 100644 --- a/pkg/stores/sqlproxy/proxy_store_test.go +++ b/pkg/stores/sqlproxy/proxy_store_test.go @@ -82,7 +82,7 @@ func TestNewProxyStore(t *testing.T) { nsSchema := baseNSSchema scc.EXPECT().SetColumns(context.Background(), &nsSchema).Return(nil) cg.EXPECT().TableAdminClient(nil, &nsSchema, "", &WarningBuffer{}).Return(ri, nil) - cf.EXPECT().CacheFor(context.Background(), [][]string{{`id`}, {`metadata`, `state`, `name`}, {"metadata", "labels[field.cattle.io/projectId]"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(&nsSchema), false, true).Return(c, nil) + cf.EXPECT().CacheFor(context.Background(), [][]string{{`id`}, {`metadata`, `state`, `name`}, {"metadata", "labels", "field.cattle.io/projectId"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(&nsSchema), false, true).Return(c, nil) s, err := NewProxyStore(context.Background(), scc, cg, rn, nil, cf) assert.Nil(t, err) @@ -149,7 +149,7 @@ func TestNewProxyStore(t *testing.T) { nsSchema := baseNSSchema scc.EXPECT().SetColumns(context.Background(), &nsSchema).Return(nil) cg.EXPECT().TableAdminClient(nil, &nsSchema, "", &WarningBuffer{}).Return(ri, nil) - cf.EXPECT().CacheFor(context.Background(), [][]string{{`id`}, {`metadata`, `state`, `name`}, {"metadata", "labels[field.cattle.io/projectId]"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(&nsSchema), false, true).Return(factory.Cache{}, fmt.Errorf("error")) + cf.EXPECT().CacheFor(context.Background(), [][]string{{`id`}, {`metadata`, `state`, `name`}, {"metadata", "labels", "field.cattle.io/projectId"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(&nsSchema), false, true).Return(factory.Cache{}, fmt.Errorf("error")) s, err := NewProxyStore(context.Background(), scc, cg, rn, nil, cf) assert.Nil(t, err) @@ -652,7 +652,7 @@ func TestReset(t *testing.T) { cf.EXPECT().Reset().Return(nil) cs.EXPECT().SetColumns(gomock.Any(), gomock.Any()).Return(nil) cg.EXPECT().TableAdminClient(nil, &nsSchema, "", &WarningBuffer{}).Return(ri, nil) - cf.EXPECT().CacheFor(context.Background(), [][]string{{`id`}, {`metadata`, `state`, `name`}, {"metadata", "labels[field.cattle.io/projectId]"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(&nsSchema), false, true).Return(nsc2, nil) + cf.EXPECT().CacheFor(context.Background(), [][]string{{`id`}, {`metadata`, `state`, `name`}, {"metadata", "labels", "field.cattle.io/projectId"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(&nsSchema), false, true).Return(nsc2, nil) tb.EXPECT().GetTransformFunc(attributes.GVK(&nsSchema)).Return(func(obj interface{}) (interface{}, error) { return obj, nil }) err := s.Reset() assert.Nil(t, err) @@ -759,7 +759,7 @@ func TestReset(t *testing.T) { cf.EXPECT().Reset().Return(nil) cs.EXPECT().SetColumns(gomock.Any(), gomock.Any()).Return(nil) cg.EXPECT().TableAdminClient(nil, &nsSchema, "", &WarningBuffer{}).Return(ri, nil) - cf.EXPECT().CacheFor(context.Background(), [][]string{{`id`}, {`metadata`, `state`, `name`}, {"metadata", "labels[field.cattle.io/projectId]"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(&nsSchema), false, true).Return(factory.Cache{}, fmt.Errorf("error")) + cf.EXPECT().CacheFor(context.Background(), [][]string{{`id`}, {`metadata`, `state`, `name`}, {"metadata", "labels", "field.cattle.io/projectId"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(&nsSchema), false, true).Return(factory.Cache{}, fmt.Errorf("error")) tb.EXPECT().GetTransformFunc(attributes.GVK(&nsSchema)).Return(func(obj interface{}) (interface{}, error) { return obj, nil }) err := s.Reset() assert.NotNil(t, err)