dynamo/deploy/operator/internal/controller/dynamographdeployment_contr...

2386 lines
72 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.
*/
package controller
import (
"context"
"testing"
configv1alpha1 "github.com/ai-dynamo/dynamo/deploy/operator/api/config/v1alpha1"
"github.com/ai-dynamo/dynamo/deploy/operator/api/v1alpha1"
"github.com/ai-dynamo/dynamo/deploy/operator/internal/checkpoint"
commonconsts "github.com/ai-dynamo/dynamo/deploy/operator/internal/consts"
"github.com/ai-dynamo/dynamo/deploy/operator/internal/controller_common"
grovev1alpha1 "github.com/ai-dynamo/grove/operator/api/core/v1alpha1"
"github.com/onsi/gomega"
autoscalingv1 "k8s.io/api/autoscaling/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/scale"
"k8s.io/client-go/tools/record"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
func TestDynamoGraphDeploymentReconciler_reconcileScalingAdapters(t *testing.T) {
// Register custom types with the scheme
if err := v1alpha1.AddToScheme(scheme.Scheme); err != nil {
t.Fatalf("Failed to add v1alpha1 to scheme: %v", err)
}
tests := []struct {
name string
dgd *v1alpha1.DynamoGraphDeployment
existingAdapters []v1alpha1.DynamoGraphDeploymentScalingAdapter
expectedAdapterCount int
expectedAdapters map[string]int32 // map of adapter name to expected replicas
expectDeleted []string // adapter names that should be deleted
}{
{
name: "creates adapters for services with scalingAdapter.enabled=true",
dgd: &v1alpha1.DynamoGraphDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd",
Namespace: "default",
},
Spec: v1alpha1.DynamoGraphDeploymentSpec{
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"Frontend": {
Replicas: ptr.To(int32(2)),
ScalingAdapter: &v1alpha1.ScalingAdapter{
Enabled: true,
},
},
"decode": {
Replicas: ptr.To(int32(3)),
ScalingAdapter: &v1alpha1.ScalingAdapter{
Enabled: true,
},
},
},
},
},
expectedAdapterCount: 2,
expectedAdapters: map[string]int32{
"test-dgd-frontend": 2,
"test-dgd-decode": 3,
},
},
{
name: "uses default replicas when not specified",
dgd: &v1alpha1.DynamoGraphDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd",
Namespace: "default",
},
Spec: v1alpha1.DynamoGraphDeploymentSpec{
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"worker": {
ScalingAdapter: &v1alpha1.ScalingAdapter{
Enabled: true,
},
},
},
},
},
expectedAdapterCount: 1,
expectedAdapters: map[string]int32{
"test-dgd-worker": 1, // default replicas
},
},
{
name: "skips adapter creation when not enabled",
dgd: &v1alpha1.DynamoGraphDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd",
Namespace: "default",
},
Spec: v1alpha1.DynamoGraphDeploymentSpec{
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"Frontend": {
Replicas: ptr.To(int32(2)),
ScalingAdapter: &v1alpha1.ScalingAdapter{
Enabled: true,
},
},
"decode": {
Replicas: ptr.To(int32(3)),
// No ScalingAdapter or Enabled=false means no adapter created
},
},
},
},
expectedAdapterCount: 1,
expectedAdapters: map[string]int32{
"test-dgd-frontend": 2,
},
},
{
name: "deletes adapter when service is removed",
dgd: &v1alpha1.DynamoGraphDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd",
Namespace: "default",
UID: "test-uid",
},
Spec: v1alpha1.DynamoGraphDeploymentSpec{
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"Frontend": {
Replicas: ptr.To(int32(2)),
ScalingAdapter: &v1alpha1.ScalingAdapter{
Enabled: true,
},
},
},
},
},
existingAdapters: []v1alpha1.DynamoGraphDeploymentScalingAdapter{
{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-frontend",
Namespace: "default",
Labels: map[string]string{
commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
},
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: "nvidia.com/v1alpha1",
Kind: "DynamoGraphDeployment",
Name: "test-dgd",
UID: "test-uid",
},
},
},
Spec: v1alpha1.DynamoGraphDeploymentScalingAdapterSpec{
Replicas: 2,
DGDRef: v1alpha1.DynamoGraphDeploymentServiceRef{
Name: "test-dgd",
ServiceName: "Frontend",
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-removed",
Namespace: "default",
Labels: map[string]string{
commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
},
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: "nvidia.com/v1alpha1",
Kind: "DynamoGraphDeployment",
Name: "test-dgd",
UID: "test-uid",
},
},
},
Spec: v1alpha1.DynamoGraphDeploymentScalingAdapterSpec{
Replicas: 1,
DGDRef: v1alpha1.DynamoGraphDeploymentServiceRef{
Name: "test-dgd",
ServiceName: "removed",
},
},
},
},
expectedAdapterCount: 1,
expectedAdapters: map[string]int32{
"test-dgd-frontend": 2,
},
expectDeleted: []string{"test-dgd-removed"},
},
{
name: "deletes adapter when scalingAdapter.enabled is not set",
dgd: &v1alpha1.DynamoGraphDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd",
Namespace: "default",
UID: "test-uid",
},
Spec: v1alpha1.DynamoGraphDeploymentSpec{
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"Frontend": {
Replicas: ptr.To(int32(2)),
// No ScalingAdapter means adapter should be deleted
},
},
},
},
existingAdapters: []v1alpha1.DynamoGraphDeploymentScalingAdapter{
{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-frontend",
Namespace: "default",
Labels: map[string]string{
commonconsts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
},
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: "nvidia.com/v1alpha1",
Kind: "DynamoGraphDeployment",
Name: "test-dgd",
UID: "test-uid",
},
},
},
Spec: v1alpha1.DynamoGraphDeploymentScalingAdapterSpec{
Replicas: 2,
DGDRef: v1alpha1.DynamoGraphDeploymentServiceRef{
Name: "test-dgd",
ServiceName: "Frontend",
},
},
},
},
expectedAdapterCount: 0,
expectedAdapters: map[string]int32{},
expectDeleted: []string{"test-dgd-frontend"},
},
{
name: "adapter name uses lowercase service name",
dgd: &v1alpha1.DynamoGraphDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "my-dgd",
Namespace: "default",
},
Spec: v1alpha1.DynamoGraphDeploymentSpec{
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"MyService": {
Replicas: ptr.To(int32(1)),
ScalingAdapter: &v1alpha1.ScalingAdapter{
Enabled: true,
},
},
},
},
},
expectedAdapterCount: 1,
expectedAdapters: map[string]int32{
"my-dgd-myservice": 1,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Build initial objects
var initObjs []client.Object
initObjs = append(initObjs, tt.dgd)
for i := range tt.existingAdapters {
initObjs = append(initObjs, &tt.existingAdapters[i])
}
// Create fake client
fakeClient := fake.NewClientBuilder().
WithScheme(scheme.Scheme).
WithObjects(initObjs...).
Build()
// Create reconciler
r := &DynamoGraphDeploymentReconciler{
Client: fakeClient,
Recorder: record.NewFakeRecorder(10),
}
// Run reconcileScalingAdapters
ctx := context.Background()
err := r.reconcileScalingAdapters(ctx, tt.dgd)
if err != nil {
t.Fatalf("reconcileScalingAdapters() error = %v", err)
}
// Verify adapters
adapterList := &v1alpha1.DynamoGraphDeploymentScalingAdapterList{}
if err := fakeClient.List(ctx, adapterList, client.InNamespace("default")); err != nil {
t.Fatalf("Failed to list adapters: %v", err)
}
if len(adapterList.Items) != tt.expectedAdapterCount {
t.Errorf("Expected %d adapters, got %d", tt.expectedAdapterCount, len(adapterList.Items))
}
// Check expected adapters exist with correct replicas
for name, expectedReplicas := range tt.expectedAdapters {
adapter := &v1alpha1.DynamoGraphDeploymentScalingAdapter{}
err := fakeClient.Get(ctx, types.NamespacedName{Name: name, Namespace: "default"}, adapter)
if err != nil {
t.Errorf("Expected adapter %s to exist, but got error: %v", name, err)
continue
}
if adapter.Spec.Replicas != expectedReplicas {
t.Errorf("Adapter %s has replicas=%d, expected %d", name, adapter.Spec.Replicas, expectedReplicas)
}
}
// Check that deleted adapters don't exist
for _, name := range tt.expectDeleted {
adapter := &v1alpha1.DynamoGraphDeploymentScalingAdapter{}
err := fakeClient.Get(ctx, types.NamespacedName{Name: name, Namespace: "default"}, adapter)
if err == nil {
t.Errorf("Expected adapter %s to be deleted, but it still exists", name)
}
}
})
}
}
func TestDynamoGraphDeploymentReconciler_createCheckpointCR_reusesExistingCapture(t *testing.T) {
if err := v1alpha1.AddToScheme(scheme.Scheme); err != nil {
t.Fatalf("Failed to add v1alpha1 to scheme: %v", err)
}
ctx := context.Background()
identity := v1alpha1.DynamoCheckpointIdentity{
Model: "meta-llama/Llama-2-7b-hf",
BackendFramework: "vllm",
}
hash, err := checkpoint.ComputeIdentityHash(identity)
if err != nil {
t.Fatalf("Failed to compute checkpoint hash: %v", err)
}
existing := &v1alpha1.DynamoCheckpoint{
ObjectMeta: metav1.ObjectMeta{
Name: "existing-worker-checkpoint",
Namespace: "default",
},
Spec: v1alpha1.DynamoCheckpointSpec{
Identity: identity,
Job: v1alpha1.DynamoCheckpointJobConfig{
PodTemplateSpec: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "main",
Image: "keep-existing:latest",
}},
},
},
},
},
Status: v1alpha1.DynamoCheckpointStatus{
IdentityHash: hash,
},
}
reconciler := &DynamoGraphDeploymentReconciler{
Client: fake.NewClientBuilder().
WithScheme(scheme.Scheme).
WithObjects(existing).
Build(),
Config: &configv1alpha1.OperatorConfiguration{},
Recorder: record.NewFakeRecorder(10),
}
dgd := &v1alpha1.DynamoGraphDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd",
Namespace: "default",
},
}
component := &v1alpha1.DynamoComponentDeploymentSharedSpec{
ComponentType: string(commonconsts.ComponentTypeWorker),
Checkpoint: &v1alpha1.ServiceCheckpointConfig{
Enabled: true,
Mode: v1alpha1.CheckpointModeAuto,
Identity: &v1alpha1.DynamoCheckpointIdentity{
Model: identity.Model,
BackendFramework: identity.BackendFramework,
TensorParallelSize: 1,
PipelineParallelSize: 1,
ExtraParameters: map[string]string{},
},
},
ExtraPodSpec: &v1alpha1.ExtraPodSpec{
MainContainer: &corev1.Container{
Name: "main",
Image: "new-writer:latest",
},
},
}
ckpt, err := reconciler.createCheckpointCR(ctx, dgd, "worker", component)
if err != nil {
t.Fatalf("createCheckpointCR() error = %v", err)
}
if ckpt.Name != "existing-worker-checkpoint" {
t.Fatalf("createCheckpointCR() returned checkpoint %s, want existing-worker-checkpoint", ckpt.Name)
}
updated := &v1alpha1.DynamoCheckpoint{}
if err := reconciler.Get(ctx, types.NamespacedName{Name: "existing-worker-checkpoint", Namespace: "default"}, updated); err != nil {
t.Fatalf("Failed to get checkpoint: %v", err)
}
if len(updated.Spec.Job.PodTemplateSpec.Spec.Containers) != 1 {
t.Fatalf("expected one job container, got %d", len(updated.Spec.Job.PodTemplateSpec.Spec.Containers))
}
if updated.Spec.Job.PodTemplateSpec.Spec.Containers[0].Image != "keep-existing:latest" {
t.Fatalf("existing job image was mutated to %s", updated.Spec.Job.PodTemplateSpec.Spec.Containers[0].Image)
}
}
func TestDynamoGraphDeploymentReconciler_reconcileCheckpoints_checkpointRefSkipsAutoCreateWhileReferencedCRIsNotReady(t *testing.T) {
if err := v1alpha1.AddToScheme(scheme.Scheme); err != nil {
t.Fatalf("Failed to add v1alpha1 to scheme: %v", err)
}
ctx := context.Background()
identity := v1alpha1.DynamoCheckpointIdentity{
Model: "meta-llama/Llama-2-7b-hf",
BackendFramework: "vllm",
}
hash, err := checkpoint.ComputeIdentityHash(identity)
if err != nil {
t.Fatalf("Failed to compute checkpoint hash: %v", err)
}
referenced := &v1alpha1.DynamoCheckpoint{
ObjectMeta: metav1.ObjectMeta{
Name: "friendly-checkpoint",
Namespace: "default",
},
Spec: v1alpha1.DynamoCheckpointSpec{
Identity: identity,
Job: v1alpha1.DynamoCheckpointJobConfig{
PodTemplateSpec: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "main",
Image: "keep-existing:latest",
}},
},
},
},
},
Status: v1alpha1.DynamoCheckpointStatus{
Phase: v1alpha1.DynamoCheckpointPhaseCreating,
IdentityHash: hash,
},
}
reconciler := &DynamoGraphDeploymentReconciler{
Client: fake.NewClientBuilder().
WithScheme(scheme.Scheme).
WithObjects(referenced).
WithStatusSubresource(referenced).
Build(),
Config: &configv1alpha1.OperatorConfiguration{},
Recorder: record.NewFakeRecorder(10),
}
ref := friendlyCheckpointName
dgd := &v1alpha1.DynamoGraphDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd",
Namespace: "default",
},
Spec: v1alpha1.DynamoGraphDeploymentSpec{
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"worker": {
ComponentType: string(commonconsts.ComponentTypeWorker),
Checkpoint: &v1alpha1.ServiceCheckpointConfig{
Enabled: true,
Mode: v1alpha1.CheckpointModeAuto,
CheckpointRef: &ref,
},
},
},
},
}
checkpointStatuses, checkpointInfos, err := reconciler.reconcileCheckpoints(ctx, dgd)
if err != nil {
t.Fatalf("reconcileCheckpoints() error = %v", err)
}
info, ok := checkpointInfos["worker"]
if !ok {
t.Fatalf("expected checkpoint info for worker service")
}
if info.Ready {
t.Fatalf("expected referenced checkpoint to remain not ready")
}
if !info.Exists {
t.Fatalf("expected referenced checkpoint to exist")
}
if info.Hash != hash {
t.Fatalf("checkpoint hash = %s, want %s", info.Hash, hash)
}
if checkpointStatuses["worker"].CheckpointName != "friendly-checkpoint" {
t.Fatalf("checkpoint status name = %s, want friendly-checkpoint", checkpointStatuses["worker"].CheckpointName)
}
checkpoints := &v1alpha1.DynamoCheckpointList{}
if err := reconciler.List(ctx, checkpoints, client.InNamespace("default")); err != nil {
t.Fatalf("failed to list checkpoints: %v", err)
}
if len(checkpoints.Items) != 1 {
t.Fatalf("expected only the referenced checkpoint to exist, found %d", len(checkpoints.Items))
}
if checkpoints.Items[0].Name != "friendly-checkpoint" {
t.Fatalf("unexpected checkpoint %s", checkpoints.Items[0].Name)
}
}
func TestDynamoGraphDeploymentReconciler_reconcileCheckpoints_autoModeWaitsForExistingCreatingCheckpoint(t *testing.T) {
if err := v1alpha1.AddToScheme(scheme.Scheme); err != nil {
t.Fatalf("Failed to add v1alpha1 to scheme: %v", err)
}
ctx := context.Background()
identity := v1alpha1.DynamoCheckpointIdentity{
Model: "meta-llama/Llama-2-7b-hf",
BackendFramework: "vllm",
}
hash, err := checkpoint.ComputeIdentityHash(identity)
if err != nil {
t.Fatalf("Failed to compute checkpoint hash: %v", err)
}
existing := &v1alpha1.DynamoCheckpoint{
ObjectMeta: metav1.ObjectMeta{
Name: "existing-worker-checkpoint",
Namespace: "default",
},
Spec: v1alpha1.DynamoCheckpointSpec{
Identity: identity,
Job: v1alpha1.DynamoCheckpointJobConfig{
PodTemplateSpec: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "main",
Image: "keep-existing:latest",
}},
},
},
},
},
Status: v1alpha1.DynamoCheckpointStatus{
Phase: v1alpha1.DynamoCheckpointPhaseCreating,
IdentityHash: hash,
},
}
reconciler := &DynamoGraphDeploymentReconciler{
Client: fake.NewClientBuilder().
WithScheme(scheme.Scheme).
WithObjects(existing).
WithStatusSubresource(existing).
Build(),
Config: &configv1alpha1.OperatorConfiguration{},
Recorder: record.NewFakeRecorder(10),
}
dgd := &v1alpha1.DynamoGraphDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd",
Namespace: "default",
},
Spec: v1alpha1.DynamoGraphDeploymentSpec{
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"worker": {
ComponentType: string(commonconsts.ComponentTypeWorker),
Checkpoint: &v1alpha1.ServiceCheckpointConfig{
Enabled: true,
Mode: v1alpha1.CheckpointModeAuto,
Identity: &v1alpha1.DynamoCheckpointIdentity{
Model: identity.Model,
BackendFramework: identity.BackendFramework,
},
},
ExtraPodSpec: &v1alpha1.ExtraPodSpec{
MainContainer: &corev1.Container{
Name: "main",
Image: "new-writer:latest",
},
},
},
},
},
}
checkpointStatuses, checkpointInfos, err := reconciler.reconcileCheckpoints(ctx, dgd)
if err != nil {
t.Fatalf("reconcileCheckpoints() error = %v", err)
}
info, ok := checkpointInfos["worker"]
if !ok {
t.Fatalf("expected checkpoint info for worker service")
}
if info.Ready {
t.Fatalf("expected existing checkpoint to remain not ready")
}
if !info.Exists {
t.Fatalf("expected existing checkpoint to be detected")
}
if info.Hash != hash {
t.Fatalf("checkpoint hash = %s, want %s", info.Hash, hash)
}
if checkpointStatuses["worker"].CheckpointName != "existing-worker-checkpoint" {
t.Fatalf("checkpoint status name = %s, want existing-worker-checkpoint", checkpointStatuses["worker"].CheckpointName)
}
updated := &v1alpha1.DynamoCheckpoint{}
if err := reconciler.Get(ctx, types.NamespacedName{Name: "existing-worker-checkpoint", Namespace: "default"}, updated); err != nil {
t.Fatalf("Failed to get checkpoint: %v", err)
}
if len(updated.Spec.Job.PodTemplateSpec.Spec.Containers) != 1 {
t.Fatalf("expected one job container, got %d", len(updated.Spec.Job.PodTemplateSpec.Spec.Containers))
}
if updated.Spec.Job.PodTemplateSpec.Spec.Containers[0].Image != "keep-existing:latest" {
t.Fatalf("existing job image was mutated to %s", updated.Spec.Job.PodTemplateSpec.Spec.Containers[0].Image)
}
}
// mockScaleClient implements scale.ScalesGetter for testing
type mockScaleClient struct{}
func (m *mockScaleClient) Scales(namespace string) scale.ScaleInterface {
return &mockScaleInterface{}
}
// mockScaleInterface implements scale.ScaleInterface for testing
type mockScaleInterface struct{}
func (m *mockScaleInterface) Get(ctx context.Context, resource schema.GroupResource, name string, opts metav1.GetOptions) (*autoscalingv1.Scale, error) {
// Return a dummy scale object - we don't actually need scaling in the test
return &autoscalingv1.Scale{}, nil
}
func (m *mockScaleInterface) Update(ctx context.Context, resource schema.GroupResource, scale *autoscalingv1.Scale, opts metav1.UpdateOptions) (*autoscalingv1.Scale, error) {
// Return success without actually doing anything
return scale, nil
}
func (m *mockScaleInterface) Patch(ctx context.Context, gvr schema.GroupVersionResource, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions) (*autoscalingv1.Scale, error) {
// Return a dummy scale object
return &autoscalingv1.Scale{}, nil
}
func Test_reconcileGroveResources(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
dgdSpec v1alpha1.DynamoGraphDeploymentSpec
existingGroveResources []client.Object
wantReconcileResult ReconcileResult
}{
{
name: "singular frontend service with 2 replicas - creates a PodClique with 2 replicas - ready",
dgdSpec: v1alpha1.DynamoGraphDeploymentSpec{
BackendFramework: "vllm",
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"frontend": {
ComponentType: string(commonconsts.ComponentTypeFrontend),
Replicas: ptr.To(int32(2)),
},
},
},
existingGroveResources: []client.Object{
&grovev1alpha1.PodClique{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-0-frontend",
Namespace: "default",
},
Spec: grovev1alpha1.PodCliqueSpec{
Replicas: 2,
},
Status: grovev1alpha1.PodCliqueStatus{
Replicas: 2,
UpdatedReplicas: 2,
ReadyReplicas: 2,
ObservedGeneration: ptr.To(int64(1)),
},
},
},
wantReconcileResult: ReconcileResult{
State: v1alpha1.DGDStateSuccessful,
Reason: "all_resources_are_ready",
Message: "All resources are ready",
ServiceStatus: map[string]v1alpha1.ServiceReplicaStatus{
"frontend": {
ComponentKind: v1alpha1.ComponentKindPodClique,
ComponentName: "test-dgd-0-frontend",
ComponentNames: []string{"test-dgd-0-frontend"},
Replicas: 2,
UpdatedReplicas: 2,
ReadyReplicas: ptr.To(int32(2)),
},
},
},
},
{
name: "frontend service with 1 replica, decode service with 2 replicas - 2 PodCliques - one unready",
dgdSpec: v1alpha1.DynamoGraphDeploymentSpec{
BackendFramework: "vllm",
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"frontend": {
ComponentType: string(commonconsts.ComponentTypeFrontend),
Replicas: ptr.To(int32(1)),
},
"decode": {
ComponentType: string(commonconsts.ComponentTypeDecode),
Replicas: ptr.To(int32(2)),
},
},
},
existingGroveResources: []client.Object{
&grovev1alpha1.PodClique{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-0-frontend",
Namespace: "default",
},
Spec: grovev1alpha1.PodCliqueSpec{
Replicas: 1,
},
Status: grovev1alpha1.PodCliqueStatus{
Replicas: 1,
UpdatedReplicas: 1,
ReadyReplicas: 1,
ObservedGeneration: ptr.To(int64(1)),
},
},
&grovev1alpha1.PodClique{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-0-decode",
Namespace: "default",
},
Spec: grovev1alpha1.PodCliqueSpec{
Replicas: 2,
},
Status: grovev1alpha1.PodCliqueStatus{
Replicas: 2,
UpdatedReplicas: 1,
ReadyReplicas: 1, // Only 1 ready, but 2 desired
ObservedGeneration: ptr.To(int64(1)),
},
},
},
wantReconcileResult: ReconcileResult{
State: v1alpha1.DGDStatePending,
Reason: "some_resources_are_not_ready",
Message: Message("Resources not ready: test-dgd: podclique/test-dgd-0-decode: desired=2, ready=1"),
ServiceStatus: map[string]v1alpha1.ServiceReplicaStatus{
"frontend": {
ComponentKind: v1alpha1.ComponentKindPodClique,
ComponentName: "test-dgd-0-frontend",
ComponentNames: []string{"test-dgd-0-frontend"},
Replicas: 1,
UpdatedReplicas: 1,
ReadyReplicas: ptr.To(int32(1)),
},
"decode": {
ComponentKind: v1alpha1.ComponentKindPodClique,
ComponentName: "test-dgd-0-decode",
ComponentNames: []string{"test-dgd-0-decode"},
Replicas: 2,
UpdatedReplicas: 1,
ReadyReplicas: ptr.To(int32(1)),
},
},
},
},
{
name: "decode worker multinode (PCSG), prefill worker multinode (PCSG) - both ready",
dgdSpec: v1alpha1.DynamoGraphDeploymentSpec{
BackendFramework: "vllm",
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"decode": {
ComponentType: string(commonconsts.ComponentTypeDecode),
Replicas: ptr.To(int32(1)),
Multinode: &v1alpha1.MultinodeSpec{
NodeCount: 2,
},
},
"prefill": {
ComponentType: string(commonconsts.ComponentTypeWorker),
Replicas: ptr.To(int32(1)),
Multinode: &v1alpha1.MultinodeSpec{
NodeCount: 4,
},
},
},
},
existingGroveResources: []client.Object{
&grovev1alpha1.PodCliqueScalingGroup{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-0-decode",
Namespace: "default",
},
Spec: grovev1alpha1.PodCliqueScalingGroupSpec{
Replicas: 1,
},
Status: grovev1alpha1.PodCliqueScalingGroupStatus{
Replicas: 1,
UpdatedReplicas: 1,
AvailableReplicas: 1,
ObservedGeneration: ptr.To(int64(1)),
},
},
&grovev1alpha1.PodCliqueScalingGroup{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-0-prefill",
Namespace: "default",
},
Spec: grovev1alpha1.PodCliqueScalingGroupSpec{
Replicas: 1,
},
Status: grovev1alpha1.PodCliqueScalingGroupStatus{
Replicas: 1,
UpdatedReplicas: 1,
AvailableReplicas: 1,
ObservedGeneration: ptr.To(int64(1)),
},
},
},
wantReconcileResult: ReconcileResult{
State: v1alpha1.DGDStateSuccessful,
Reason: "all_resources_are_ready",
Message: "All resources are ready",
ServiceStatus: map[string]v1alpha1.ServiceReplicaStatus{
"decode": {
ComponentKind: v1alpha1.ComponentKindPodCliqueScalingGroup,
ComponentName: "test-dgd-0-decode",
ComponentNames: []string{"test-dgd-0-decode"},
Replicas: 1,
UpdatedReplicas: 1,
AvailableReplicas: ptr.To(int32(1)),
},
"prefill": {
ComponentKind: v1alpha1.ComponentKindPodCliqueScalingGroup,
ComponentName: "test-dgd-0-prefill",
ComponentNames: []string{"test-dgd-0-prefill"},
Replicas: 1,
UpdatedReplicas: 1,
AvailableReplicas: ptr.To(int32(1)),
},
},
},
},
{
name: "frontend worker (PodClique), aggregated worker multinode (PCSG) - PCSG unready",
dgdSpec: v1alpha1.DynamoGraphDeploymentSpec{
BackendFramework: "vllm",
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"frontend": {
ComponentType: string(commonconsts.ComponentTypeFrontend),
Replicas: ptr.To(int32(1)),
},
"aggregated": {
ComponentType: string(commonconsts.ComponentTypeWorker),
Replicas: ptr.To(int32(2)),
Multinode: &v1alpha1.MultinodeSpec{
NodeCount: 8,
},
},
},
},
existingGroveResources: []client.Object{
&grovev1alpha1.PodClique{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-0-frontend",
Namespace: "default",
},
Spec: grovev1alpha1.PodCliqueSpec{
Replicas: 1,
},
Status: grovev1alpha1.PodCliqueStatus{
Replicas: 1,
UpdatedReplicas: 1,
ReadyReplicas: 1,
ObservedGeneration: ptr.To(int64(1)),
},
},
&grovev1alpha1.PodCliqueScalingGroup{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-0-aggregated",
Namespace: "default",
},
Spec: grovev1alpha1.PodCliqueScalingGroupSpec{
Replicas: 2,
},
Status: grovev1alpha1.PodCliqueScalingGroupStatus{
Replicas: 2,
UpdatedReplicas: 2,
AvailableReplicas: 1, // Only 1 available, but 2 desired
ObservedGeneration: ptr.To(int64(1)),
},
},
},
wantReconcileResult: ReconcileResult{
State: v1alpha1.DGDStatePending,
Reason: "some_resources_are_not_ready",
Message: Message("Resources not ready: test-dgd: pcsg/test-dgd-0-aggregated: desired=2, available=1"),
ServiceStatus: map[string]v1alpha1.ServiceReplicaStatus{
"frontend": {
ComponentKind: v1alpha1.ComponentKindPodClique,
ComponentName: "test-dgd-0-frontend",
ComponentNames: []string{"test-dgd-0-frontend"},
Replicas: 1,
UpdatedReplicas: 1,
ReadyReplicas: ptr.To(int32(1)),
},
"aggregated": {
ComponentKind: v1alpha1.ComponentKindPodCliqueScalingGroup,
ComponentName: "test-dgd-0-aggregated",
ComponentNames: []string{"test-dgd-0-aggregated"},
Replicas: 2,
UpdatedReplicas: 2,
AvailableReplicas: ptr.To(int32(1)),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := gomega.NewGomegaWithT(t)
s := scheme.Scheme
err := v1alpha1.AddToScheme(s)
g.Expect(err).NotTo(gomega.HaveOccurred())
err = grovev1alpha1.AddToScheme(s)
g.Expect(err).NotTo(gomega.HaveOccurred())
dgd := &v1alpha1.DynamoGraphDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd",
Namespace: "default",
},
Spec: tt.dgdSpec,
}
var objects []client.Object
objects = append(objects, dgd)
objects = append(objects, tt.existingGroveResources...)
fakeKubeClient := fake.NewClientBuilder().
WithScheme(s).
WithObjects(objects...).
WithStatusSubresource(objects...).
Build()
recorder := record.NewFakeRecorder(100)
reconciler := &DynamoGraphDeploymentReconciler{
Client: fakeKubeClient,
Recorder: recorder,
Config: &configv1alpha1.OperatorConfiguration{},
RuntimeConfig: &controller_common.RuntimeConfig{},
ScaleClient: &mockScaleClient{},
DockerSecretRetriever: &mockDockerSecretRetriever{
GetSecretsFunc: func(namespace, imageName string) ([]string, error) {
return []string{}, nil
},
},
}
result, err := reconciler.reconcileGroveResources(ctx, dgd, nil, nil)
g.Expect(err).NotTo(gomega.HaveOccurred())
g.Expect(result).To(gomega.Equal(tt.wantReconcileResult))
})
}
}
func Test_computeRestartStatus(t *testing.T) {
ctx := context.Background()
newID := "restart-1"
oldID := "restart-0"
tests := []struct {
name string
dgdSpec v1alpha1.DynamoGraphDeploymentSpec
dgdStatus v1alpha1.DynamoGraphDeploymentStatus
existingResources []client.Object
groveEnabled bool
wantRestartStatus *v1alpha1.RestartStatus
}{
{
name: "no restart requested - returns nil",
},
{
name: "no restart at time - returns nil",
dgdSpec: v1alpha1.DynamoGraphDeploymentSpec{
Restart: &v1alpha1.Restart{},
},
},
{
name: "no restart requested but has completed status - preserves status",
dgdStatus: v1alpha1.DynamoGraphDeploymentStatus{
Restart: &v1alpha1.RestartStatus{
ObservedID: oldID,
Phase: v1alpha1.RestartPhaseCompleted,
},
},
wantRestartStatus: &v1alpha1.RestartStatus{
ObservedID: oldID,
Phase: v1alpha1.RestartPhaseCompleted,
},
},
{
name: "no restart requested but has restarting status - returns nil",
dgdStatus: v1alpha1.DynamoGraphDeploymentStatus{
Restart: &v1alpha1.RestartStatus{
ObservedID: oldID,
Phase: v1alpha1.RestartPhaseRestarting,
},
},
},
{
name: "restart already processed (completed) - returns existing status",
dgdSpec: v1alpha1.DynamoGraphDeploymentSpec{
Restart: &v1alpha1.Restart{
ID: newID,
},
},
dgdStatus: v1alpha1.DynamoGraphDeploymentStatus{
Restart: &v1alpha1.RestartStatus{
ObservedID: newID,
Phase: v1alpha1.RestartPhaseCompleted,
},
},
wantRestartStatus: &v1alpha1.RestartStatus{
ObservedID: newID,
Phase: v1alpha1.RestartPhaseCompleted,
},
},
{
name: "restart already processed (failed) - returns existing status",
dgdSpec: v1alpha1.DynamoGraphDeploymentSpec{
Restart: &v1alpha1.Restart{
ID: newID,
},
},
dgdStatus: v1alpha1.DynamoGraphDeploymentStatus{
Restart: &v1alpha1.RestartStatus{
ObservedID: newID,
Phase: v1alpha1.RestartPhaseFailed,
},
},
wantRestartStatus: &v1alpha1.RestartStatus{
ObservedID: newID,
Phase: v1alpha1.RestartPhaseFailed,
},
},
{
name: "parallel restart - all services complete (DCD pathway)",
dgdSpec: v1alpha1.DynamoGraphDeploymentSpec{
Restart: &v1alpha1.Restart{
ID: newID,
Strategy: &v1alpha1.RestartStrategy{
Type: v1alpha1.RestartStrategyTypeParallel,
},
},
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"frontend": {
Replicas: ptr.To(int32(1)),
},
},
},
dgdStatus: v1alpha1.DynamoGraphDeploymentStatus{
Restart: &v1alpha1.RestartStatus{
ObservedID: newID,
Phase: v1alpha1.RestartPhaseRestarting,
InProgress: []string{"frontend"},
},
},
existingResources: []client.Object{
&v1alpha1.DynamoComponentDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-frontend",
Namespace: "default",
Generation: 1,
},
Status: v1alpha1.DynamoComponentDeploymentStatus{
ObservedGeneration: 1,
Conditions: []metav1.Condition{
{
Type: v1alpha1.DynamoGraphDeploymentConditionTypeAvailable,
Status: metav1.ConditionTrue,
},
},
},
},
},
wantRestartStatus: &v1alpha1.RestartStatus{
ObservedID: newID,
Phase: v1alpha1.RestartPhaseCompleted,
},
},
{
name: "parallel restart - services still restarting (DCD pathway)",
dgdSpec: v1alpha1.DynamoGraphDeploymentSpec{
BackendFramework: "vllm",
Restart: &v1alpha1.Restart{
ID: newID,
Strategy: &v1alpha1.RestartStrategy{
Type: v1alpha1.RestartStrategyTypeParallel,
},
},
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"frontend": {
ServiceName: "frontend",
ComponentType: string(commonconsts.ComponentTypeFrontend),
Replicas: ptr.To(int32(1)),
},
"decode": {
ServiceName: "decode",
ComponentType: string(commonconsts.ComponentTypeDecode),
Replicas: ptr.To(int32(2)),
},
},
},
dgdStatus: v1alpha1.DynamoGraphDeploymentStatus{
Restart: &v1alpha1.RestartStatus{
ObservedID: newID,
Phase: v1alpha1.RestartPhaseRestarting,
InProgress: []string{"frontend", "decode"},
},
},
existingResources: []client.Object{
&v1alpha1.DynamoComponentDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-frontend",
Namespace: "default",
Generation: 1,
},
Status: v1alpha1.DynamoComponentDeploymentStatus{
ObservedGeneration: 1,
Conditions: []metav1.Condition{
{
Type: v1alpha1.DynamoGraphDeploymentConditionTypeAvailable,
Status: metav1.ConditionTrue,
},
},
},
},
&v1alpha1.DynamoComponentDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-decode",
Namespace: "default",
Generation: 2,
},
Status: v1alpha1.DynamoComponentDeploymentStatus{
ObservedGeneration: 1, // Not yet caught up
Conditions: []metav1.Condition{
{
Type: v1alpha1.DynamoGraphDeploymentConditionTypeAvailable,
Status: metav1.ConditionFalse,
},
},
},
},
},
wantRestartStatus: &v1alpha1.RestartStatus{
ObservedID: newID,
Phase: v1alpha1.RestartPhaseRestarting,
InProgress: []string{"decode"},
},
},
{
name: "sequential restart - first service starting",
dgdSpec: v1alpha1.DynamoGraphDeploymentSpec{
Restart: &v1alpha1.Restart{
ID: newID,
Strategy: &v1alpha1.RestartStrategy{
Type: v1alpha1.RestartStrategyTypeSequential,
Order: []string{"frontend", "decode"},
},
},
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"frontend": {
Replicas: ptr.To(int32(1)),
},
"decode": {
Replicas: ptr.To(int32(2)),
},
},
},
wantRestartStatus: &v1alpha1.RestartStatus{
ObservedID: newID,
Phase: v1alpha1.RestartPhaseRestarting,
InProgress: []string{"frontend"},
},
},
{
name: "sequential restart - first service done, moving to second",
dgdSpec: v1alpha1.DynamoGraphDeploymentSpec{
Restart: &v1alpha1.Restart{
ID: newID,
Strategy: &v1alpha1.RestartStrategy{
Type: v1alpha1.RestartStrategyTypeSequential,
Order: []string{"frontend", "decode"},
},
},
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"frontend": {
Replicas: ptr.To(int32(1)),
},
"decode": {
Replicas: ptr.To(int32(2)),
},
},
},
dgdStatus: v1alpha1.DynamoGraphDeploymentStatus{
Restart: &v1alpha1.RestartStatus{
ObservedID: newID,
Phase: v1alpha1.RestartPhaseRestarting,
InProgress: []string{"frontend"},
},
},
existingResources: []client.Object{
&v1alpha1.DynamoComponentDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-frontend",
Namespace: "default",
Generation: 1,
},
Status: v1alpha1.DynamoComponentDeploymentStatus{
ObservedGeneration: 1,
Conditions: []metav1.Condition{
{
Type: v1alpha1.DynamoGraphDeploymentConditionTypeAvailable,
Status: metav1.ConditionTrue,
},
},
},
},
},
wantRestartStatus: &v1alpha1.RestartStatus{
ObservedID: newID,
Phase: v1alpha1.RestartPhaseRestarting,
InProgress: []string{"decode"},
},
},
{
name: "sequential restart - all services complete",
dgdSpec: v1alpha1.DynamoGraphDeploymentSpec{
Restart: &v1alpha1.Restart{
ID: newID,
Strategy: &v1alpha1.RestartStrategy{
Type: v1alpha1.RestartStrategyTypeSequential,
},
},
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"frontend": {
Replicas: ptr.To(int32(1)),
},
},
},
dgdStatus: v1alpha1.DynamoGraphDeploymentStatus{
Restart: &v1alpha1.RestartStatus{
ObservedID: newID,
Phase: v1alpha1.RestartPhaseRestarting,
InProgress: []string{"frontend"},
},
},
existingResources: []client.Object{
&v1alpha1.DynamoComponentDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-frontend",
Namespace: "default",
Generation: 1,
},
Status: v1alpha1.DynamoComponentDeploymentStatus{
ObservedGeneration: 1,
Conditions: []metav1.Condition{
{
Type: v1alpha1.DynamoGraphDeploymentConditionTypeAvailable,
Status: metav1.ConditionTrue,
},
},
},
},
},
wantRestartStatus: &v1alpha1.RestartStatus{
ObservedID: newID,
Phase: v1alpha1.RestartPhaseCompleted,
},
},
{
name: "default strategy (sequential) - no strategy specified",
dgdSpec: v1alpha1.DynamoGraphDeploymentSpec{
Restart: &v1alpha1.Restart{
ID: newID,
},
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"frontend": {
Replicas: ptr.To(int32(1)),
},
},
},
dgdStatus: v1alpha1.DynamoGraphDeploymentStatus{},
wantRestartStatus: &v1alpha1.RestartStatus{
ObservedID: newID,
Phase: v1alpha1.RestartPhaseRestarting,
InProgress: []string{"frontend"},
},
},
{
name: "parallel restart with empty services - returns completed",
dgdSpec: v1alpha1.DynamoGraphDeploymentSpec{
Restart: &v1alpha1.Restart{
ID: newID,
Strategy: &v1alpha1.RestartStrategy{
Type: v1alpha1.RestartStrategyTypeParallel,
},
},
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{},
},
dgdStatus: v1alpha1.DynamoGraphDeploymentStatus{},
wantRestartStatus: &v1alpha1.RestartStatus{
ObservedID: newID,
Phase: v1alpha1.RestartPhaseCompleted,
},
},
{
name: "parallel restart - new request with ready resources should NOT complete immediately (race condition fix)",
dgdSpec: v1alpha1.DynamoGraphDeploymentSpec{
Restart: &v1alpha1.Restart{
ID: newID,
Strategy: &v1alpha1.RestartStrategy{
Type: v1alpha1.RestartStrategyTypeParallel,
},
},
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"frontend": {
Replicas: ptr.To(int32(1)),
},
},
},
dgdStatus: v1alpha1.DynamoGraphDeploymentStatus{
// No existing restart status - brand new restart request
},
existingResources: []client.Object{
// DCD is READY - simulating state BEFORE restart annotation is applied
&v1alpha1.DynamoComponentDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-frontend",
Namespace: "default",
Generation: 1,
},
Status: v1alpha1.DynamoComponentDeploymentStatus{
ObservedGeneration: 1,
Conditions: []metav1.Condition{
{
Type: v1alpha1.DynamoGraphDeploymentConditionTypeAvailable,
Status: metav1.ConditionTrue,
},
},
},
},
},
wantRestartStatus: &v1alpha1.RestartStatus{
ObservedID: newID,
Phase: v1alpha1.RestartPhaseRestarting, // NOT Completed!
InProgress: []string{"frontend"},
},
},
{
name: "Grove pathway - parallel restart complete",
dgdSpec: v1alpha1.DynamoGraphDeploymentSpec{
Restart: &v1alpha1.Restart{
ID: newID,
Strategy: &v1alpha1.RestartStrategy{
Type: v1alpha1.RestartStrategyTypeParallel,
},
},
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"frontend": {
Replicas: ptr.To(int32(1)),
},
},
},
dgdStatus: v1alpha1.DynamoGraphDeploymentStatus{
Restart: &v1alpha1.RestartStatus{
ObservedID: newID,
Phase: v1alpha1.RestartPhaseRestarting,
InProgress: []string{"frontend"},
},
},
existingResources: []client.Object{
&grovev1alpha1.PodCliqueSet{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd",
Namespace: "default",
Generation: 1,
},
Status: grovev1alpha1.PodCliqueSetStatus{
ObservedGeneration: ptr.To(int64(1)),
},
},
&grovev1alpha1.PodClique{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-0-frontend",
Namespace: "default",
Generation: 1,
},
Spec: grovev1alpha1.PodCliqueSpec{
Replicas: 1,
},
Status: grovev1alpha1.PodCliqueStatus{
Replicas: 1,
UpdatedReplicas: 1,
ReadyReplicas: 1,
ObservedGeneration: ptr.To(int64(1)),
},
},
},
groveEnabled: true,
wantRestartStatus: &v1alpha1.RestartStatus{
ObservedID: newID,
Phase: v1alpha1.RestartPhaseCompleted,
},
},
{
name: "Grove pathway - sequential restart in progress",
dgdSpec: v1alpha1.DynamoGraphDeploymentSpec{
Restart: &v1alpha1.Restart{
ID: newID,
Strategy: &v1alpha1.RestartStrategy{
Type: v1alpha1.RestartStrategyTypeSequential,
Order: []string{"frontend"},
},
},
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"frontend": {
Replicas: ptr.To(int32(2)),
},
},
},
dgdStatus: v1alpha1.DynamoGraphDeploymentStatus{
Restart: &v1alpha1.RestartStatus{
ObservedID: newID,
Phase: v1alpha1.RestartPhaseRestarting,
InProgress: []string{"frontend"},
},
},
existingResources: []client.Object{
&grovev1alpha1.PodClique{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-0-frontend",
Namespace: "default",
Generation: 1,
},
Spec: grovev1alpha1.PodCliqueSpec{
Replicas: 2,
},
Status: grovev1alpha1.PodCliqueStatus{
Replicas: 2,
UpdatedReplicas: 1, // Not fully updated
ReadyReplicas: 1,
ObservedGeneration: ptr.To(int64(1)),
},
},
},
groveEnabled: true,
wantRestartStatus: &v1alpha1.RestartStatus{
ObservedID: newID,
Phase: v1alpha1.RestartPhaseRestarting,
InProgress: []string{"frontend"},
},
},
{
name: "parallel restart - new restart request during ongoing restart resets to all services (DCD pathway)",
dgdSpec: v1alpha1.DynamoGraphDeploymentSpec{
Restart: &v1alpha1.Restart{
ID: newID, // NEW timestamp
Strategy: &v1alpha1.RestartStrategy{
Type: v1alpha1.RestartStrategyTypeParallel,
},
},
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"frontend": {
Replicas: ptr.To(int32(1)),
},
"decode": {
Replicas: ptr.To(int32(1)),
},
"completed": {
Replicas: ptr.To(int32(1)),
},
},
},
dgdStatus: v1alpha1.DynamoGraphDeploymentStatus{
Restart: &v1alpha1.RestartStatus{
ObservedID: oldID,
Phase: v1alpha1.RestartPhaseRestarting,
InProgress: []string{"frontend", "decode"}, // completed service already done
},
},
existingResources: []client.Object{
// All services are now ready (simulating state after new restart timestamp is applied)
&v1alpha1.DynamoComponentDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-frontend",
Namespace: "default",
Generation: 2,
},
Status: v1alpha1.DynamoComponentDeploymentStatus{
ObservedGeneration: 2,
Conditions: []metav1.Condition{
{Type: v1alpha1.DynamoGraphDeploymentConditionTypeAvailable, Status: metav1.ConditionFalse},
},
},
},
&v1alpha1.DynamoComponentDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-decode",
Namespace: "default",
Generation: 2,
},
Status: v1alpha1.DynamoComponentDeploymentStatus{
ObservedGeneration: 2,
Conditions: []metav1.Condition{
{Type: v1alpha1.DynamoGraphDeploymentConditionTypeAvailable, Status: metav1.ConditionFalse},
},
},
},
&v1alpha1.DynamoComponentDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-completed",
Namespace: "default",
Generation: 2,
},
Status: v1alpha1.DynamoComponentDeploymentStatus{
ObservedGeneration: 2,
Conditions: []metav1.Condition{
{Type: v1alpha1.DynamoGraphDeploymentConditionTypeAvailable, Status: metav1.ConditionFalse},
},
},
},
},
wantRestartStatus: &v1alpha1.RestartStatus{
ObservedID: newID,
Phase: v1alpha1.RestartPhaseRestarting,
InProgress: []string{"completed", "decode", "frontend"}, // ALL services, sorted
},
},
{
name: "sequential restart - new restart request during ongoing restart resets to first service (DCD pathway)",
dgdSpec: v1alpha1.DynamoGraphDeploymentSpec{
Restart: &v1alpha1.Restart{
ID: newID, // NEW timestamp
Strategy: &v1alpha1.RestartStrategy{
Type: v1alpha1.RestartStrategyTypeSequential,
Order: []string{"frontend", "decode", "worker"},
},
},
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"frontend": {
Replicas: ptr.To(int32(1)),
},
"decode": {
Replicas: ptr.To(int32(1)),
},
"worker": {
Replicas: ptr.To(int32(1)),
},
},
},
dgdStatus: v1alpha1.DynamoGraphDeploymentStatus{
Restart: &v1alpha1.RestartStatus{
ObservedID: oldID,
Phase: v1alpha1.RestartPhaseRestarting,
InProgress: []string{"decode"},
},
},
existingResources: []client.Object{
&v1alpha1.DynamoComponentDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-frontend",
Namespace: "default",
Generation: 2,
},
Status: v1alpha1.DynamoComponentDeploymentStatus{
ObservedGeneration: 2,
Conditions: []metav1.Condition{
{Type: v1alpha1.DynamoGraphDeploymentConditionTypeAvailable, Status: metav1.ConditionTrue},
},
},
},
&v1alpha1.DynamoComponentDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-decode",
Namespace: "default",
Generation: 2,
},
Status: v1alpha1.DynamoComponentDeploymentStatus{
ObservedGeneration: 2,
Conditions: []metav1.Condition{
{Type: v1alpha1.DynamoGraphDeploymentConditionTypeAvailable, Status: metav1.ConditionTrue},
},
},
},
&v1alpha1.DynamoComponentDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-worker",
Namespace: "default",
Generation: 2,
},
Status: v1alpha1.DynamoComponentDeploymentStatus{
ObservedGeneration: 2,
Conditions: []metav1.Condition{
{Type: v1alpha1.DynamoGraphDeploymentConditionTypeAvailable, Status: metav1.ConditionTrue},
},
},
},
},
wantRestartStatus: &v1alpha1.RestartStatus{
ObservedID: newID,
Phase: v1alpha1.RestartPhaseRestarting,
InProgress: []string{"frontend"}, // Reset to FIRST service
},
},
{
name: "rolling update in progress + new restart request - superseded",
dgdSpec: v1alpha1.DynamoGraphDeploymentSpec{
Restart: &v1alpha1.Restart{
ID: newID,
},
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"frontend": {
Replicas: ptr.To(int32(1)),
},
},
},
dgdStatus: v1alpha1.DynamoGraphDeploymentStatus{
RollingUpdate: &v1alpha1.RollingUpdateStatus{
Phase: v1alpha1.RollingUpdatePhaseInProgress,
},
},
wantRestartStatus: &v1alpha1.RestartStatus{
ObservedID: newID,
Phase: v1alpha1.RestartPhaseSuperseded,
},
},
{
name: "rolling update pending + restart already in progress - superseded",
dgdSpec: v1alpha1.DynamoGraphDeploymentSpec{
Restart: &v1alpha1.Restart{
ID: newID,
},
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"frontend": {
Replicas: ptr.To(int32(1)),
},
},
},
dgdStatus: v1alpha1.DynamoGraphDeploymentStatus{
Restart: &v1alpha1.RestartStatus{
ObservedID: oldID,
Phase: v1alpha1.RestartPhaseRestarting,
InProgress: []string{"frontend"},
},
RollingUpdate: &v1alpha1.RollingUpdateStatus{
Phase: v1alpha1.RollingUpdatePhasePending,
},
},
wantRestartStatus: &v1alpha1.RestartStatus{
ObservedID: newID,
Phase: v1alpha1.RestartPhaseSuperseded,
},
},
{
name: "rolling update completed + restart request - normal processing",
dgdSpec: v1alpha1.DynamoGraphDeploymentSpec{
Restart: &v1alpha1.Restart{
ID: newID,
},
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"frontend": {
Replicas: ptr.To(int32(1)),
},
},
},
dgdStatus: v1alpha1.DynamoGraphDeploymentStatus{
RollingUpdate: &v1alpha1.RollingUpdateStatus{
Phase: v1alpha1.RollingUpdatePhaseCompleted,
},
},
wantRestartStatus: &v1alpha1.RestartStatus{
ObservedID: newID,
Phase: v1alpha1.RestartPhaseRestarting,
InProgress: []string{"frontend"},
},
},
{
name: "restart already processed as superseded - returns existing status",
dgdSpec: v1alpha1.DynamoGraphDeploymentSpec{
Restart: &v1alpha1.Restart{
ID: newID,
},
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"frontend": {
Replicas: ptr.To(int32(1)),
},
},
},
dgdStatus: v1alpha1.DynamoGraphDeploymentStatus{
Restart: &v1alpha1.RestartStatus{
ObservedID: newID,
Phase: v1alpha1.RestartPhaseSuperseded,
},
},
wantRestartStatus: &v1alpha1.RestartStatus{
ObservedID: newID,
Phase: v1alpha1.RestartPhaseSuperseded,
},
},
{
name: "no restart requested but has superseded status - preserves status",
dgdStatus: v1alpha1.DynamoGraphDeploymentStatus{
Restart: &v1alpha1.RestartStatus{
ObservedID: oldID,
Phase: v1alpha1.RestartPhaseSuperseded,
},
},
wantRestartStatus: &v1alpha1.RestartStatus{
ObservedID: oldID,
Phase: v1alpha1.RestartPhaseSuperseded,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := gomega.NewGomegaWithT(t)
s := scheme.Scheme
err := v1alpha1.AddToScheme(s)
g.Expect(err).NotTo(gomega.HaveOccurred())
err = grovev1alpha1.AddToScheme(s)
g.Expect(err).NotTo(gomega.HaveOccurred())
dgd := &v1alpha1.DynamoGraphDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd",
Namespace: "default",
},
Spec: tt.dgdSpec,
Status: tt.dgdStatus,
}
var objects []client.Object
objects = append(objects, dgd)
objects = append(objects, tt.existingResources...)
fakeKubeClient := fake.NewClientBuilder().
WithScheme(s).
WithObjects(objects...).
WithStatusSubresource(objects...).
Build()
recorder := record.NewFakeRecorder(100)
reconciler := &DynamoGraphDeploymentReconciler{
Client: fakeKubeClient,
Recorder: recorder,
Config: &configv1alpha1.OperatorConfiguration{},
RuntimeConfig: &controller_common.RuntimeConfig{
GroveEnabled: tt.groveEnabled,
},
}
result := reconciler.computeRestartStatus(ctx, dgd)
if tt.wantRestartStatus == nil {
g.Expect(result).To(gomega.BeNil())
return
}
g.Expect(result).NotTo(gomega.BeNil())
g.Expect(result).To(gomega.Equal(tt.wantRestartStatus))
})
}
}
func Test_reconcileDynamoComponentsDeployments(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
dgdSpec v1alpha1.DynamoGraphDeploymentSpec
existingDCDs []client.Object
wantReconcileResult ReconcileResult
}{
{
name: "single service - DCD ready (Available condition = True)",
dgdSpec: v1alpha1.DynamoGraphDeploymentSpec{
BackendFramework: "vllm",
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"frontend": {
ServiceName: "frontend",
DynamoNamespace: ptr.To("default"),
ComponentType: string(commonconsts.ComponentTypeFrontend),
Replicas: ptr.To(int32(2)),
},
},
},
existingDCDs: []client.Object{
&v1alpha1.DynamoComponentDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-frontend",
Namespace: "default",
},
Spec: v1alpha1.DynamoComponentDeploymentSpec{
BackendFramework: "vllm",
DynamoComponentDeploymentSharedSpec: v1alpha1.DynamoComponentDeploymentSharedSpec{
ServiceName: "frontend",
Replicas: ptr.To(int32(2)),
},
},
Status: v1alpha1.DynamoComponentDeploymentStatus{
Conditions: []metav1.Condition{
{
Type: v1alpha1.DynamoGraphDeploymentConditionTypeAvailable,
Status: metav1.ConditionTrue,
},
},
Service: &v1alpha1.ServiceReplicaStatus{
ComponentKind: v1alpha1.ComponentKindDeployment,
ComponentName: "test-dgd-frontend-deployment",
Replicas: 2,
UpdatedReplicas: 2,
ReadyReplicas: ptr.To(int32(2)),
AvailableReplicas: ptr.To(int32(2)),
},
},
},
},
wantReconcileResult: ReconcileResult{
State: v1alpha1.DGDStateSuccessful,
Reason: "all_resources_are_ready",
Message: "All resources are ready",
ServiceStatus: map[string]v1alpha1.ServiceReplicaStatus{
"frontend": {
ComponentKind: v1alpha1.ComponentKindDeployment,
ComponentName: "test-dgd-frontend-deployment",
Replicas: 2,
UpdatedReplicas: 2,
ReadyReplicas: ptr.To(int32(2)),
AvailableReplicas: ptr.To(int32(2)),
},
},
},
},
{
name: "single service - DCD not ready (Available condition = False)",
dgdSpec: v1alpha1.DynamoGraphDeploymentSpec{
BackendFramework: "vllm",
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"frontend": {
ServiceName: "frontend",
DynamoNamespace: ptr.To("default"),
ComponentType: string(commonconsts.ComponentTypeFrontend),
Replicas: ptr.To(int32(2)),
},
},
},
existingDCDs: []client.Object{
&v1alpha1.DynamoComponentDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-frontend",
Namespace: "default",
},
Spec: v1alpha1.DynamoComponentDeploymentSpec{
BackendFramework: "vllm",
DynamoComponentDeploymentSharedSpec: v1alpha1.DynamoComponentDeploymentSharedSpec{
ServiceName: "frontend",
Replicas: ptr.To(int32(2)),
},
},
Status: v1alpha1.DynamoComponentDeploymentStatus{
Conditions: []metav1.Condition{
{
Type: v1alpha1.DynamoGraphDeploymentConditionTypeAvailable,
Status: metav1.ConditionFalse,
},
},
Service: &v1alpha1.ServiceReplicaStatus{
ComponentKind: v1alpha1.ComponentKindDeployment,
ComponentName: "test-dgd-frontend-deployment",
Replicas: 2,
UpdatedReplicas: 1,
ReadyReplicas: ptr.To(int32(1)),
AvailableReplicas: ptr.To(int32(0)),
},
},
},
},
wantReconcileResult: ReconcileResult{
State: v1alpha1.DGDStatePending,
Reason: "some_resources_are_not_ready",
Message: "Resources not ready: test-dgd-frontend: Component deployment not ready - Available condition not true",
ServiceStatus: map[string]v1alpha1.ServiceReplicaStatus{
"frontend": {
ComponentKind: v1alpha1.ComponentKindDeployment,
ComponentName: "test-dgd-frontend-deployment",
Replicas: 2,
UpdatedReplicas: 1,
ReadyReplicas: ptr.To(int32(1)),
AvailableReplicas: ptr.To(int32(0)),
},
},
},
},
{
name: "multiple services - all DCDs ready",
dgdSpec: v1alpha1.DynamoGraphDeploymentSpec{
BackendFramework: "vllm",
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"frontend": {
ServiceName: "frontend",
DynamoNamespace: ptr.To("default"),
ComponentType: string(commonconsts.ComponentTypeFrontend),
Replicas: ptr.To(int32(1)),
},
"decode": {
ServiceName: "decode",
DynamoNamespace: ptr.To("default"),
ComponentType: string(commonconsts.ComponentTypeDecode),
Replicas: ptr.To(int32(2)),
},
"prefill": {
ServiceName: "prefill",
DynamoNamespace: ptr.To("default"),
ComponentType: string(commonconsts.ComponentTypePrefill),
Replicas: ptr.To(int32(3)),
},
},
},
existingDCDs: []client.Object{
&v1alpha1.DynamoComponentDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-frontend",
Namespace: "default",
},
Spec: v1alpha1.DynamoComponentDeploymentSpec{
BackendFramework: "vllm",
DynamoComponentDeploymentSharedSpec: v1alpha1.DynamoComponentDeploymentSharedSpec{
ServiceName: "frontend",
Replicas: ptr.To(int32(1)),
},
},
Status: v1alpha1.DynamoComponentDeploymentStatus{
Conditions: []metav1.Condition{
{
Type: v1alpha1.DynamoGraphDeploymentConditionTypeAvailable,
Status: metav1.ConditionTrue,
},
},
Service: &v1alpha1.ServiceReplicaStatus{
ComponentKind: v1alpha1.ComponentKindDeployment,
ComponentName: "test-dgd-frontend-deployment",
Replicas: 1,
UpdatedReplicas: 1,
ReadyReplicas: ptr.To(int32(1)),
AvailableReplicas: ptr.To(int32(1)),
},
},
},
&v1alpha1.DynamoComponentDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-decode-e1f2a6fe",
Namespace: "default",
},
Spec: v1alpha1.DynamoComponentDeploymentSpec{
BackendFramework: "vllm",
DynamoComponentDeploymentSharedSpec: v1alpha1.DynamoComponentDeploymentSharedSpec{
ServiceName: "decode",
Replicas: ptr.To(int32(2)),
},
},
Status: v1alpha1.DynamoComponentDeploymentStatus{
Conditions: []metav1.Condition{
{
Type: v1alpha1.DynamoGraphDeploymentConditionTypeAvailable,
Status: metav1.ConditionTrue,
},
},
Service: &v1alpha1.ServiceReplicaStatus{
ComponentKind: v1alpha1.ComponentKindDeployment,
ComponentName: "test-dgd-decode-e1f2a6fe-deployment",
Replicas: 2,
UpdatedReplicas: 2,
ReadyReplicas: ptr.To(int32(2)),
AvailableReplicas: ptr.To(int32(2)),
},
},
},
&v1alpha1.DynamoComponentDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-prefill-e1f2a6fe",
Namespace: "default",
},
Spec: v1alpha1.DynamoComponentDeploymentSpec{
BackendFramework: "vllm",
DynamoComponentDeploymentSharedSpec: v1alpha1.DynamoComponentDeploymentSharedSpec{
ServiceName: "prefill",
Replicas: ptr.To(int32(3)),
},
},
Status: v1alpha1.DynamoComponentDeploymentStatus{
Conditions: []metav1.Condition{
{
Type: v1alpha1.DynamoGraphDeploymentConditionTypeAvailable,
Status: metav1.ConditionTrue,
},
},
Service: &v1alpha1.ServiceReplicaStatus{
ComponentKind: v1alpha1.ComponentKindDeployment,
ComponentName: "test-dgd-prefill-e1f2a6fe-deployment",
Replicas: 3,
UpdatedReplicas: 3,
ReadyReplicas: ptr.To(int32(3)),
AvailableReplicas: ptr.To(int32(3)),
},
},
},
},
wantReconcileResult: ReconcileResult{
State: v1alpha1.DGDStateSuccessful,
Reason: "all_resources_are_ready",
Message: "All resources are ready",
ServiceStatus: map[string]v1alpha1.ServiceReplicaStatus{
"frontend": {
ComponentKind: v1alpha1.ComponentKindDeployment,
ComponentName: "test-dgd-frontend-deployment",
Replicas: 1,
UpdatedReplicas: 1,
ReadyReplicas: ptr.To(int32(1)),
AvailableReplicas: ptr.To(int32(1)),
},
"decode": {
ComponentKind: v1alpha1.ComponentKindDeployment,
ComponentName: "test-dgd-decode-e1f2a6fe-deployment",
Replicas: 2,
UpdatedReplicas: 2,
ReadyReplicas: ptr.To(int32(2)),
AvailableReplicas: ptr.To(int32(2)),
},
"prefill": {
ComponentKind: v1alpha1.ComponentKindDeployment,
ComponentName: "test-dgd-prefill-e1f2a6fe-deployment",
Replicas: 3,
UpdatedReplicas: 3,
ReadyReplicas: ptr.To(int32(3)),
AvailableReplicas: ptr.To(int32(3)),
},
},
},
},
{
name: "multiple services - some DCDs ready, some not ready",
dgdSpec: v1alpha1.DynamoGraphDeploymentSpec{
BackendFramework: "vllm",
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"frontend": {
ServiceName: "frontend",
DynamoNamespace: ptr.To("default"),
ComponentType: string(commonconsts.ComponentTypeFrontend),
Replicas: ptr.To(int32(1)),
},
"decode": {
ServiceName: "decode",
DynamoNamespace: ptr.To("default"),
ComponentType: string(commonconsts.ComponentTypeDecode),
Replicas: ptr.To(int32(2)),
},
"prefill": {
ServiceName: "prefill",
DynamoNamespace: ptr.To("default"),
ComponentType: string(commonconsts.ComponentTypePrefill),
Replicas: ptr.To(int32(3)),
},
},
},
existingDCDs: []client.Object{
&v1alpha1.DynamoComponentDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-frontend",
Namespace: "default",
},
Spec: v1alpha1.DynamoComponentDeploymentSpec{
BackendFramework: "vllm",
DynamoComponentDeploymentSharedSpec: v1alpha1.DynamoComponentDeploymentSharedSpec{
ServiceName: "frontend",
Replicas: ptr.To(int32(1)),
},
},
Status: v1alpha1.DynamoComponentDeploymentStatus{
Conditions: []metav1.Condition{
{
Type: v1alpha1.DynamoGraphDeploymentConditionTypeAvailable,
Status: metav1.ConditionTrue,
},
},
Service: &v1alpha1.ServiceReplicaStatus{
ComponentKind: v1alpha1.ComponentKindDeployment,
ComponentName: "test-dgd-frontend-deployment",
Replicas: 1,
UpdatedReplicas: 1,
ReadyReplicas: ptr.To(int32(1)),
AvailableReplicas: ptr.To(int32(1)),
},
},
},
&v1alpha1.DynamoComponentDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-decode-e1f2a6fe",
Namespace: "default",
},
Spec: v1alpha1.DynamoComponentDeploymentSpec{
BackendFramework: "vllm",
DynamoComponentDeploymentSharedSpec: v1alpha1.DynamoComponentDeploymentSharedSpec{
ServiceName: "decode",
Replicas: ptr.To(int32(2)),
},
},
Status: v1alpha1.DynamoComponentDeploymentStatus{
Conditions: []metav1.Condition{
{
Type: v1alpha1.DynamoGraphDeploymentConditionTypeAvailable,
Status: metav1.ConditionFalse,
},
},
Service: &v1alpha1.ServiceReplicaStatus{
ComponentKind: v1alpha1.ComponentKindDeployment,
ComponentName: "test-dgd-decode-e1f2a6fe-deployment",
Replicas: 2,
UpdatedReplicas: 1,
ReadyReplicas: ptr.To(int32(1)),
AvailableReplicas: ptr.To(int32(0)),
},
},
},
&v1alpha1.DynamoComponentDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-prefill-e1f2a6fe",
Namespace: "default",
},
Spec: v1alpha1.DynamoComponentDeploymentSpec{
BackendFramework: "vllm",
DynamoComponentDeploymentSharedSpec: v1alpha1.DynamoComponentDeploymentSharedSpec{
ServiceName: "prefill",
Replicas: ptr.To(int32(3)),
},
},
Status: v1alpha1.DynamoComponentDeploymentStatus{
Conditions: []metav1.Condition{
{
Type: v1alpha1.DynamoGraphDeploymentConditionTypeAvailable,
Status: metav1.ConditionTrue,
},
},
Service: &v1alpha1.ServiceReplicaStatus{
ComponentKind: v1alpha1.ComponentKindDeployment,
ComponentName: "test-dgd-prefill-e1f2a6fe-deployment",
Replicas: 3,
UpdatedReplicas: 3,
ReadyReplicas: ptr.To(int32(3)),
AvailableReplicas: ptr.To(int32(3)),
},
},
},
},
wantReconcileResult: ReconcileResult{
State: v1alpha1.DGDStatePending,
Reason: "some_resources_are_not_ready",
Message: "Resources not ready: test-dgd-decode-e1f2a6fe: Component deployment not ready - Available condition not true",
ServiceStatus: map[string]v1alpha1.ServiceReplicaStatus{
"frontend": {
ComponentKind: v1alpha1.ComponentKindDeployment,
ComponentName: "test-dgd-frontend-deployment",
Replicas: 1,
UpdatedReplicas: 1,
ReadyReplicas: ptr.To(int32(1)),
AvailableReplicas: ptr.To(int32(1)),
},
"decode": {
ComponentKind: v1alpha1.ComponentKindDeployment,
ComponentName: "test-dgd-decode-e1f2a6fe-deployment",
Replicas: 2,
UpdatedReplicas: 1,
ReadyReplicas: ptr.To(int32(1)),
AvailableReplicas: ptr.To(int32(0)),
},
"prefill": {
ComponentKind: v1alpha1.ComponentKindDeployment,
ComponentName: "test-dgd-prefill-e1f2a6fe-deployment",
Replicas: 3,
UpdatedReplicas: 3,
ReadyReplicas: ptr.To(int32(3)),
AvailableReplicas: ptr.To(int32(3)),
},
},
},
},
{
name: "multiple services - all DCDs not ready",
dgdSpec: v1alpha1.DynamoGraphDeploymentSpec{
BackendFramework: "vllm",
Services: map[string]*v1alpha1.DynamoComponentDeploymentSharedSpec{
"frontend": {
ServiceName: "frontend",
DynamoNamespace: ptr.To("default"),
ComponentType: string(commonconsts.ComponentTypeFrontend),
Replicas: ptr.To(int32(1)),
},
"decode": {
ServiceName: "decode",
DynamoNamespace: ptr.To("default"),
ComponentType: string(commonconsts.ComponentTypeDecode),
Replicas: ptr.To(int32(2)),
},
},
},
existingDCDs: []client.Object{
&v1alpha1.DynamoComponentDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-frontend",
Namespace: "default",
},
Spec: v1alpha1.DynamoComponentDeploymentSpec{
BackendFramework: "vllm",
DynamoComponentDeploymentSharedSpec: v1alpha1.DynamoComponentDeploymentSharedSpec{
ServiceName: "frontend",
Replicas: ptr.To(int32(1)),
},
},
Status: v1alpha1.DynamoComponentDeploymentStatus{
Conditions: []metav1.Condition{
{
Type: v1alpha1.DynamoGraphDeploymentConditionTypeAvailable,
Status: metav1.ConditionFalse,
},
},
Service: &v1alpha1.ServiceReplicaStatus{
ComponentKind: v1alpha1.ComponentKindDeployment,
ComponentName: "test-dgd-frontend-deployment",
Replicas: 1,
UpdatedReplicas: 0,
ReadyReplicas: ptr.To(int32(0)),
AvailableReplicas: ptr.To(int32(0)),
},
},
},
&v1alpha1.DynamoComponentDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd-decode-5f3d46ba",
Namespace: "default",
},
Spec: v1alpha1.DynamoComponentDeploymentSpec{
BackendFramework: "vllm",
DynamoComponentDeploymentSharedSpec: v1alpha1.DynamoComponentDeploymentSharedSpec{
ServiceName: "decode",
Replicas: ptr.To(int32(2)),
},
},
Status: v1alpha1.DynamoComponentDeploymentStatus{
Conditions: []metav1.Condition{
{
Type: v1alpha1.DynamoGraphDeploymentConditionTypeAvailable,
Status: metav1.ConditionFalse,
},
},
Service: &v1alpha1.ServiceReplicaStatus{
ComponentKind: v1alpha1.ComponentKindDeployment,
ComponentName: "test-dgd-decode-5f3d46ba-deployment",
Replicas: 2,
UpdatedReplicas: 1,
ReadyReplicas: ptr.To(int32(1)),
AvailableReplicas: ptr.To(int32(0)),
},
},
},
},
wantReconcileResult: ReconcileResult{
State: v1alpha1.DGDStatePending,
Reason: "some_resources_are_not_ready",
Message: "Resources not ready: test-dgd-decode-5f3d46ba: Component deployment not ready - Available condition not true; test-dgd-frontend: Component deployment not ready - Available condition not true",
ServiceStatus: map[string]v1alpha1.ServiceReplicaStatus{
"frontend": {
ComponentKind: v1alpha1.ComponentKindDeployment,
ComponentName: "test-dgd-frontend-deployment",
Replicas: 1,
UpdatedReplicas: 0,
ReadyReplicas: ptr.To(int32(0)),
AvailableReplicas: ptr.To(int32(0)),
},
"decode": {
ComponentKind: v1alpha1.ComponentKindDeployment,
ComponentName: "test-dgd-decode-5f3d46ba-deployment",
Replicas: 2,
UpdatedReplicas: 1,
ReadyReplicas: ptr.To(int32(1)),
AvailableReplicas: ptr.To(int32(0)),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := gomega.NewGomegaWithT(t)
s := scheme.Scheme
err := v1alpha1.AddToScheme(s)
g.Expect(err).NotTo(gomega.HaveOccurred())
dgd := &v1alpha1.DynamoGraphDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dgd",
Namespace: "default",
},
Spec: tt.dgdSpec,
}
var objects []client.Object
objects = append(objects, dgd)
objects = append(objects, tt.existingDCDs...)
fakeKubeClient := fake.NewClientBuilder().
WithScheme(s).
WithObjects(objects...).
WithStatusSubresource(objects...).
Build()
recorder := record.NewFakeRecorder(100)
reconciler := &DynamoGraphDeploymentReconciler{
Client: fakeKubeClient,
Recorder: recorder,
Config: &configv1alpha1.OperatorConfiguration{},
RuntimeConfig: &controller_common.RuntimeConfig{},
}
result, err := reconciler.reconcileDynamoComponentsDeployments(ctx, dgd, nil)
g.Expect(err).NotTo(gomega.HaveOccurred())
g.Expect(result).To(gomega.Equal(tt.wantReconcileResult))
})
}
}