Skip to content

Commit c13949e

Browse files
authored
[RayService] Add an envtest for RayService happy path (#2868)
Signed-off-by: kaihsun <[email protected]>
1 parent 64da63b commit c13949e

File tree

1 file changed

+126
-45
lines changed

1 file changed

+126
-45
lines changed

ray-operator/controllers/ray/rayservice_controller_test.go

Lines changed: 126 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ import (
4141
// +kubebuilder:scaffold:imports
4242
)
4343

44-
func rayServiceTemplate(name string, namespace string, serveAppName string) *rayv1.RayService {
45-
serveConfigV2 := fmt.Sprintf(`
44+
func serveConfigV2Template(serveAppName string) string {
45+
return fmt.Sprintf(`
4646
applications:
4747
- name: %s
4848
import_path: fruit.deployment_graph
@@ -68,7 +68,10 @@ func rayServiceTemplate(name string, namespace string, serveAppName string) *ray
6868
price: 1
6969
ray_actor_options:
7070
num_cpus: 0.1`, serveAppName)
71+
}
7172

73+
func rayServiceTemplate(name string, namespace string, serveAppName string) *rayv1.RayService {
74+
serveConfigV2 := serveConfigV2Template(serveAppName)
7275
return &rayv1.RayService{
7376
ObjectMeta: metav1.ObjectMeta{
7477
Name: name,
@@ -115,15 +118,130 @@ func rayServiceTemplate(name string, namespace string, serveAppName string) *ray
115118
}
116119
}
117120

118-
var _ = Context("Inside the default namespace", func() {
119-
ctx := context.TODO()
120-
var workerPods corev1.PodList
121+
func endpointsTemplate(name string, namespace string) *corev1.Endpoints {
122+
return &corev1.Endpoints{
123+
ObjectMeta: metav1.ObjectMeta{
124+
Name: name,
125+
Namespace: namespace,
126+
},
127+
Subsets: []corev1.EndpointSubset{
128+
{
129+
Addresses: []corev1.EndpointAddress{
130+
{
131+
IP: "10.9.8.7",
132+
},
133+
},
134+
},
135+
},
136+
}
137+
}
138+
139+
var _ = Context("RayService env tests", func() {
140+
Describe("RayService happy path", Ordered, func() {
141+
// This test case simulates the most common scenario in the RayService code path:
142+
// (1) Create a RayService custom resource
143+
// (2) The RayService controller creates a pending RayCluster
144+
// (3) The serve application becomes ready on the pending RayCluster
145+
// (4) The Kubernetes head and serve services are created
146+
// (5) The pending RayCluster transitions to become the active RayCluster
147+
ctx := context.Background()
148+
namespace := "default"
149+
serveAppName := "app1"
150+
rayService := rayServiceTemplate("test-happy-path", namespace, serveAppName)
151+
rayCluster := &rayv1.RayCluster{}
152+
153+
It("Create a RayService custom resource", func() {
154+
err := k8sClient.Create(ctx, rayService)
155+
Expect(err).NotTo(HaveOccurred(), "failed to create RayService resource")
156+
Eventually(
157+
getResourceFunc(ctx, client.ObjectKey{Name: rayService.Name, Namespace: namespace}, rayService),
158+
time.Second*3, time.Millisecond*500).Should(BeNil(), "RayService: %v", rayService.Name)
159+
})
160+
161+
It("Conditions should be initialized correctly", func() {
162+
Eventually(
163+
func() bool {
164+
return meta.IsStatusConditionTrue(rayService.Status.Conditions, string(rayv1.UpgradeInProgress))
165+
},
166+
time.Second*3, time.Millisecond*500).Should(BeFalse(), "UpgradeInProgress condition: %v", rayService.Status.Conditions)
167+
Eventually(
168+
func() bool {
169+
return meta.IsStatusConditionTrue(rayService.Status.Conditions, string(rayv1.RayServiceReady))
170+
},
171+
time.Second*3, time.Millisecond*500).Should(BeFalse(), "RayServiceReady condition: %v", rayService.Status.Conditions)
172+
})
173+
174+
It("Should create a pending RayCluster", func() {
175+
Eventually(
176+
getPreparingRayClusterNameFunc(ctx, rayService),
177+
time.Second*15, time.Millisecond*500).Should(Not(BeEmpty()), "Pending RayCluster name: %v", rayService.Status.PendingServiceStatus.RayClusterName)
178+
})
179+
180+
It("Promote the pending RayCluster to the active RayCluster", func() {
181+
// Update the status of the head Pod to Running. Note that the default fake dashboard client
182+
// will return a healthy serve application status.
183+
pendingRayClusterName := rayService.Status.PendingServiceStatus.RayClusterName
184+
updateHeadPodToRunningAndReady(ctx, pendingRayClusterName, namespace)
185+
186+
// Make sure the pending RayCluster becomes the active RayCluster.
187+
Eventually(
188+
getRayClusterNameFunc(ctx, rayService),
189+
time.Second*15, time.Millisecond*500).Should(Equal(pendingRayClusterName), "Active RayCluster name: %v", rayService.Status.ActiveServiceStatus.RayClusterName)
190+
191+
// Initialize RayCluster for the following tests.
192+
Eventually(
193+
getResourceFunc(ctx, client.ObjectKey{Name: rayService.Status.ActiveServiceStatus.RayClusterName, Namespace: namespace}, rayCluster),
194+
time.Second*3, time.Millisecond*500).Should(BeNil(), "RayCluster: %v", rayCluster.Name)
195+
})
196+
197+
It("Check the serve application status in the RayService status", func() {
198+
// Check the serve application status in the RayService status.
199+
// The serve application should be healthy.
200+
Eventually(
201+
checkServiceHealth(ctx, rayService),
202+
time.Second*3, time.Millisecond*500).Should(BeTrue(), "RayService status: %v", rayService.Status)
203+
})
204+
205+
It("Should create a new head service resource", func() {
206+
svc := &corev1.Service{}
207+
headSvcName, err := utils.GenerateHeadServiceName(utils.RayServiceCRD, rayService.Spec.RayClusterSpec, rayService.Name)
208+
Expect(err).ToNot(HaveOccurred())
209+
Eventually(
210+
getResourceFunc(ctx, client.ObjectKey{Name: headSvcName, Namespace: namespace}, svc),
211+
time.Second*3, time.Millisecond*500).Should(BeNil(), "Head service: %v", svc)
212+
// TODO: Verify the head service by checking labels and annotations.
213+
})
214+
215+
It("Should create a new serve service resource", func() {
216+
svc := &corev1.Service{}
217+
Eventually(
218+
getResourceFunc(ctx, client.ObjectKey{Name: utils.GenerateServeServiceName(rayService.Name), Namespace: namespace}, svc),
219+
time.Second*3, time.Millisecond*500).Should(BeNil(), "Serve service: %v", svc)
220+
// TODO: Verify the serve service by checking labels and annotations.
221+
})
121222

122-
serveAppName := "app1"
123-
rayService := rayServiceTemplate("rayservice-sample", "default", serveAppName)
124-
myRayCluster := &rayv1.RayCluster{}
223+
It("The RayServiceReady condition should be true when the number of endpoints is greater than 0", func() {
224+
endpoints := endpointsTemplate(utils.GenerateServeServiceName(rayService.Name), namespace)
225+
err := k8sClient.Create(ctx, endpoints)
226+
Expect(err).NotTo(HaveOccurred(), "failed to create Endpoints resource")
227+
Eventually(func() int32 {
228+
if err := k8sClient.Get(ctx, client.ObjectKey{Name: rayService.Name, Namespace: namespace}, rayService); err != nil {
229+
return 0
230+
}
231+
return rayService.Status.NumServeEndpoints
232+
}, time.Second*3, time.Millisecond*500).Should(BeNumerically(">", 0), "RayService status: %v", rayService.Status)
233+
Expect(meta.IsStatusConditionTrue(rayService.Status.Conditions, string(rayv1.RayServiceReady))).Should(BeTrue())
234+
})
235+
})
125236

126237
Describe("When creating a rayservice", Ordered, func() {
238+
ctx := context.TODO()
239+
var workerPods corev1.PodList
240+
241+
serveAppName := "app1"
242+
rayService := rayServiceTemplate("rayservice-sample", "default", serveAppName)
243+
myRayCluster := &rayv1.RayCluster{}
244+
127245
It("should create a rayservice object", func() {
128246
err := k8sClient.Create(ctx, rayService)
129247
Expect(err).NotTo(HaveOccurred(), "failed to create test RayService resource")
@@ -135,15 +253,6 @@ var _ = Context("Inside the default namespace", func() {
135253
time.Second*3, time.Millisecond*500).Should(BeNil(), "My myRayService = %v", rayService.Name)
136254
})
137255

138-
It("should initialize conditions correctly", func() {
139-
Eventually(func() bool {
140-
if err := k8sClient.Get(ctx, client.ObjectKey{Name: rayService.Name, Namespace: "default"}, rayService); err != nil {
141-
return false
142-
}
143-
return meta.IsStatusConditionFalse(rayService.Status.Conditions, string(rayv1.RayServiceReady)) && meta.IsStatusConditionFalse(rayService.Status.Conditions, string(rayv1.UpgradeInProgress))
144-
}, time.Second*3, time.Millisecond*500).Should(BeTrue(), "My myRayService conditions = %v", rayService.Status.Conditions)
145-
})
146-
147256
It("should create a raycluster object", func() {
148257
Eventually(
149258
getPreparingRayClusterNameFunc(ctx, rayService),
@@ -203,34 +312,6 @@ var _ = Context("Inside the default namespace", func() {
203312
Expect(svc.Spec.Selector[utils.RayClusterLabelKey]).Should(Equal(myRayCluster.Name))
204313
})
205314

206-
It("should have true Ready condition when number of endpoints is greater than 0", func() {
207-
endpoints := &corev1.Endpoints{
208-
ObjectMeta: metav1.ObjectMeta{
209-
Name: utils.GenerateServeServiceName(rayService.Name),
210-
Namespace: "default",
211-
},
212-
Subsets: []corev1.EndpointSubset{
213-
{
214-
Addresses: []corev1.EndpointAddress{
215-
{
216-
IP: "10.9.8.7",
217-
},
218-
},
219-
},
220-
},
221-
}
222-
err := k8sClient.Create(ctx, endpoints)
223-
Expect(err).NotTo(HaveOccurred(), "failed to create test Endpoints resource")
224-
225-
Eventually(func() int32 {
226-
if err := k8sClient.Get(ctx, client.ObjectKey{Name: rayService.Name, Namespace: "default"}, rayService); err != nil {
227-
return 0
228-
}
229-
return rayService.Status.NumServeEndpoints
230-
}, time.Second*3, time.Millisecond*500).Should(BeNumerically(">", 0), "My myRayService status = %v", rayService.Status)
231-
Expect(meta.IsStatusConditionTrue(rayService.Status.Conditions, string(rayv1.RayServiceReady))).Should(BeTrue())
232-
})
233-
234315
It("should update a rayservice object and switch to new Ray Cluster", func() {
235316
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
236317
Eventually(

0 commit comments

Comments
 (0)