/* * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package validation import ( "strings" "testing" nvidiacomv1alpha1 "github.com/ai-dynamo/dynamo/deploy/operator/api/v1alpha1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestDynamoGraphDeploymentRequestValidator_Validate(t *testing.T) { validConfig := `{"engine": {"backend": "vllm"}, "deployment": {"model": "test-model"}}` configWithDifferentBackend := `{"engine": {"backend": "sglang"}}` configWithDifferentModel := `{"deployment": {"model": "different-model"}}` invalidYAML := `{invalid yaml` tests := []struct { name string request *nvidiacomv1alpha1.DynamoGraphDeploymentRequest isClusterWide bool wantErr bool errMsg string wantWarnings bool expectedWarning string errContains bool }{ { name: "valid request", request: &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{ ObjectMeta: metav1.ObjectMeta{ Name: "test-dgdr", Namespace: "default", }, Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Backend: "vllm", ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{ ProfilerImage: "profiler:latest", Config: &apiextensionsv1.JSON{ Raw: []byte(validConfig), }, }, }, }, isClusterWide: true, wantErr: false, }, { name: "missing profiler image", request: &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{ ObjectMeta: metav1.ObjectMeta{ Name: "test-dgdr", Namespace: "default", }, Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Backend: "vllm", ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{ ProfilerImage: "", Config: &apiextensionsv1.JSON{ Raw: []byte(validConfig), }, }, }, }, isClusterWide: true, wantErr: true, errMsg: "spec.profilingConfig.profilerImage is required", }, { name: "missing profiling config", request: &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{ ObjectMeta: metav1.ObjectMeta{ Name: "test-dgdr", Namespace: "default", }, Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Backend: "vllm", ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{ ProfilerImage: "profiler:latest", Config: nil, }, }, }, isClusterWide: true, wantErr: true, errMsg: "spec.profilingConfig.config is required and must not be empty", }, { name: "empty profiling config", request: &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{ ObjectMeta: metav1.ObjectMeta{ Name: "test-dgdr", Namespace: "default", }, Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Backend: "vllm", ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{ ProfilerImage: "profiler:latest", Config: &apiextensionsv1.JSON{ Raw: []byte{}, }, }, }, }, isClusterWide: true, wantErr: true, errMsg: "spec.profilingConfig.config is required and must not be empty", }, { name: "enableGpuDiscovery true for cluster-wide operator", request: &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{ ObjectMeta: metav1.ObjectMeta{ Name: "test-dgdr", Namespace: "default", }, Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Backend: "vllm", EnableGpuDiscovery: true, ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{ ProfilerImage: "profiler:latest", Config: &apiextensionsv1.JSON{ Raw: []byte(validConfig), }, }, }, }, isClusterWide: true, wantErr: false, }, { name: "enableGpuDiscovery true for namespace-restricted operator", request: &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{ ObjectMeta: metav1.ObjectMeta{ Name: "test-dgdr", Namespace: "default", }, Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Backend: "vllm", EnableGpuDiscovery: true, ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{ ProfilerImage: "profiler:latest", Config: &apiextensionsv1.JSON{ Raw: []byte(validConfig), }, }, }, }, isClusterWide: false, wantErr: true, errMsg: "spec.enableGpuDiscovery can only be set to true for cluster-wide operators. Namespace-restricted operators cannot access cluster nodes for GPU discovery. Please set enableGpuDiscovery to false and provide hardware configuration (hardware.min_num_gpus_per_engine, hardware.max_num_gpus_per_engine, hardware.num_gpus_per_node) in spec.profilingConfig.config", }, { name: "enableGpuDiscovery false for namespace-restricted operator", request: &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{ ObjectMeta: metav1.ObjectMeta{ Name: "test-dgdr", Namespace: "default", }, Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Backend: "vllm", EnableGpuDiscovery: false, ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{ ProfilerImage: "profiler:latest", Config: &apiextensionsv1.JSON{ Raw: []byte(validConfig), }, }, }, }, isClusterWide: false, wantErr: false, }, { name: "invalid config YAML", request: &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{ ObjectMeta: metav1.ObjectMeta{ Name: "test-dgdr", Namespace: "default", }, Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Backend: "vllm", ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{ ProfilerImage: "profiler:latest", Config: &apiextensionsv1.JSON{ Raw: []byte(invalidYAML), }, }, }, }, isClusterWide: true, wantErr: true, errMsg: "failed to parse spec.profilingConfig.config: error converting YAML to JSON: yaml: line 1: did not find expected ',' or '}'", }, { name: "warning for different backend in config", request: &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{ ObjectMeta: metav1.ObjectMeta{ Name: "test-dgdr", Namespace: "default", }, Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Backend: "vllm", ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{ ProfilerImage: "profiler:latest", Config: &apiextensionsv1.JSON{ Raw: []byte(configWithDifferentBackend), }, }, }, }, isClusterWide: true, wantErr: false, wantWarnings: true, expectedWarning: "spec.profilingConfig.config.engine.backend (sglang) will be overwritten by spec.backend (vllm)", }, { name: "warning for different model in config", request: &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{ ObjectMeta: metav1.ObjectMeta{ Name: "test-dgdr", Namespace: "default", }, Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Backend: "vllm", ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{ ProfilerImage: "profiler:latest", Config: &apiextensionsv1.JSON{ Raw: []byte(configWithDifferentModel), }, }, }, }, isClusterWide: true, wantErr: false, wantWarnings: true, expectedWarning: "spec.profilingConfig.config.deployment.model (different-model) will be overwritten by spec.model (llama-3-8b)", }, { name: "multiple errors (missing profiler image, missing config, and enableGpuDiscovery for namespace-restricted)", request: &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{ ObjectMeta: metav1.ObjectMeta{ Name: "test-dgdr", Namespace: "default", }, Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Backend: "vllm", EnableGpuDiscovery: true, ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{ ProfilerImage: "", Config: nil, }, }, }, isClusterWide: false, wantErr: true, errMsg: "spec.profilingConfig.profilerImage is required\nspec.profilingConfig.config is required and must not be empty\nspec.enableGpuDiscovery can only be set to true for cluster-wide operators", errContains: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { validator := NewDynamoGraphDeploymentRequestValidator(tt.request, tt.isClusterWide) warnings, err := validator.Validate() if (err != nil) != tt.wantErr { t.Errorf("DynamoGraphDeploymentRequestValidator.Validate() error = %v, wantErr %v", err, tt.wantErr) return } if tt.wantErr { if tt.errContains { // For multiple errors, check that all expected error messages are present errStr := err.Error() for _, expectedMsg := range strings.Split(tt.errMsg, "\n") { if !strings.Contains(errStr, expectedMsg) { t.Errorf("DynamoGraphDeploymentRequestValidator.Validate() error message = %v, want to contain %v", errStr, expectedMsg) } } } else { if err.Error() != tt.errMsg { t.Errorf("DynamoGraphDeploymentRequestValidator.Validate() error message = %v, want %v", err.Error(), tt.errMsg) } } } if tt.wantWarnings && len(warnings) == 0 { t.Errorf("DynamoGraphDeploymentRequestValidator.Validate() expected warnings but got none") } if tt.wantWarnings && len(warnings) > 0 && warnings[0] != tt.expectedWarning { t.Errorf("DynamoGraphDeploymentRequestValidator.Validate() warning = %v, want %v", warnings[0], tt.expectedWarning) } }) } } func TestDynamoGraphDeploymentRequestValidator_ValidateUpdate(t *testing.T) { validConfig := `{"engine": {"backend": "vllm"}}` tests := []struct { name string oldRequest *nvidiacomv1alpha1.DynamoGraphDeploymentRequest newRequest *nvidiacomv1alpha1.DynamoGraphDeploymentRequest wantErr bool wantWarnings bool }{ { name: "no changes", oldRequest: &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{ Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Backend: "vllm", ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{ ProfilerImage: "profiler:latest", Config: &apiextensionsv1.JSON{ Raw: []byte(validConfig), }, }, }, }, newRequest: &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{ Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Backend: "vllm", ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{ ProfilerImage: "profiler:latest", Config: &apiextensionsv1.JSON{ Raw: []byte(validConfig), }, }, }, }, wantErr: false, }, { name: "changing model name is allowed", oldRequest: &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{ Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Backend: "vllm", ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{ ProfilerImage: "profiler:latest", Config: &apiextensionsv1.JSON{ Raw: []byte(validConfig), }, }, }, }, newRequest: &nvidiacomv1alpha1.DynamoGraphDeploymentRequest{ Spec: nvidiacomv1alpha1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-70b", Backend: "vllm", ProfilingConfig: nvidiacomv1alpha1.ProfilingConfigSpec{ ProfilerImage: "profiler:latest", Config: &apiextensionsv1.JSON{ Raw: []byte(validConfig), }, }, }, }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { validator := NewDynamoGraphDeploymentRequestValidator(tt.newRequest, true) warnings, err := validator.ValidateUpdate(tt.oldRequest) if (err != nil) != tt.wantErr { t.Errorf("DynamoGraphDeploymentRequestValidator.ValidateUpdate() error = %v, wantErr %v", err, tt.wantErr) return } if tt.wantWarnings && len(warnings) == 0 { t.Errorf("DynamoGraphDeploymentRequestValidator.ValidateUpdate() expected warnings but got none") } }) } }