Repository: ray-project/kuberay
Pull request #3083: [feat] validate the name length of RayCluster, RayService, and RayJob
Author: login: rueian
Created: 2025-02-20T22:14:52Z | Merged: 2025-03-08T03:03:35Z

## Why are these changes needed?

Part of the https://github.com/ray-project/kuberay/issues/3076, adding length validation to RayCluster, RayService, and RayJob to make sure we don't cut k8s service names:

1. RayCluster: 63 - len("-serve-svc") = `53 characters` at most.
2. RayService: 53 - len("-xxxxx") = `47 characters` at most.
3. RayJob: 53 - len("-xxxxx") = `47 characters` at most.

After https://github.com/ray-project/kuberay/pull/3101, the longest suffix for a K8s service is now "-serve-svc", which takes 10 characters. Therefore, we can only use 63-10=53 characters for the name of a RayCluster.

After https://github.com/ray-project/kuberay/pull/3102, we now add a "-" and 5 random characters to the name of either RayService or RayJob to create the underlying RayCluster. Therefore, they can only have 53-6=47 characters for their name.

Any RayCluster, RayService, or RayJob with a name exceeding the above limit will not be reconciled further and will receive an invalid spec event, which can be observed by `kubectl describe`.



## Related issue number

<!-- For example: "Closes #1234" -->

## Checks

- [x] I've made sure the tests are passing.
- Testing Strategy
  - [x] Unit tests
  - [x] Manual tests
  - [ ] This PR is not tested :(


Linked issue #1234: [Refactor] Separate the logic for retrieving Serve application statuses and checking Serve application statuses
### Search before asking

- [X] I had searched in the [issues](https://github.com/ray-project/kuberay/issues) and found no similar feature requirement.


### Description

Separate the logic for retrieving Serve application statuses from checking Serve application statuses into two separate functions.

https://github.com/ray-project/kuberay/pull/1230#discussion_r1260241124

### Use case

_No response_

### Related issues

_No response_

### Are you willing to submit a PR?

- [X] Yes I am willing to submit a PR!

Diff:
diff --git a/ray-operator/controllers/ray/common/ingress.go b/ray-operator/controllers/ray/common/ingress.go
index 618c6d7dab5..24fa0d41ca9 100644
--- a/ray-operator/controllers/ray/common/ingress.go
+++ b/ray-operator/controllers/ray/common/ingress.go
@@ -21,7 +21,7 @@ func BuildIngressForHeadService(ctx context.Context, cluster rayv1.RayCluster) (
 
 	labels := map[string]string{
 		utils.RayClusterLabelKey:                cluster.Name,
-		utils.RayIDLabelKey:                     utils.GenerateIdentifier(cluster.Name, rayv1.HeadNode),
+		utils.RayIDLabelKey:                     utils.CheckLabel(utils.GenerateIdentifier(cluster.Name, rayv1.HeadNode)),
 		utils.KubernetesApplicationNameLabelKey: utils.ApplicationName,
 		utils.KubernetesCreatedByLabelKey:       utils.ComponentName,
 	}
diff --git a/ray-operator/controllers/ray/common/route.go b/ray-operator/controllers/ray/common/route.go
index 14e41236d75..c220192660d 100644
--- a/ray-operator/controllers/ray/common/route.go
+++ b/ray-operator/controllers/ray/common/route.go
@@ -15,7 +15,7 @@ import (
 func BuildRouteForHeadService(cluster rayv1.RayCluster) (*routev1.Route, error) {
 	labels := map[string]string{
 		utils.RayClusterLabelKey:                cluster.Name,
-		utils.RayIDLabelKey:                     utils.GenerateIdentifier(cluster.Name, rayv1.HeadNode),
+		utils.RayIDLabelKey:                     utils.CheckLabel(utils.GenerateIdentifier(cluster.Name, rayv1.HeadNode)),
 		utils.KubernetesApplicationNameLabelKey: utils.ApplicationName,
 		utils.KubernetesCreatedByLabelKey:       utils.ComponentName,
 	}
diff --git a/ray-operator/controllers/ray/raycluster_controller.go b/ray-operator/controllers/ray/raycluster_controller.go
index 71cce81e556..0fafb8ebca3 100644
--- a/ray-operator/controllers/ray/raycluster_controller.go
+++ b/ray-operator/controllers/ray/raycluster_controller.go
@@ -220,8 +220,15 @@ func (r *RayClusterReconciler) rayClusterReconcile(ctx context.Context, instance
 		return ctrl.Result{}, nil
 	}
 
+	if err := utils.ValidateRayClusterMetadata(instance.ObjectMeta); err != nil {
+		logger.Error(err, "The RayCluster metadata is invalid")
+		r.Recorder.Eventf(instance, corev1.EventTypeWarning, string(utils.InvalidRayClusterMetadata),
+			"The RayCluster metadata is invalid %s/%s: %v", instance.Namespace, instance.Name, err)
+		return ctrl.Result{}, nil
+	}
+
 	if err := utils.ValidateRayClusterSpec(&instance.Spec, instance.Annotations); err != nil {
-		logger.Error(err, fmt.Sprintf("The RayCluster spec is invalid %s/%s", instance.Namespace, instance.Name))
+		logger.Error(err, "The RayCluster spec is invalid")
 		r.Recorder.Eventf(instance, corev1.EventTypeWarning, string(utils.InvalidRayClusterSpec),
 			"The RayCluster spec is invalid %s/%s: %v", instance.Namespace, instance.Name, err)
 		return ctrl.Result{}, nil
@@ -1019,8 +1026,6 @@ func (r *RayClusterReconciler) createHeadRoute(ctx context.Context, route *route
 func (r *RayClusterReconciler) createService(ctx context.Context, svc *corev1.Service, instance *rayv1.RayCluster) error {
 	logger := ctrl.LoggerFrom(ctx)
 
-	// making sure the name is valid
-	svc.Name = utils.CheckName(svc.Name)
 	if err := controllerutil.SetControllerReference(instance, svc, r.Scheme); err != nil {
 		return err
 	}
diff --git a/ray-operator/controllers/ray/raycluster_controller_test.go b/ray-operator/controllers/ray/raycluster_controller_test.go
index f3d2802eb77..9cd2086042a 100644
--- a/ray-operator/controllers/ray/raycluster_controller_test.go
+++ b/ray-operator/controllers/ray/raycluster_controller_test.go
@@ -19,6 +19,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"strings"
 	"time"
 
 	"k8s.io/apimachinery/pkg/api/meta"
@@ -1330,4 +1331,44 @@ var _ = Context("Inside the default namespace", func() {
 				time.Second*3, time.Millisecond*500).Should(BeTrue())
 		})
 	})
+
+	Describe("RayCluster with a maximum name", func() {
+		ctx := context.Background()
+		namespace := "default"
+		rayCluster := rayClusterTemplate(strings.Repeat("r", utils.MaxRayClusterNameLength), namespace)
+		numOfHosts := int32(4)
+		rayCluster.Spec.WorkerGroupSpecs[0].NumOfHosts = numOfHosts // This creates the -headless service.
+		rayCluster.Annotations = make(map[string]string)
+		rayCluster.Annotations[utils.EnableServeServiceKey] = "true" // This creates the -serve-svc service, which is the longest svc suffix.
+		workerPods := corev1.PodList{}
+		workerFilters := common.RayClusterGroupPodsAssociationOptions(rayCluster, rayCluster.Spec.WorkerGroupSpecs[0].GroupName).ToListOptions()
+
+		It("Create a RayCluster custom resource", func() {
+			err := k8sClient.Create(ctx, rayCluster)
+			Expect(err).NotTo(HaveOccurred(), "Failed to create RayCluster")
+			Eventually(
+				getResourceFunc(ctx, client.ObjectKey{Name: rayCluster.Name, Namespace: namespace}, rayCluster),
+				time.Second*3, time.Millisecond*500).Should(BeNil(), "Should be able to see RayCluster: %v", rayCluster.Name)
+		})
+
+		It("Check the number of worker Pods", func() {
+			numWorkerPods := 3 * int(numOfHosts)
+			Eventually(
+				listResourceFunc(ctx, &workerPods, workerFilters...),
+				time.Second*3, time.Millisecond*500).Should(Equal(numWorkerPods), fmt.Sprintf("workerGroup %v", workerPods.Items))
+		})
+
+		It("Check RayCluster service names", func() {
+			var svc corev1.Service
+			Eventually(
+				getResourceFunc(ctx, client.ObjectKey{Name: rayCluster.Name + "-head-svc", Namespace: namespace}, &svc),
+				time.Second*3, time.Millisecond*500).Should(BeNil())
+			Eventually(
+				getResourceFunc(ctx, client.ObjectKey{Name: rayCluster.Name + "-serve-svc", Namespace: namespace}, &svc),
+				time.Second*3, time.Millisecond*500).Should(BeNil())
+			Eventually(
+				getResourceFunc(ctx, client.ObjectKey{Name: rayCluster.Name + "-headless", Namespace: namespace}, &svc),
+				time.Second*3, time.Millisecond*500).Should(BeNil())
+		})
+	})
 })
diff --git a/ray-operator/controllers/ray/rayjob_controller.go b/ray-operator/controllers/ray/rayjob_controller.go
index fd03ac32078..f67754167b3 100644
--- a/ray-operator/controllers/ray/rayjob_controller.go
+++ b/ray-operator/controllers/ray/rayjob_controller.go
@@ -127,6 +127,13 @@ func (r *RayJobReconciler) Reconcile(ctx context.Context, request ctrl.Request)
 		return ctrl.Result{RequeueAfter: RayJobDefaultRequeueDuration}, err
 	}
 
+	if err := utils.ValidateRayJobMetadata(rayJobInstance.ObjectMeta); err != nil {
+		logger.Error(err, "The RayJob metadata is invalid")
+		r.Recorder.Eventf(rayJobInstance, corev1.EventTypeWarning, string(utils.InvalidRayJobMetadata),
+			"The RayJob metadata is invalid %s/%s: %v", rayJobInstance.Namespace, rayJobInstance.Name, err)
+		return ctrl.Result{RequeueAfter: RayJobDefaultRequeueDuration}, err
+	}
+
 	if err := utils.ValidateRayJobSpec(rayJobInstance); err != nil {
 		logger.Error(err, "The RayJob spec is invalid")
 		r.Recorder.Eventf(rayJobInstance, corev1.EventTypeWarning, string(utils.InvalidRayJobSpec),
diff --git a/ray-operator/controllers/ray/rayjob_controller_test.go b/ray-operator/controllers/ray/rayjob_controller_test.go
index b39b54b4532..f4c53690d78 100644
--- a/ray-operator/controllers/ray/rayjob_controller_test.go
+++ b/ray-operator/controllers/ray/rayjob_controller_test.go
@@ -18,6 +18,7 @@ package ray
 import (
 	"context"
 	"os"
+	"strings"
 	"time"
 
 	"k8s.io/apimachinery/pkg/api/resource"
@@ -314,6 +315,43 @@ var _ = Context("RayJob with different submission modes", func() {
 			})
 		})
 
+		Describe("Successful RayJob in K8sJobMode with a maximum name", Ordered, func() {
+			ctx := context.Background()
+			namespace := "default"
+			rayJob := rayJobTemplate(strings.Repeat("j", utils.MaxRayJobNameLength), namespace)
+			rayCluster := &rayv1.RayCluster{}
+
+			It("Create a RayJob custom resource", func() {
+				err := k8sClient.Create(ctx, rayJob)
+				Expect(err).NotTo(HaveOccurred(), "Failed to create RayJob")
+				Eventually(
+					getResourceFunc(ctx, client.ObjectKey{Name: rayJob.Name, Namespace: namespace}, rayJob),
+					time.Second*3, time.Millisecond*500).Should(BeNil(), "Should be able to see RayJob: %v", rayJob.Name)
+			})
+
+			It("RayJobs's JobDeploymentStatus transitions from New to Initializing.", func() {
+				Eventually(
+					getRayJobDeploymentStatus(ctx, rayJob),
+					time.Second*3, time.Millisecond*500).Should(Equal(rayv1.JobDeploymentStatusInitializing), "JobDeploymentStatus = %v", rayJob.Status.JobDeploymentStatus)
+
+				// In Initializing state, Status.RayClusterName, Status.JobId, and Status.StartTime must be set.
+				Expect(rayJob.Status.RayClusterName).NotTo(BeEmpty())
+				Expect(rayJob.Status.JobId).NotTo(BeEmpty())
+				Expect(rayJob.Status.StartTime).NotTo(BeNil())
+			})
+
+			It("In Initializing state, the RayCluster should eventually be created.", func() {
+				var svc corev1.Service
+				Eventually(
+					getResourceFunc(ctx, client.ObjectKey{Name: rayJob.Status.RayClusterName, Namespace: namespace}, rayCluster),
+					time.Second*3, time.Millisecond*500).Should(BeNil(), "RayCluster %v not found", rayJob.Status.RayClusterName)
+				// Also check its service name.
+				Eventually(
+					getResourceFunc(ctx, client.ObjectKey{Name: rayJob.Status.RayClusterName + "-head-svc", Namespace: namespace}, &svc),
+					time.Second*3, time.Millisecond*500).Should(BeNil())
+			})
+		})
+
 		Describe("Invalid RayJob in K8sJobMode", Ordered, func() {
 			ctx := context.Background()
 			namespace := "default"
diff --git a/ray-operator/controllers/ray/rayservice_controller.go b/ray-operator/controllers/ray/rayservice_controller.go
index 251a30a7aeb..c6a725847f1 100644
--- a/ray-operator/controllers/ray/rayservice_controller.go
+++ b/ray-operator/controllers/ray/rayservice_controller.go
@@ -115,6 +115,11 @@ func (r *RayServiceReconciler) Reconcile(ctx context.Context, request ctrl.Reque
 	}
 	originalRayServiceInstance := rayServiceInstance.DeepCopy()
 
+	if err := utils.ValidateRayServiceMetadata(rayServiceInstance.ObjectMeta); err != nil {
+		r.Recorder.Eventf(rayServiceInstance, corev1.EventTypeWarning, string(utils.InvalidRayServiceMetadata),
+			"The RayService metadata is invalid %s/%s: %v", rayServiceInstance.Namespace, rayServiceInstance.Name, err)
+		return ctrl.Result{RequeueAfter: ServiceDefaultRequeueDuration}, err
+	}
 	if err := utils.ValidateRayServiceSpec(rayServiceInstance); err != nil {
 		r.Recorder.Eventf(rayServiceInstance, corev1.EventTypeWarning, string(utils.InvalidRayServiceSpec),
 			"The RayService spec is invalid %s/%s: %v", rayServiceInstance.Namespace, rayServiceInstance.Name, err)
diff --git a/ray-operator/controllers/ray/rayservice_controller_test.go b/ray-operator/controllers/ray/rayservice_controller_test.go
index 5880afdeec1..a2d90b257ae 100644
--- a/ray-operator/controllers/ray/rayservice_controller_test.go
+++ b/ray-operator/controllers/ray/rayservice_controller_test.go
@@ -20,6 +20,7 @@ import (
 	"fmt"
 	"math/rand/v2"
 	"strconv"
+	"strings"
 	"time"
 
 	"github.com/ray-project/kuberay/ray-operator/controllers/ray/utils"
@@ -145,6 +146,55 @@ func appendNewWorkerGroup(workerGroupSpecs []rayv1.WorkerGroupSpec, newGroupName
 }
 
 var _ = Context("RayService env tests", func() {
+	Describe("RayService with a maximum name", Ordered, func() {
+		ctx := context.Background()
+		namespace := "default"
+		serveAppName := "app1"
+		rayService := rayServiceTemplate(strings.Repeat("s", utils.MaxRayServiceNameLength), namespace, serveAppName)
+		rayCluster := &rayv1.RayCluster{}
+
+		It("Create a RayService custom resource", func() {
+			err := k8sClient.Create(ctx, rayService)
+			Expect(err).NotTo(HaveOccurred(), "failed to create RayService resource")
+			Eventually(
+				getResourceFunc(ctx, client.ObjectKey{Name: rayService.Name, Namespace: namespace}, rayService),
+				time.Second*3, time.Millisecond*500).Should(BeNil(), "RayService: %v", rayService.Name)
+		})
+
+		It("Should create a pending RayCluster", func() {
+			Eventually(
+				getPreparingRayClusterNameFunc(ctx, rayService),
+				time.Second*3, time.Millisecond*500).Should(Not(BeEmpty()), "Pending RayCluster name: %v", rayService.Status.PendingServiceStatus.RayClusterName)
+		})
+
+		It("Promote the pending RayCluster to the active RayCluster", func() {
+			// Update the status of the head Pod to Running. Note that the default fake dashboard client
+			// will return a healthy serve application status.
+			pendingRayClusterName := rayService.Status.PendingServiceStatus.RayClusterName
+			updateHeadPodToRunningAndReady(ctx, pendingRayClusterName, namespace)
+
+			// Make sure the pending RayCluster becomes the active RayCluster.
+			Eventually(
+				getRayClusterNameFunc(ctx, rayService),
+				time.Second*3, time.Millisecond*500).Should(Equal(pendingRayClusterName), "Active RayCluster name: %v", rayService.Status.ActiveServiceStatus.RayClusterName)
+
+			// Initialize RayCluster for the following tests.
+			Eventually(
+				getResourceFunc(ctx, client.ObjectKey{Name: rayService.Status.ActiveServiceStatus.RayClusterName, Namespace: namespace}, rayCluster),
+				time.Second*3, time.Millisecond*500).Should(BeNil(), "RayCluster: %v", rayCluster.Name)
+		})
+
+		It("Check RayService service names", func() {
+			var svc corev1.Service
+			Eventually(
+				getResourceFunc(ctx, client.ObjectKey{Name: rayService.Name + "-head-svc", Namespace: namespace}, &svc),
+				time.Second*3, time.Millisecond*500).Should(BeNil())
+			Eventually(
+				getResourceFunc(ctx, client.ObjectKey{Name: rayService.Name + "-serve-svc", Namespace: namespace}, &svc),
+				time.Second*3, time.Millisecond*500).Should(BeNil())
+		})
+	})
+
 	Describe("Autoscaler updates RayCluster should not trigger zero downtime upgrade", Ordered, func() {
 		// If Autoscaler scales up the pending or active RayCluster, zero downtime upgrade should not be triggered.
 		ctx := context.Background()
diff --git a/ray-operator/controllers/ray/utils/constant.go b/ray-operator/controllers/ray/utils/constant.go
index 58a7e43a620..6cab242e49d 100644
--- a/ray-operator/controllers/ray/utils/constant.go
+++ b/ray-operator/controllers/ray/utils/constant.go
@@ -192,6 +192,17 @@ const (
 	KubeRayController = "ray.io/kuberay-operator"
 
 	ServeConfigLRUSize = 1000
+
+	// MaxRayClusterNameLength is the maximum RayCluster name to make sure we don't truncate
+	// their k8s service names. Currently, "-serve-svc" is the longest service suffix:
+	// 63 - len("-serve-svc") == 53, so the name should not be longer than 53 characters.
+	MaxRayClusterNameLength = 53
+	// MaxRayServiceNameLength is the maximum RayService name to make sure it pass the RayCluster validation.
+	// Minus 6 since we append 6 characters to the RayService name to create the cluster (GenerateRayClusterName).
+	MaxRayServiceNameLength = MaxRayClusterNameLength - 6
+	// MaxRayJobNameLength is the maximum RayJob name to make sure it pass the RayCluster validation
+	// Minus 6 since we append 6 characters to the RayJob name to create the cluster (GenerateRayClusterName).
+	MaxRayJobNameLength = MaxRayClusterNameLength - 6
 )
 
 type ServiceType string
@@ -237,8 +248,9 @@ type K8sEventType string
 
 const (
 	// RayCluster event list
-	InvalidRayClusterStatus K8sEventType = "InvalidRayClusterStatus"
-	InvalidRayClusterSpec   K8sEventType = "InvalidRayClusterSpec"
+	InvalidRayClusterStatus   K8sEventType = "InvalidRayClusterStatus"
+	InvalidRayClusterSpec     K8sEventType = "InvalidRayClusterSpec"
+	InvalidRayClusterMetadata K8sEventType = "InvalidRayClusterMetadata"
 	// Head Pod event list
 	CreatedHeadPod        K8sEventType = "CreatedHeadPod"
 	FailedToCreateHeadPod K8sEventType = "FailedToCreateHeadPod"
@@ -258,6 +270,7 @@ const (
 
 	// RayJob event list
 	InvalidRayJobSpec             K8sEventType = "InvalidRayJobSpec"
+	InvalidRayJobMetadata         K8sEventType = "InvalidRayJobMetadata"
 	InvalidRayJobStatus           K8sEventType = "InvalidRayJobStatus"
 	CreatedRayJobSubmitter        K8sEventType = "CreatedRayJobSubmitter"
 	DeletedRayJobSubmitter        K8sEventType = "DeletedRayJobSubmitter"
@@ -272,6 +285,7 @@ const (
 
 	// RayService event list
 	InvalidRayServiceSpec           K8sEventType = "InvalidRayServiceSpec"
+	InvalidRayServiceMetadata       K8sEventType = "InvalidRayServiceMetadata"
 	UpdatedHeadPodServeLabel        K8sEventType = "UpdatedHeadPodServeLabel"
 	UpdatedServeApplications        K8sEventType = "UpdatedServeApplications"
 	FailedToUpdateHeadPodServeLabel K8sEventType = "FailedToUpdateHeadPodServeLabel"
diff --git a/ray-operator/controllers/ray/utils/util.go b/ray-operator/controllers/ray/utils/util.go
index d2f02172615..77f7a1f4f7f 100644
--- a/ray-operator/controllers/ray/utils/util.go
+++ b/ray-operator/controllers/ray/utils/util.go
@@ -262,9 +262,9 @@ func GetNamespace(metaData metav1.ObjectMeta) string {
 func GenerateHeadServiceName(crdType CRDType, clusterSpec rayv1.RayClusterSpec, ownerName string) (string, error) {
 	switch crdType {
 	case RayServiceCRD:
-		return CheckName(fmt.Sprintf("%s-%s-%s", ownerName, rayv1.HeadNode, "svc")), nil
+		return fmt.Sprintf("%s-%s-%s", ownerName, rayv1.HeadNode, "svc"), nil
 	case RayClusterCRD:
-		headSvcName := CheckName(fmt.Sprintf("%s-%s-%s", ownerName, rayv1.HeadNode, "svc"))
+		headSvcName := fmt.Sprintf("%s-%s-%s", ownerName, rayv1.HeadNode, "svc")
 		if clusterSpec.HeadGroupSpec.HeadService != nil && clusterSpec.HeadGroupSpec.HeadService.Name != "" {
 			headSvcName = clusterSpec.HeadGroupSpec.HeadService.Name
 		}
@@ -293,7 +293,7 @@ func ExtractRayIPFromFQDN(fqdnRayIP string) string {
 
 // GenerateServeServiceName generates name for serve service.
 func GenerateServeServiceName(serviceName string) string {
-	return CheckName(fmt.Sprintf("%s-%s-%s", serviceName, ServeName, "svc"))
+	return fmt.Sprintf("%s-%s-%s", serviceName, ServeName, "svc")
 }
 
 // GenerateServeServiceLabel generates label value for serve service selector.
diff --git a/ray-operator/controllers/ray/utils/validation.go b/ray-operator/controllers/ray/utils/validation.go
index c811c1a496d..eb3d3a182e5 100644
--- a/ray-operator/controllers/ray/utils/validation.go
+++ b/ray-operator/controllers/ray/utils/validation.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 
 	"k8s.io/apimachinery/pkg/api/meta"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	rayv1 "github.com/ray-project/kuberay/ray-operator/apis/ray/v1"
 	"github.com/ray-project/kuberay/ray-operator/pkg/features"
@@ -19,6 +20,13 @@ func ValidateRayClusterStatus(instance *rayv1.RayCluster) error {
 	return nil
 }
 
+func ValidateRayClusterMetadata(metadata metav1.ObjectMeta) error {
+	if len(metadata.Name) > MaxRayClusterNameLength {
+		return fmt.Errorf("RayCluster name should be no more than %d characters", MaxRayClusterNameLength)
+	}
+	return nil
+}
+
 // Validation for invalid Ray Cluster configurations.
 func ValidateRayClusterSpec(spec *rayv1.RayClusterSpec, annotations map[string]string) error {
 	if len(spec.HeadGroupSpec.Template.Spec.Containers) == 0 {
@@ -97,6 +105,13 @@ func ValidateRayJobStatus(rayJob *rayv1.RayJob) error {
 	return nil
 }
 
+func ValidateRayJobMetadata(metadata metav1.ObjectMeta) error {
+	if len(metadata.Name) > MaxRayJobNameLength {
+		return fmt.Errorf("RayJob name should be no more than %d characters", MaxRayJobNameLength)
+	}
+	return nil
+}
+
 func ValidateRayJobSpec(rayJob *rayv1.RayJob) error {
 	// KubeRay has some limitations for the suspend operation. The limitations are a subset of the limitations of
 	// Kueue (https://kueue.sigs.k8s.io/docs/tasks/run_rayjobs/#c-limitations). For example, KubeRay allows users
@@ -157,6 +172,13 @@ func ValidateRayJobSpec(rayJob *rayv1.RayJob) error {
 	return nil
 }
 
+func ValidateRayServiceMetadata(metadata metav1.ObjectMeta) error {
+	if len(metadata.Name) > MaxRayServiceNameLength {
+		return fmt.Errorf("RayService name should be no more than %d characters", MaxRayServiceNameLength)
+	}
+	return nil
+}
+
 func ValidateRayServiceSpec(rayService *rayv1.RayService) error {
 	if headSvc := rayService.Spec.RayClusterSpec.HeadGroupSpec.HeadService; headSvc != nil && headSvc.Name != "" {
 		return fmt.Errorf("spec.rayClusterConfig.headGroupSpec.headService.metadata.name should not be set")
diff --git a/ray-operator/controllers/ray/utils/validation_test.go b/ray-operator/controllers/ray/utils/validation_test.go
index 60eecc7c57a..296a98b670f 100644
--- a/ray-operator/controllers/ray/utils/validation_test.go
+++ b/ray-operator/controllers/ray/utils/validation_test.go
@@ -2,6 +2,7 @@ package utils
 
 import (
 	"fmt"
+	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -369,6 +370,42 @@ func TestValidateRayClusterSpecRedisUsername(t *testing.T) {
 	}
 }
 
+func TestValidateRayClusterSpecNames(t *testing.T) {
+	tests := []struct {
+		name         string
+		errorMessage string
+		metadata     metav1.ObjectMeta
+		expectError  bool
+	}{
+		{
+			name: "RayCluster name is too long (> MaxRayClusterNameLength characters)",
+			metadata: metav1.ObjectMeta{
+				Name: strings.Repeat("A", MaxRayClusterNameLength+1),
+			},
+			expectError:  true,
+			errorMessage: fmt.Sprintf("RayCluster name should be no more than %d characters", MaxRayClusterNameLength),
+		},
+		{
+			name: "RayCluster name is ok (== MaxRayClusterNameLength)",
+			metadata: metav1.ObjectMeta{
+				Name: strings.Repeat("A", MaxRayClusterNameLength),
+			},
+			expectError: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := ValidateRayClusterMetadata(tt.metadata)
+			if tt.expectError {
+				require.Error(t, err)
+				assert.EqualError(t, err, tt.errorMessage)
+			} else {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
+
 func TestValidateRayClusterSpecEmptyContainers(t *testing.T) {
 	headGroupSpecWithOneContainer := rayv1.HeadGroupSpec{
 		Template: corev1.PodTemplateSpec{
@@ -716,6 +753,18 @@ func TestValidateRayJobSpec(t *testing.T) {
 	require.ErrorContains(t, err, "headGroupSpec should have at least one container")
 }
 
+func TestValidateRayJobMetadata(t *testing.T) {
+	err := ValidateRayJobMetadata(metav1.ObjectMeta{
+		Name: strings.Repeat("j", MaxRayJobNameLength+1),
+	})
+	require.ErrorContains(t, err, fmt.Sprintf("RayJob name should be no more than %d characters", MaxRayJobNameLength))
+
+	err = ValidateRayJobMetadata(metav1.ObjectMeta{
+		Name: strings.Repeat("j", MaxRayJobNameLength),
+	})
+	require.NoError(t, err)
+}
+
 func TestValidateRayServiceSpec(t *testing.T) {
 	err := ValidateRayServiceSpec(&rayv1.RayService{
 		Spec: rayv1.RayServiceSpec{
@@ -747,3 +796,15 @@ func TestValidateRayServiceSpec(t *testing.T) {
 	})
 	require.Error(t, err, "spec.UpgradeSpec.Type is invalid")
 }
+
+func TestValidateRayServiceMetadata(t *testing.T) {
+	err := ValidateRayServiceMetadata(metav1.ObjectMeta{
+		Name: strings.Repeat("j", MaxRayServiceNameLength+1),
+	})
+	require.ErrorContains(t, err, fmt.Sprintf("RayService name should be no more than %d characters", MaxRayServiceNameLength))
+
+	err = ValidateRayServiceMetadata(metav1.ObjectMeta{
+		Name: strings.Repeat("j", MaxRayServiceNameLength),
+	})
+	require.NoError(t, err)
+}
diff --git a/ray-operator/test/e2e/raycluster_gcs_ft_test.go b/ray-operator/test/e2e/raycluster_gcs_ft_test.go
index 13395e85ef4..c6a77cbc6dc 100644
--- a/ray-operator/test/e2e/raycluster_gcs_ft_test.go
+++ b/ray-operator/test/e2e/raycluster_gcs_ft_test.go
@@ -21,7 +21,7 @@ const (
 	redisAddress  = "redis:6379"
 )
 
-func TestRayClusterGCSFaultTolerence(t *testing.T) {
+func TestRayClusterGCSFaultTolerance(t *testing.T) {
 	test := With(t)
 	g := NewWithT(t)
 


Reviews:
user: login: rueian
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-21T02:25:41Z
user: login: rueian
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-02T23:15:38Z
user: login: kevin85421
body: I felt that adding E2E tests for this was a bit overkill. Does env tests check the K8s resource name length?
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T03:05:02Z
user: login: rueian
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T06:54:17Z
user: login: kevin85421
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T07:19:18Z
user: login: kevin85421
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T07:21:53Z
user: login: rueian
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T07:55:53Z
user: login: rueian
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T07:55:56Z
user: login: rueian
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T07:56:28Z
user: login: rueian
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T07:56:39Z
user: login: rueian
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T07:56:49Z
user: login: rueian
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T07:56:59Z
user: login: rueian
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T07:57:06Z
user: login: MortalHappiness
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T09:06:50Z
user: login: rueian
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T17:41:03Z
user: login: kevin85421
body: Just left some nit comments. Other LGTM
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-07T20:58:00Z
user: login: rueian
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-07T21:19:20Z
user: login: rueian
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-07T21:19:24Z
user: login: rueian
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-07T21:19:26Z
user: login: rueian
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-07T21:19:29Z
user: login: MortalHappiness
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-08T03:01:45Z

Review comments:
diff_hunk: @@ -15,7 +15,7 @@ import (
 func BuildRouteForHeadService(cluster rayv1.RayCluster) (*routev1.Route, error) {
 	labels := map[string]string{
 		utils.RayClusterLabelKey:                cluster.Name,
-		utils.RayIDLabelKey:                     utils.GenerateIdentifier(cluster.Name, rayv1.HeadNode),
+		utils.RayIDLabelKey:                     utils.CheckLabel(utils.GenerateIdentifier(cluster.Name, rayv1.HeadNode)),
path: ray-operator/controllers/ray/common/route.go
user: login: rueian
body: This is an old bug that forgets to chunk the value for `utils.RayIDLabelKey`.
created_at: 2025-02-21T02:23:08Z
author_association: COLLABORATOR
diff_hunk: @@ -21,7 +21,7 @@ func BuildIngressForHeadService(ctx context.Context, cluster rayv1.RayCluster) (
 
 	labels := map[string]string{
 		utils.RayClusterLabelKey:                cluster.Name,
-		utils.RayIDLabelKey:                     utils.GenerateIdentifier(cluster.Name, rayv1.HeadNode),
+		utils.RayIDLabelKey:                     utils.CheckLabel(utils.GenerateIdentifier(cluster.Name, rayv1.HeadNode)),
path: ray-operator/controllers/ray/common/ingress.go
user: login: rueian
body: This is an old bug that forgets to chunk the value for `utils.RayIDLabelKey`.
created_at: 2025-02-21T02:23:35Z
author_association: COLLABORATOR
diff_hunk: @@ -21,7 +22,7 @@ const (
 	redisAddress  = "redis:6379"
 )
 
-func TestRayClusterGCSFaultTolerence(t *testing.T) {
+func TestRayClusterGCSFaultTolerance(t *testing.T) {
path: ray-operator/test/e2e/raycluster_gcs_ft_test.go
user: login: rueian
body: Fix typo.
created_at: 2025-02-21T02:24:13Z
author_association: COLLABORATOR
diff_hunk: @@ -128,6 +129,97 @@ func TestRayClusterGCSFaultTolerence(t *testing.T) {
 	})
 }
 
+func TestRayClusterGCSFTWithMaximumNames(t *testing.T) {
+	test := With(t)
+	g := NewWithT(t)
+
+	// Create a namespace
+	namespace := test.NewTestNamespace()
+	testScriptAC := newConfigMap(namespace.Name, files(test, "test_detached_actor_1.py", "test_detached_actor_2.py"))
+	testScript, err := test.Client().Core().CoreV1().ConfigMaps(namespace.Name).Apply(test.Ctx(), testScriptAC, TestApplyOptions)
+	g.Expect(err).NotTo(HaveOccurred())
+
+	test.T().Run("Test Maximum Cluster Name and Group name (63 characters)", func(_ *testing.T) {
+		maximumRayClusterName := strings.Repeat("r", 63)
+		maximumWorkerGroupName := strings.Repeat("w", 63)
path: ray-operator/test/e2e/raycluster_gcs_ft_test.go
user: login: rueian
body: Test the complex RayCluster (GCSFT+Ingress) with maximum names to make sure it works.
created_at: 2025-02-21T02:25:35Z
author_association: COLLABORATOR
diff_hunk: @@ -1019,8 +1019,6 @@ func (r *RayClusterReconciler) createHeadRoute(ctx context.Context, route *route
 func (r *RayClusterReconciler) createService(ctx context.Context, svc *corev1.Service, instance *rayv1.RayCluster) error {
 	logger := ctrl.LoggerFrom(ctx)
 
-	// making sure the name is valid
-	svc.Name = utils.CheckName(svc.Name)
path: ray-operator/controllers/ray/raycluster_controller.go
user: login: rueian
body: No more `utils.CheckName` to cut service names.
created_at: 2025-03-02T23:14:15Z
author_association: COLLABORATOR
diff_hunk: @@ -262,9 +262,9 @@ func GetNamespace(metaData metav1.ObjectMeta) string {
 func GenerateHeadServiceName(crdType CRDType, clusterSpec rayv1.RayClusterSpec, ownerName string) (string, error) {
 	switch crdType {
 	case RayServiceCRD:
-		return CheckName(fmt.Sprintf("%s-%s-%s", ownerName, rayv1.HeadNode, "svc")), nil
+		return fmt.Sprintf("%s-%s-%s", ownerName, rayv1.HeadNode, "svc"), nil
 	case RayClusterCRD:
-		headSvcName := CheckName(fmt.Sprintf("%s-%s-%s", ownerName, rayv1.HeadNode, "svc"))
+		headSvcName := fmt.Sprintf("%s-%s-%s", ownerName, rayv1.HeadNode, "svc")
path: ray-operator/controllers/ray/utils/util.go
user: login: rueian
body: No more `utils.CheckName` to cut service names.
created_at: 2025-03-02T23:14:41Z
author_association: COLLABORATOR
diff_hunk: @@ -293,7 +293,7 @@ func ExtractRayIPFromFQDN(fqdnRayIP string) string {
 
 // GenerateServeServiceName generates name for serve service.
 func GenerateServeServiceName(serviceName string) string {
-	return CheckName(fmt.Sprintf("%s-%s-%s", serviceName, ServeName, "svc"))
+	return fmt.Sprintf("%s-%s-%s", serviceName, ServeName, "svc")
path: ray-operator/controllers/ray/utils/util.go
user: login: rueian
body: No more `utils.CheckName` to cut service names.
created_at: 2025-03-02T23:14:46Z
author_association: COLLABORATOR
diff_hunk: @@ -20,7 +20,11 @@ func ValidateRayClusterStatus(instance *rayv1.RayCluster) error {
 }
 
 // Validation for invalid Ray Cluster configurations.
-func ValidateRayClusterSpec(spec *rayv1.RayClusterSpec, annotations map[string]string) error {
+func ValidateRayClusterSpec(name string, spec *rayv1.RayClusterSpec, annotations map[string]string) error {
path: ray-operator/controllers/ray/utils/validation.go
user: login: kevin85421
body: Passing CR name into `ValidateRayClusterSpec` seems to be an abstraction leak. Maybe create a new function like `ValidateMetadata` instead?
created_at: 2025-03-06T02:59:53Z
author_association: MEMBER
diff_hunk: @@ -20,7 +20,11 @@ func ValidateRayClusterStatus(instance *rayv1.RayCluster) error {
 }
 
 // Validation for invalid Ray Cluster configurations.
-func ValidateRayClusterSpec(spec *rayv1.RayClusterSpec, annotations map[string]string) error {
+func ValidateRayClusterSpec(name string, spec *rayv1.RayClusterSpec, annotations map[string]string) error {
path: ray-operator/controllers/ray/utils/validation.go
user: login: rueian
body: Done!
created_at: 2025-03-06T06:54:17Z
author_association: COLLABORATOR
diff_hunk: @@ -220,6 +220,13 @@ func (r *RayClusterReconciler) rayClusterReconcile(ctx context.Context, instance
 		return ctrl.Result{}, nil
 	}
 
+	if err := utils.ValidateRayClusterMetadata(instance.ObjectMeta); err != nil {
+		logger.Error(err, fmt.Sprintf("The RayCluster metadata is invalid %s/%s", instance.Namespace, instance.Name))
path: ray-operator/controllers/ray/raycluster_controller.go
user: login: kevin85421
body: Avoid using `fmt.Sprintf` in `logger`.
created_at: 2025-03-06T06:58:39Z
author_association: MEMBER
diff_hunk: @@ -220,6 +220,13 @@ func (r *RayClusterReconciler) rayClusterReconcile(ctx context.Context, instance
 		return ctrl.Result{}, nil
 	}
 
+	if err := utils.ValidateRayClusterMetadata(instance.ObjectMeta); err != nil {
+		logger.Error(err, fmt.Sprintf("The RayCluster metadata is invalid %s/%s", instance.Namespace, instance.Name))
+		r.Recorder.Eventf(instance, corev1.EventTypeWarning, string(utils.InvalidRayClusterSpec),
path: ray-operator/controllers/ray/raycluster_controller.go
user: login: kevin85421
body: InvalidRayClusterSpec -> InvalidaRayClusterMetadata?
created_at: 2025-03-06T06:59:26Z
author_association: MEMBER
diff_hunk: @@ -98,6 +106,10 @@ func ValidateRayJobStatus(rayJob *rayv1.RayJob) error {
 }
 
 func ValidateRayJobSpec(rayJob *rayv1.RayJob) error {
+	if len(rayJob.Name) > MaxRayJobNameLength {
path: ray-operator/controllers/ray/utils/validation.go
user: login: kevin85421
body: maybe we can (1) separate the logic into `ValidateRayJobMetadata` /  `ValidateRayServiceMetadata` (2) only pass CR spec to the function `ValidateXXXXSpec` as a follow up PR. This will be a good first issue.
created_at: 2025-03-06T07:03:31Z
author_association: MEMBER
diff_hunk: @@ -1330,4 +1331,35 @@ var _ = Context("Inside the default namespace", func() {
 				time.Second*3, time.Millisecond*500).Should(BeTrue())
 		})
 	})
+
+	Describe("RayCluster with a maximum name", func() {
+		ctx := context.Background()
+		namespace := "default"
+		rayCluster := rayClusterTemplate(strings.Repeat("r", utils.MaxRayClusterNameLength), namespace)
+		numOfHosts := int32(4)
+		rayCluster.Spec.WorkerGroupSpecs[0].NumOfHosts = numOfHosts
path: ray-operator/controllers/ray/raycluster_controller_test.go
user: login: kevin85421
body: why do we need to set `numOfHosts`?
created_at: 2025-03-06T07:06:49Z
author_association: MEMBER
diff_hunk: @@ -1330,4 +1331,35 @@ var _ = Context("Inside the default namespace", func() {
 				time.Second*3, time.Millisecond*500).Should(BeTrue())
 		})
 	})
+
+	Describe("RayCluster with a maximum name", func() {
+		ctx := context.Background()
+		namespace := "default"
+		rayCluster := rayClusterTemplate(strings.Repeat("r", utils.MaxRayClusterNameLength), namespace)
+		numOfHosts := int32(4)
+		rayCluster.Spec.WorkerGroupSpecs[0].NumOfHosts = numOfHosts
+		rayCluster.Annotations = make(map[string]string)
+		rayCluster.Annotations[utils.EnableServeServiceKey] = "true"
path: ray-operator/controllers/ray/raycluster_controller_test.go
user: login: kevin85421
body: would you mind adding comments to explain why this test requires this (i.e. `-serve-svc` is the longest suffix name)?
created_at: 2025-03-06T07:09:05Z
author_association: MEMBER
diff_hunk: @@ -1330,4 +1331,35 @@ var _ = Context("Inside the default namespace", func() {
 				time.Second*3, time.Millisecond*500).Should(BeTrue())
 		})
 	})
+
+	Describe("RayCluster with a maximum name", func() {
+		ctx := context.Background()
+		namespace := "default"
+		rayCluster := rayClusterTemplate(strings.Repeat("r", utils.MaxRayClusterNameLength), namespace)
+		numOfHosts := int32(4)
+		rayCluster.Spec.WorkerGroupSpecs[0].NumOfHosts = numOfHosts
+		rayCluster.Annotations = make(map[string]string)
+		rayCluster.Annotations[utils.EnableServeServiceKey] = "true"
+
+		It("Create a RayCluster custom resource", func() {
+			err := k8sClient.Create(ctx, rayCluster)
+			Expect(err).NotTo(HaveOccurred(), "Failed to create RayCluster")
+			Eventually(
+				getResourceFunc(ctx, client.ObjectKey{Name: rayCluster.Name, Namespace: namespace}, rayCluster),
+				time.Second*3, time.Millisecond*500).Should(BeNil(), "Should be able to see RayCluster: %v", rayCluster.Name)
+		})
+
+		It("Check RayCluster service names", func() {
path: ray-operator/controllers/ray/raycluster_controller_test.go
user: login: kevin85421
body: maybe also check Pods are created
created_at: 2025-03-06T07:10:01Z
author_association: MEMBER
diff_hunk: @@ -145,6 +146,56 @@ func appendNewWorkerGroup(workerGroupSpecs []rayv1.WorkerGroupSpec, newGroupName
 }
 
 var _ = Context("RayService env tests", func() {
+	Describe("RayService with a maximum name", Ordered, func() {
+		// If Autoscaler scales up the pending or active RayCluster, zero downtime upgrade should not be triggered.
path: ray-operator/controllers/ray/rayservice_controller_test.go
user: login: kevin85421
body: why do we need this comment?
created_at: 2025-03-06T07:17:50Z
author_association: MEMBER
diff_hunk: @@ -145,6 +146,56 @@ func appendNewWorkerGroup(workerGroupSpecs []rayv1.WorkerGroupSpec, newGroupName
 }
 
 var _ = Context("RayService env tests", func() {
+	Describe("RayService with a maximum name", Ordered, func() {
+		// If Autoscaler scales up the pending or active RayCluster, zero downtime upgrade should not be triggered.
+		ctx := context.Background()
+		namespace := "default"
+		serveAppName := "app1"
+		rayService := rayServiceTemplate(strings.Repeat("s", utils.MaxRayServiceNameLength), namespace, serveAppName)
+		rayCluster := &rayv1.RayCluster{}
+
+		It("Create a RayService custom resource", func() {
+			err := k8sClient.Create(ctx, rayService)
+			Expect(err).NotTo(HaveOccurred(), "failed to create RayService resource")
+			Eventually(
+				getResourceFunc(ctx, client.ObjectKey{Name: rayService.Name, Namespace: namespace}, rayService),
+				time.Second*3, time.Millisecond*500).Should(BeNil(), "RayService: %v", rayService.Name)
+		})
+
+		It("Should create a pending RayCluster", func() {
+			Eventually(
+				getPreparingRayClusterNameFunc(ctx, rayService),
+				time.Second*15, time.Millisecond*500).Should(Not(BeEmpty()), "Pending RayCluster name: %v", rayService.Status.PendingServiceStatus.RayClusterName)
path: ray-operator/controllers/ray/rayservice_controller_test.go
user: login: kevin85421
body: Change all `time.Second*15` in this PR to `time.Second*3` for consistency.
created_at: 2025-03-06T07:20:30Z
author_association: MEMBER
diff_hunk: @@ -220,6 +220,13 @@ func (r *RayClusterReconciler) rayClusterReconcile(ctx context.Context, instance
 		return ctrl.Result{}, nil
 	}
 
+	if err := utils.ValidateRayClusterMetadata(instance.ObjectMeta); err != nil {
+		logger.Error(err, fmt.Sprintf("The RayCluster metadata is invalid %s/%s", instance.Namespace, instance.Name))
path: ray-operator/controllers/ray/raycluster_controller.go
user: login: rueian
body: done.
created_at: 2025-03-06T07:55:53Z
author_association: COLLABORATOR
diff_hunk: @@ -220,6 +220,13 @@ func (r *RayClusterReconciler) rayClusterReconcile(ctx context.Context, instance
 		return ctrl.Result{}, nil
 	}
 
+	if err := utils.ValidateRayClusterMetadata(instance.ObjectMeta); err != nil {
+		logger.Error(err, fmt.Sprintf("The RayCluster metadata is invalid %s/%s", instance.Namespace, instance.Name))
+		r.Recorder.Eventf(instance, corev1.EventTypeWarning, string(utils.InvalidRayClusterSpec),
path: ray-operator/controllers/ray/raycluster_controller.go
user: login: rueian
body: done.
created_at: 2025-03-06T07:55:56Z
author_association: COLLABORATOR
diff_hunk: @@ -1330,4 +1331,35 @@ var _ = Context("Inside the default namespace", func() {
 				time.Second*3, time.Millisecond*500).Should(BeTrue())
 		})
 	})
+
+	Describe("RayCluster with a maximum name", func() {
+		ctx := context.Background()
+		namespace := "default"
+		rayCluster := rayClusterTemplate(strings.Repeat("r", utils.MaxRayClusterNameLength), namespace)
+		numOfHosts := int32(4)
+		rayCluster.Spec.WorkerGroupSpecs[0].NumOfHosts = numOfHosts
path: ray-operator/controllers/ray/raycluster_controller_test.go
user: login: rueian
body: We need it to create the -headless service. A comment is added.
created_at: 2025-03-06T07:56:27Z
author_association: COLLABORATOR
diff_hunk: @@ -1330,4 +1331,35 @@ var _ = Context("Inside the default namespace", func() {
 				time.Second*3, time.Millisecond*500).Should(BeTrue())
 		})
 	})
+
+	Describe("RayCluster with a maximum name", func() {
+		ctx := context.Background()
+		namespace := "default"
+		rayCluster := rayClusterTemplate(strings.Repeat("r", utils.MaxRayClusterNameLength), namespace)
+		numOfHosts := int32(4)
+		rayCluster.Spec.WorkerGroupSpecs[0].NumOfHosts = numOfHosts
+		rayCluster.Annotations = make(map[string]string)
+		rayCluster.Annotations[utils.EnableServeServiceKey] = "true"
path: ray-operator/controllers/ray/raycluster_controller_test.go
user: login: rueian
body: Sure, a comment is added.
created_at: 2025-03-06T07:56:39Z
author_association: COLLABORATOR
diff_hunk: @@ -1330,4 +1331,35 @@ var _ = Context("Inside the default namespace", func() {
 				time.Second*3, time.Millisecond*500).Should(BeTrue())
 		})
 	})
+
+	Describe("RayCluster with a maximum name", func() {
+		ctx := context.Background()
+		namespace := "default"
+		rayCluster := rayClusterTemplate(strings.Repeat("r", utils.MaxRayClusterNameLength), namespace)
+		numOfHosts := int32(4)
+		rayCluster.Spec.WorkerGroupSpecs[0].NumOfHosts = numOfHosts
+		rayCluster.Annotations = make(map[string]string)
+		rayCluster.Annotations[utils.EnableServeServiceKey] = "true"
+
+		It("Create a RayCluster custom resource", func() {
+			err := k8sClient.Create(ctx, rayCluster)
+			Expect(err).NotTo(HaveOccurred(), "Failed to create RayCluster")
+			Eventually(
+				getResourceFunc(ctx, client.ObjectKey{Name: rayCluster.Name, Namespace: namespace}, rayCluster),
+				time.Second*3, time.Millisecond*500).Should(BeNil(), "Should be able to see RayCluster: %v", rayCluster.Name)
+		})
+
+		It("Check RayCluster service names", func() {
path: ray-operator/controllers/ray/raycluster_controller_test.go
user: login: rueian
body: ok! Added above.
created_at: 2025-03-06T07:56:49Z
author_association: COLLABORATOR
diff_hunk: @@ -145,6 +146,56 @@ func appendNewWorkerGroup(workerGroupSpecs []rayv1.WorkerGroupSpec, newGroupName
 }
 
 var _ = Context("RayService env tests", func() {
+	Describe("RayService with a maximum name", Ordered, func() {
+		// If Autoscaler scales up the pending or active RayCluster, zero downtime upgrade should not be triggered.
path: ray-operator/controllers/ray/rayservice_controller_test.go
user: login: rueian
body: deleted. thanks!
created_at: 2025-03-06T07:56:59Z
author_association: COLLABORATOR
diff_hunk: @@ -145,6 +146,56 @@ func appendNewWorkerGroup(workerGroupSpecs []rayv1.WorkerGroupSpec, newGroupName
 }
 
 var _ = Context("RayService env tests", func() {
+	Describe("RayService with a maximum name", Ordered, func() {
+		// If Autoscaler scales up the pending or active RayCluster, zero downtime upgrade should not be triggered.
+		ctx := context.Background()
+		namespace := "default"
+		serveAppName := "app1"
+		rayService := rayServiceTemplate(strings.Repeat("s", utils.MaxRayServiceNameLength), namespace, serveAppName)
+		rayCluster := &rayv1.RayCluster{}
+
+		It("Create a RayService custom resource", func() {
+			err := k8sClient.Create(ctx, rayService)
+			Expect(err).NotTo(HaveOccurred(), "failed to create RayService resource")
+			Eventually(
+				getResourceFunc(ctx, client.ObjectKey{Name: rayService.Name, Namespace: namespace}, rayService),
+				time.Second*3, time.Millisecond*500).Should(BeNil(), "RayService: %v", rayService.Name)
+		})
+
+		It("Should create a pending RayCluster", func() {
+			Eventually(
+				getPreparingRayClusterNameFunc(ctx, rayService),
+				time.Second*15, time.Millisecond*500).Should(Not(BeEmpty()), "Pending RayCluster name: %v", rayService.Status.PendingServiceStatus.RayClusterName)
path: ray-operator/controllers/ray/rayservice_controller_test.go
user: login: rueian
body: done.
created_at: 2025-03-06T07:57:06Z
author_association: COLLABORATOR
diff_hunk: @@ -98,6 +106,10 @@ func ValidateRayJobStatus(rayJob *rayv1.RayJob) error {
 }
 
 func ValidateRayJobSpec(rayJob *rayv1.RayJob) error {
+	if len(rayJob.Name) > MaxRayJobNameLength {
path: ray-operator/controllers/ray/utils/validation.go
user: login: MortalHappiness
body: +1. It's weird that we have `ValidateRayClusterMetadata` and `ValidateRayCluseterSpec`, but only `ValidateRayJobSpec` and no `ValidateRayJobMetadata`.
created_at: 2025-03-06T08:55:25Z
author_association: MEMBER
diff_hunk: @@ -98,6 +106,10 @@ func ValidateRayJobStatus(rayJob *rayv1.RayJob) error {
 }
 
 func ValidateRayJobSpec(rayJob *rayv1.RayJob) error {
+	if len(rayJob.Name) > MaxRayJobNameLength {
path: ray-operator/controllers/ray/utils/validation.go
user: login: rueian
body: done. I can open the issue for (2) after this is merged.
created_at: 2025-03-06T17:41:03Z
author_association: COLLABORATOR
diff_hunk: @@ -220,8 +220,15 @@ func (r *RayClusterReconciler) rayClusterReconcile(ctx context.Context, instance
 		return ctrl.Result{}, nil
 	}
 
+	if err := utils.ValidateRayClusterMetadata(instance.ObjectMeta); err != nil {
+		logger.Error(err, "The RayCluster metadata is invalid", "namespace", instance.Namespace, "name", instance.Name)
path: ray-operator/controllers/ray/raycluster_controller.go
user: login: kevin85421
body: Do we need `namespace` and `name`? The context information should be added automatically.
created_at: 2025-03-07T20:44:40Z
author_association: MEMBER
diff_hunk: @@ -369,6 +370,55 @@ func TestValidateRayClusterSpecRedisUsername(t *testing.T) {
 	}
 }
 
+func TestValidateRayClusterSpecNames(t *testing.T) {
+	tests := []struct {
+		rayCluster   *rayv1.RayCluster
+		name         string
+		errorMessage string
+		expectError  bool
+	}{
+		{
+			name: "RayCluster name is too long (> MaxRayClusterNameLength characters)",
+			rayCluster: &rayv1.RayCluster{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: strings.Repeat("A", MaxRayClusterNameLength+1),
+				},
+			},
+			expectError:  true,
+			errorMessage: fmt.Sprintf("RayCluster name should be no more than %d characters", MaxRayClusterNameLength),
+		},
+		{
+			name: "Both RayCluster name is ok (== MaxRayClusterNameLength)",
path: ray-operator/controllers/ray/utils/validation_test.go
user: login: kevin85421
body: what does "both" refer to?
created_at: 2025-03-07T20:47:37Z
author_association: MEMBER
diff_hunk: @@ -369,6 +370,55 @@ func TestValidateRayClusterSpecRedisUsername(t *testing.T) {
 	}
 }
 
+func TestValidateRayClusterSpecNames(t *testing.T) {
+	tests := []struct {
+		rayCluster   *rayv1.RayCluster
+		name         string
+		errorMessage string
+		expectError  bool
+	}{
+		{
+			name: "RayCluster name is too long (> MaxRayClusterNameLength characters)",
+			rayCluster: &rayv1.RayCluster{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: strings.Repeat("A", MaxRayClusterNameLength+1),
+				},
+			},
+			expectError:  true,
+			errorMessage: fmt.Sprintf("RayCluster name should be no more than %d characters", MaxRayClusterNameLength),
+		},
+		{
+			name: "Both RayCluster name is ok (== MaxRayClusterNameLength)",
+			rayCluster: &rayv1.RayCluster{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: strings.Repeat("A", MaxRayClusterNameLength),
+				},
+				Spec: rayv1.RayClusterSpec{
path: ray-operator/controllers/ray/utils/validation_test.go
user: login: kevin85421
body: is cluster spec needed for this test?
created_at: 2025-03-07T20:48:26Z
author_association: MEMBER
diff_hunk: @@ -220,8 +220,15 @@ func (r *RayClusterReconciler) rayClusterReconcile(ctx context.Context, instance
 		return ctrl.Result{}, nil
 	}
 
+	if err := utils.ValidateRayClusterMetadata(instance.ObjectMeta); err != nil {
+		logger.Error(err, "The RayCluster metadata is invalid", "namespace", instance.Namespace, "name", instance.Name)
+		r.Recorder.Eventf(instance, corev1.EventTypeWarning, string(utils.InvalidRayClusterMetadata),
+			"The RayCluster metadata is invalid %s/%s: %v", instance.Namespace, instance.Name, err)
+		return ctrl.Result{}, nil
+	}
+
 	if err := utils.ValidateRayClusterSpec(&instance.Spec, instance.Annotations); err != nil {
-		logger.Error(err, fmt.Sprintf("The RayCluster spec is invalid %s/%s", instance.Namespace, instance.Name))
+		logger.Error(err, "The RayCluster spec is invalid", "namespace", instance.Namespace, "name", instance.Name)
path: ray-operator/controllers/ray/raycluster_controller.go
user: login: kevin85421
body: Do we need namespace and name?
created_at: 2025-03-07T20:53:27Z
author_association: MEMBER
diff_hunk: @@ -220,8 +220,15 @@ func (r *RayClusterReconciler) rayClusterReconcile(ctx context.Context, instance
 		return ctrl.Result{}, nil
 	}
 
+	if err := utils.ValidateRayClusterMetadata(instance.ObjectMeta); err != nil {
+		logger.Error(err, "The RayCluster metadata is invalid", "namespace", instance.Namespace, "name", instance.Name)
+		r.Recorder.Eventf(instance, corev1.EventTypeWarning, string(utils.InvalidRayClusterMetadata),
+			"The RayCluster metadata is invalid %s/%s: %v", instance.Namespace, instance.Name, err)
+		return ctrl.Result{}, nil
+	}
+
 	if err := utils.ValidateRayClusterSpec(&instance.Spec, instance.Annotations); err != nil {
-		logger.Error(err, fmt.Sprintf("The RayCluster spec is invalid %s/%s", instance.Namespace, instance.Name))
+		logger.Error(err, "The RayCluster spec is invalid", "namespace", instance.Namespace, "name", instance.Name)
path: ray-operator/controllers/ray/raycluster_controller.go
user: login: rueian
body: Removed.
created_at: 2025-03-07T21:19:19Z
author_association: COLLABORATOR
diff_hunk: @@ -369,6 +370,55 @@ func TestValidateRayClusterSpecRedisUsername(t *testing.T) {
 	}
 }
 
+func TestValidateRayClusterSpecNames(t *testing.T) {
+	tests := []struct {
+		rayCluster   *rayv1.RayCluster
+		name         string
+		errorMessage string
+		expectError  bool
+	}{
+		{
+			name: "RayCluster name is too long (> MaxRayClusterNameLength characters)",
+			rayCluster: &rayv1.RayCluster{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: strings.Repeat("A", MaxRayClusterNameLength+1),
+				},
+			},
+			expectError:  true,
+			errorMessage: fmt.Sprintf("RayCluster name should be no more than %d characters", MaxRayClusterNameLength),
+		},
+		{
+			name: "Both RayCluster name is ok (== MaxRayClusterNameLength)",
+			rayCluster: &rayv1.RayCluster{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: strings.Repeat("A", MaxRayClusterNameLength),
+				},
+				Spec: rayv1.RayClusterSpec{
path: ray-operator/controllers/ray/utils/validation_test.go
user: login: rueian
body: Removed.
created_at: 2025-03-07T21:19:24Z
author_association: COLLABORATOR
diff_hunk: @@ -369,6 +370,55 @@ func TestValidateRayClusterSpecRedisUsername(t *testing.T) {
 	}
 }
 
+func TestValidateRayClusterSpecNames(t *testing.T) {
+	tests := []struct {
+		rayCluster   *rayv1.RayCluster
+		name         string
+		errorMessage string
+		expectError  bool
+	}{
+		{
+			name: "RayCluster name is too long (> MaxRayClusterNameLength characters)",
+			rayCluster: &rayv1.RayCluster{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: strings.Repeat("A", MaxRayClusterNameLength+1),
+				},
+			},
+			expectError:  true,
+			errorMessage: fmt.Sprintf("RayCluster name should be no more than %d characters", MaxRayClusterNameLength),
+		},
+		{
+			name: "Both RayCluster name is ok (== MaxRayClusterNameLength)",
path: ray-operator/controllers/ray/utils/validation_test.go
user: login: rueian
body: Removed.
created_at: 2025-03-07T21:19:26Z
author_association: COLLABORATOR
diff_hunk: @@ -220,8 +220,15 @@ func (r *RayClusterReconciler) rayClusterReconcile(ctx context.Context, instance
 		return ctrl.Result{}, nil
 	}
 
+	if err := utils.ValidateRayClusterMetadata(instance.ObjectMeta); err != nil {
+		logger.Error(err, "The RayCluster metadata is invalid", "namespace", instance.Namespace, "name", instance.Name)
path: ray-operator/controllers/ray/raycluster_controller.go
user: login: rueian
body: Removed.
created_at: 2025-03-07T21:19:28Z
author_association: COLLABORATOR

PR comments:
user: login: davidxia
created_at: 2025-03-03T13:47:38Z
body: Would it be useful to also validate in webhooks like here? https://github.com/ray-project/kuberay/blob/6c4a77d56e37dc0c23e70fc103e40a1ddbbb6e24/ray-operator/pkg/webhooks/v1/raycluster_webhook.go#L79-L84

These webhooks can optionally be enabled by user (disabled by default). They prevent creation of invalid resources with a more obvious error than the controller events.
author_association: COLLABORATOR
user: login: rueian
created_at: 2025-03-03T16:11:56Z
body: Hi @davidxia, while it would certainly be useful to replicate the validation to the webhook, we don’t do it now. Most Kuberay users don’t use the validation webhook.
author_association: COLLABORATOR
user: login: rueian
created_at: 2025-03-06T06:54:36Z
body: > I felt that adding E2E tests for this was a bit overkill. Does env tests check the K8s resource name length?

Sure, I changed them to env tests.
author_association: COLLABORATOR
user: login: kevin85421
created_at: 2025-03-07T20:58:31Z
body: @MortalHappiness please review another iteration. Thanks!
author_association: MEMBER