/* * 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 v1alpha1 import ( "encoding/json" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" v1beta1 "github.com/ai-dynamo/dynamo/deploy/operator/api/v1beta1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) // newV1alpha1DGDR builds a fully-populated v1alpha1 DGDR for use in tests. func newV1alpha1DGDR() *DynamoGraphDeploymentRequest { profilingBlob := map[string]interface{}{ "sla": map[string]interface{}{ "ttft": float64(500), "itl": float64(20), "isl": float64(2048), "osl": float64(512), }, "deployment": map[string]interface{}{ "modelCache": map[string]interface{}{ "pvcName": "model-pvc", "modelPathInPvc": "llama-3", "pvcMountPath": "/data/model", }, }, "planner": map[string]interface{}{ "enable_load_scaling": false, }, "extra_key": "preserved", } blobRaw, _ := json.Marshal(profilingBlob) trueVal := true return &DynamoGraphDeploymentRequest{ ObjectMeta: metav1.ObjectMeta{ Name: "test-dgdr", Namespace: "default", }, Spec: DynamoGraphDeploymentRequestSpec{ Model: "meta-llama/Llama-3.1-8B", Backend: "vllm", AutoApply: true, UseMocker: true, ProfilingConfig: ProfilingConfigSpec{ ProfilerImage: "nvcr.io/nvidia/dynamo:latest", OutputPVC: "output-pvc", Config: &apiextensionsv1.JSON{Raw: blobRaw}, ConfigMapRef: &ConfigMapKeySelector{Name: "base-config", Key: "disagg.yaml"}, }, EnableGPUDiscovery: &trueVal, DeploymentOverrides: &DeploymentOverridesSpec{ Name: "my-dgd", Namespace: "prod", Labels: map[string]string{"team": "ml"}, }, }, Status: DynamoGraphDeploymentRequestStatus{ State: DGDRStateProfiling, Backend: "vllm", ObservedGeneration: 3, ProfilingResults: "configmap/profiling-cm", Deployment: &DeploymentStatus{ Name: "my-dgd", Namespace: "prod", State: "initializing", Created: true, }, }, } } // newV1beta1DGDR builds a fully-populated v1beta1 DGDR for use in tests. func newV1beta1DGDR() *v1beta1.DynamoGraphDeploymentRequest { ttft := float64(300) itl := float64(15) isl := int32(1024) osl := int32(256) rawDGD, _ := json.Marshal(map[string]interface{}{"apiVersion": "nvidia.com/v1alpha1", "kind": "DynamoGraphDeployment"}) rawPlanner, _ := json.Marshal(map[string]interface{}{"enable_load_scaling": false}) autoApplyFalse := false return &v1beta1.DynamoGraphDeploymentRequest{ ObjectMeta: metav1.ObjectMeta{ Name: "hub-dgdr", Namespace: "default", }, Spec: v1beta1.DynamoGraphDeploymentRequestSpec{ Model: "Qwen/Qwen3-32B", Backend: v1beta1.BackendTypeVllm, AutoApply: &autoApplyFalse, Image: "nvcr.io/nvidia/dynamo:0.3.2", SLA: &v1beta1.SLASpec{ TTFT: &ttft, ITL: &itl, }, Workload: &v1beta1.WorkloadSpec{ ISL: &isl, OSL: &osl, }, ModelCache: &v1beta1.ModelCacheSpec{ PVCName: "qwen-pvc", PVCModelPath: "qwen3-32b", PVCMountPath: "/models", }, Features: &v1beta1.FeaturesSpec{ Mocker: &v1beta1.MockerSpec{Enabled: true}, Planner: &runtime.RawExtension{Raw: rawPlanner}, }, }, Status: v1beta1.DynamoGraphDeploymentRequestStatus{ Phase: v1beta1.DGDRPhaseDeployed, ObservedGeneration: 2, DGDName: "hub-dgd", ProfilingJobName: "profiling-job-1", ProfilingResults: &v1beta1.ProfilingResultsStatus{ SelectedConfig: &runtime.RawExtension{Raw: rawDGD}, }, }, } } // TestConvertTo_SpecFields verifies that key v1alpha1 spec fields land in the correct v1beta1 locations. func TestConvertTo_SpecFields(t *testing.T) { src := newV1alpha1DGDR() dst := &v1beta1.DynamoGraphDeploymentRequest{} if err := src.ConvertTo(dst); err != nil { t.Fatalf("ConvertTo() error = %v", err) } // Simple 1:1 fields if dst.Spec.Model != src.Spec.Model { t.Errorf("Model: got %q, want %q", dst.Spec.Model, src.Spec.Model) } if string(dst.Spec.Backend) != src.Spec.Backend { t.Errorf("Backend: got %q, want %q", dst.Spec.Backend, src.Spec.Backend) } if dst.Spec.AutoApply == nil || *dst.Spec.AutoApply != src.Spec.AutoApply { t.Errorf("AutoApply: got %v, want %v", dst.Spec.AutoApply, src.Spec.AutoApply) } // ProfilerImage → Image if dst.Spec.Image != src.Spec.ProfilingConfig.ProfilerImage { t.Errorf("Image: got %q, want %q", dst.Spec.Image, src.Spec.ProfilingConfig.ProfilerImage) } // UseMocker → Features.Mocker.Enabled if dst.Spec.Features == nil || dst.Spec.Features.Mocker == nil { t.Fatal("Features.Mocker is nil") } if !dst.Spec.Features.Mocker.Enabled { t.Error("Features.Mocker.Enabled: got false, want true") } // SLA from JSON blob if dst.Spec.SLA == nil { t.Fatal("SLA is nil") } if dst.Spec.SLA.TTFT == nil || *dst.Spec.SLA.TTFT != 500 { t.Errorf("SLA.TTFT: got %v, want 500", dst.Spec.SLA.TTFT) } if dst.Spec.SLA.ITL == nil || *dst.Spec.SLA.ITL != 20 { t.Errorf("SLA.ITL: got %v, want 20", dst.Spec.SLA.ITL) } // Workload from JSON blob if dst.Spec.Workload == nil { t.Fatal("Workload is nil") } if dst.Spec.Workload.ISL == nil || *dst.Spec.Workload.ISL != 2048 { t.Errorf("Workload.ISL: got %v, want 2048", dst.Spec.Workload.ISL) } if dst.Spec.Workload.OSL == nil || *dst.Spec.Workload.OSL != 512 { t.Errorf("Workload.OSL: got %v, want 512", dst.Spec.Workload.OSL) } // ModelCache from JSON blob if dst.Spec.ModelCache == nil { t.Fatal("ModelCache is nil") } if dst.Spec.ModelCache.PVCName != "model-pvc" { t.Errorf("ModelCache.PVCName: got %q, want %q", dst.Spec.ModelCache.PVCName, "model-pvc") } if dst.Spec.ModelCache.PVCModelPath != "llama-3" { t.Errorf("ModelCache.PVCModelPath: got %q, want %q", dst.Spec.ModelCache.PVCModelPath, "llama-3") } if dst.Spec.ModelCache.PVCMountPath != "/data/model" { t.Errorf("ModelCache.PVCMountPath: got %q, want %q", dst.Spec.ModelCache.PVCMountPath, "/data/model") } // EnableGPUDiscovery → annotation if dst.Annotations[annDGDREnableGPUDisc] != "true" { t.Errorf("annDGDREnableGPUDisc annotation: got %q, want %q", dst.Annotations[annDGDREnableGPUDisc], "true") } // OutputPVC → annotation if dst.Annotations[annDGDROutputPVC] != "output-pvc" { t.Errorf("annDGDROutputPVC annotation: got %q, want %q", dst.Annotations[annDGDROutputPVC], "output-pvc") } // DeploymentOverrides → annotation if dst.Annotations[annDGDRDeployOverrides] == "" { t.Error("annDGDRDeployOverrides annotation is empty") } } // TestConvertTo_StatusFields verifies that key v1alpha1 status fields land in the correct v1beta1 locations. func TestConvertTo_StatusFields(t *testing.T) { src := newV1alpha1DGDR() dst := &v1beta1.DynamoGraphDeploymentRequest{} if err := src.ConvertTo(dst); err != nil { t.Fatalf("ConvertTo() error = %v", err) } // Profiling state → Profiling phase if dst.Status.Phase != v1beta1.DGDRPhaseProfiling { t.Errorf("Status.Phase: got %q, want %q", dst.Status.Phase, v1beta1.DGDRPhaseProfiling) } if dst.Status.ObservedGeneration != 3 { t.Errorf("Status.ObservedGeneration: got %d, want 3", dst.Status.ObservedGeneration) } // Deployment.Name → DGDName if dst.Status.DGDName != "my-dgd" { t.Errorf("Status.DGDName: got %q, want %q", dst.Status.DGDName, "my-dgd") } // Backend → annotation if dst.Annotations[annDGDRStatusBackend] != "vllm" { t.Errorf("annDGDRStatusBackend annotation: got %q, want %q", dst.Annotations[annDGDRStatusBackend], "vllm") } // ProfilingResults → annotation if dst.Annotations[annDGDRProfilingResults] != "configmap/profiling-cm" { t.Errorf("annDGDRProfilingResults annotation: got %q, want %q", dst.Annotations[annDGDRProfilingResults], "configmap/profiling-cm") } } // TestAlpha1RoundTrip verifies v1alpha1 → v1beta1 → v1alpha1 preserves all round-tripped fields. func TestAlpha1RoundTrip(t *testing.T) { original := newV1alpha1DGDR() // Step 1: v1alpha1 → v1beta1 hub := &v1beta1.DynamoGraphDeploymentRequest{} if err := original.ConvertTo(hub); err != nil { t.Fatalf("ConvertTo() error = %v", err) } // Step 2: v1beta1 → v1alpha1 restored := &DynamoGraphDeploymentRequest{} if err := restored.ConvertFrom(hub); err != nil { t.Fatalf("ConvertFrom() error = %v", err) } // --- Spec checks --- // ProfilingConfig.Config (raw JSON blob) is verified separately below. if diff := cmp.Diff(original.Spec, restored.Spec, cmpopts.IgnoreFields(ProfilingConfigSpec{}, "Config")); diff != "" { t.Errorf("Spec mismatch after round-trip (-want +got):\n%s", diff) } // JSON blob round-trip: SLA fields re-emerge in ProfilingConfig.Config if restored.Spec.ProfilingConfig.Config == nil { t.Fatal("ProfilingConfig.Config is nil after round-trip") } var blob map[string]interface{} if err := json.Unmarshal(restored.Spec.ProfilingConfig.Config.Raw, &blob); err != nil { t.Fatalf("failed to unmarshal restored ProfilingConfig.Config: %v", err) } slaMap, _ := blob["sla"].(map[string]interface{}) if slaMap == nil { t.Fatal("sla key missing in restored JSON blob") } if slaMap["ttft"] != float64(500) { t.Errorf("blob sla.ttft: got %v, want 500", slaMap["ttft"]) } if slaMap["isl"] != float64(2048) { t.Errorf("blob sla.isl: got %v, want 2048", slaMap["isl"]) } // Verify unknown keys are preserved via the annotation round-trip if blob["extra_key"] != "preserved" { t.Errorf("extra_key: got %v, want %q", blob["extra_key"], "preserved") } // Planner round-trip via applyPlannerFromBlob / mergePlannerIntoBlob plannerMap, _ := blob["planner"].(map[string]interface{}) if plannerMap == nil { t.Fatal("planner key missing in restored JSON blob") } if plannerMap["enable_load_scaling"] != false { t.Errorf("planner.enable_load_scaling: got %v, want false", plannerMap["enable_load_scaling"]) } // --- Status checks --- if diff := cmp.Diff(original.Status, restored.Status); diff != "" { t.Errorf("Status mismatch after round-trip (-want +got):\n%s", diff) } } // TestHubRoundTrip verifies v1beta1 → v1alpha1 → v1beta1 preserves all round-tripped fields. func TestHubRoundTrip(t *testing.T) { original := newV1beta1DGDR() // Step 1: v1beta1 → v1alpha1 spoke := &DynamoGraphDeploymentRequest{} if err := spoke.ConvertFrom(original); err != nil { t.Fatalf("ConvertFrom() error = %v", err) } // Step 2: v1alpha1 → v1beta1 restored := &v1beta1.DynamoGraphDeploymentRequest{} if err := spoke.ConvertTo(restored); err != nil { t.Fatalf("ConvertTo() error = %v", err) } // --- Spec checks --- if diff := cmp.Diff(original.Spec, restored.Spec); diff != "" { t.Errorf("Spec mismatch after round-trip (-want +got):\n%s", diff) } // --- Status checks --- // Phase is intentionally lossy: DGDRPhaseDeployed → Ready → Ready if restored.Status.Phase != v1beta1.DGDRPhaseReady { t.Errorf("Status.Phase: got %q, want %q (Deployed→Ready is lossy)", restored.Status.Phase, v1beta1.DGDRPhaseReady) } if diff := cmp.Diff(original.Status, restored.Status, cmpopts.IgnoreFields(v1beta1.DynamoGraphDeploymentRequestStatus{}, "Phase")); diff != "" { t.Errorf("Status mismatch after round-trip (-want +got):\n%s", diff) } // GeneratedDeployment round-trip via ProfilingResults.SelectedConfig if restored.Status.ProfilingResults == nil || restored.Status.ProfilingResults.SelectedConfig == nil { t.Fatal("Status.ProfilingResults.SelectedConfig is nil after round-trip") } } // TestConvertTo_InvalidProfilingConfigJSON verifies that malformed JSON in ProfilingConfig.Config // returns an error rather than silently producing an incomplete conversion. func TestConvertTo_InvalidProfilingConfigJSON(t *testing.T) { src := newV1alpha1DGDR() src.Spec.ProfilingConfig.Config = &apiextensionsv1.JSON{Raw: []byte(`{not valid json`)} dst := &v1beta1.DynamoGraphDeploymentRequest{} err := src.ConvertTo(dst) if err == nil { t.Fatal("ConvertTo() expected error for invalid JSON, got nil") } }