Unverified Commit c8770464 authored by hhzhang16's avatar hhzhang16 Committed by GitHub
Browse files

feat: normalize dynamo namespace computation (#5231)


Signed-off-by: default avatarHannah Zhang <hannahz@nvidia.com>
parent abd4b5d9
......@@ -93,7 +93,6 @@ class DgdPlannerServiceConfig(BaseModel):
automatically created and mounted by the profiler; no PVC dependencies
"""
dynamoNamespace: str = "dynamo" # placeholder
componentType: str = "planner"
replicas: int = 1
extraPodSpec: PodSpec = PodSpec(
......
......@@ -261,7 +261,6 @@ def generate_dgd_config_with_planner(
# add the planner service
planner_config = DgdPlannerServiceConfig()
frontend_service = config.spec.services["Frontend"]
planner_config.dynamoNamespace = getattr(frontend_service, "dynamoNamespace", "dynamo") # type: ignore[attr-defined]
frontend_image: Optional[str] = None
if frontend_service.extraPodSpec and frontend_service.extraPodSpec.mainContainer:
frontend_image = frontend_service.extraPodSpec.mainContainer.image
......@@ -275,8 +274,6 @@ def generate_dgd_config_with_planner(
# Override profiling-specific arguments with results from profiling
# Remove and re-add to ensure correct values from profiling context
# Note: --namespace is NOT added here; planner gets it from DYN_NAMESPACE env var
# which is automatically injected by the operator based on dynamoNamespace
planner_args = [
arg
for arg in planner_args
......@@ -511,17 +508,6 @@ def _generate_mocker_config_with_planner(
# Add planner service (reuse the same planner config but with mocker backend)
mocker_planner_dict = copy.deepcopy(planner_dict)
# Get the mocker's dynamoNamespace from Frontend service
mocker_namespace = (
mocker_config.get("spec", {})
.get("services", {})
.get("Frontend", {})
.get("dynamoNamespace", "mocker-disagg")
)
# Update planner's dynamoNamespace to match mocker's namespace
mocker_planner_dict["dynamoNamespace"] = mocker_namespace
# Planner args use --key=value format, so we need to find and replace
planner_main_container = mocker_planner_dict.get("extraPodSpec", {}).get(
"mainContainer", {}
......
......@@ -14,14 +14,12 @@ spec:
value: "debug"
services:
Frontend:
dynamoNamespace: dynamo
componentType: frontend
replicas: 1
extraPodSpec:
mainContainer:
image: ${IMAGE}
VllmDecodeWorker:
dynamoNamespace: vllm-disagg
componentType: decode
replicas: 1
resources:
......@@ -40,7 +38,6 @@ spec:
- --model
- Qwen/Qwen3-0.6B
VllmPrefillWorker:
dynamoNamespace: vllm-disagg
componentType: prefill
replicas: 1
resources:
......
......@@ -20,6 +20,8 @@
package v1alpha1
import (
"fmt"
commonconsts "github.com/ai-dynamo/dynamo/deploy/operator/internal/consts"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
......@@ -309,6 +311,21 @@ func (s *DynamoComponentDeployment) GetParentGraphDeploymentNamespace() string {
return s.GetNamespace()
}
// GetDynamoNamespace returns the Dynamo namespace for this component.
func (s *DynamoComponentDeployment) GetDynamoNamespace() string {
return ComputeDynamoNamespace(s.Spec.GlobalDynamoNamespace, s.GetNamespace(), s.GetParentGraphDeploymentName())
}
// ComputeDynamoNamespace is the single source of truth for computing the Dynamo namespace.
// If globalDynamoNamespace is true, returns "dynamo" (global constant).
// Otherwise, returns {k8sNamespace}-{dgdName}.
func ComputeDynamoNamespace(globalDynamoNamespace bool, k8sNamespace, dgdName string) string {
if globalDynamoNamespace {
return commonconsts.GlobalDynamoNamespace
}
return fmt.Sprintf("%s-%s", k8sNamespace, dgdName)
}
// ModelReference identifies a model served by this component
type ModelReference struct {
// Name is the base model identifier (e.g., "llama-3-70b-instruct-v1")
......
......@@ -233,3 +233,8 @@ func (s *DynamoGraphDeployment) HasAnyMultinodeService() bool {
}
return false
}
// GetDynamoNamespaceForService returns the Dynamo namespace for a given service.
func (s *DynamoGraphDeployment) GetDynamoNamespaceForService(service *DynamoComponentDeploymentSharedSpec) string {
return ComputeDynamoNamespace(service.GlobalDynamoNamespace, s.GetNamespace(), s.GetName())
}
......@@ -851,7 +851,7 @@ func TestDynamoComponentDeploymentReconciler_generateLeaderWorkerSet(t *testing.
{Name: commonconsts.DynamoComponentEnvVar, Value: commonconsts.ComponentTypeWorker},
{Name: commonconsts.DynamoDiscoveryBackendEnvVar, Value: "kubernetes"},
{Name: "DYN_HEALTH_CHECK_ENABLED", Value: "false"},
{Name: commonconsts.DynamoNamespaceEnvVar, Value: "default"},
{Name: commonconsts.DynamoNamespaceEnvVar, Value: "default-test-lws-deploy"},
{Name: "DYN_PARENT_DGD_K8S_NAME", Value: "test-lws-deploy"},
{Name: "DYN_PARENT_DGD_K8S_NAMESPACE", Value: "default"},
{Name: "DYN_SYSTEM_ENABLED", Value: "true"},
......@@ -986,7 +986,7 @@ func TestDynamoComponentDeploymentReconciler_generateLeaderWorkerSet(t *testing.
{Name: commonconsts.DynamoComponentEnvVar, Value: commonconsts.ComponentTypeWorker},
{Name: commonconsts.DynamoDiscoveryBackendEnvVar, Value: "kubernetes"},
{Name: "DYN_HEALTH_CHECK_ENABLED", Value: "false"},
{Name: commonconsts.DynamoNamespaceEnvVar, Value: "default"},
{Name: commonconsts.DynamoNamespaceEnvVar, Value: "default-test-lws-deploy"},
{Name: "DYN_PARENT_DGD_K8S_NAME", Value: "test-lws-deploy"},
{Name: "DYN_PARENT_DGD_K8S_NAMESPACE", Value: "default"},
{Name: "DYN_SYSTEM_ENABLED", Value: "true"},
......
......@@ -154,6 +154,17 @@ func (r *DynamoGraphDeploymentReconciler) Reconcile(ctx context.Context, req ctr
logger.Info("Reconciliation done")
}()
// Handle finalizer
deleted, err := commoncontroller.HandleFinalizer(ctx, dynamoDeployment, r.Client, r)
if err != nil {
logger.Error(err, "failed to handle the finalizer")
reason = "failed_to_handle_the_finalizer"
return ctrl.Result{}, err
}
if deleted {
return ctrl.Result{}, nil
}
// Validate the DynamoGraphDeployment spec (defense in depth - only when webhooks are disabled)
if !r.Config.WebhooksEnabled {
validator := webhookvalidation.NewDynamoGraphDeploymentValidator(dynamoDeployment)
......@@ -176,15 +187,6 @@ func (r *DynamoGraphDeploymentReconciler) Reconcile(ctx context.Context, req ctr
}
}
deleted, err := commoncontroller.HandleFinalizer(ctx, dynamoDeployment, r.Client, r)
if err != nil {
logger.Error(err, "failed to handle the finalizer")
reason = "failed_to_handle_the_finalizer"
return ctrl.Result{}, err
}
if deleted {
return ctrl.Result{}, nil
}
reconcileResult, err := r.reconcileResources(ctx, dynamoDeployment)
state = reconcileResult.State
......
......@@ -337,10 +337,7 @@ func GenerateDynamoComponentsDeployments(ctx context.Context, parentDynamoGraphD
}
func getDynamoNamespace(object metav1.Object, service *v1alpha1.DynamoComponentDeploymentSharedSpec) string {
if service.GlobalDynamoNamespace {
return commonconsts.GlobalDynamoNamespace
}
return fmt.Sprintf("%s-%s", object.GetNamespace(), object.GetName())
return v1alpha1.ComputeDynamoNamespace(service.GlobalDynamoNamespace, object.GetNamespace(), object.GetName())
}
// updateDynDeploymentConfig updates the runtime config object for the given dynamoDeploymentComponent
......@@ -1071,15 +1068,15 @@ func setMetricsLabels(labels map[string]string, dynamoGraphDeployment *v1alpha1.
}
func generateComponentContext(component *v1alpha1.DynamoComponentDeploymentSharedSpec, parentGraphDeploymentName string, namespace string, numberOfNodes int32, discoveryBackend string) ComponentContext {
dynamoNamespace := v1alpha1.ComputeDynamoNamespace(component.GlobalDynamoNamespace, namespace, parentGraphDeploymentName)
componentContext := ComponentContext{
numberOfNodes: numberOfNodes,
ComponentType: component.ComponentType,
ParentGraphDeploymentName: parentGraphDeploymentName,
ParentGraphDeploymentNamespace: namespace,
DiscoveryBackend: discoveryBackend,
}
if component.DynamoNamespace != nil {
componentContext.DynamoNamespace = *component.DynamoNamespace
DynamoNamespace: dynamoNamespace,
}
return componentContext
}
......
......@@ -780,6 +780,123 @@ func Test_GetDynamoComponentDeploymentsGlobalNamespace(t *testing.T) {
}
}
// TestGenerateComponentContext tests the generateComponentContext function
// to ensure it correctly computes the DynamoNamespace from authoritative sources
// (k8s namespace + DGD name), ignoring any deprecated dynamoNamespace field.
func TestGenerateComponentContext(t *testing.T) {
tests := []struct {
name string
component *v1alpha1.DynamoComponentDeploymentSharedSpec
parentGraphDeploymentName string
namespace string
numberOfNodes int32
discoveryBackend string
expectedDynamoNamespace string
expectedComponentType string
expectedParentDGDName string
expectedParentDGDNamespace string
}{
{
name: "namespace-scoped operator: computes correct dynamo namespace",
component: &v1alpha1.DynamoComponentDeploymentSharedSpec{
ComponentType: commonconsts.ComponentTypePlanner,
// Deprecated field set to incorrect value - should be ignored
DynamoNamespace: ptr.To("old-incorrect-value"),
},
parentGraphDeploymentName: "my-deployment",
namespace: "my-namespace",
numberOfNodes: 1,
discoveryBackend: "kubernetes",
expectedDynamoNamespace: "my-namespace-my-deployment",
expectedComponentType: commonconsts.ComponentTypePlanner,
expectedParentDGDName: "my-deployment",
expectedParentDGDNamespace: "my-namespace",
},
{
name: "deprecated dynamoNamespace field is ignored",
component: &v1alpha1.DynamoComponentDeploymentSharedSpec{
ComponentType: commonconsts.ComponentTypeFrontend,
// This is the bug case: profiler sets dynamoNamespace to just DGD name
DynamoNamespace: ptr.To("vllm-disagg"),
},
parentGraphDeploymentName: "vllm-disagg",
namespace: "djangoz",
numberOfNodes: 1,
discoveryBackend: "kubernetes",
expectedDynamoNamespace: "djangoz-vllm-disagg", // Should be k8s-namespace + DGD name
expectedComponentType: commonconsts.ComponentTypeFrontend,
expectedParentDGDName: "vllm-disagg",
expectedParentDGDNamespace: "djangoz",
},
{
name: "GlobalDynamoNamespace takes precedence",
component: &v1alpha1.DynamoComponentDeploymentSharedSpec{
ComponentType: commonconsts.ComponentTypeWorker,
GlobalDynamoNamespace: true,
// Even with deprecated field set, GlobalDynamoNamespace should win
DynamoNamespace: ptr.To("should-be-ignored"),
},
parentGraphDeploymentName: "shared-frontend",
namespace: "production",
numberOfNodes: 2,
discoveryBackend: "etcd",
expectedDynamoNamespace: commonconsts.GlobalDynamoNamespace, // "dynamo"
expectedComponentType: commonconsts.ComponentTypeWorker,
expectedParentDGDName: "shared-frontend",
expectedParentDGDNamespace: "production",
},
{
name: "nil dynamoNamespace field still computes correctly",
component: &v1alpha1.DynamoComponentDeploymentSharedSpec{
ComponentType: commonconsts.ComponentTypePlanner,
DynamoNamespace: nil, // Not set at all
},
parentGraphDeploymentName: "test-dgd",
namespace: "default",
numberOfNodes: 1,
discoveryBackend: "kubernetes",
expectedDynamoNamespace: "default-test-dgd",
expectedComponentType: commonconsts.ComponentTypePlanner,
expectedParentDGDName: "test-dgd",
expectedParentDGDNamespace: "default",
},
{
name: "different namespace and DGD name combinations",
component: &v1alpha1.DynamoComponentDeploymentSharedSpec{
ComponentType: commonconsts.ComponentTypeFrontend,
},
parentGraphDeploymentName: "llama-70b-prod",
namespace: "ml-inference",
numberOfNodes: 4,
discoveryBackend: "nats",
expectedDynamoNamespace: "ml-inference-llama-70b-prod",
expectedComponentType: commonconsts.ComponentTypeFrontend,
expectedParentDGDName: "llama-70b-prod",
expectedParentDGDNamespace: "ml-inference",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := generateComponentContext(
tt.component,
tt.parentGraphDeploymentName,
tt.namespace,
tt.numberOfNodes,
tt.discoveryBackend,
)
assert.Equal(t, tt.expectedDynamoNamespace, ctx.DynamoNamespace,
"DynamoNamespace should be computed from k8s namespace + DGD name")
assert.Equal(t, tt.expectedComponentType, ctx.ComponentType)
assert.Equal(t, tt.expectedParentDGDName, ctx.ParentGraphDeploymentName)
assert.Equal(t, tt.expectedParentDGDNamespace, ctx.ParentGraphDeploymentNamespace)
assert.Equal(t, tt.numberOfNodes, ctx.numberOfNodes)
assert.Equal(t, tt.discoveryBackend, ctx.DiscoveryBackend)
})
}
}
func Test_updateDynDeploymentConfig(t *testing.T) {
type args struct {
dynamoDeploymentComponent *v1alpha1.DynamoComponentDeployment
......@@ -5139,7 +5256,7 @@ func TestGenerateBasePodSpec_Worker(t *testing.T) {
{Name: commonconsts.DynamoComponentEnvVar, Value: "worker"},
{Name: commonconsts.DynamoDiscoveryBackendEnvVar, Value: "kubernetes"},
{Name: "DYN_HEALTH_CHECK_ENABLED", Value: "false"},
{Name: commonconsts.DynamoNamespaceEnvVar, Value: ""},
{Name: commonconsts.DynamoNamespaceEnvVar, Value: "default-test-deployment"},
{Name: "DYN_PARENT_DGD_K8S_NAME", Value: "test-deployment"},
{Name: "DYN_PARENT_DGD_K8S_NAMESPACE", Value: "default"},
{Name: "DYN_SYSTEM_ENABLED", Value: "true"},
......
......@@ -41,7 +41,8 @@ func NewDynamoComponentDeploymentValidator(deployment *nvidiacomv1alpha1.DynamoC
// Returns warnings and error.
func (v *DynamoComponentDeploymentValidator) Validate() (admission.Warnings, error) {
// Validate shared spec fields using SharedSpecValidator
sharedValidator := NewSharedSpecValidator(&v.deployment.Spec.DynamoComponentDeploymentSharedSpec, "spec")
calculatedNamespace := v.deployment.GetDynamoNamespace()
sharedValidator := NewSharedSpecValidator(&v.deployment.Spec.DynamoComponentDeploymentSharedSpec, "spec", calculatedNamespace)
// DCD-specific validation would go here (currently none)
......
......@@ -226,7 +226,8 @@ func (v *DynamoGraphDeploymentValidator) validateReplicasChanges(old *nvidiacomv
func (v *DynamoGraphDeploymentValidator) validateService(serviceName string, service *nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec) (admission.Warnings, error) {
// Use SharedSpecValidator to validate service spec (which is a DynamoComponentDeploymentSharedSpec)
fieldPath := fmt.Sprintf("spec.services[%s]", serviceName)
sharedValidator := NewSharedSpecValidator(service, fieldPath)
calculatedNamespace := v.deployment.GetDynamoNamespaceForService(service)
sharedValidator := NewSharedSpecValidator(service, fieldPath, calculatedNamespace)
return sharedValidator.Validate()
}
......
......@@ -28,22 +28,38 @@ import (
// This validator is used by both DynamoComponentDeploymentValidator and DynamoGraphDeploymentValidator
// to provide consistent validation logic for shared spec fields.
type SharedSpecValidator struct {
spec *nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec
fieldPath string // e.g., "spec" for DCD, "spec.services[foo]" for DGD
spec *nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec
fieldPath string // e.g., "spec" for DCD, "spec.services[foo]" for DGD
calculatedNamespace string // The namespace that will be used: {k8s_namespace}-{dgd_name}
}
// NewSharedSpecValidator creates a new validator for DynamoComponentDeploymentSharedSpec.
// fieldPath is used to provide context in error messages (e.g., "spec" or "spec.services[main]").
func NewSharedSpecValidator(spec *nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec, fieldPath string) *SharedSpecValidator {
// calculatedNamespace is the namespace the operator will use:
// - If GlobalDynamoNamespace is true: "dynamo" (global constant)
// - Otherwise: {k8s_namespace}-{dgd_name}
func NewSharedSpecValidator(spec *nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec, fieldPath string, calculatedNamespace string) *SharedSpecValidator {
return &SharedSpecValidator{
spec: spec,
fieldPath: fieldPath,
spec: spec,
fieldPath: fieldPath,
calculatedNamespace: calculatedNamespace,
}
}
// Validate performs validation on the shared spec fields.
// Returns warnings (e.g., deprecation notices) and error if validation fails.
func (v *SharedSpecValidator) Validate() (admission.Warnings, error) {
// Collect warnings (e.g., deprecation notices)
var warnings admission.Warnings
// Warn about deprecated dynamoNamespace field
if v.spec.DynamoNamespace != nil && *v.spec.DynamoNamespace != "" {
warnings = append(warnings, fmt.Sprintf(
"%s.dynamoNamespace is deprecated and ignored. Value '%s' will be replaced with '%s'. "+
"Remove this field from your configuration",
v.fieldPath, *v.spec.DynamoNamespace, v.calculatedNamespace))
}
// Validate replicas if specified
if v.spec.Replicas != nil && *v.spec.Replicas < 0 {
return nil, fmt.Errorf("%s.replicas must be non-negative", v.fieldPath)
......@@ -68,9 +84,6 @@ func (v *SharedSpecValidator) Validate() (admission.Warnings, error) {
}
}
// Collect warnings (e.g., deprecation notices)
var warnings admission.Warnings
// Check for deprecated autoscaling field
//nolint:staticcheck // SA1019: Intentionally checking deprecated field to warn users
if v.spec.Autoscaling != nil {
......
......@@ -24,6 +24,11 @@ import (
"k8s.io/apimachinery/pkg/api/resource"
)
// ptr is a helper function to create a pointer to a string
func ptr(s string) *string {
return &s
}
func TestSharedSpecValidator_Validate(t *testing.T) {
var (
negativeReplicas = int32(-1)
......@@ -31,11 +36,12 @@ func TestSharedSpecValidator_Validate(t *testing.T) {
)
tests := []struct {
name string
spec *nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec
fieldPath string
wantErr bool
errMsg string
name string
spec *nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec
fieldPath string
calculatedNamespace string
wantErr bool
errMsg string
}{
{
name: "valid spec with all fields",
......@@ -60,17 +66,37 @@ func TestSharedSpecValidator_Validate(t *testing.T) {
Size: resource.MustParse("1Gi"),
},
},
fieldPath: "spec",
wantErr: false,
fieldPath: "spec",
calculatedNamespace: "default-my-dgd",
wantErr: false,
},
{
name: "negative replicas",
spec: &nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
Replicas: &negativeReplicas,
},
fieldPath: "spec",
wantErr: true,
errMsg: "spec.replicas must be non-negative",
fieldPath: "spec",
calculatedNamespace: "default-my-dgd",
wantErr: true,
errMsg: "spec.replicas must be non-negative",
},
{
name: "nil dynamoNamespace is allowed",
spec: &nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
DynamoNamespace: nil,
},
fieldPath: "spec",
calculatedNamespace: "default-my-dgd",
wantErr: false,
},
{
name: "empty string dynamoNamespace is allowed",
spec: &nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
DynamoNamespace: ptr(""),
},
fieldPath: "spec",
calculatedNamespace: "default-my-dgd",
wantErr: false,
},
{
name: "ingress enabled without host",
......@@ -80,9 +106,10 @@ func TestSharedSpecValidator_Validate(t *testing.T) {
Host: "",
},
},
fieldPath: "spec",
wantErr: true,
errMsg: "spec.ingress.host is required when ingress is enabled",
fieldPath: "spec",
calculatedNamespace: "default-my-dgd",
wantErr: true,
errMsg: "spec.ingress.host is required when ingress is enabled",
},
{
name: "ingress disabled - no validation",
......@@ -92,8 +119,9 @@ func TestSharedSpecValidator_Validate(t *testing.T) {
Host: "",
},
},
fieldPath: "spec",
wantErr: false,
fieldPath: "spec",
calculatedNamespace: "default-my-dgd",
wantErr: false,
},
{
name: "volume mount without mountPoint and not compilation cache",
......@@ -106,9 +134,10 @@ func TestSharedSpecValidator_Validate(t *testing.T) {
},
},
},
fieldPath: "spec",
wantErr: true,
errMsg: "spec.volumeMounts[0].mountPoint is required when useAsCompilationCache is false",
fieldPath: "spec",
calculatedNamespace: "default-my-dgd",
wantErr: true,
errMsg: "spec.volumeMounts[0].mountPoint is required when useAsCompilationCache is false",
},
{
name: "volume mount with mountPoint",
......@@ -120,8 +149,9 @@ func TestSharedSpecValidator_Validate(t *testing.T) {
},
},
},
fieldPath: "spec",
wantErr: false,
fieldPath: "spec",
calculatedNamespace: "default-my-dgd",
wantErr: false,
},
{
name: "volume mount as compilation cache without mountPoint",
......@@ -133,8 +163,9 @@ func TestSharedSpecValidator_Validate(t *testing.T) {
},
},
},
fieldPath: "spec",
wantErr: false,
fieldPath: "spec",
calculatedNamespace: "default-my-dgd",
wantErr: false,
},
{
name: "shared memory enabled without size",
......@@ -144,9 +175,10 @@ func TestSharedSpecValidator_Validate(t *testing.T) {
Size: resource.Quantity{},
},
},
fieldPath: "spec",
wantErr: true,
errMsg: "spec.sharedMemory.size is required when disabled is false",
fieldPath: "spec",
calculatedNamespace: "default-my-dgd",
wantErr: true,
errMsg: "spec.sharedMemory.size is required when disabled is false",
},
{
name: "shared memory enabled with size",
......@@ -156,8 +188,9 @@ func TestSharedSpecValidator_Validate(t *testing.T) {
Size: resource.MustParse("2Gi"),
},
},
fieldPath: "spec",
wantErr: false,
fieldPath: "spec",
calculatedNamespace: "default-my-dgd",
wantErr: false,
},
{
name: "shared memory disabled without size",
......@@ -167,23 +200,25 @@ func TestSharedSpecValidator_Validate(t *testing.T) {
Size: resource.Quantity{},
},
},
fieldPath: "spec",
wantErr: false,
fieldPath: "spec",
calculatedNamespace: "default-my-dgd",
wantErr: false,
},
{
name: "custom field path for service validation",
spec: &nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
Replicas: &negativeReplicas,
},
fieldPath: "spec.services[main]",
wantErr: true,
errMsg: "spec.services[main].replicas must be non-negative",
fieldPath: "spec.services[main]",
calculatedNamespace: "default-my-dgd",
wantErr: true,
errMsg: "spec.services[main].replicas must be non-negative",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
validator := NewSharedSpecValidator(tt.spec, tt.fieldPath)
validator := NewSharedSpecValidator(tt.spec, tt.fieldPath, tt.calculatedNamespace)
_, err := validator.Validate()
if (err != nil) != tt.wantErr {
......@@ -202,18 +237,21 @@ func TestSharedSpecValidator_Validate_Warnings(t *testing.T) {
validReplicas := int32(3)
tests := []struct {
name string
spec *nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec
fieldPath string
wantWarnings int
name string
spec *nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec
fieldPath string
calculatedNamespace string
wantWarnings int
wantWarningContains string // optional substring to check in warning
}{
{
name: "no warnings for spec without autoscaling",
spec: &nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
Replicas: &validReplicas,
},
fieldPath: "spec",
wantWarnings: 0,
fieldPath: "spec",
calculatedNamespace: "default-my-dgd",
wantWarnings: 0,
},
{
name: "warning for deprecated autoscaling field enabled",
......@@ -226,14 +264,26 @@ func TestSharedSpecValidator_Validate_Warnings(t *testing.T) {
MaxReplicas: 10,
},
},
fieldPath: "spec",
wantWarnings: 1,
fieldPath: "spec",
calculatedNamespace: "default-my-dgd",
wantWarnings: 1,
},
{
name: "warning for deprecated dynamoNamespace field shows calculated namespace",
spec: &nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
Replicas: &validReplicas,
DynamoNamespace: ptr("my-custom-namespace"),
},
fieldPath: "spec.services[Frontend]",
calculatedNamespace: "hannahz-trtllm-disagg",
wantWarnings: 1,
wantWarningContains: "Value 'my-custom-namespace' will be replaced with 'hannahz-trtllm-disagg'",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
validator := NewSharedSpecValidator(tt.spec, tt.fieldPath)
validator := NewSharedSpecValidator(tt.spec, tt.fieldPath, tt.calculatedNamespace)
warnings, err := validator.Validate()
if err != nil {
......@@ -244,6 +294,34 @@ func TestSharedSpecValidator_Validate_Warnings(t *testing.T) {
if len(warnings) != tt.wantWarnings {
t.Errorf("SharedSpecValidator.Validate() warnings count = %d, want %d", len(warnings), tt.wantWarnings)
}
if tt.wantWarningContains != "" && len(warnings) > 0 {
found := false
for _, w := range warnings {
if contains(w, tt.wantWarningContains) {
found = true
break
}
}
if !found {
t.Errorf("SharedSpecValidator.Validate() warnings = %v, want warning containing %q", warnings, tt.wantWarningContains)
}
}
})
}
}
// contains checks if s contains substr
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
(len(s) > 0 && len(substr) > 0 && findSubstring(s, substr)))
}
func findSubstring(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}
......@@ -8,7 +8,6 @@ metadata:
spec:
services:
Frontend:
dynamoNamespace: mocker-agg
componentType: frontend
replicas: 1
extraPodSpec:
......@@ -16,7 +15,6 @@ spec:
image: my-registry/mocker-runtime:my-tag
decode:
envFromSecret: hf-token-secret
dynamoNamespace: mocker-agg
componentType: worker
subComponentType: decode
replicas: 1
......
......@@ -8,7 +8,6 @@ metadata:
spec:
services:
Frontend:
dynamoNamespace: mocker-disagg
componentType: frontend
replicas: 1
extraPodSpec:
......@@ -16,7 +15,6 @@ spec:
image: my-registry/mocker-runtime:my-tag
prefill:
envFromSecret: hf-token-secret
dynamoNamespace: mocker-disagg
componentType: worker
subComponentType: prefill
replicas: 1
......@@ -40,7 +38,6 @@ spec:
- --is-prefill-worker
decode:
envFromSecret: hf-token-secret
dynamoNamespace: mocker-disagg
componentType: worker
subComponentType: decode
replicas: 1
......
......@@ -8,7 +8,6 @@ metadata:
spec:
services:
Frontend:
dynamoNamespace: sglang-agg
componentType: frontend
replicas: 1
extraPodSpec:
......@@ -16,7 +15,6 @@ spec:
image: my-registry/sglang-runtime:my-tag
decode:
envFromSecret: hf-token-secret
dynamoNamespace: sglang-agg
componentType: worker
replicas: 1
resources:
......
......@@ -11,7 +11,6 @@ spec:
value: "1"
services:
Frontend:
dynamoNamespace: sglang-agg
componentType: frontend
replicas: 1
extraPodSpec:
......@@ -19,7 +18,6 @@ spec:
image: my-registry/sglang-runtime:my-tag
decode:
envFromSecret: hf-token-secret
dynamoNamespace: sglang-agg
componentType: worker
replicas: 1
resources:
......
......@@ -8,7 +8,6 @@ metadata:
spec:
services:
Frontend:
dynamoNamespace: sglang-agg-router
componentType: frontend
replicas: 1
extraPodSpec:
......@@ -19,7 +18,6 @@ spec:
value: kv
decode:
envFromSecret: hf-token-secret
dynamoNamespace: sglang-agg-router
componentType: worker
replicas: 1
resources:
......
......@@ -17,7 +17,6 @@ spec:
backendFramework: sglang
services:
Frontend:
dynamoNamespace: sglang-disagg-multinode
componentType: frontend
replicas: 1
extraPodSpec:
......@@ -27,7 +26,6 @@ spec:
multinode:
nodeCount: 2
envFromSecret: hf-token-secret
dynamoNamespace: sglang-disagg-multinode
componentType: worker
replicas: 1
resources:
......@@ -64,7 +62,6 @@ spec:
multinode:
nodeCount: 2
envFromSecret: hf-token-secret
dynamoNamespace: sglang-disagg-multinode
componentType: worker
replicas: 1
resources:
......
......@@ -8,7 +8,6 @@ metadata:
spec:
services:
Frontend:
dynamoNamespace: sglang-disagg
componentType: frontend
replicas: 1
extraPodSpec:
......@@ -16,7 +15,6 @@ spec:
image: my-registry/sglang-runtime:my-tag
decode:
envFromSecret: hf-token-secret
dynamoNamespace: sglang-disagg
componentType: worker
subComponentType: decode
replicas: 1
......@@ -52,7 +50,6 @@ spec:
- "0.0.0.0"
prefill:
envFromSecret: hf-token-secret
dynamoNamespace: sglang-disagg
componentType: worker
subComponentType: prefill
replicas: 1
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment