Skip to content

Commit f7f85dd

Browse files
committed
Add validation logic
Signed-off-by: Ryan O'Leary <ryanaoleary@google.com>
1 parent ee7b3ba commit f7f85dd

File tree

2 files changed

+157
-0
lines changed

2 files changed

+157
-0
lines changed

ray-operator/controllers/ray/utils/validation.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package utils
33
import (
44
errstd "errors"
55
"fmt"
6+
"strings"
67

78
corev1 "k8s.io/api/core/v1"
89
"k8s.io/apimachinery/pkg/api/meta"
@@ -33,16 +34,51 @@ func ValidateRayClusterMetadata(metadata metav1.ObjectMeta) error {
3334
return nil
3435
}
3536

37+
// validateRayGroupResourcesAndLabels checks for conflicting resource definitions and invalid labels.
38+
func validateRayGroupResourcesAndLabels(groupName string, rayStartParams, resources, labels map[string]string) error {
39+
hasRayStartResources := rayStartParams["num-cpus"] != "" ||
40+
rayStartParams["num-gpus"] != "" ||
41+
rayStartParams["memory"] != "" ||
42+
rayStartParams["resources"] != ""
43+
if hasRayStartResources && len(resources) > 0 {
44+
return fmt.Errorf("resource fields should not be set in both rayStartParams and Resources for %s group. Please use only one", groupName)
45+
}
46+
47+
if _, ok := rayStartParams["labels"]; ok {
48+
return fmt.Errorf("rayStartParams['labels'] is not supported for %s group. Please use the top-level Labels field instead", groupName)
49+
}
50+
51+
// Validate syntax for the top-level Labels field is parsable.
52+
for key, val := range labels {
53+
if strings.Contains(key, ",") {
54+
return fmt.Errorf("label key for %s group cannot contain commas, but found: '%s'", groupName, key)
55+
}
56+
if strings.Contains(val, ",") {
57+
return fmt.Errorf("label value for key '%s' in %s group cannot contain commas, but found: '%s'", key, groupName, val)
58+
}
59+
}
60+
61+
return nil
62+
}
63+
3664
// Validation for invalid Ray Cluster configurations.
3765
func ValidateRayClusterSpec(spec *rayv1.RayClusterSpec, annotations map[string]string) error {
3866
if len(spec.HeadGroupSpec.Template.Spec.Containers) == 0 {
3967
return fmt.Errorf("headGroupSpec should have at least one container")
4068
}
4169

70+
if err := validateRayGroupResourcesAndLabels("Head", spec.HeadGroupSpec.RayStartParams, spec.HeadGroupSpec.Resources, spec.HeadGroupSpec.Labels); err != nil {
71+
return err
72+
}
73+
4274
for _, workerGroup := range spec.WorkerGroupSpecs {
4375
if len(workerGroup.Template.Spec.Containers) == 0 {
4476
return fmt.Errorf("workerGroupSpec should have at least one container")
4577
}
78+
79+
if err := validateRayGroupResourcesAndLabels(workerGroup.GroupName, workerGroup.RayStartParams, workerGroup.Resources, workerGroup.Labels); err != nil {
80+
return err
81+
}
4682
}
4783

4884
if annotations[RayFTEnabledAnnotationKey] != "" && spec.GcsFaultToleranceOptions != nil {

ray-operator/controllers/ray/utils/validation_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,127 @@ func TestValidateRayClusterSpecAutoscaler(t *testing.T) {
683683
}
684684
}
685685

686+
func TestValidateRayClusterSpec_ResourcesAndLabels(t *testing.T) {
687+
// Util function to create a RayCluster spec.
688+
createSpec := func() rayv1.RayClusterSpec {
689+
return rayv1.RayClusterSpec{
690+
HeadGroupSpec: rayv1.HeadGroupSpec{
691+
Template: podTemplateSpec(nil, nil),
692+
},
693+
WorkerGroupSpecs: []rayv1.WorkerGroupSpec{
694+
{
695+
GroupName: "worker-group",
696+
Template: podTemplateSpec(nil, nil),
697+
},
698+
},
699+
}
700+
}
701+
702+
tests := []struct {
703+
name string
704+
errorMessage string
705+
spec rayv1.RayClusterSpec
706+
expectError bool
707+
}{
708+
{
709+
name: "Invalid: Head group has resources in both rayStartParams and top-level Resources",
710+
spec: func() rayv1.RayClusterSpec {
711+
s := createSpec()
712+
s.HeadGroupSpec.RayStartParams = map[string]string{"num-cpus": "1"}
713+
s.HeadGroupSpec.Resources = map[string]string{"CPU": "1"}
714+
return s
715+
}(),
716+
expectError: true,
717+
errorMessage: "resource fields should not be set in both rayStartParams and Resources for Head group. Please use only one",
718+
},
719+
{
720+
name: "Invalid: Worker group has resources in both rayStartParams and .Resources",
721+
spec: func() rayv1.RayClusterSpec {
722+
s := createSpec()
723+
s.WorkerGroupSpecs[0].RayStartParams = map[string]string{"num-gpus": "1"}
724+
s.WorkerGroupSpecs[0].Resources = map[string]string{"GPU": "1"}
725+
return s
726+
}(),
727+
expectError: true,
728+
errorMessage: "resource fields should not be set in both rayStartParams and Resources for worker-group group. Please use only one",
729+
},
730+
{
731+
name: "Valid: Only rayStartParams resources are set for head",
732+
spec: func() rayv1.RayClusterSpec {
733+
s := createSpec()
734+
s.HeadGroupSpec.RayStartParams = map[string]string{
735+
"num-cpus": "2",
736+
"memory": "4G",
737+
"resources": "{\"TPU\": \"8\"}",
738+
}
739+
return s
740+
}(),
741+
expectError: false,
742+
},
743+
{
744+
name: "Valid: Only .Resources field is set for worker",
745+
spec: func() rayv1.RayClusterSpec {
746+
s := createSpec()
747+
s.WorkerGroupSpecs[0].Resources = map[string]string{"CPU": "2", "memory": "4G", "TPU": "8"}
748+
return s
749+
}(),
750+
expectError: false,
751+
},
752+
{
753+
name: "Invalid: Head group has 'labels' in rayStartParams",
754+
spec: func() rayv1.RayClusterSpec {
755+
s := createSpec()
756+
s.HeadGroupSpec.RayStartParams = map[string]string{"labels": "ray.io/node-group=worker-group-1"}
757+
return s
758+
}(),
759+
expectError: true,
760+
errorMessage: "rayStartParams['labels'] is not supported for Head group. Please use the top-level Labels field instead",
761+
},
762+
{
763+
name: "Invalid: Worker group has 'labels' in rayStartParams",
764+
spec: func() rayv1.RayClusterSpec {
765+
s := createSpec()
766+
s.WorkerGroupSpecs[0].RayStartParams = map[string]string{"labels": "ray.io/node-group=worker-group-1"}
767+
return s
768+
}(),
769+
expectError: true,
770+
errorMessage: "rayStartParams['labels'] is not supported for worker-group group. Please use the top-level Labels field instead",
771+
},
772+
{
773+
name: "Valid: Only .Labels field is set",
774+
spec: func() rayv1.RayClusterSpec {
775+
s := createSpec()
776+
s.HeadGroupSpec.Labels = map[string]string{"ray.io/market-type": "on-demand"}
777+
s.WorkerGroupSpecs[0].Labels = map[string]string{"ray.io/accelerator-type": "TPU-V6E"}
778+
return s
779+
}(),
780+
expectError: false,
781+
},
782+
{
783+
name: "Invalid: .Labels field values contain commas",
784+
spec: func() rayv1.RayClusterSpec {
785+
s := createSpec()
786+
s.HeadGroupSpec.Labels = map[string]string{"invalid,labels,with,commas": "value"}
787+
return s
788+
}(),
789+
expectError: true,
790+
errorMessage: "label key for Head group cannot contain commas, but found: 'invalid,labels,with,commas'",
791+
},
792+
}
793+
794+
for _, tt := range tests {
795+
t.Run(tt.name, func(t *testing.T) {
796+
err := ValidateRayClusterSpec(&tt.spec, nil)
797+
if tt.expectError {
798+
require.Error(t, err)
799+
assert.EqualError(t, err, tt.errorMessage)
800+
} else {
801+
require.NoError(t, err)
802+
}
803+
})
804+
}
805+
}
806+
686807
func TestValidateRayJobStatus(t *testing.T) {
687808
tests := []struct {
688809
name string

0 commit comments

Comments
 (0)