diff --git a/pkg/sqlcache/informer/informer.go b/pkg/sqlcache/informer/informer.go index b0a8b703..6556578f 100644 --- a/pkg/sqlcache/informer/informer.go +++ b/pkg/sqlcache/informer/informer.go @@ -48,6 +48,7 @@ type WatchFilter struct { type ByOptionsLister interface { ListByOptions(ctx context.Context, lo *sqltypes.ListOptions, partitions []partition.Partition, namespace string) (*unstructured.UnstructuredList, int, string, error) Watch(ctx context.Context, options WatchOptions, eventsCh chan<- watch.Event) error + GetLatestResourceVersion() []string } // this is set to a var so that it can be overridden by test code for mocking purposes diff --git a/pkg/sqlcache/informer/informer_mocks_test.go b/pkg/sqlcache/informer/informer_mocks_test.go index 610f279c..dc3b2ea8 100644 --- a/pkg/sqlcache/informer/informer_mocks_test.go +++ b/pkg/sqlcache/informer/informer_mocks_test.go @@ -44,6 +44,20 @@ func (m *MockByOptionsLister) EXPECT() *MockByOptionsListerMockRecorder { return m.recorder } +// GetLatestResourceVersion mocks base method. +func (m *MockByOptionsLister) GetLatestResourceVersion() []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLatestResourceVersion") + ret0, _ := ret[0].([]string) + return ret0 +} + +// GetLatestResourceVersion indicates an expected call of GetLatestResourceVersion. +func (mr *MockByOptionsListerMockRecorder) GetLatestResourceVersion() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLatestResourceVersion", reflect.TypeOf((*MockByOptionsLister)(nil).GetLatestResourceVersion)) +} + // ListByOptions mocks base method. func (m *MockByOptionsLister) ListByOptions(ctx context.Context, lo *sqltypes.ListOptions, partitions []partition.Partition, namespace string) (*unstructured.UnstructuredList, int, string, error) { m.ctrl.T.Helper() diff --git a/pkg/sqlcache/informer/listoption_indexer.go b/pkg/sqlcache/informer/listoption_indexer.go index 7a268d1a..ddb9f36d 100644 --- a/pkg/sqlcache/informer/listoption_indexer.go +++ b/pkg/sqlcache/informer/listoption_indexer.go @@ -285,6 +285,16 @@ func NewListOptionIndexer(ctx context.Context, s Store, opts ListOptionIndexerOp return l, nil } +func (l *ListOptionIndexer) GetLatestResourceVersion() []string { + var latestRV []string + + l.latestRVLock.RLock() + latestRV = []string{l.latestRV} + l.latestRVLock.RUnlock() + + return latestRV +} + func (l *ListOptionIndexer) Watch(ctx context.Context, opts WatchOptions, eventsCh chan<- watch.Event) error { l.latestRVLock.RLock() latestRV := l.latestRV diff --git a/pkg/stores/sqlpartition/listprocessor/processor.go b/pkg/stores/sqlpartition/listprocessor/processor.go index 4fee222c..60ed1f69 100644 --- a/pkg/stores/sqlpartition/listprocessor/processor.go +++ b/pkg/stores/sqlpartition/listprocessor/processor.go @@ -4,6 +4,7 @@ package listprocessor import ( "context" "fmt" + "net/http" "regexp" "strconv" "strings" @@ -152,8 +153,9 @@ func ParseQuery(apiOp *types.APIRequest, namespaceCache Cache) (sqltypes.ListOpt if err != nil { return opts, err } - if projOrNSFilters == nil { - return opts, apierror.NewAPIError(validation.NotFound, fmt.Sprintf("could not find any namespaces named [%s] or namespaces belonging to project named [%s]", projectsOrNamespaces, projectsOrNamespaces)) + if len(projOrNSFilters) == 0 { + return opts, apierror.NewAPIError(validation.ErrorCode{Code: "No Data", Status: http.StatusNoContent}, + fmt.Sprintf("could not find any namespaces named [%s] or namespaces belonging to project named [%s]", projectsOrNamespaces, projectsOrNamespaces)) } if op == sqltypes.NotEq { for _, filter := range projOrNSFilters { @@ -182,7 +184,7 @@ func splitQuery(query string) []string { } func parseNamespaceOrProjectFilters(ctx context.Context, projOrNS string, op sqltypes.Op, namespaceInformer Cache) ([]sqltypes.Filter, error) { - var filters []sqltypes.Filter + filters := []sqltypes.Filter{} for _, pn := range strings.Split(projOrNS, ",") { uList, _, _, err := namespaceInformer.ListByOptions(ctx, &sqltypes.ListOptions{ Filters: []sqltypes.OrFilter{ diff --git a/pkg/stores/sqlpartition/listprocessor/processor_test.go b/pkg/stores/sqlpartition/listprocessor/processor_test.go index da1efe71..f26ff8aa 100644 --- a/pkg/stores/sqlpartition/listprocessor/processor_test.go +++ b/pkg/stores/sqlpartition/listprocessor/processor_test.go @@ -152,25 +152,14 @@ func TestParseQuery(t *testing.T) { }) tests = append(tests, testCase{ description: "ParseQuery() with no errors returned should returned no errors. If projectsornamespaces is not empty" + - " and nsc does not return namespaces, an error should be returned.", + " and nsc does not return namespaces, it should return an empty filter array", req: &types.APIRequest{ Request: &http.Request{ URL: &url.URL{RawQuery: "projectsornamespaces=somethin"}, }, }, expectedLO: sqltypes.ListOptions{ - Filters: []sqltypes.OrFilter{ - { - Filters: []sqltypes.Filter{ - { - Field: []string{"metadata", "namespace"}, - Matches: []string{"ns1"}, - Op: sqltypes.Eq, - Partial: false, - }, - }, - }, - }, + Filters: []sqltypes.OrFilter{}, Pagination: sqltypes.Pagination{ Page: 1, }, diff --git a/pkg/stores/sqlproxy/proxy_store.go b/pkg/stores/sqlproxy/proxy_store.go index fa25afc8..bdf1e847 100644 --- a/pkg/stores/sqlproxy/proxy_store.go +++ b/pkg/stores/sqlproxy/proxy_store.go @@ -781,10 +781,6 @@ func (s *Store) Delete(apiOp *types.APIRequest, schema *types.APISchema, id stri // - a continue token, if there are more pages after the returned one // - an error instead of all of the above if anything went wrong func (s *Store) ListByPartitions(apiOp *types.APIRequest, apiSchema *types.APISchema, partitions []partition.Partition) (*unstructured.UnstructuredList, int, string, error) { - opts, err := listprocessor.ParseQuery(apiOp, s.namespaceCache) - if err != nil { - return nil, 0, "", err - } // warnings from inside the informer are discarded buffer := WarningBuffer{} client, err := s.clientGetter.TableAdminClient(apiOp, apiSchema, "", &buffer) @@ -803,6 +799,23 @@ func (s *Store) ListByPartitions(apiOp *types.APIRequest, apiSchema *types.APISc if err != nil { return nil, 0, "", fmt.Errorf("cachefor %v: %w", gvk, err) } + + opts, err := listprocessor.ParseQuery(apiOp, s.namespaceCache) + if err != nil { + var apiError *apierror.APIError + if errors.As(err, &apiError) { + if apiError.Code.Status == http.StatusNoContent { + list := &unstructured.UnstructuredList{} + resourceVersion := inf.ByOptionsLister.GetLatestResourceVersion() + if len(resourceVersion) > 0 { + list.SetResourceVersion(resourceVersion[0]) + } + return list, 0, "", nil + } + } + return nil, 0, "", err + } + if gvk.Group == "ext.cattle.io" && (gvk.Kind == "Token" || gvk.Kind == "Kubeconfig") { accessSet := accesscontrol.AccessSetFromAPIRequest(apiOp) // See https://github.com/rancher/rancher/blob/7266e5e624f0d610c76ab0af33e30f5b72e11f61/pkg/ext/stores/tokens/tokens.go#L1186C2-L1195C3 diff --git a/pkg/stores/sqlproxy/proxy_store_test.go b/pkg/stores/sqlproxy/proxy_store_test.go index 8624d732..e8b3990e 100644 --- a/pkg/stores/sqlproxy/proxy_store_test.go +++ b/pkg/stores/sqlproxy/proxy_store_test.go @@ -260,6 +260,7 @@ func TestListByPartitions(t *testing.T) { cg := NewMockClientGetter(gomock.NewController(t)) cf := NewMockCacheFactory(gomock.NewController(t)) tb := NewMockTransformBuilder(gomock.NewController(t)) + ri := NewMockResourceInterface(gomock.NewController(t)) s := &Store{ ctx: context.Background(), @@ -313,6 +314,9 @@ func TestListByPartitions(t *testing.T) { copy(listToReturn.Items, expectedItems) nsi.EXPECT().ListByOptions(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, 0, "", fmt.Errorf("error")).Times(2) + cg.EXPECT().TableAdminClient(req, schema, "", &WarningBuffer{}).Return(ri, nil) + tb.EXPECT().GetTransformFunc(attributes.GVK(schema), gomock.Any(), false).Return(func(obj interface{}) (interface{}, error) { return obj, nil }) + cf.EXPECT().CacheFor(context.Background(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(schema), attributes.Namespaced(schema), true) _, err := listprocessor.ParseQuery(req, nsi) assert.NotNil(t, err) diff --git a/pkg/stores/sqlproxy/sql_informer_mocks_test.go b/pkg/stores/sqlproxy/sql_informer_mocks_test.go index cc39ad6d..f32a69d5 100644 --- a/pkg/stores/sqlproxy/sql_informer_mocks_test.go +++ b/pkg/stores/sqlproxy/sql_informer_mocks_test.go @@ -45,6 +45,20 @@ func (m *MockByOptionsLister) EXPECT() *MockByOptionsListerMockRecorder { return m.recorder } +// GetLatestResourceVersion mocks base method. +func (m *MockByOptionsLister) GetLatestResourceVersion() []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLatestResourceVersion") + ret0, _ := ret[0].([]string) + return ret0 +} + +// GetLatestResourceVersion indicates an expected call of GetLatestResourceVersion. +func (mr *MockByOptionsListerMockRecorder) GetLatestResourceVersion() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLatestResourceVersion", reflect.TypeOf((*MockByOptionsLister)(nil).GetLatestResourceVersion)) +} + // ListByOptions mocks base method. func (m *MockByOptionsLister) ListByOptions(ctx context.Context, lo *sqltypes.ListOptions, partitions []partition.Partition, namespace string) (*unstructured.UnstructuredList, int, string, error) { m.ctrl.T.Helper()