Skip to content

Commit 44d22fd

Browse files
authored
[apiserver] List services with pagination (#3309)
1 parent 747708b commit 44d22fd

File tree

8 files changed

+122
-10
lines changed

8 files changed

+122
-10
lines changed

apiserver/pkg/http/client.go

+5
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,10 @@ func (krc *KuberayAPIServerClient) ListRayServices(request *api.ListRayServicesR
463463
return nil, nil, fmt.Errorf("failed to create http request for url '%s': %w", getURL, err)
464464
}
465465

466+
q := httpRequest.URL.Query()
467+
q.Set("pageSize", strconv.FormatInt(int64(request.PageSize), 10))
468+
q.Set("pageToken", request.PageToken)
469+
httpRequest.URL.RawQuery = q.Encode()
466470
httpRequest.Header.Add("Accept", "application/json")
467471

468472
bodyBytes, status, err := krc.executeHttpRequest(httpRequest, getURL)
@@ -473,6 +477,7 @@ func (krc *KuberayAPIServerClient) ListRayServices(request *api.ListRayServicesR
473477
if err := krc.unmarshaler.Unmarshal(bodyBytes, response); err != nil {
474478
return nil, status, fmt.Errorf("failed to unmarshal: %+w", err)
475479
}
480+
476481
return response, nil, nil
477482
}
478483

apiserver/pkg/manager/resource_manager.go

+6-4
Original file line numberDiff line numberDiff line change
@@ -276,24 +276,26 @@ func (r *ResourceManager) GetService(ctx context.Context, serviceName, namespace
276276
return getServiceByName(ctx, client, serviceName)
277277
}
278278

279-
func (r *ResourceManager) ListServices(ctx context.Context, namespace string) ([]*rayv1api.RayService, error) {
279+
func (r *ResourceManager) ListServices(ctx context.Context, namespace string, pageToken string, pageSize int32) ([]*rayv1api.RayService, string /*nextPageToken*/, error) {
280280
labelSelector := metav1.LabelSelector{
281281
MatchLabels: map[string]string{
282282
util.KubernetesManagedByLabelKey: util.ComponentName,
283283
},
284284
}
285285
rayServiceList, err := r.getRayServiceClient(namespace).List(ctx, metav1.ListOptions{
286286
LabelSelector: labels.Set(labelSelector.MatchLabels).String(),
287+
Limit: int64(pageSize),
288+
Continue: pageToken,
287289
})
288290
if err != nil {
289-
return nil, util.Wrap(err, fmt.Sprintf("List RayService failed in %s", namespace))
291+
return nil, "" /*nextPageToken*/, util.Wrap(err, fmt.Sprintf("List RayService failed in %s with next page token %s and page limit %d", namespace, pageToken, pageSize))
290292
}
291293
rayServices := make([]*rayv1api.RayService, 0)
292294
for _, service := range rayServiceList.Items {
293295
rayServices = append(rayServices, &service)
294296
}
295297

296-
return rayServices, nil
298+
return rayServices, rayServiceList.Continue, nil
297299
}
298300

299301
func (r *ResourceManager) ListAllServices(ctx context.Context) ([]*rayv1api.RayService, error) {
@@ -305,7 +307,7 @@ func (r *ResourceManager) ListAllServices(ctx context.Context) ([]*rayv1api.RayS
305307
}
306308

307309
for _, namespace := range namespaces.Items {
308-
servicesByNamespace, err := r.ListServices(ctx, namespace.Name)
310+
servicesByNamespace, _, err := r.ListServices(ctx, namespace.Name, "" /*pageToken*/, 0 /*pageSize*/)
309311
if err != nil {
310312
return nil, util.Wrap(err, "List All Rayservices failed")
311313
}

apiserver/pkg/server/serve_server.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,11 @@ func (s *RayServiceServer) ListRayServices(ctx context.Context, request *api.Lis
8787
if request.Namespace == "" {
8888
return nil, util.NewInvalidInputError("ray service namespace is empty. Please specify a valid value.")
8989
}
90-
services, err := s.resourceManager.ListServices(ctx, request.Namespace)
90+
services, nextPageToken, err := s.resourceManager.ListServices(ctx, request.Namespace, request.PageToken, request.PageSize)
9191
if err != nil {
9292
return nil, util.Wrap(err, "failed to list rayservice.")
9393
}
94+
9495
serviceEventMap := make(map[string][]corev1.Event)
9596
for _, service := range services {
9697
serviceEvents, err := s.resourceManager.GetServiceEvents(ctx, *service)
@@ -101,7 +102,8 @@ func (s *RayServiceServer) ListRayServices(ctx context.Context, request *api.Lis
101102
serviceEventMap[service.Name] = serviceEvents
102103
}
103104
return &api.ListRayServicesResponse{
104-
Services: model.FromCrdToAPIServices(services, serviceEventMap),
105+
Services: model.FromCrdToAPIServices(services, serviceEventMap),
106+
NextPageToken: nextPageToken,
105107
}, nil
106108
}
107109

apiserver/test/e2e/service_server_e2e_test.go

+101-2
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,107 @@ func TestGetServicesInNamespace(t *testing.T) {
253253
require.Equal(t, tCtx.GetNamespaceName(), response.Services[0].Namespace)
254254
}
255255

256+
func TestGetServicesInNamespaceWithPagination(t *testing.T) {
257+
const serviceCount = 2
258+
expectedServiceNames := make([]string, 0, serviceCount)
259+
260+
tCtx, err := NewEnd2EndTestingContext(t)
261+
require.NoError(t, err, "No error expected when creating testing context")
262+
263+
tCtx.CreateComputeTemplate(t)
264+
t.Cleanup(func() {
265+
tCtx.DeleteComputeTemplate(t)
266+
})
267+
268+
for ii := 0; ii < serviceCount; ii++ {
269+
testServiceRequest := createTestServiceV2(t, tCtx)
270+
t.Cleanup(func() {
271+
tCtx.DeleteRayService(t, testServiceRequest.Service.Name)
272+
})
273+
expectedServiceNames = append(expectedServiceNames, testServiceRequest.Service.Name)
274+
}
275+
276+
// Test pagination with limit 1, which is less than the total number of services.
277+
t.Run("Test pagination return part of the result services", func(t *testing.T) {
278+
// Used to check all services have been returned.
279+
gotServices := []bool{false, false}
280+
281+
pageToken := ""
282+
for ii := 0; ii < serviceCount; ii++ {
283+
response, actualRPCStatus, err := tCtx.GetRayAPIServerClient().ListRayServices(&api.ListRayServicesRequest{
284+
Namespace: tCtx.GetNamespaceName(),
285+
PageToken: pageToken,
286+
PageSize: int32(1),
287+
})
288+
289+
require.NoError(t, err, "No error expected")
290+
require.Nil(t, actualRPCStatus, "No RPC status expected")
291+
require.NotNil(t, response, "A response is expected")
292+
require.NotEmpty(t, response.Services, "A list of service is required")
293+
require.Len(t, response.Services, 1)
294+
295+
for _, curService := range response.Services {
296+
for jj := 0; jj < serviceCount; jj++ {
297+
if expectedServiceNames[jj] == curService.Name {
298+
gotServices[jj] = true
299+
break
300+
}
301+
}
302+
}
303+
304+
// Check next page token.
305+
pageToken = response.NextPageToken
306+
if ii == serviceCount-1 {
307+
require.Empty(t, pageToken, "Last page token should be empty")
308+
} else {
309+
require.NotEmpty(t, pageToken, "Non-last page token should be non empty")
310+
}
311+
}
312+
313+
// Check all services created have been returned.
314+
for idx := 0; idx < serviceCount; idx++ {
315+
if !gotServices[idx] {
316+
t.Errorf("ListServices did not return expected services %s", expectedServiceNames[idx])
317+
}
318+
}
319+
})
320+
321+
// Test pagination with limit 3, which is larger than the total number of services.
322+
t.Run("Test pagination return all result services", func(t *testing.T) {
323+
// Used to check all services have been returned.
324+
gotServices := []bool{false, false}
325+
326+
pageToken := ""
327+
response, actualRPCStatus, err := tCtx.GetRayAPIServerClient().ListRayServices(&api.ListRayServicesRequest{
328+
Namespace: tCtx.GetNamespaceName(),
329+
PageToken: pageToken,
330+
PageSize: serviceCount + 1,
331+
})
332+
333+
require.NoError(t, err, "No error expected")
334+
require.Nil(t, actualRPCStatus, "No RPC status expected")
335+
require.NotNil(t, response, "A response is expected")
336+
require.NotEmpty(t, response.Services, "A list of services is required")
337+
require.Len(t, response.Services, serviceCount)
338+
require.Empty(t, pageToken, "Page token should be empty")
339+
for _, curService := range response.Services {
340+
for jj := 0; jj < serviceCount; jj++ {
341+
if expectedServiceNames[jj] == curService.Name {
342+
gotServices[jj] = true
343+
break
344+
}
345+
}
346+
}
347+
348+
// Check all services created have been returned.
349+
for idx := 0; idx < serviceCount; idx++ {
350+
if !gotServices[idx] {
351+
t.Errorf("ListServices did not return expected services %s", expectedServiceNames[idx])
352+
}
353+
}
354+
})
355+
}
356+
256357
func TestGetService(t *testing.T) {
257358
tCtx, err := NewEnd2EndTestingContext(t)
258359
require.NoError(t, err, "No error expected when creating testing context")
@@ -366,9 +467,7 @@ func createTestServiceV2(t *testing.T, tCtx *End2EndTestingContext) *api.CreateR
366467
require.NoError(t, err, "No error expected")
367468
require.Nil(t, actualRPCStatus, "No RPC status expected")
368469
require.NotNil(t, actualService, "A service is expected")
369-
370470
checkRayServiceCreatedSuccessfully(t, tCtx, actualService.Name)
371-
372471
return testServiceRequest
373472
}
374473

proto/go_client/serve.pb.go

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

proto/kuberay_api.swagger.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -2053,7 +2053,7 @@
20532053
},
20542054
"nextPageToken": {
20552055
"type": "string",
2056-
"description": "The token to list the next page of RayServices.",
2056+
"description": "The token to list the next page of RayServices.\n\nIf there are no more clusters, this field will be empty.",
20572057
"readOnly": true
20582058
}
20592059
}

proto/serve.proto

+2
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ message ListRayServicesResponse {
113113
// The total number of RayServices for the given query.
114114
int32 total_size = 2 [(google.api.field_behavior) = OUTPUT_ONLY];
115115
// The token to list the next page of RayServices.
116+
//
117+
// If there are no more clusters, this field will be empty.
116118
string next_page_token = 3 [(google.api.field_behavior) = OUTPUT_ONLY];
117119
}
118120

proto/swagger/serve.swagger.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,7 @@
580580
},
581581
"nextPageToken": {
582582
"type": "string",
583-
"description": "The token to list the next page of RayServices.",
583+
"description": "The token to list the next page of RayServices.\n\nIf there are no more clusters, this field will be empty.",
584584
"readOnly": true
585585
}
586586
}

0 commit comments

Comments
 (0)