708 lines
27 KiB
Go
708 lines
27 KiB
Go
/*
|
|
* 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.
|
|
*/
|
|
|
|
// Conversion between v1alpha1 and v1beta1 DynamoGraphDeploymentRequest (DGDR).
|
|
//
|
|
// The two API versions have fundamentally different shapes: v1alpha1 stores SLA,
|
|
// workload, and model-cache configuration inside an opaque JSON blob
|
|
// (ProfilingConfig.Config), while v1beta1 breaks these out into typed structs.
|
|
// This file bridges the two representations.
|
|
//
|
|
// # Spec field mapping
|
|
//
|
|
// 1:1 simple mappings (value copied, path or wrapper type differs):
|
|
//
|
|
// v1alpha1 v1beta1
|
|
// ────────────────────────────────────────── ──────────────────────────────────────
|
|
// Spec.Model (string) Spec.Model (string)
|
|
// Spec.Backend (string) Spec.Backend (BackendType)
|
|
// Spec.AutoApply (bool) Spec.AutoApply (*bool)
|
|
// Spec.UseMocker (bool) Spec.Features.Mocker.Enabled (bool)
|
|
// Spec.ProfilingConfig.ProfilerImage Spec.Image (string)
|
|
// Spec.DeploymentOverrides.WorkersImage (no v1beta1 equivalent yet — TODO: overrides.dgd)
|
|
//
|
|
// JSON blob → structured fields (parsed/reconstructed on each trip):
|
|
//
|
|
// Blob key path v1beta1 field
|
|
// ────────────────────────────────────────── ──────────────────────────────────────
|
|
// sla.ttft Spec.SLA.TTFT (*float64)
|
|
// sla.itl Spec.SLA.ITL (*float64)
|
|
// sla.isl Spec.Workload.ISL (*int32)
|
|
// sla.osl Spec.Workload.OSL (*int32)
|
|
// deployment.modelCache.pvcName Spec.ModelCache.PVCName (string)
|
|
// deployment.modelCache.modelPathInPvc Spec.ModelCache.PVCModelPath (string)
|
|
// deployment.modelCache.pvcMountPath Spec.ModelCache.PVCMountPath (string)
|
|
//
|
|
// The full JSON blob is also preserved as the annotation
|
|
// nvidia.com/dgdr-profiling-config so that unknown keys survive the round-trip.
|
|
// On ConvertFrom the blob is loaded from the annotation first, then the
|
|
// structured v1beta1 fields are written on top (structured fields win).
|
|
//
|
|
// Structural reshaping (same data, different container type):
|
|
//
|
|
// v1alpha1 v1beta1
|
|
// ────────────────────────────────────────── ──────────────────────────────────────
|
|
// ProfilingConfig.Resources Overrides.ProfilingJob.Template.
|
|
// (*corev1.ResourceRequirements) Spec.Containers[0].Resources
|
|
// ProfilingConfig.Tolerations Overrides.ProfilingJob.Template.
|
|
// ([]corev1.Toleration) Spec.Tolerations
|
|
//
|
|
// Annotation-only (no v1beta1 equivalent; round-tripped via ObjectMeta annotations):
|
|
//
|
|
// v1alpha1 field Annotation key
|
|
// ────────────────────────────────────────── ──────────────────────────────────────
|
|
// Spec.EnableGPUDiscovery nvidia.com/dgdr-enable-gpu-discovery
|
|
// Spec.ProfilingConfig.ConfigMapRef nvidia.com/dgdr-config-map-ref
|
|
// Spec.ProfilingConfig.OutputPVC nvidia.com/dgdr-output-pvc
|
|
// Spec.ProfilingConfig.Config (full blob) nvidia.com/dgdr-profiling-config
|
|
// Spec.DeploymentOverrides.{Name, nvidia.com/dgdr-deployment-overrides
|
|
// Namespace,Labels,Annotations}
|
|
//
|
|
// Planner config (opaque blob stored verbatim under blob["planner"]):
|
|
//
|
|
// v1beta1 blob key
|
|
// ────────────────────────────────────────── ──────────────────────────────────────
|
|
// Features.Planner (*runtime.RawExtension) planner.* (JSON fields written directly)
|
|
//
|
|
// v1beta1-only fields with no v1alpha1 equivalent (omitted / TODO):
|
|
//
|
|
// Hardware.*, Workload.{Concurrency,RequestRate}, SLA.{E2ELatency},
|
|
// Features.{KVRouter}, SearchStrategy
|
|
//
|
|
// # Status field mapping
|
|
//
|
|
// 1:1 simple mappings:
|
|
//
|
|
// v1alpha1 v1beta1
|
|
// ────────────────────────────────────────── ──────────────────────────────────────
|
|
// Status.ObservedGeneration Status.ObservedGeneration
|
|
// Status.Conditions Status.Conditions
|
|
// Status.GeneratedDeployment Status.ProfilingResults.SelectedConfig
|
|
// Status.Deployment.Name Status.DGDName
|
|
//
|
|
// State ↔ Phase (many-to-one, context-dependent):
|
|
//
|
|
// v1alpha1 State → v1beta1 Phase Notes
|
|
// ─────────────────────── ───────────────── ─────────────────────────────────
|
|
// "" / "Pending" Pending
|
|
// "Profiling" Profiling
|
|
// "Ready" Ready or Deployed Deployed if Deployment.Created
|
|
// "Deploying" Deploying
|
|
// "DeploymentDeleted" Ready lossy
|
|
// "Failed" Failed
|
|
//
|
|
// v1beta1 Phase → v1alpha1 State
|
|
// ─────────────────────── ─────────────────
|
|
// Pending "Pending"
|
|
// Profiling "Profiling"
|
|
// Ready "Ready"
|
|
// Deploying "Deploying"
|
|
// Deployed "Ready" lossy
|
|
// Failed "Failed"
|
|
//
|
|
// Annotation-only status fields (no v1beta1 equivalent):
|
|
//
|
|
// v1alpha1 field Annotation key
|
|
// ────────────────────────────────────────── ──────────────────────────────────────
|
|
// Status.Backend nvidia.com/dgdr-status-backend
|
|
// Status.ProfilingResults (string ref, nvidia.com/dgdr-profiling-results
|
|
// e.g. "configmap/<name>") (not the v1beta1 struct — see below)
|
|
// Status.Deployment.{Namespace,State, nvidia.com/dgdr-deployment-status
|
|
// Created} (JSON-encoded; Name maps to DGDName)
|
|
//
|
|
// Note on ProfilingResults naming collision: the two versions both have a field
|
|
// called "ProfilingResults" but with entirely different types. v1alpha1 has a
|
|
// plain string (a configmap reference). v1beta1 has a struct with Pareto and
|
|
// SelectedConfig. The string is annotation-preserved; the struct's
|
|
// SelectedConfig maps from v1alpha1 GeneratedDeployment (see 1:1 table above).
|
|
//
|
|
// Note: v1alpha1 DeploymentStatus{Name,Namespace,State,Created} and v1beta1
|
|
// DeploymentInfoStatus{Replicas,AvailableReplicas} share no common fields —
|
|
// they track different aspects of the DGD. Only Deployment.Name ↔ DGDName
|
|
// overlaps. The rest of DeploymentStatus is round-tripped via annotation;
|
|
// DeploymentInfoStatus has no v1alpha1 source and is left empty.
|
|
//
|
|
// v1beta1-only status fields with no v1alpha1 equivalent (omitted / TODO):
|
|
//
|
|
// Status.DeploymentInfo.{Replicas,AvailableReplicas},
|
|
// Status.ProfilingPhase, Status.ProfilingJobName,
|
|
// Status.ProfilingResults.Pareto
|
|
|
|
package v1alpha1
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
v1beta1 "github.com/ai-dynamo/dynamo/deploy/operator/api/v1beta1"
|
|
batchv1 "k8s.io/api/batch/v1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"sigs.k8s.io/controller-runtime/pkg/conversion"
|
|
)
|
|
|
|
// Annotation keys used to round-trip v1alpha1 fields that have no v1beta1 equivalent.
|
|
const (
|
|
annDGDRConfigMapRef = "nvidia.com/dgdr-config-map-ref"
|
|
annDGDROutputPVC = "nvidia.com/dgdr-output-pvc"
|
|
annDGDREnableGPUDisc = "nvidia.com/dgdr-enable-gpu-discovery"
|
|
annDGDRDeployOverrides = "nvidia.com/dgdr-deployment-overrides"
|
|
annDGDRProfilingConfig = "nvidia.com/dgdr-profiling-config"
|
|
annDGDRStatusBackend = "nvidia.com/dgdr-status-backend"
|
|
annDGDRProfilingResults = "nvidia.com/dgdr-profiling-results"
|
|
annDGDRDeploymentStatus = "nvidia.com/dgdr-deployment-status"
|
|
annDGDRProfilingJobName = "nvidia.com/dgdr-profiling-job-name"
|
|
)
|
|
|
|
// ConvertTo converts this DynamoGraphDeploymentRequest (v1alpha1) to the Hub version (v1beta1).
|
|
func (src *DynamoGraphDeploymentRequest) ConvertTo(dstRaw conversion.Hub) error {
|
|
dst, ok := dstRaw.(*v1beta1.DynamoGraphDeploymentRequest)
|
|
if !ok {
|
|
return fmt.Errorf("expected *v1beta1.DynamoGraphDeploymentRequest but got %T", dstRaw)
|
|
}
|
|
|
|
dst.ObjectMeta = src.ObjectMeta
|
|
|
|
if err := convertDGDRSpecTo(&src.Spec, &dst.Spec, dst); err != nil {
|
|
return err
|
|
}
|
|
|
|
convertDGDRStatusTo(&src.Status, &dst.Status, dst)
|
|
|
|
return nil
|
|
}
|
|
|
|
// ConvertFrom converts from the Hub version (v1beta1) to this DynamoGraphDeploymentRequest (v1alpha1).
|
|
func (dst *DynamoGraphDeploymentRequest) ConvertFrom(srcRaw conversion.Hub) error {
|
|
src, ok := srcRaw.(*v1beta1.DynamoGraphDeploymentRequest)
|
|
if !ok {
|
|
return fmt.Errorf("expected *v1beta1.DynamoGraphDeploymentRequest but got %T", srcRaw)
|
|
}
|
|
|
|
dst.ObjectMeta = src.ObjectMeta
|
|
|
|
convertDGDRSpecFrom(&src.Spec, &dst.Spec, src)
|
|
convertDGDRStatusFrom(&src.Status, &dst.Status, src)
|
|
|
|
// ProfilingJobName — no v1alpha1 status field; store as annotation for round-trip
|
|
if src.Status.ProfilingJobName != "" {
|
|
if dst.Annotations == nil {
|
|
dst.Annotations = make(map[string]string)
|
|
}
|
|
dst.Annotations[annDGDRProfilingJobName] = src.Status.ProfilingJobName
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// setAnnotation initialises the annotation map if needed and sets a key.
|
|
func setAnnotation(obj *v1beta1.DynamoGraphDeploymentRequest, key, value string) {
|
|
if obj.Annotations == nil {
|
|
obj.Annotations = make(map[string]string)
|
|
}
|
|
obj.Annotations[key] = value
|
|
}
|
|
|
|
// convertDGDRSpecTo converts the v1alpha1 Spec into the v1beta1 Spec.
|
|
func convertDGDRSpecTo(src *DynamoGraphDeploymentRequestSpec, dst *v1beta1.DynamoGraphDeploymentRequestSpec, dstObj *v1beta1.DynamoGraphDeploymentRequest) error {
|
|
dst.Model = src.Model
|
|
dst.AutoApply = &src.AutoApply
|
|
|
|
if src.Backend != "" {
|
|
dst.Backend = v1beta1.BackendType(src.Backend)
|
|
}
|
|
if src.DeploymentOverrides != nil && src.DeploymentOverrides.WorkersImage != "" {
|
|
dst.Image = src.DeploymentOverrides.WorkersImage
|
|
}
|
|
if src.UseMocker {
|
|
if dst.Features == nil {
|
|
dst.Features = &v1beta1.FeaturesSpec{}
|
|
}
|
|
dst.Features.Mocker = &v1beta1.MockerSpec{Enabled: true}
|
|
}
|
|
if src.EnableGPUDiscovery != nil && *src.EnableGPUDiscovery {
|
|
setAnnotation(dstObj, annDGDREnableGPUDisc, "true")
|
|
}
|
|
|
|
if src.ProfilingConfig.Config != nil && src.ProfilingConfig.Config.Raw != nil {
|
|
var blob map[string]interface{}
|
|
if err := json.Unmarshal(src.ProfilingConfig.Config.Raw, &blob); err != nil {
|
|
return fmt.Errorf("failed to parse ProfilingConfig.Config: %w", err)
|
|
}
|
|
applySLAAndWorkloadFromBlob(blob, dst)
|
|
applyModelCacheFromBlob(blob, dst)
|
|
applyPlannerFromBlob(blob, dst)
|
|
setAnnotation(dstObj, annDGDRProfilingConfig, string(src.ProfilingConfig.Config.Raw))
|
|
}
|
|
|
|
// ProfilerImage → Image (the profiler runs in the frontend image)
|
|
// TODO: In a future MR, backend inference images will be managed separately via overrides.dgd.
|
|
if src.ProfilingConfig.ProfilerImage != "" {
|
|
dst.Image = src.ProfilingConfig.ProfilerImage
|
|
}
|
|
if src.ProfilingConfig.ConfigMapRef != nil {
|
|
if data, err := json.Marshal(src.ProfilingConfig.ConfigMapRef); err == nil {
|
|
setAnnotation(dstObj, annDGDRConfigMapRef, string(data))
|
|
}
|
|
}
|
|
if src.ProfilingConfig.OutputPVC != "" {
|
|
setAnnotation(dstObj, annDGDROutputPVC, src.ProfilingConfig.OutputPVC)
|
|
}
|
|
|
|
convertProfilingResourcesToOverrides(&src.ProfilingConfig, dst)
|
|
convertDeploymentOverridesToAnnotation(src.DeploymentOverrides, dstObj)
|
|
|
|
return nil
|
|
}
|
|
|
|
// applySLAAndWorkloadFromBlob extracts SLA and Workload fields from the v1alpha1 JSON blob.
|
|
// Both are nested under blob["sla"] in the v1alpha1 schema.
|
|
func applySLAAndWorkloadFromBlob(blob map[string]interface{}, dst *v1beta1.DynamoGraphDeploymentRequestSpec) {
|
|
slaRaw, ok := blob["sla"]
|
|
if !ok {
|
|
return
|
|
}
|
|
slaMap, ok := slaRaw.(map[string]interface{})
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
if dst.SLA == nil {
|
|
dst.SLA = &v1beta1.SLASpec{}
|
|
}
|
|
if v, ok := slaMap["ttft"].(float64); ok {
|
|
dst.SLA.TTFT = &v
|
|
}
|
|
if v, ok := slaMap["itl"].(float64); ok {
|
|
dst.SLA.ITL = &v
|
|
}
|
|
|
|
if v, ok := slaMap["isl"].(float64); ok {
|
|
if dst.Workload == nil {
|
|
dst.Workload = &v1beta1.WorkloadSpec{}
|
|
}
|
|
isl := int32(v)
|
|
dst.Workload.ISL = &isl
|
|
}
|
|
if v, ok := slaMap["osl"].(float64); ok {
|
|
if dst.Workload == nil {
|
|
dst.Workload = &v1beta1.WorkloadSpec{}
|
|
}
|
|
osl := int32(v)
|
|
dst.Workload.OSL = &osl
|
|
}
|
|
}
|
|
|
|
// applyModelCacheFromBlob extracts ModelCache from blob["deployment"]["modelCache"].
|
|
func applyModelCacheFromBlob(blob map[string]interface{}, dst *v1beta1.DynamoGraphDeploymentRequestSpec) {
|
|
deployRaw, ok := blob["deployment"]
|
|
if !ok {
|
|
return
|
|
}
|
|
deployMap, ok := deployRaw.(map[string]interface{})
|
|
if !ok {
|
|
return
|
|
}
|
|
mcRaw, ok := deployMap["modelCache"]
|
|
if !ok {
|
|
return
|
|
}
|
|
mcMap, ok := mcRaw.(map[string]interface{})
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
mc := &v1beta1.ModelCacheSpec{}
|
|
if v, ok := mcMap["pvcName"].(string); ok {
|
|
mc.PVCName = v
|
|
}
|
|
if v, ok := mcMap["modelPathInPvc"].(string); ok {
|
|
mc.PVCModelPath = v
|
|
}
|
|
if v, ok := mcMap["pvcMountPath"].(string); ok {
|
|
mc.PVCMountPath = v
|
|
}
|
|
dst.ModelCache = mc
|
|
}
|
|
|
|
// convertProfilingResourcesToOverrides maps ProfilingConfig Resources and Tolerations
|
|
// into the v1beta1 Overrides.ProfilingJob pod spec.
|
|
func convertProfilingResourcesToOverrides(src *ProfilingConfigSpec, dst *v1beta1.DynamoGraphDeploymentRequestSpec) {
|
|
if src.Resources == nil && len(src.Tolerations) == 0 {
|
|
return
|
|
}
|
|
if dst.Overrides == nil {
|
|
dst.Overrides = &v1beta1.OverridesSpec{}
|
|
}
|
|
if dst.Overrides.ProfilingJob == nil {
|
|
dst.Overrides.ProfilingJob = &batchv1.JobSpec{
|
|
Template: corev1.PodTemplateSpec{
|
|
Spec: corev1.PodSpec{},
|
|
},
|
|
}
|
|
}
|
|
podSpec := &dst.Overrides.ProfilingJob.Template.Spec
|
|
|
|
if src.Resources != nil {
|
|
if len(podSpec.Containers) == 0 {
|
|
podSpec.Containers = []corev1.Container{{}}
|
|
}
|
|
podSpec.Containers[0].Resources = *src.Resources
|
|
}
|
|
if len(src.Tolerations) > 0 {
|
|
podSpec.Tolerations = src.Tolerations
|
|
}
|
|
}
|
|
|
|
// convertDeploymentOverridesToAnnotation serialises the DeploymentOverrides metadata fields
|
|
// (Name, Namespace, Labels, Annotations) into an annotation for round-trip.
|
|
// WorkersImage is handled separately via dst.Image.
|
|
func convertDeploymentOverridesToAnnotation(src *DeploymentOverridesSpec, dstObj *v1beta1.DynamoGraphDeploymentRequest) {
|
|
if src == nil {
|
|
return
|
|
}
|
|
overrides := struct {
|
|
Name string `json:"name,omitempty"`
|
|
Namespace string `json:"namespace,omitempty"`
|
|
Labels map[string]string `json:"labels,omitempty"`
|
|
Annotations map[string]string `json:"annotations,omitempty"`
|
|
}{
|
|
Name: src.Name,
|
|
Namespace: src.Namespace,
|
|
Labels: src.Labels,
|
|
Annotations: src.Annotations,
|
|
}
|
|
if overrides.Name == "" && overrides.Namespace == "" && len(overrides.Labels) == 0 && len(overrides.Annotations) == 0 {
|
|
return
|
|
}
|
|
if data, err := json.Marshal(overrides); err == nil {
|
|
setAnnotation(dstObj, annDGDRDeployOverrides, string(data))
|
|
}
|
|
}
|
|
|
|
// convertDGDRSpecFrom converts the v1beta1 Spec back into the v1alpha1 Spec.
|
|
func convertDGDRSpecFrom(src *v1beta1.DynamoGraphDeploymentRequestSpec, dst *DynamoGraphDeploymentRequestSpec, srcObj *v1beta1.DynamoGraphDeploymentRequest) {
|
|
dst.Model = src.Model
|
|
if src.AutoApply != nil {
|
|
dst.AutoApply = *src.AutoApply
|
|
} else {
|
|
dst.AutoApply = true // v1beta1 default
|
|
}
|
|
|
|
if src.Backend != "" {
|
|
dst.Backend = string(src.Backend)
|
|
}
|
|
if src.Features != nil && src.Features.Mocker != nil {
|
|
dst.UseMocker = src.Features.Mocker.Enabled
|
|
}
|
|
|
|
if srcObj.Annotations != nil {
|
|
if v, ok := srcObj.Annotations[annDGDREnableGPUDisc]; ok && v == "true" {
|
|
trueVal := true
|
|
dst.EnableGPUDiscovery = &trueVal
|
|
}
|
|
}
|
|
|
|
// Reconstruct the JSON blob: start from the round-trip annotation (preserves unknown
|
|
// keys), then overwrite with structured v1beta1 fields (structured fields win).
|
|
var blob map[string]interface{}
|
|
if srcObj.Annotations != nil {
|
|
if rawBlob, ok := srcObj.Annotations[annDGDRProfilingConfig]; ok && rawBlob != "" {
|
|
_ = json.Unmarshal([]byte(rawBlob), &blob) // best-effort
|
|
}
|
|
}
|
|
if src.SLA != nil || src.Workload != nil {
|
|
if blob == nil {
|
|
blob = make(map[string]interface{})
|
|
}
|
|
mergeSLAWorkloadIntoBlob(src, blob)
|
|
}
|
|
if src.ModelCache != nil {
|
|
if blob == nil {
|
|
blob = make(map[string]interface{})
|
|
}
|
|
mergeModelCacheIntoBlob(src.ModelCache, blob)
|
|
}
|
|
if src.Features != nil && src.Features.Planner != nil {
|
|
if blob == nil {
|
|
blob = make(map[string]interface{})
|
|
}
|
|
mergePlannerIntoBlob(src.Features.Planner, blob)
|
|
}
|
|
if blob != nil {
|
|
if data, err := json.Marshal(blob); err == nil {
|
|
dst.ProfilingConfig.Config = &apiextensionsv1.JSON{Raw: data}
|
|
}
|
|
}
|
|
|
|
// Image → ProfilerImage (round-trip; see ConvertTo for rationale)
|
|
// TODO: In a future MR, backend images will come from overrides.dgd; worker image
|
|
// (v1alpha1 DeploymentOverrides.WorkersImage) has no v1beta1 equivalent yet.
|
|
if src.Image != "" {
|
|
dst.ProfilingConfig.ProfilerImage = src.Image
|
|
}
|
|
|
|
restoreAnnotationFields(srcObj, dst)
|
|
restoreProfilingJobResources(src, dst)
|
|
}
|
|
|
|
// mergeSLAWorkloadIntoBlob writes SLA and Workload structured fields back into the JSON blob,
|
|
// overwriting any existing values for those keys.
|
|
func mergeSLAWorkloadIntoBlob(src *v1beta1.DynamoGraphDeploymentRequestSpec, blob map[string]interface{}) {
|
|
slaMap, _ := blob["sla"].(map[string]interface{})
|
|
if slaMap == nil {
|
|
slaMap = make(map[string]interface{})
|
|
}
|
|
if src.SLA != nil {
|
|
if src.SLA.TTFT != nil {
|
|
slaMap["ttft"] = *src.SLA.TTFT
|
|
}
|
|
if src.SLA.ITL != nil {
|
|
slaMap["itl"] = *src.SLA.ITL
|
|
}
|
|
}
|
|
if src.Workload != nil {
|
|
if src.Workload.ISL != nil {
|
|
slaMap["isl"] = float64(*src.Workload.ISL)
|
|
}
|
|
if src.Workload.OSL != nil {
|
|
slaMap["osl"] = float64(*src.Workload.OSL)
|
|
}
|
|
}
|
|
blob["sla"] = slaMap
|
|
}
|
|
|
|
// mergeModelCacheIntoBlob writes ModelCache structured fields back into blob["deployment"]["modelCache"].
|
|
func mergeModelCacheIntoBlob(mc *v1beta1.ModelCacheSpec, blob map[string]interface{}) {
|
|
deployMap, _ := blob["deployment"].(map[string]interface{})
|
|
if deployMap == nil {
|
|
deployMap = make(map[string]interface{})
|
|
}
|
|
mcMap := make(map[string]interface{})
|
|
if mc.PVCName != "" {
|
|
mcMap["pvcName"] = mc.PVCName
|
|
}
|
|
if mc.PVCModelPath != "" {
|
|
mcMap["modelPathInPvc"] = mc.PVCModelPath
|
|
}
|
|
if mc.PVCMountPath != "" {
|
|
mcMap["pvcMountPath"] = mc.PVCMountPath
|
|
}
|
|
if len(mcMap) > 0 {
|
|
deployMap["modelCache"] = mcMap
|
|
blob["deployment"] = deployMap
|
|
}
|
|
}
|
|
|
|
// mergePlannerIntoBlob writes the planner RawExtension into blob["planner"].
|
|
// The RawExtension is the full PlannerConfig JSON blob (opaque to Go).
|
|
func mergePlannerIntoBlob(planner *runtime.RawExtension, blob map[string]interface{}) {
|
|
if planner == nil || planner.Raw == nil {
|
|
return
|
|
}
|
|
var plannerMap map[string]interface{}
|
|
if err := json.Unmarshal(planner.Raw, &plannerMap); err != nil || len(plannerMap) == 0 {
|
|
return
|
|
}
|
|
blob["planner"] = plannerMap
|
|
}
|
|
|
|
// applyPlannerFromBlob extracts blob["planner"] and populates v1beta1 Features.Planner.
|
|
func applyPlannerFromBlob(blob map[string]interface{}, dst *v1beta1.DynamoGraphDeploymentRequestSpec) {
|
|
plannerRaw, ok := blob["planner"]
|
|
if !ok {
|
|
return
|
|
}
|
|
plannerMap, ok := plannerRaw.(map[string]interface{})
|
|
if !ok || len(plannerMap) == 0 {
|
|
return
|
|
}
|
|
raw, err := json.Marshal(plannerMap)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if dst.Features == nil {
|
|
dst.Features = &v1beta1.FeaturesSpec{}
|
|
}
|
|
dst.Features.Planner = &runtime.RawExtension{Raw: raw}
|
|
}
|
|
|
|
// restoreAnnotationFields restores v1alpha1 spec fields that were annotation-preserved
|
|
// during ConvertTo: ConfigMapRef, OutputPVC, and DeploymentOverrides.
|
|
func restoreAnnotationFields(srcObj *v1beta1.DynamoGraphDeploymentRequest, dst *DynamoGraphDeploymentRequestSpec) {
|
|
if srcObj.Annotations == nil {
|
|
return
|
|
}
|
|
if v, ok := srcObj.Annotations[annDGDRConfigMapRef]; ok && v != "" {
|
|
var ref ConfigMapKeySelector
|
|
if err := json.Unmarshal([]byte(v), &ref); err == nil {
|
|
dst.ProfilingConfig.ConfigMapRef = &ref
|
|
}
|
|
}
|
|
if v, ok := srcObj.Annotations[annDGDROutputPVC]; ok {
|
|
dst.ProfilingConfig.OutputPVC = v
|
|
}
|
|
if v, ok := srcObj.Annotations[annDGDRDeployOverrides]; ok && v != "" {
|
|
var overrides struct {
|
|
Name string `json:"name,omitempty"`
|
|
Namespace string `json:"namespace,omitempty"`
|
|
Labels map[string]string `json:"labels,omitempty"`
|
|
Annotations map[string]string `json:"annotations,omitempty"`
|
|
}
|
|
if err := json.Unmarshal([]byte(v), &overrides); err == nil {
|
|
if dst.DeploymentOverrides == nil {
|
|
dst.DeploymentOverrides = &DeploymentOverridesSpec{}
|
|
}
|
|
dst.DeploymentOverrides.Name = overrides.Name
|
|
dst.DeploymentOverrides.Namespace = overrides.Namespace
|
|
dst.DeploymentOverrides.Labels = overrides.Labels
|
|
dst.DeploymentOverrides.Annotations = overrides.Annotations
|
|
}
|
|
}
|
|
}
|
|
|
|
// restoreProfilingJobResources restores Resources and Tolerations from
|
|
// v1beta1 Overrides.ProfilingJob back into v1alpha1 ProfilingConfig.
|
|
func restoreProfilingJobResources(src *v1beta1.DynamoGraphDeploymentRequestSpec, dst *DynamoGraphDeploymentRequestSpec) {
|
|
if src.Overrides == nil || src.Overrides.ProfilingJob == nil {
|
|
return
|
|
}
|
|
podSpec := &src.Overrides.ProfilingJob.Template.Spec
|
|
if len(podSpec.Containers) > 0 {
|
|
res := podSpec.Containers[0].Resources
|
|
if len(res.Requests) > 0 || len(res.Limits) > 0 {
|
|
dst.ProfilingConfig.Resources = &res
|
|
}
|
|
}
|
|
if len(podSpec.Tolerations) > 0 {
|
|
dst.ProfilingConfig.Tolerations = podSpec.Tolerations
|
|
}
|
|
}
|
|
|
|
// convertDGDRStatusTo converts the v1alpha1 Status into the v1beta1 Status.
|
|
func convertDGDRStatusTo(src *DynamoGraphDeploymentRequestStatus, dst *v1beta1.DynamoGraphDeploymentRequestStatus, dstObj *v1beta1.DynamoGraphDeploymentRequest) {
|
|
dst.Phase = dgdrStateToPhase(string(src.State), src.Deployment)
|
|
dst.ObservedGeneration = src.ObservedGeneration
|
|
dst.Conditions = src.Conditions
|
|
|
|
if src.Backend != "" {
|
|
setAnnotation(dstObj, annDGDRStatusBackend, src.Backend)
|
|
}
|
|
if src.ProfilingResults != "" {
|
|
setAnnotation(dstObj, annDGDRProfilingResults, src.ProfilingResults)
|
|
}
|
|
if src.GeneratedDeployment != nil {
|
|
if dst.ProfilingResults == nil {
|
|
dst.ProfilingResults = &v1beta1.ProfilingResultsStatus{}
|
|
}
|
|
dst.ProfilingResults.SelectedConfig = src.GeneratedDeployment
|
|
}
|
|
if src.Deployment != nil {
|
|
dst.DGDName = src.Deployment.Name
|
|
if data, err := json.Marshal(src.Deployment); err == nil {
|
|
setAnnotation(dstObj, annDGDRDeploymentStatus, string(data))
|
|
}
|
|
}
|
|
|
|
// ProfilingJobName — read from annotation (stored during ConvertFrom for round-trip)
|
|
if ann, ok := dstObj.Annotations[annDGDRProfilingJobName]; ok && ann != "" {
|
|
dst.ProfilingJobName = ann
|
|
}
|
|
}
|
|
|
|
// convertDGDRStatusFrom converts the v1beta1 Status back into the v1alpha1 Status.
|
|
func convertDGDRStatusFrom(src *v1beta1.DynamoGraphDeploymentRequestStatus, dst *DynamoGraphDeploymentRequestStatus, srcObj *v1beta1.DynamoGraphDeploymentRequest) {
|
|
dst.State = DGDRState(dgdrPhaseToState(src.Phase))
|
|
dst.ObservedGeneration = src.ObservedGeneration
|
|
dst.Conditions = src.Conditions
|
|
|
|
if srcObj.Annotations != nil {
|
|
if v, ok := srcObj.Annotations[annDGDRStatusBackend]; ok {
|
|
dst.Backend = v
|
|
}
|
|
if v, ok := srcObj.Annotations[annDGDRProfilingResults]; ok {
|
|
dst.ProfilingResults = v
|
|
}
|
|
}
|
|
|
|
if src.ProfilingResults != nil && src.ProfilingResults.SelectedConfig != nil {
|
|
dst.GeneratedDeployment = src.ProfilingResults.SelectedConfig
|
|
}
|
|
|
|
if srcObj.Annotations != nil {
|
|
if v, ok := srcObj.Annotations[annDGDRDeploymentStatus]; ok && v != "" {
|
|
var depStatus DeploymentStatus
|
|
if err := json.Unmarshal([]byte(v), &depStatus); err == nil {
|
|
dst.Deployment = &depStatus
|
|
}
|
|
}
|
|
}
|
|
// If no annotation but we have DGDName, create a minimal deployment status.
|
|
// Created is left false so the v1alpha1 controller does not skip re-creating the DGD.
|
|
if dst.Deployment == nil && src.DGDName != "" {
|
|
dst.Deployment = &DeploymentStatus{
|
|
Name: src.DGDName,
|
|
Created: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
// dgdrStateToPhase maps v1alpha1 state strings to v1beta1 DGDRPhase.
|
|
func dgdrStateToPhase(state string, deployment *DeploymentStatus) v1beta1.DGDRPhase {
|
|
switch state {
|
|
case "", string(DGDRStatePending):
|
|
return v1beta1.DGDRPhasePending
|
|
case string(DGDRStateProfiling):
|
|
return v1beta1.DGDRPhaseProfiling
|
|
case string(DGDRStateReady):
|
|
// If there is a deployment that was created, it means we are actually Deployed
|
|
if deployment != nil && deployment.Created {
|
|
return v1beta1.DGDRPhaseDeployed
|
|
}
|
|
return v1beta1.DGDRPhaseReady
|
|
case string(DGDRStateDeploying):
|
|
return v1beta1.DGDRPhaseDeploying
|
|
case string(DGDRStateDeploymentDeleted):
|
|
return v1beta1.DGDRPhaseReady
|
|
case string(DGDRStateFailed):
|
|
return v1beta1.DGDRPhaseFailed
|
|
default:
|
|
return v1beta1.DGDRPhasePending
|
|
}
|
|
}
|
|
|
|
// dgdrPhaseToState maps v1beta1 DGDRPhase to v1alpha1 state strings.
|
|
func dgdrPhaseToState(phase v1beta1.DGDRPhase) string {
|
|
switch phase {
|
|
case v1beta1.DGDRPhasePending:
|
|
return string(DGDRStatePending)
|
|
case v1beta1.DGDRPhaseProfiling:
|
|
return string(DGDRStateProfiling)
|
|
case v1beta1.DGDRPhaseReady:
|
|
return string(DGDRStateReady)
|
|
case v1beta1.DGDRPhaseDeploying:
|
|
return string(DGDRStateDeploying)
|
|
case v1beta1.DGDRPhaseDeployed:
|
|
return string(DGDRStateReady) // lossy
|
|
case v1beta1.DGDRPhaseFailed:
|
|
return string(DGDRStateFailed)
|
|
default:
|
|
return string(DGDRStatePending)
|
|
}
|
|
}
|