Skip to content

Commit 0721d8f

Browse files
authored
[apiserver] ListAllServices with pagination (#3490)
1 parent 801f081 commit 0721d8f

File tree

8 files changed

+159
-32
lines changed

8 files changed

+159
-32
lines changed

apiserver/pkg/http/client.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -481,14 +481,18 @@ func (krc *KuberayAPIServerClient) ListRayServices(request *api.ListRayServicesR
481481
return response, nil, nil
482482
}
483483

484-
// Finds all ray services in a given namespace.
485-
func (krc *KuberayAPIServerClient) ListAllRayServices() (*api.ListAllRayServicesResponse, *rpcStatus.Status, error) {
484+
// Finds all ray services in all namespaces.
485+
func (krc *KuberayAPIServerClient) ListAllRayServices(request *api.ListAllRayServicesRequest) (*api.ListAllRayServicesResponse, *rpcStatus.Status, error) {
486486
getURL := krc.baseURL + "/apis/v1/services"
487487
httpRequest, err := http.NewRequestWithContext(context.TODO(), "GET", getURL, nil)
488488
if err != nil {
489489
return nil, nil, fmt.Errorf("failed to create http request for url '%s': %w", getURL, err)
490490
}
491491

492+
q := httpRequest.URL.Query()
493+
q.Set("pageSize", strconv.FormatInt(int64(request.PageSize), 10))
494+
q.Set("pageToken", request.PageToken)
495+
httpRequest.URL.RawQuery = q.Encode()
492496
httpRequest.Header.Add("Accept", "application/json")
493497

494498
bodyBytes, status, err := krc.executeHttpRequest(httpRequest, getURL)

apiserver/pkg/manager/resource_manager.go

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -298,24 +298,6 @@ func (r *ResourceManager) ListServices(ctx context.Context, namespace string, pa
298298
return rayServices, rayServiceList.Continue, nil
299299
}
300300

301-
func (r *ResourceManager) ListAllServices(ctx context.Context) ([]*rayv1api.RayService, error) {
302-
rayServices := make([]*rayv1api.RayService, 0)
303-
304-
namespaces, err := r.getKubernetesNamespaceClient().List(ctx, metav1.ListOptions{})
305-
if err != nil {
306-
return nil, util.Wrap(err, "Failed to fetch all Kubernetes namespaces")
307-
}
308-
309-
for _, namespace := range namespaces.Items {
310-
servicesByNamespace, _, err := r.ListServices(ctx, namespace.Name, "" /*pageToken*/, 0 /*pageSize*/)
311-
if err != nil {
312-
return nil, util.Wrap(err, "List All Rayservices failed")
313-
}
314-
rayServices = append(rayServices, servicesByNamespace...)
315-
}
316-
return rayServices, nil
317-
}
318-
319301
func (r *ResourceManager) DeleteService(ctx context.Context, serviceName, namespace string) error {
320302
client := r.getRayServiceClient(namespace)
321303
service, err := getServiceByName(ctx, client, serviceName)

apiserver/pkg/server/serve_server.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@ func (s *RayServiceServer) ListRayServices(ctx context.Context, request *api.Lis
108108
}, nil
109109
}
110110

111-
func (s *RayServiceServer) ListAllRayServices(ctx context.Context, _ *api.ListAllRayServicesRequest) (*api.ListAllRayServicesResponse, error) {
112-
services, err := s.resourceManager.ListAllServices(ctx)
111+
func (s *RayServiceServer) ListAllRayServices(ctx context.Context, request *api.ListAllRayServicesRequest) (*api.ListAllRayServicesResponse, error) {
112+
services, nextPageToken, err := s.resourceManager.ListServices(ctx, "" /*namespace*/, request.PageToken, request.PageSize)
113113
if err != nil {
114114
return nil, util.Wrap(err, "list all services failed.")
115115
}
@@ -123,7 +123,8 @@ func (s *RayServiceServer) ListAllRayServices(ctx context.Context, _ *api.ListAl
123123
serviceEventMap[service.Name] = serviceEvents
124124
}
125125
return &api.ListAllRayServicesResponse{
126-
Services: model.FromCrdToAPIServices(services, serviceEventMap),
126+
Services: model.FromCrdToAPIServices(services, serviceEventMap),
127+
NextPageToken: nextPageToken,
127128
}, nil
128129
}
129130

apiserver/test/e2e/service_server_e2e_test.go

Lines changed: 145 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ func TestGetAllServices(t *testing.T) {
216216
tCtx.DeleteRayService(t, testServiceRequest.Service.Name)
217217
})
218218

219-
response, actualRPCStatus, err := tCtx.GetRayAPIServerClient().ListAllRayServices()
219+
response, actualRPCStatus, err := tCtx.GetRayAPIServerClient().ListAllRayServices(&api.ListAllRayServicesRequest{})
220220
require.NoError(t, err, "No error expected")
221221
require.Nil(t, actualRPCStatus, "No RPC status expected")
222222
require.NotNil(t, response, "A response is expected")
@@ -225,6 +225,144 @@ func TestGetAllServices(t *testing.T) {
225225
require.Equal(t, tCtx.GetNamespaceName(), response.Services[0].Namespace)
226226
}
227227

228+
func TestGetAllServicesWithPagination(t *testing.T) {
229+
const numberOfNamespaces = 3
230+
const numberOfService = 2
231+
const totalServices = numberOfNamespaces * numberOfService
232+
233+
type targetService struct {
234+
namespace string
235+
service string
236+
}
237+
238+
tCtxs := make([]*End2EndTestingContext, 0, numberOfNamespaces)
239+
expectedServices := make([]targetService, 0, totalServices)
240+
241+
// Create services for each namespace
242+
for i := 0; i < numberOfNamespaces; i++ {
243+
tCtx, err := NewEnd2EndTestingContext(t)
244+
require.NoError(t, err, "No error expected when creating testing context")
245+
246+
tCtx.CreateComputeTemplate(t)
247+
t.Cleanup(func() {
248+
tCtx.DeleteComputeTemplate(t)
249+
})
250+
251+
for j := 0; j < numberOfService; j++ {
252+
testServiceRequest := createTestServiceV2(t, tCtx)
253+
t.Cleanup(func() {
254+
tCtx.DeleteRayService(t, testServiceRequest.Service.Name)
255+
})
256+
expectedServices = append(expectedServices, targetService{
257+
namespace: tCtx.GetNamespaceName(),
258+
service: testServiceRequest.Service.Name,
259+
})
260+
}
261+
262+
tCtxs = append(tCtxs, tCtx)
263+
}
264+
265+
var pageToken string
266+
tCtx := tCtxs[0]
267+
268+
// Test pagination with limit less than the total number of services in all namespaces.
269+
t.Run("Test pagination return part of the result services", func(t *testing.T) {
270+
pageToken = ""
271+
gotServices := make(map[targetService]bool, totalServices)
272+
for _, expectedService := range expectedServices {
273+
gotServices[expectedService] = false
274+
}
275+
276+
for i := 0; i < totalServices; i++ {
277+
response, actualRPCStatus, err := tCtx.GetRayAPIServerClient().ListAllRayServices(&api.ListAllRayServicesRequest{
278+
PageToken: pageToken,
279+
PageSize: int32(1),
280+
})
281+
require.NoError(t, err, "No error expected")
282+
require.Nil(t, actualRPCStatus, "No RPC status expected")
283+
require.NotNil(t, response, "A response is expected")
284+
require.NotEmpty(t, response.Services, "A list of service is required")
285+
require.Len(t, response.Services, 1, "Got %d services in response, expected %d", len(response.Services), 1)
286+
287+
pageToken = response.NextPageToken
288+
if i == totalServices-1 {
289+
require.Empty(t, pageToken, "No continue token is expected")
290+
} else {
291+
require.NotEmpty(t, pageToken, "A continue token is expected")
292+
}
293+
294+
for _, service := range response.Services {
295+
key := targetService{namespace: service.Namespace, service: service.Name}
296+
seen, exist := gotServices[key]
297+
298+
// Check if this service is in expectedServices list
299+
require.True(t, exist,
300+
"ListAllRayServices returned an unexpected service: namespace=%s, name=%s",
301+
key.namespace, key.service)
302+
303+
// Check if we've already seen this service before (duplicate)
304+
require.False(t, seen,
305+
"ListAllRayServices returned duplicated service: namespace=%s, name=%s",
306+
key.namespace, key.service)
307+
308+
gotServices[key] = true
309+
}
310+
}
311+
312+
// Check all services were found
313+
for _, expectedService := range expectedServices {
314+
require.True(t, gotServices[expectedService],
315+
"ListAllRayServices did not return expected service %s from namespace %s",
316+
expectedService.service, expectedService.namespace)
317+
}
318+
})
319+
320+
// Test pagination with limit larger than the total number of services in all namespaces.
321+
t.Run("Test pagination return all result services", func(t *testing.T) {
322+
pageToken = ""
323+
gotServices := make(map[targetService]bool, totalServices)
324+
for _, expectedService := range expectedServices {
325+
gotServices[expectedService] = false
326+
}
327+
328+
response, actualRPCStatus, err := tCtx.GetRayAPIServerClient().ListAllRayServices(&api.ListAllRayServicesRequest{
329+
PageToken: pageToken,
330+
PageSize: int32(totalServices + 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, totalServices, "Got %d services in response, expected %d", len(response.Services), totalServices)
338+
require.Empty(t, response.NextPageToken, "Page token should be empty")
339+
340+
for _, service := range response.Services {
341+
key := targetService{namespace: service.Namespace, service: service.Name}
342+
seen, exist := gotServices[key]
343+
344+
// Check if this service is in expectedServices list
345+
require.True(t, exist,
346+
"ListAllRayServices returned an unexpected service: namespace=%s, name=%s",
347+
key.namespace, key.service)
348+
349+
// Check if we've already seen this service before (duplicate)
350+
require.False(t, seen,
351+
"ListAllRayServices returned duplicated service: namespace=%s, name=%s",
352+
key.namespace, key.service)
353+
354+
gotServices[key] = true
355+
}
356+
357+
// Check all services were found
358+
for _, expectedService := range expectedServices {
359+
require.True(t, gotServices[expectedService],
360+
"ListAllRayServices did not return expected service %s from namespace %s",
361+
expectedService.service, expectedService.namespace)
362+
}
363+
})
364+
}
365+
228366
func TestGetServicesInNamespace(t *testing.T) {
229367
tCtx, err := NewEnd2EndTestingContext(t)
230368
require.NoError(t, err, "No error expected when creating testing context")
@@ -308,9 +446,9 @@ func TestGetServicesInNamespaceWithPagination(t *testing.T) {
308446

309447
// Check all services created have been returned.
310448
for idx := 0; idx < serviceCount; idx++ {
311-
if !gotServices[idx] {
312-
t.Errorf("ListServices did not return expected services %s", expectedServiceNames[idx])
313-
}
449+
require.True(t, gotServices[idx],
450+
"ListServices did not return expected services %s",
451+
expectedServiceNames[idx])
314452
}
315453
})
316454

@@ -343,9 +481,9 @@ func TestGetServicesInNamespaceWithPagination(t *testing.T) {
343481

344482
// Check all services created have been returned.
345483
for idx := 0; idx < serviceCount; idx++ {
346-
if !gotServices[idx] {
347-
t.Errorf("ListServices did not return expected services %s", expectedServiceNames[idx])
348-
}
484+
require.True(t, gotServices[idx],
485+
"ListServices did not return expected services %s",
486+
expectedServiceNames[idx])
349487
}
350488
})
351489
}

proto/go_client/serve.pb.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

proto/kuberay_api.swagger.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2041,7 +2041,7 @@
20412041
},
20422042
"nextPageToken": {
20432043
"type": "string",
2044-
"description": "The token to list the next page of RayServices.",
2044+
"description": "The token to list the next page of RayServices.\nIf there is no more service, this field will be empty.",
20452045
"readOnly": true
20462046
}
20472047
}

proto/serve.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ message ListAllRayServicesResponse {
135135
// The total number of RayServices for the given query.
136136
int32 total_size = 2 [(google.api.field_behavior) = OUTPUT_ONLY];
137137
// The token to list the next page of RayServices.
138+
// If there is no more service, this field will be empty.
138139
string next_page_token = 3 [(google.api.field_behavior) = OUTPUT_ONLY];
139140
}
140141

proto/swagger/serve.swagger.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,7 @@
563563
},
564564
"nextPageToken": {
565565
"type": "string",
566-
"description": "The token to list the next page of RayServices.",
566+
"description": "The token to list the next page of RayServices.\nIf there is no more service, this field will be empty.",
567567
"readOnly": true
568568
}
569569
}

0 commit comments

Comments
 (0)