/* * SPDX-FileCopyrightText: Copyright (c) 2025-2026 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" nvidiacomv1beta1 "github.com/ai-dynamo/dynamo/deploy/operator/api/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestDynamoGraphDeploymentRequestValidator_Validate(t *testing.T) { vram := float64(81920) gpuCount := int32(8) // errMsg: if non-empty, an error is expected and each newline-separated substring must appear in it. tests := []struct { name string request *nvidiacomv1beta1.DynamoGraphDeploymentRequest isClusterWide bool gpuDiscoveryEnabled bool errMsg string }{ { name: "valid request", request: &nvidiacomv1beta1.DynamoGraphDeploymentRequest{ ObjectMeta: metav1.ObjectMeta{Name: "test-dgdr", Namespace: "default"}, Spec: nvidiacomv1beta1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Backend: nvidiacomv1beta1.BackendTypeVllm, Image: "profiler:latest", }, }, isClusterWide: true, }, { name: "thorough + auto is invalid", request: &nvidiacomv1beta1.DynamoGraphDeploymentRequest{ ObjectMeta: metav1.ObjectMeta{Name: "test-dgdr", Namespace: "default"}, Spec: nvidiacomv1beta1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Image: "profiler:latest", Backend: nvidiacomv1beta1.BackendTypeAuto, SearchStrategy: nvidiacomv1beta1.SearchStrategyThorough, }, }, isClusterWide: true, errMsg: `spec.searchStrategy "thorough" is incompatible with spec.backend "auto"`, }, { name: "rapid + auto is valid (default combination)", request: &nvidiacomv1beta1.DynamoGraphDeploymentRequest{ ObjectMeta: metav1.ObjectMeta{Name: "test-dgdr", Namespace: "default"}, Spec: nvidiacomv1beta1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Image: "profiler:latest", Backend: nvidiacomv1beta1.BackendTypeAuto, SearchStrategy: nvidiacomv1beta1.SearchStrategyRapid, }, }, isClusterWide: true, }, { name: "thorough + vllm is valid", request: &nvidiacomv1beta1.DynamoGraphDeploymentRequest{ ObjectMeta: metav1.ObjectMeta{Name: "test-dgdr", Namespace: "default"}, Spec: nvidiacomv1beta1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Image: "profiler:latest", Backend: nvidiacomv1beta1.BackendTypeVllm, SearchStrategy: nvidiacomv1beta1.SearchStrategyThorough, }, }, isClusterWide: true, }, { name: "thorough + trtllm is valid", request: &nvidiacomv1beta1.DynamoGraphDeploymentRequest{ ObjectMeta: metav1.ObjectMeta{Name: "test-dgdr", Namespace: "default"}, Spec: nvidiacomv1beta1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Image: "profiler:latest", Backend: nvidiacomv1beta1.BackendTypeTrtllm, SearchStrategy: nvidiacomv1beta1.SearchStrategyThorough, }, }, isClusterWide: true, }, { name: "thorough + sglang is valid", request: &nvidiacomv1beta1.DynamoGraphDeploymentRequest{ ObjectMeta: metav1.ObjectMeta{Name: "test-dgdr", Namespace: "default"}, Spec: nvidiacomv1beta1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Image: "profiler:latest", Backend: nvidiacomv1beta1.BackendTypeSglang, SearchStrategy: nvidiacomv1beta1.SearchStrategyThorough, }, }, isClusterWide: true, }, { name: "namespace-scoped operator with manual hardware config (should pass)", request: &nvidiacomv1beta1.DynamoGraphDeploymentRequest{ ObjectMeta: metav1.ObjectMeta{Name: "test-dgdr", Namespace: "default"}, Spec: nvidiacomv1beta1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Backend: nvidiacomv1beta1.BackendTypeVllm, Image: "profiler:latest", Hardware: &nvidiacomv1beta1.HardwareSpec{ GPUSKU: "H100-SXM5-80GB", VRAMMB: &vram, NumGPUsPerNode: &gpuCount, }, }, }, isClusterWide: false, gpuDiscoveryEnabled: false, }, { name: "namespace-scoped operator with GPU discovery enabled (should pass without manual config)", request: &nvidiacomv1beta1.DynamoGraphDeploymentRequest{ ObjectMeta: metav1.ObjectMeta{Name: "test-dgdr", Namespace: "default"}, Spec: nvidiacomv1beta1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Backend: nvidiacomv1beta1.BackendTypeVllm, Image: "profiler:latest", }, }, isClusterWide: false, gpuDiscoveryEnabled: true, }, { name: "namespace-scoped operator with GPU discovery disabled and no hardware config (should error)", request: &nvidiacomv1beta1.DynamoGraphDeploymentRequest{ ObjectMeta: metav1.ObjectMeta{Name: "test-dgdr", Namespace: "default"}, Spec: nvidiacomv1beta1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Backend: nvidiacomv1beta1.BackendTypeVllm, Image: "profiler:latest", }, }, isClusterWide: false, gpuDiscoveryEnabled: false, errMsg: "GPU hardware configuration required: GPU discovery is disabled", }, { name: "thorough+auto is invalid regardless of image", request: &nvidiacomv1beta1.DynamoGraphDeploymentRequest{ ObjectMeta: metav1.ObjectMeta{Name: "test-dgdr", Namespace: "default"}, Spec: nvidiacomv1beta1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Backend: nvidiacomv1beta1.BackendTypeAuto, SearchStrategy: nvidiacomv1beta1.SearchStrategyThorough, Image: "", }, }, isClusterWide: true, errMsg: "spec.searchStrategy", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { validator := NewDynamoGraphDeploymentRequestValidator(tt.request, tt.isClusterWide, tt.gpuDiscoveryEnabled) _, err := validator.Validate() wantErr := tt.errMsg != "" if (err != nil) != wantErr { t.Errorf("Validate() error = %v, wantErr %v", err, wantErr) return } if wantErr { for _, msg := range strings.Split(tt.errMsg, "\n") { if !strings.Contains(err.Error(), msg) { t.Errorf("Validate() error %q does not contain %q", err.Error(), msg) } } } }) } } func TestDynamoGraphDeploymentRequestValidator_ValidateUpdate(t *testing.T) { tests := []struct { name string oldRequest *nvidiacomv1beta1.DynamoGraphDeploymentRequest newRequest *nvidiacomv1beta1.DynamoGraphDeploymentRequest wantErr bool errMsg string wantWarnings bool }{ { name: "no changes", oldRequest: &nvidiacomv1beta1.DynamoGraphDeploymentRequest{ Spec: nvidiacomv1beta1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Backend: nvidiacomv1beta1.BackendTypeVllm, Image: "profiler:latest", }, }, newRequest: &nvidiacomv1beta1.DynamoGraphDeploymentRequest{ Spec: nvidiacomv1beta1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Backend: nvidiacomv1beta1.BackendTypeVllm, Image: "profiler:latest", }, }, wantErr: false, }, { name: "changing model name is allowed when not in immutable phase", oldRequest: &nvidiacomv1beta1.DynamoGraphDeploymentRequest{ Spec: nvidiacomv1beta1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Backend: nvidiacomv1beta1.BackendTypeVllm, Image: "profiler:latest", }, }, newRequest: &nvidiacomv1beta1.DynamoGraphDeploymentRequest{ Spec: nvidiacomv1beta1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-70b", Backend: nvidiacomv1beta1.BackendTypeVllm, Image: "profiler:latest", }, }, wantErr: false, }, { name: "spec change rejected during Profiling phase", oldRequest: &nvidiacomv1beta1.DynamoGraphDeploymentRequest{ Spec: nvidiacomv1beta1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Backend: nvidiacomv1beta1.BackendTypeVllm, Image: "profiler:latest", }, Status: nvidiacomv1beta1.DynamoGraphDeploymentRequestStatus{ Phase: nvidiacomv1beta1.DGDRPhaseProfiling, }, }, newRequest: &nvidiacomv1beta1.DynamoGraphDeploymentRequest{ Spec: nvidiacomv1beta1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-70b", Backend: nvidiacomv1beta1.BackendTypeVllm, Image: "profiler:latest", }, }, wantErr: true, errMsg: "spec updates are forbidden while the resource is in phase", }, { name: "spec change rejected during Deploying phase", oldRequest: &nvidiacomv1beta1.DynamoGraphDeploymentRequest{ Spec: nvidiacomv1beta1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Backend: nvidiacomv1beta1.BackendTypeVllm, Image: "profiler:latest", }, Status: nvidiacomv1beta1.DynamoGraphDeploymentRequestStatus{ Phase: nvidiacomv1beta1.DGDRPhaseDeploying, }, }, newRequest: &nvidiacomv1beta1.DynamoGraphDeploymentRequest{ Spec: nvidiacomv1beta1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-70b", Backend: nvidiacomv1beta1.BackendTypeVllm, Image: "profiler:latest", }, }, wantErr: true, errMsg: "spec updates are forbidden while the resource is in phase", }, { name: "spec change rejected during Deployed phase", oldRequest: &nvidiacomv1beta1.DynamoGraphDeploymentRequest{ Spec: nvidiacomv1beta1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Backend: nvidiacomv1beta1.BackendTypeVllm, Image: "profiler:latest", }, Status: nvidiacomv1beta1.DynamoGraphDeploymentRequestStatus{ Phase: nvidiacomv1beta1.DGDRPhaseDeployed, }, }, newRequest: &nvidiacomv1beta1.DynamoGraphDeploymentRequest{ Spec: nvidiacomv1beta1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-70b", Backend: nvidiacomv1beta1.BackendTypeVllm, Image: "profiler:latest", }, }, wantErr: true, errMsg: "spec updates are forbidden while the resource is in phase", }, { name: "no spec change during immutable phase is allowed", oldRequest: &nvidiacomv1beta1.DynamoGraphDeploymentRequest{ Spec: nvidiacomv1beta1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Backend: nvidiacomv1beta1.BackendTypeVllm, Image: "profiler:latest", }, Status: nvidiacomv1beta1.DynamoGraphDeploymentRequestStatus{ Phase: nvidiacomv1beta1.DGDRPhaseProfiling, }, }, newRequest: &nvidiacomv1beta1.DynamoGraphDeploymentRequest{ Spec: nvidiacomv1beta1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Backend: nvidiacomv1beta1.BackendTypeVllm, Image: "profiler:latest", }, }, wantErr: false, }, { name: "spec change allowed during Failed phase", oldRequest: &nvidiacomv1beta1.DynamoGraphDeploymentRequest{ Spec: nvidiacomv1beta1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-8b", Backend: nvidiacomv1beta1.BackendTypeVllm, Image: "profiler:latest", }, Status: nvidiacomv1beta1.DynamoGraphDeploymentRequestStatus{ Phase: nvidiacomv1beta1.DGDRPhaseFailed, }, }, newRequest: &nvidiacomv1beta1.DynamoGraphDeploymentRequest{ Spec: nvidiacomv1beta1.DynamoGraphDeploymentRequestSpec{ Model: "llama-3-70b", Backend: nvidiacomv1beta1.BackendTypeVllm, Image: "profiler:latest", }, }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { validator := NewDynamoGraphDeploymentRequestValidator(tt.newRequest, true, true) warnings, err := validator.ValidateUpdate(tt.oldRequest) if (err != nil) != tt.wantErr { t.Errorf("ValidateUpdate() error = %v, wantErr %v", err, tt.wantErr) return } if tt.wantErr && tt.errMsg != "" { if !strings.Contains(err.Error(), tt.errMsg) { t.Errorf("ValidateUpdate() error %q does not contain %q", err.Error(), tt.errMsg) } } if tt.wantWarnings && len(warnings) == 0 { t.Errorf("ValidateUpdate() expected warnings but got none") } if !tt.wantWarnings && len(warnings) > 0 { t.Errorf("ValidateUpdate() unexpected warnings: %v", warnings) } }) } }